From bc2de829697795cecebc696b342a11c2a95314aa Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 08:27:57 -0400 Subject: [PATCH 01/16] Test and Fix CMake's `CacheVariable` Schema --- cppython/plugins/cmake/schema.py | 20 +++++----- tests/unit/plugins/cmake/test_schema.py | 51 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 tests/unit/plugins/cmake/test_schema.py diff --git a/cppython/plugins/cmake/schema.py b/cppython/plugins/cmake/schema.py index dba1005d..d7456c26 100644 --- a/cppython/plugins/cmake/schema.py +++ b/cppython/plugins/cmake/schema.py @@ -5,7 +5,7 @@ configuration presets, and synchronization data. """ -from enum import Enum, auto +from enum import StrEnum from pathlib import Path from typing import Annotated @@ -15,20 +15,20 @@ from cppython.core.schema import CPPythonModel, SyncData -class VariableType(Enum): +class VariableType(StrEnum): """Defines the types of variables that can be used in CMake cache. Args: Enum: Base class for creating enumerations. """ - BOOL = (auto(),) # Boolean ON/OFF value. - PATH = (auto(),) # Path to a directory. - FILEPATH = (auto(),) # Path to a file. - STRING = (auto(),) # Generic string value. - INTERNAL = (auto(),) # Do not present in GUI at all. - STATIC = (auto(),) # Value managed by CMake, do not change. - UNINITIALIZED = auto() # Type not yet specified. + BOOL = 'BOOL' + PATH = 'PATH' + FILEPATH = 'FILEPATH' + STRING = 'STRING' + INTERNAL = 'INTERNAL' + STATIC = 'STATIC' + UNINITIALIZED = 'UNINITIALIZED' class CacheVariable(CPPythonModel, extra='forbid'): @@ -39,7 +39,7 @@ class CacheVariable(CPPythonModel, extra='forbid'): value: The value of the variable, which can be a boolean or string. """ - type: None | VariableType + type: None | VariableType = None value: bool | str diff --git a/tests/unit/plugins/cmake/test_schema.py b/tests/unit/plugins/cmake/test_schema.py new file mode 100644 index 00000000..382f4f3e --- /dev/null +++ b/tests/unit/plugins/cmake/test_schema.py @@ -0,0 +1,51 @@ +"""Tests for the CMake schema""" + +from cppython.plugins.cmake.schema import CacheVariable, VariableType + + +class TestCacheVariable: + """Tests for the CacheVariable class""" + + @staticmethod + def test_cache_variable_bool() -> None: + """Tests the CacheVariable class with a boolean value""" + var = CacheVariable(type=VariableType.BOOL, value=True) + assert var.type == VariableType.BOOL + assert var.value is True + + @staticmethod + def test_cache_variable_string() -> None: + """Tests the CacheVariable class with a string value""" + var = CacheVariable(type=VariableType.STRING, value='SomeValue') + assert var.type == VariableType.STRING + assert var.value == 'SomeValue' + + @staticmethod + def test_cache_variable_null_type() -> None: + """Tests the CacheVariable class with a null type""" + var = CacheVariable(type=None, value='Unset') + assert var.type is None + assert var.value == 'Unset' + + @staticmethod + def test_cache_variable_type_enum_values() -> None: + """Tests the CacheVariable class with enum values""" + # Ensure all CMake types are present + expected = {'BOOL', 'PATH', 'FILEPATH', 'STRING', 'INTERNAL', 'STATIC', 'UNINITIALIZED'} + actual = {v.value for v in VariableType} + assert expected == actual + + @staticmethod + def test_cache_variable_bool_value_as_string() -> None: + """Tests the CacheVariable class with a boolean value as a string""" + # CMake allows bool as "TRUE"/"FALSE" as well + var = CacheVariable(type=VariableType.BOOL, value='TRUE') + assert var.value == 'TRUE' + + @staticmethod + def test_cache_variable_type_optional() -> None: + """Tests the CacheVariable class with an optional type""" + # type is optional + var = CacheVariable(value='SomeValue') + assert var.type is None + assert var.value == 'SomeValue' From 248e6a218b570e1ad94cff9a078713913f92ae6b Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 08:29:21 -0400 Subject: [PATCH 02/16] Fix Lint Issues --- cppython/builder.py | 12 ++++++------ cppython/core/plugin_schema/generator.py | 5 +++-- cppython/core/plugin_schema/provider.py | 5 +++-- cppython/core/plugin_schema/scm.py | 4 ++-- cppython/core/schema.py | 8 ++++---- cppython/defaults.py | 8 ++++---- cppython/plugins/cmake/builder.py | 6 ++++-- cppython/plugins/cmake/plugin.py | 6 +++--- cppython/plugins/cmake/resolution.py | 2 -- cppython/plugins/cmake/schema.py | 4 ++++ cppython/plugins/conan/builder.py | 4 ++-- cppython/plugins/conan/plugin.py | 6 +++--- cppython/plugins/git/plugin.py | 16 +++++----------- cppython/plugins/vcpkg/plugin.py | 6 +++--- cppython/test/mock/generator.py | 8 ++++---- cppython/test/mock/provider.py | 6 +++--- cppython/test/mock/scm.py | 15 ++++++++++----- cppython/test/pytest/base_classes.py | 9 ++++++--- pyproject.toml | 1 - tests/unit/core/test_resolution.py | 9 ++++++--- tests/unit/plugins/cmake/test_presets.py | 2 +- tests/unit/plugins/cmake/test_schema.py | 8 -------- 22 files changed, 76 insertions(+), 74 deletions(-) diff --git a/cppython/builder.py b/cppython/builder.py index 4a2019de..beded73c 100644 --- a/cppython/builder.py +++ b/cppython/builder.py @@ -4,11 +4,11 @@ from importlib.metadata import entry_points from inspect import getmodule from logging import Logger -from typing import Any +from typing import Any, cast from cppython.core.plugin_schema.generator import Generator from cppython.core.plugin_schema.provider import Provider -from cppython.core.plugin_schema.scm import SCM +from cppython.core.plugin_schema.scm import SCM, SupportedSCMFeatures from cppython.core.resolution import ( PluginBuildData, PluginCPPythonData, @@ -262,7 +262,7 @@ def select_scm(self, scm_plugins: list[type[SCM]], project_data: ProjectData) -> The selected SCM plugin type """ for scm_type in scm_plugins: - if scm_type.features(project_data.project_root).repository: + if cast(SupportedSCMFeatures, scm_type.features(project_data.project_root)).repository: return scm_type self._logger.info('No SCM plugin was found that supports the given path') @@ -317,7 +317,7 @@ def create_scm( cppython_plugin_data = resolve_cppython_plugin(core_data.cppython_data, scm_type) scm_data = resolve_scm(core_data.project_data, cppython_plugin_data) - plugin = scm_type(scm_data) + plugin = cast(SCM, scm_type(scm_data)) return plugin @@ -354,7 +354,7 @@ def create_generator( cppython_data=cppython_plugin_data, ) - return generator_type(generator_data, core_plugin_data, generator_configuration) + return cast(Generator, generator_type(generator_data, core_plugin_data, generator_configuration)) def create_provider( self, @@ -389,7 +389,7 @@ def create_provider( cppython_data=cppython_plugin_data, ) - return provider_type(provider_data, core_plugin_data, provider_configuration) + return cast(Provider, provider_type(provider_data, core_plugin_data, provider_configuration)) class Builder: diff --git a/cppython/core/plugin_schema/generator.py b/cppython/core/plugin_schema/generator.py index 0a812664..83bb373f 100644 --- a/cppython/core/plugin_schema/generator.py +++ b/cppython/core/plugin_schema/generator.py @@ -10,6 +10,7 @@ DataPlugin, DataPluginGroupData, SupportedDataFeatures, + SupportedFeatures, SyncData, ) @@ -58,13 +59,13 @@ def __init__( @staticmethod @abstractmethod - def features(directory: DirectoryPath) -> SupportedGeneratorFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the generator plugin to CPPython Args: directory: The root directory where features are evaluated Returns: - The supported features + The supported features - `SupportedGeneratorFeatures`. Cast to this type to help us avoid generic typing """ raise NotImplementedError diff --git a/cppython/core/plugin_schema/provider.py b/cppython/core/plugin_schema/provider.py index b8189767..3c34c562 100644 --- a/cppython/core/plugin_schema/provider.py +++ b/cppython/core/plugin_schema/provider.py @@ -11,6 +11,7 @@ DataPlugin, DataPluginGroupData, SupportedDataFeatures, + SupportedFeatures, SyncData, ) @@ -67,14 +68,14 @@ def __init__( @staticmethod @abstractmethod - def features(directory: DirectoryPath) -> SupportedProviderFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the Provider plugin to CPPython Args: directory: The root directory where features are evaluated Returns: - The supported features + The supported features - `SupportedProviderFeatures`. Cast to this type to help us avoid generic typing """ raise NotImplementedError diff --git a/cppython/core/plugin_schema/scm.py b/cppython/core/plugin_schema/scm.py index 4e5be7c7..8726121d 100644 --- a/cppython/core/plugin_schema/scm.py +++ b/cppython/core/plugin_schema/scm.py @@ -31,14 +31,14 @@ def __init__(self, group_data: SCMPluginGroupData) -> None: @staticmethod @abstractmethod - def features(directory: DirectoryPath) -> SupportedSCMFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the SCM plugin to CPPython Args: directory: The root directory where features are evaluated Returns: - The supported features + The supported features - `SupportedSCMFeatures`. Cast to this type to help us avoid generic typing """ raise NotImplementedError diff --git a/cppython/core/schema.py b/cppython/core/schema.py index db0dd2ab..0d29be3c 100644 --- a/cppython/core/schema.py +++ b/cppython/core/schema.py @@ -44,7 +44,7 @@ class ProjectConfiguration(CPPythonModel, extra='forbid'): bool, Field(description='Debug mode. Additional processing will happen to expose more debug information') ] = False - @field_validator('verbosity') + @field_validator('verbosity') # type: ignore @classmethod def min_max(cls, value: int) -> int: """Validator that clamps the input value @@ -118,7 +118,7 @@ class CPPythonData(CPPythonModel, extra='forbid'): scm_name: TypeName dependencies: list[Requirement] - @field_validator('configuration_path', 'install_path', 'tool_path', 'build_path') + @field_validator('configuration_path', 'install_path', 'tool_path', 'build_path') # type: ignore @classmethod def validate_absolute_path(cls, value: Path) -> Path: """Enforce the input is an absolute path @@ -234,14 +234,14 @@ def __init__( @staticmethod @abstractmethod - def features(directory: DirectoryPath) -> SupportedDataFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the data plugin to CPPython Args: directory: The root directory where features are evaluated Returns: - The supported features + The supported features - `SupportedDataFeatures`. Cast to this type to help us avoid generic typing """ raise NotImplementedError diff --git a/cppython/defaults.py b/cppython/defaults.py index eb002673..2d732928 100644 --- a/cppython/defaults.py +++ b/cppython/defaults.py @@ -7,7 +7,7 @@ SCMPluginGroupData, SupportedSCMFeatures, ) -from cppython.core.schema import Information +from cppython.core.schema import Information, SupportedFeatures class DefaultSCM(SCM): @@ -18,11 +18,11 @@ def __init__(self, group_data: SCMPluginGroupData) -> None: self.group_data = group_data @staticmethod - def features(_: DirectoryPath) -> SupportedSCMFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the SCM plugin to CPPython Returns: - The supported features + The supported features - `SupportedGeneratorFeatures`. Cast to this type to help us avoid generic typing """ return SupportedSCMFeatures(repository=True) @@ -36,7 +36,7 @@ def information() -> Information: return Information() @staticmethod - def version(_: DirectoryPath) -> str: + def version(directory: DirectoryPath) -> str: """Extracts the system's version metadata Returns: diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index d489c2a1..2df251ba 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -19,7 +19,7 @@ def generate_provider_preset(provider_data: CMakeSyncData) -> CMakePresets: provider_directory: The base directory to place the preset files provider_data: The providers synchronization data """ - generated_configure_preset = ConfigurePreset(name=provider_data.provider_name) + generated_configure_preset = ConfigurePreset(name=provider_data.provider_name, hidden=True) # Toss in that sync data from the provider generated_configure_preset.cacheVariables = { @@ -67,7 +67,7 @@ def generate_cppython_preset( Returns: A CMakePresets object """ - generated_configure_preset = ConfigurePreset(name='cppython', inherits=provider_data.provider_name) + generated_configure_preset = ConfigurePreset(name='cppython', inherits=provider_data.provider_name, hidden=True) generated_preset = CMakePresets(configurePresets=[generated_configure_preset]) # Get the relative path to the provider preset file @@ -120,6 +120,7 @@ def generate_root_preset(preset_file: Path, cppython_preset_file: Path, cmake_da Args: preset_file: Preset file to modify cppython_preset_file: Path to the cppython preset file to include + cmake_data: The CMake data to use Returns: A CMakePresets object @@ -180,6 +181,7 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path, cmake_data Args: preset_file: Preset file to modify cppython_preset_file: Path to the cppython preset file to include + cmake_data: The CMake data to use """ initial_root_preset = None diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index 374e5c01..e321e554 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -8,7 +8,7 @@ GeneratorPluginGroupData, SupportedGeneratorFeatures, ) -from cppython.core.schema import CorePluginData, Information, SyncData +from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData from cppython.plugins.cmake.builder import Builder from cppython.plugins.cmake.resolution import resolve_cmake_data from cppython.plugins.cmake.schema import CMakeSyncData @@ -28,11 +28,11 @@ def __init__(self, group_data: GeneratorPluginGroupData, core_data: CorePluginDa self._provider_directory = self._cppython_preset_directory / 'providers' @staticmethod - def features(_: Path) -> SupportedGeneratorFeatures: + def features(directory: Path) -> SupportedFeatures: """Queries if CMake is supported Returns: - Supported? + The supported features - `SupportedGeneratorFeatures`. Cast to this type to help us avoid generic typing """ return SupportedGeneratorFeatures() diff --git a/cppython/plugins/cmake/resolution.py b/cppython/plugins/cmake/resolution.py index aecad095..27111627 100644 --- a/cppython/plugins/cmake/resolution.py +++ b/cppython/plugins/cmake/resolution.py @@ -24,6 +24,4 @@ def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMake if not modified_preset_file.is_absolute(): modified_preset_file = root_directory / modified_preset_file - - return CMakeData(preset_file=modified_preset_file, configuration_name=parsed_data.configuration_name) diff --git a/cppython/plugins/cmake/schema.py b/cppython/plugins/cmake/schema.py index d7456c26..90f0f41b 100644 --- a/cppython/plugins/cmake/schema.py +++ b/cppython/plugins/cmake/schema.py @@ -47,6 +47,10 @@ class ConfigurePreset(CPPythonModel, extra='allow'): """Partial Configure Preset specification to allow cache variable injection""" name: str + hidden: Annotated[bool | None, Field(description='If true, the preset is hidden and cannot be used directly.')] = ( + None + ) + inherits: Annotated[ str | list[str] | None, Field(description='The inherits field allows inheriting from other presets.') ] = None diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 1712b07d..396e6103 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -83,11 +83,11 @@ def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef: # Insert the new statement after the last attribute assignment if last_attribute is not None: - new_body = list(updated_node.body.body) + new_body = [item for item in updated_node.body.body] index = new_body.index(last_attribute) new_body.insert(index + 1, new_statement) else: - new_body = [new_statement] + list(updated_node.body.body) + new_body = [new_statement] + [item for item in updated_node.body.body] return updated_node.with_changes(body=updated_node.body.with_changes(body=new_body)) def _replace_requires( diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 079e73a4..5a3ee74d 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -12,7 +12,7 @@ from cppython.core.plugin_schema.generator import SyncConsumer from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures -from cppython.core.schema import CorePluginData, Information, SyncData +from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import CMakeSyncData from cppython.plugins.conan.builder import Builder @@ -47,14 +47,14 @@ def _download_file(url: str, file: Path) -> None: out_file.write(content) @staticmethod - def features(directory: Path) -> SupportedProviderFeatures: + def features(directory: Path) -> SupportedFeatures: """Queries conan support Args: directory: The directory to query Returns: - Supported features + Supported features - `SupportedProviderFeatures`. Cast to this type to help us avoid generic typing """ return SupportedProviderFeatures() diff --git a/cppython/plugins/git/plugin.py b/cppython/plugins/git/plugin.py index bbd6ac1b..cc2fc176 100644 --- a/cppython/plugins/git/plugin.py +++ b/cppython/plugins/git/plugin.py @@ -1,9 +1,4 @@ -"""Git SCM Plugin - -This module implements the Git SCM plugin for CPPython. It provides -functionality for interacting with Git repositories, including feature -detection, version extraction, and project description retrieval. -""" +"""Git SCM Plugin""" from pathlib import Path @@ -15,7 +10,7 @@ SCMPluginGroupData, SupportedSCMFeatures, ) -from cppython.core.schema import Information +from cppython.core.schema import Information, SupportedFeatures class GitSCM(SCM): @@ -26,14 +21,14 @@ def __init__(self, group_data: SCMPluginGroupData) -> None: self.group_data = group_data @staticmethod - def features(directory: Path) -> SupportedSCMFeatures: + def features(directory: Path) -> SupportedFeatures: """Broadcasts the shared features of the SCM plugin to CPPython Args: directory: The root directory where features are evaluated Returns: - The supported features + The supported features - `SupportedSCMFeatures`. Cast to this type to help us avoid generic typing """ is_repository = True try: @@ -52,8 +47,7 @@ def information() -> Information: """ return Information() - @staticmethod - def version(_: Path) -> str: + def version(self, directory: Path) -> str: """Extracts the system's version metadata Returns: diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index 063addae..3055f578 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -12,7 +12,7 @@ ProviderPluginGroupData, SupportedProviderFeatures, ) -from cppython.core.schema import CorePluginData, Information, SyncData +from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import CMakeSyncData from cppython.plugins.vcpkg.resolution import generate_manifest, resolve_vcpkg_data @@ -33,14 +33,14 @@ def __init__( self.data: VcpkgData = resolve_vcpkg_data(configuration_data, core_data) @staticmethod - def features(directory: Path) -> SupportedProviderFeatures: + def features(directory: Path) -> SupportedFeatures: """Queries vcpkg support Args: directory: The directory to query Returns: - Supported features + Supported features - `SupportedProviderFeatures`. Cast to this type to help us avoid generic typing """ return SupportedProviderFeatures() diff --git a/cppython/test/mock/generator.py b/cppython/test/mock/generator.py index 6c998694..85e96d3f 100644 --- a/cppython/test/mock/generator.py +++ b/cppython/test/mock/generator.py @@ -9,7 +9,7 @@ GeneratorPluginGroupData, SupportedGeneratorFeatures, ) -from cppython.core.schema import CorePluginData, CPPythonModel, Information, SyncData +from cppython.core.schema import CorePluginData, CPPythonModel, Information, SupportedFeatures, SyncData class MockSyncData(SyncData): @@ -32,11 +32,11 @@ def __init__( self.configuration_data = MockGeneratorData(**configuration_data) @staticmethod - def features(_: DirectoryPath) -> SupportedGeneratorFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the generator plugin to CPPython Returns: - The supported features + The supported features - `SupportedGeneratorFeatures`. Cast to this type to help us avoid generic typing """ return SupportedGeneratorFeatures() @@ -58,5 +58,5 @@ def sync_types() -> list[type[SyncData]]: """ return [MockSyncData] - def sync(self, _: SyncData) -> None: + def sync(self, sync_data: SyncData) -> None: """Synchronizes generator files and state with the providers input""" diff --git a/cppython/test/mock/provider.py b/cppython/test/mock/provider.py index bfc0ef79..091a9d59 100644 --- a/cppython/test/mock/provider.py +++ b/cppython/test/mock/provider.py @@ -10,7 +10,7 @@ ProviderPluginGroupData, SupportedProviderFeatures, ) -from cppython.core.schema import CorePluginData, CPPythonModel, Information, SyncData +from cppython.core.schema import CorePluginData, CPPythonModel, Information, SupportedFeatures, SyncData from cppython.test.mock.generator import MockSyncData @@ -32,11 +32,11 @@ def __init__( self.configuration_data = MockProviderData(**configuration_data) @staticmethod - def features(_: DirectoryPath) -> SupportedProviderFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the Provider plugin to CPPython Returns: - The supported features + The supported features - `SupportedProviderFeatures`. Cast to this type to help us avoid generic typing """ return SupportedProviderFeatures() diff --git a/cppython/test/mock/scm.py b/cppython/test/mock/scm.py index 9058406e..c2c9cee7 100644 --- a/cppython/test/mock/scm.py +++ b/cppython/test/mock/scm.py @@ -7,7 +7,7 @@ SCMPluginGroupData, SupportedSCMFeatures, ) -from cppython.core.schema import Information +from cppython.core.schema import Information, SupportedFeatures class MockSCM(SCM): @@ -18,11 +18,14 @@ def __init__(self, group_data: SCMPluginGroupData) -> None: self.group_data = group_data @staticmethod - def features(_: DirectoryPath) -> SupportedSCMFeatures: + def features(directory: DirectoryPath) -> SupportedFeatures: """Broadcasts the shared features of the SCM plugin to CPPython + Args: + directory: The root directory where features are evaluated + Returns: - The supported features + The supported features - `SupportedSCMFeatures`. Cast to this type to help us avoid generic typing """ return SupportedSCMFeatures(repository=True) @@ -35,10 +38,12 @@ def information() -> Information: """ return Information() - @staticmethod - def version(_: DirectoryPath) -> str: + def version(self, directory: DirectoryPath) -> str: """Extracts the system's version metadata + Args: + directory: The input directory + Returns: A version """ diff --git a/cppython/test/pytest/base_classes.py b/cppython/test/pytest/base_classes.py index 884542fa..a675bd4b 100644 --- a/cppython/test/pytest/base_classes.py +++ b/cppython/test/pytest/base_classes.py @@ -265,7 +265,8 @@ def fixture_plugin_group_data( Returns: The plugin configuration """ - project_data.project_root = tmp_path_factory.mktemp('workspace-') + workspace_path = tmp_path_factory.mktemp('workspace-') + project_data = ProjectData(project_root=workspace_path, verbosity=project_data.verbosity) # Install path is already pinned to a temp directory to share downloaded resources cppython_plugin_data.build_path = project_data.project_root / 'build' cppython_plugin_data.tool_path = project_data.project_root / 'tool' @@ -355,7 +356,8 @@ def fixture_plugin_group_data( Returns: The plugin configuration """ - project_data.project_root = tmp_path_factory.mktemp('workspace-') + workspace_path = tmp_path_factory.mktemp('workspace-') + project_data = ProjectData(project_root=workspace_path, verbosity=project_data.verbosity) # Install path is already pinned to a temp directory to share downloaded resources cppython_plugin_data.build_path = project_data.project_root / 'build' cppython_plugin_data.tool_path = project_data.project_root / 'tool' @@ -444,7 +446,8 @@ def fixture_plugin_group_data( Returns: The plugin configuration """ - project_data.project_root = tmp_path_factory.mktemp('workspace-') + workspace_path = tmp_path_factory.mktemp('workspace-') + project_data = ProjectData(project_root=workspace_path, verbosity=project_data.verbosity) # Install path is already pinned to a temp directory to share downloaded resources cppython_plugin_data.build_path = project_data.project_root / 'build' cppython_plugin_data.tool_path = project_data.project_root / 'tool' diff --git a/pyproject.toml b/pyproject.toml index 45c7ee60..420fa119 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,6 @@ strict = true [tool.ruff] line-length = 120 -preview = true [tool.ruff.lint] ignore = ["D206", "D300", "D415", "E111", "E114", "E117"] diff --git a/tests/unit/core/test_resolution.py b/tests/unit/core/test_resolution.py index 3a9b49f6..d7565753 100644 --- a/tests/unit/core/test_resolution.py +++ b/tests/unit/core/test_resolution.py @@ -104,7 +104,8 @@ def test_generator_resolve(project_configuration: ProjectConfiguration) -> None: cppython_local_configuration, cppython_global_configuration, project_data, plugin_build_data ) - MockGenerator = type('MockGenerator', (Generator,), {}) + class MockGenerator(Generator): + """Mock generator for testing""" cppython_plugin_data = resolve_cppython_plugin(cppython_data, MockGenerator) @@ -127,7 +128,8 @@ def test_provider_resolve(project_configuration: ProjectConfiguration) -> None: cppython_local_configuration, cppython_global_configuration, project_data, plugin_build_data ) - MockProvider = type('MockProvider', (Provider,), {}) + class MockProvider(Provider): + """Mock provider for testing""" cppython_plugin_data = resolve_cppython_plugin(cppython_data, MockProvider) @@ -150,7 +152,8 @@ def test_scm_resolve(project_configuration: ProjectConfiguration) -> None: cppython_local_configuration, cppython_global_configuration, project_data, plugin_build_data ) - MockSCM = type('MockSCM', (SCM,), {}) + class MockSCM(SCM): + """Mock SCM for testing""" cppython_plugin_data = resolve_cppython_plugin(cppython_data, MockSCM) diff --git a/tests/unit/plugins/cmake/test_presets.py b/tests/unit/plugins/cmake/test_presets.py index 4d5852a3..da0c2afb 100644 --- a/tests/unit/plugins/cmake/test_presets.py +++ b/tests/unit/plugins/cmake/test_presets.py @@ -22,7 +22,7 @@ def test_generate_root_preset_new(tmp_path: Path) -> None: result = builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data) assert result.configurePresets is not None assert any(p.name == 'test-configuration' for p in result.configurePresets) - + preset = next(p for p in result.configurePresets if p.name == 'test-configuration') assert preset.inherits == 'cppython' diff --git a/tests/unit/plugins/cmake/test_schema.py b/tests/unit/plugins/cmake/test_schema.py index 382f4f3e..765c9742 100644 --- a/tests/unit/plugins/cmake/test_schema.py +++ b/tests/unit/plugins/cmake/test_schema.py @@ -27,14 +27,6 @@ def test_cache_variable_null_type() -> None: assert var.type is None assert var.value == 'Unset' - @staticmethod - def test_cache_variable_type_enum_values() -> None: - """Tests the CacheVariable class with enum values""" - # Ensure all CMake types are present - expected = {'BOOL', 'PATH', 'FILEPATH', 'STRING', 'INTERNAL', 'STATIC', 'UNINITIALIZED'} - actual = {v.value for v in VariableType} - assert expected == actual - @staticmethod def test_cache_variable_bool_value_as_string() -> None: """Tests the CacheVariable class with a boolean value as a string""" From 8502bf444075b7a644eb3485fef2530ff7cd82cd Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 08:40:58 -0400 Subject: [PATCH 03/16] Update Project Files --- .gitignore | 8 ++++++++ .vscode/extensions.json | 4 ++-- .vscode/settings.json | 10 +++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index c140dc26..7a6f244d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,18 @@ TestResults/ *.pyc dist/ .vs/ +.venv/ + +#Pytest +.pytest_cache/ #PDM .pdm-python __pypackages__/ +.pdm-build/ + +#ruff +.ruff_cache/ #Coverage .coverage diff --git a/.vscode/extensions.json b/.vscode/extensions.json index bd12d5eb..aa989eed 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,8 @@ { "recommendations": [ - "ms-python.mypy-type-checker", "asciidoctor.asciidoctor-vscode", "charliermarsh.ruff", - "tamasfe.even-better-toml" + "tamasfe.even-better-toml", + "meta.pyrefly" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index e7cf253f..caac882e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,10 +5,14 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - }, "mypy-type-checker.reportingScope": "workspace", "mypy-type-checker.preferDaemon": true, "mypy-type-checker.importStrategy": "fromEnvironment", + "[python]": { + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + } } \ No newline at end of file From d936c2df1359e033cf2293b875f75d75c397b2c3 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 15:51:35 -0400 Subject: [PATCH 04/16] Template: Remove Toolchain Usage --- cppython/plugins/conan/builder.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 396e6103..957d1f17 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -137,10 +137,6 @@ class MyProject(ConanFile): def layout(self): cmake_layout(self) - def generate(self): - tc = CMakeToolchain(self) - tc.generate() - def build(self): cmake = CMake(self) cmake.configure() From 62cfd6d80cb1f3246ac59924ef1c9009aa00ffcc Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 10 Jul 2025 17:09:03 -0400 Subject: [PATCH 05/16] Update Chore --- pdm.lock | 969 ++++++++++++++++++++----------------------------- pyproject.toml | 39 +- 2 files changed, 407 insertions(+), 601 deletions(-) diff --git a/pdm.lock b/pdm.lock index febcc9bb..6ee69411 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "cmake", "conan", "git", "lint", "pdm", "pytest", "test"] -strategy = ["inherit_metadata"] +groups = ["default", "cmake", "conan", "git", "lint", "pdm", "pytest", "release", "test"] +strategy = [] lock_version = "4.5.0" -content_hash = "sha256:6e9ed604be2a15d13ab7b237c5a867e4e0c880b8b474a53afa34af3ede702751" +content_hash = "sha256:285d4e6ecf91477f083161753ad4b59416cfb56c5c5076bcc8028f7429129061" [[metadata.targets]] requires_python = ">=3.13" @@ -13,12 +13,7 @@ requires_python = ">=3.13" [[package]] name = "annotated-types" version = "0.7.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -groups = ["default"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] +summary = "" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -27,14 +22,10 @@ files = [ [[package]] name = "anyio" version = "4.9.0" -requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["pdm"] +summary = "" dependencies = [ - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.5; python_version < \"3.13\"", + "idna", + "sniffio", ] files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, @@ -44,9 +35,7 @@ files = [ [[package]] name = "blinker" version = "1.9.0" -requires_python = ">=3.9" -summary = "Fast, simple object-to-object and broadcast signaling" -groups = ["pdm"] +summary = "" files = [ {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, @@ -54,48 +43,41 @@ files = [ [[package]] name = "certifi" -version = "2025.1.31" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default", "conan", "pdm"] +version = "2025.7.9" +summary = "" files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39"}, + {file = "certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079"}, ] [[package]] name = "charset-normalizer" -version = "3.4.1" -requires_python = ">=3.7" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["default", "conan", "pdm"] -files = [ - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +version = "3.4.2" +summary = "" +files = [ + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] name = "click" version = "8.1.8" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -groups = ["default"] +summary = "" dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", + "colorama; sys_platform == \"win32\"", ] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, @@ -104,41 +86,34 @@ files = [ [[package]] name = "cmake" -version = "4.0.0" -requires_python = ">=3.7" -summary = "CMake is an open-source, cross-platform family of tools designed to build, test and package software" -groups = ["cmake"] -dependencies = [ - "importlib-metadata>=1.4; python_version < \"3.8\"", -] -files = [ - {file = "cmake-4.0.0-py3-none-macosx_10_10_universal2.whl", hash = "sha256:2f01499980f5824092c08e8fe0893c31d4f3783c8475ea2d7cd0706c7dc646a3"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2877636e057a8227b1792614bb0eebab37632c73bae3b4939b2b20416248cf76"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:26d0e992eea03d7ad8a2f320884f8eaf8178c3cf23f2f5f004cdca8354adf137"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9af3ef2931c84557d58383169cc3cad6852de625c1fd8883ee696ac436ab1eb3"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:340558bf3b50876380ac036d9e8a0e8c30ef28b071097cbceb3929519c021d4a"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9b7b1da83219d563cae0685c989d8aedf58480de1e64f3de2f51364606272f0"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f23eaade0cf683c938849962c09b3c752cf96cddc872288c80620466e6acf0ce"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dda13b113de7dba00f20587011c1b9b90708a22fe8fef530a46bfb4a4ee2bd2"}, - {file = "cmake-4.0.0-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:8606d0228529d9cb688fc8e4e31ae14236526fad586680aa3e15f6dd69c76488"}, - {file = "cmake-4.0.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:ba414b29459526bb10be13ecd38d022f0b5ebed2fec33bdae66d1568ddcf2e2e"}, - {file = "cmake-4.0.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:6a89cf41a770763a2132b32514dfd8000e1147ecec8cb5ad3d5d83041faea790"}, - {file = "cmake-4.0.0-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:02dd7305ed88d9c98930116fa66b327034e01080b6688886d478b099bd6bf7ba"}, - {file = "cmake-4.0.0-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:e1092881c07c5c892448aad04bdc36357804f37ad2ff42272fd1a940f679aa1a"}, - {file = "cmake-4.0.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:098ceee569eedc7853792f8b4fc6cc5b2995c1481b8bc13074aa565c2ebbac0d"}, - {file = "cmake-4.0.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4b2e75474ee412ca6f7f224b14a883206adc423daff1745752ce815cc5fbb599"}, - {file = "cmake-4.0.0-py3-none-win32.whl", hash = "sha256:e27776fbb5a101a9c8b71f9f360918d0985dfd7d1f057fa90713f8f2125e0e73"}, - {file = "cmake-4.0.0-py3-none-win_amd64.whl", hash = "sha256:a7ec8b997f75da5310c571f79ce560bc8941013549e47144bfa49025ced79b00"}, - {file = "cmake-4.0.0-py3-none-win_arm64.whl", hash = "sha256:31c4a1a3490f142c965de644e65987d1c76a2b90dec71a9a001c85bf535a5e58"}, - {file = "cmake-4.0.0.tar.gz", hash = "sha256:b929ab7d2ebd6b9a81018b0248ea75edb3dc4ad69367e8c75fb0f6774bb6e962"}, +version = "4.0.3" +summary = "" +files = [ + {file = "cmake-4.0.3-py3-none-macosx_10_10_universal2.whl", hash = "sha256:f2adfb459747025f40f9d3bdd1f3a485d43e866c0c4eb66373d1fcd666b13e4a"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04c40c92fdcaa96c66a5731b5b3fbbdf87da99cc68fdd30ff30b90c34d222986"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d41b83d061bcc375a7a5f2942ba523a7563368d296d91260f9d8a53a10f5e5e5"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:434f84fdf1e21578974876b8414dc47afeaea62027d9adc37a943a6bb08eb053"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beec48371a4b906fe398758ded5df57fc16e9bb14fd34244d9d66ee35862fb9f"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47dc28bee6cfb4de00c7cf7e87d565b5c86eb4088da81b60a49e214fcdd4ffda"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10fdc972b3211915b65cc89e8cd24e1a26c9bd684ee71c3f369fb488f2c4388"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d840e780c48c5df1330879d50615176896e8e6eee554507d21ce8e2f1a5f0ff8"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:6ef63bbabcbe3b89c1d80547913b6caceaad57987a27e7afc79ebc88ecd829e4"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:67103f2bcce8f57b8705ba8e353f18fdc3684a346eee97dc5f94d11575a424c6"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:880a1e1ae26d440d7e4f604fecbf839728ca7b096c870f2e7359855cc4828532"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:c403b660bbff1fd4d7f1c5d9e015ea27566e49ca9461e260c9758f2fd4e5e813"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:2a66ecdd4c3238484cb0c377d689c086a9b8b533e25329f73d21bd1c38f1ae86"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:004e58b1a1a384c2ca799c9c41ac4ed86ac3b80129462992c43c1121f8729ffd"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:133dbc33f995cb97a4456d83d67fa0a7a798f53f979454359140588baa928f43"}, + {file = "cmake-4.0.3-py3-none-win32.whl", hash = "sha256:3e07bdd14e69ea67d1e67a4f5225ac2fd91ee9e349c440143cdddd7368be1f46"}, + {file = "cmake-4.0.3-py3-none-win_amd64.whl", hash = "sha256:9a349ff2b4a7c63c896061676bc0f4e6994f373d54314d79ba3608ee7fa75442"}, + {file = "cmake-4.0.3-py3-none-win_arm64.whl", hash = "sha256:94a52e67b264a51089907c9e74ca5a9e2f3e65c57c457e0f40f02629a0de74d8"}, + {file = "cmake-4.0.3.tar.gz", hash = "sha256:215732f09ea8a7088fe1ab46bbd61669437217278d709fd3851bf8211e8c59e3"}, ] [[package]] name = "colorama" version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -groups = ["default", "conan", "pytest", "test"] +summary = "" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -146,111 +121,70 @@ files = [ [[package]] name = "conan" -version = "2.15.1" -requires_python = ">=3.6" -summary = "Conan C/C++ package manager" -groups = ["conan"] -dependencies = [ - "Jinja2<4.0.0,>=3.0", - "PyYAML<7.0,>=6.0", - "colorama<0.5.0,>=0.4.3", - "distro<=1.8.0,>=1.4.0; platform_system == \"Linux\" or platform_system == \"FreeBSD\"", - "fasteners>=0.15", - "patch-ng<1.19,>=1.18.0", - "python-dateutil<3,>=2.8.0", - "requests<3.0.0,>=2.25", - "urllib3<2.1,>=1.26.6", +version = "2.18.1" +summary = "" +dependencies = [ + "colorama", + "distro; platform_system == \"FreeBSD\" or sys_platform == \"linux\"", + "fasteners", + "jinja2", + "patch-ng", + "python-dateutil", + "pyyaml", + "requests", + "urllib3", ] files = [ - {file = "conan-2.15.1.tar.gz", hash = "sha256:c4114e197f7908409766ad16cea758f088ebc926f8426212b2a6a62829f996a3"}, + {file = "conan-2.18.1.tar.gz", hash = "sha256:5d8e9fac7614de9297933f65de8f17db14851a871cebc962f4856b7c294f43c5"}, ] [[package]] name = "coverage" -version = "7.8.0" -requires_python = ">=3.9" -summary = "Code coverage measurement for Python" -groups = ["test"] -files = [ - {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, - {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, - {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, - {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, - {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, - {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, - {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, -] - -[[package]] -name = "coverage" -version = "7.8.0" -extras = ["toml"] -requires_python = ">=3.9" -summary = "Code coverage measurement for Python" -groups = ["test"] -dependencies = [ - "coverage==7.8.0", - "tomli; python_full_version <= \"3.11.0a6\"", -] -files = [ - {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, - {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, - {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, - {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, - {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, - {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, - {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, +version = "7.9.2" +summary = "" +files = [ + {file = "coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038"}, + {file = "coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868"}, + {file = "coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a"}, + {file = "coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b"}, + {file = "coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694"}, + {file = "coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5"}, + {file = "coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac"}, + {file = "coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926"}, + {file = "coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd"}, + {file = "coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb"}, + {file = "coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4"}, + {file = "coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b"}, ] [[package]] name = "dep-logic" -version = "0.5.0" -requires_python = ">=3.8" -summary = "Python dependency specifications supporting logical operations" -groups = ["pdm"] +version = "0.5.1" +summary = "" dependencies = [ - "packaging>=22", + "packaging", ] files = [ - {file = "dep_logic-0.5.0-py3-none-any.whl", hash = "sha256:f16a73ec5baf1f126e253f6a6249c80999818e608f84677736591ac623c516a6"}, - {file = "dep_logic-0.5.0.tar.gz", hash = "sha256:be92e772f15d2563edd6b8694a6818846ad2822310dfc8f39cd20ebb0b03e329"}, + {file = "dep_logic-0.5.1-py3-none-any.whl", hash = "sha256:6073e955945a0224440465f9382f4bf5b4be4c630c6f412bf9506639c13a3d22"}, + {file = "dep_logic-0.5.1.tar.gz", hash = "sha256:cfd10877277d3cbb6e66fd48f316ba6c284701af0e67d52eaaf10275753354a7"}, ] [[package]] name = "distlib" version = "0.3.9" -summary = "Distribution utilities" -groups = ["pdm"] +summary = "" files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -259,10 +193,7 @@ files = [ [[package]] name = "distro" version = "1.8.0" -requires_python = ">=3.6" -summary = "Distro - an OS platform information API" -groups = ["conan"] -marker = "platform_system == \"Linux\" or platform_system == \"FreeBSD\"" +summary = "" files = [ {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, @@ -270,30 +201,25 @@ files = [ [[package]] name = "dulwich" -version = "0.22.8" -requires_python = ">=3.9" -summary = "Python Git Library" -groups = ["git"] +version = "0.23.2" +summary = "" dependencies = [ - "urllib3>=1.25", + "urllib3", ] files = [ - {file = "dulwich-0.22.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbade3342376be1cd2409539fe1b901d2d57a531106bbae204da921ef4456a74"}, - {file = "dulwich-0.22.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71420ffb6deebc59b2ce875e63d814509f9c1dc89c76db962d547aebf15670c7"}, - {file = "dulwich-0.22.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a626adbfac44646a125618266a24133763bdc992bf8bd0702910d67e6b994443"}, - {file = "dulwich-0.22.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f1476c9c4e4ede95714d06c4831883a26680e37b040b8b6230f506e5ba39f51"}, - {file = "dulwich-0.22.8-cp313-cp313-win32.whl", hash = "sha256:b2b31913932bb5bd41658dd398b33b1a2d4d34825123ad54e40912cfdfe60003"}, - {file = "dulwich-0.22.8-cp313-cp313-win_amd64.whl", hash = "sha256:7a44e5a61a7989aca1e301d39cfb62ad2f8853368682f524d6e878b4115d823d"}, - {file = "dulwich-0.22.8-py3-none-any.whl", hash = "sha256:ffc7a02e62b72884de58baaa3b898b7f6427893e79b1289ffa075092efe59181"}, - {file = "dulwich-0.22.8.tar.gz", hash = "sha256:701547310415de300269331abe29cb5717aa1ea377af826bf513d0adfb1c209b"}, + {file = "dulwich-0.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e44dec7e36bc035da0ec3df6c1564810699e319ba41b71d17750dd7452e1b2fc"}, + {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:398ba1c0e1581071cdcb38a681e0ff1e046aa8f31bad3bc368266f499c4ddf9e"}, + {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:432c6eeac5edf97ff7090fbac7cda708167ee90e5afa78652d252e87e397f425"}, + {file = "dulwich-0.23.2-cp313-cp313-win32.whl", hash = "sha256:8555980e8509d7f76e80de58d1eb7bd2c1c317942b7a3c9c113d81dfc287f4c0"}, + {file = "dulwich-0.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:2b042dca31de4d4a0e88e4dbe20afe804a640c8882eec0de5093bffb34b75370"}, + {file = "dulwich-0.23.2-py3-none-any.whl", hash = "sha256:0b0439d309cf808f7955f74776981d9ac9dc1ec715aa39798de9b22bb95ac163"}, + {file = "dulwich-0.23.2.tar.gz", hash = "sha256:a152ebb0e95bc0f23768be563f80ff1e719bf5c4f5c2696be4fa8ab625a39879"}, ] [[package]] name = "fasteners" version = "0.19" -requires_python = ">=3.6" -summary = "A python package that provides useful locks" -groups = ["conan"] +summary = "" files = [ {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, @@ -302,9 +228,7 @@ files = [ [[package]] name = "filelock" version = "3.18.0" -requires_python = ">=3.9" -summary = "A platform independent file lock." -groups = ["pdm"] +summary = "" files = [ {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, @@ -312,71 +236,59 @@ files = [ [[package]] name = "findpython" -version = "0.6.3" -requires_python = ">=3.8" -summary = "A utility to find python versions on your system" -groups = ["pdm"] +version = "0.7.0" +summary = "" dependencies = [ - "packaging>=20", + "packaging", + "platformdirs", ] files = [ - {file = "findpython-0.6.3-py3-none-any.whl", hash = "sha256:a85bb589b559cdf1b87227cc233736eb7cad894b9e68021ee498850611939ebc"}, - {file = "findpython-0.6.3.tar.gz", hash = "sha256:5863ea55556d8aadc693481a14ac4f3624952719efc1c5591abb0b4a9e965c94"}, + {file = "findpython-0.7.0-py3-none-any.whl", hash = "sha256:f53cfcc29536f5b83c962cf922bba8ff6d6a3c2a05fda6a45aa58a47d005d8fc"}, + {file = "findpython-0.7.0.tar.gz", hash = "sha256:8b31647c76352779a3c1a0806699b68e6a7bdc0b5c2ddd9af2a07a0d40c673dc"}, ] [[package]] name = "h11" -version = "0.14.0" -requires_python = ">=3.7" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -groups = ["pdm"] -dependencies = [ - "typing-extensions; python_version < \"3.8\"", -] +version = "0.16.0" +summary = "" files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] name = "hishel" -version = "0.1.2" -requires_python = ">=3.9" -summary = "Persistent cache implementation for httpx and httpcore" -groups = ["pdm"] +version = "0.1.3" +summary = "" dependencies = [ - "httpx>=0.28.0", + "httpx", ] files = [ - {file = "hishel-0.1.2-py3-none-any.whl", hash = "sha256:802b4e446017f4867efdb26d3417670991ad1b4826d24331110871fe8957b5d0"}, - {file = "hishel-0.1.2.tar.gz", hash = "sha256:6643450bfb1cfa2ecd6002769f6f5069d0d048c9c1f1e29a98a48302d5875092"}, + {file = "hishel-0.1.3-py3-none-any.whl", hash = "sha256:bae3ba9970ffc56f90014aea2b3019158fb0a5b0b635a56f414ba6b96651966e"}, + {file = "hishel-0.1.3.tar.gz", hash = "sha256:db3e07429cb739dcda851ff9b35b0f3e7589e21b90ee167df54336ac608b6ec3"}, ] [[package]] name = "httpcore" -version = "1.0.8" -requires_python = ">=3.8" -summary = "A minimal low-level HTTP client." -groups = ["pdm"] +version = "1.0.9" +summary = "" dependencies = [ "certifi", - "h11<0.15,>=0.13", + "h11", ] files = [ - {file = "httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be"}, - {file = "httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [[package]] name = "httpx" version = "0.28.1" -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["pdm"] +summary = "" dependencies = [ "anyio", "certifi", - "httpcore==1.*", + "httpcore", "idna", ] files = [ @@ -388,12 +300,10 @@ files = [ name = "httpx" version = "0.28.1" extras = ["socks"] -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["pdm"] +summary = "" dependencies = [ "httpx==0.28.1", - "socksio==1.*", + "socksio", ] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, @@ -403,9 +313,7 @@ files = [ [[package]] name = "id" version = "1.5.0" -requires_python = ">=3.8" -summary = "A tool for generating OIDC identities" -groups = ["pdm"] +summary = "" dependencies = [ "requests", ] @@ -417,9 +325,7 @@ files = [ [[package]] name = "idna" version = "3.10" -requires_python = ">=3.6" -summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["default", "conan", "pdm"] +summary = "" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -428,9 +334,7 @@ files = [ [[package]] name = "iniconfig" version = "2.1.0" -requires_python = ">=3.8" -summary = "brain-dead simple config-ini parsing" -groups = ["pytest", "test"] +summary = "" files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -439,9 +343,7 @@ files = [ [[package]] name = "installer" version = "0.7.0" -requires_python = ">=3.7" -summary = "A library for installing Python wheels." -groups = ["pdm"] +summary = "" files = [ {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, @@ -450,11 +352,9 @@ files = [ [[package]] name = "jinja2" version = "3.1.6" -requires_python = ">=3.7" -summary = "A very fast and expressive template engine." -groups = ["conan"] +summary = "" dependencies = [ - "MarkupSafe>=2.0", + "markupsafe", ] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, @@ -463,32 +363,41 @@ files = [ [[package]] name = "libcst" -version = "1.7.0" -requires_python = ">=3.9" -summary = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." -groups = ["conan"] -dependencies = [ - "pyyaml>=5.2", -] -files = [ - {file = "libcst-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93417d36c2a1b70d651d0e970ff73339e8dcd64d341672b68823fa0039665022"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6523731bfbdbc045ff8649130fe14a46b31ad6925f67acdc0e0d80a0c61719fd"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a252fa03ea00986f03100379f11e15d381103a09667900fb0fa2076cec19081a"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a5530b40a15dbe6fac842ef2ad87ad561760779380ccf3ade6850854d81406"}, - {file = "libcst-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0456381c939169c4f11caecdb30f7aca6f234640731f8f965849c1631930536b"}, - {file = "libcst-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8d6176a667d2db0132d133dad6bbf965f915f3071559342ca2cdbbec537ed12"}, - {file = "libcst-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:6137fe549bfbb017283c3cf85419eb0dfaa20a211ad6d525538a2494e248a84b"}, - {file = "libcst-1.7.0.tar.gz", hash = "sha256:a63f44ffa81292f183656234c7f2848653ff45c17d867db83c9335119e28aafa"}, +version = "1.8.2" +summary = "" +dependencies = [ + "pyyaml-ft", +] +files = [ + {file = "libcst-1.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08e9dca4ab6f8551794ce7ec146f86def6a82da41750cbed2c07551345fa10d3"}, + {file = "libcst-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8310521f2ccb79b5c4345750d475b88afa37bad930ab5554735f85ad5e3add30"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:da2d8b008aff72acd5a4a588491abdda1b446f17508e700f26df9be80d8442ae"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:be821d874ce8b26cbadd7277fa251a9b37f6d2326f8b5682b6fc8966b50a3a59"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f74b0bc7378ad5afcf25ac9d0367b4dbba50f6f6468faa41f5dfddcf8bf9c0f8"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b68ea4a6018abfea1f68d50f74de7d399172684c264eb09809023e2c8696fc23"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e264307ec49b2c72480422abafe80457f90b4e6e693b7ddf8a23d24b5c24001"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5d5519962ce7c72d81888fb0c09e58e308ba4c376e76bcd853b48151063d6a8"}, + {file = "libcst-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:b62aa11d6b74ed5545e58ac613d3f63095e5fd0254b3e0d1168fda991b9a6b41"}, + {file = "libcst-1.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9c2bd4ac288a9cdb7ffc3229a9ce8027a66a3fd3f2ab9e13da60f5fbfe91f3b2"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:08a8c7d9922ca6eed24e2c13a3c552b3c186af8fc78e5d4820b58487d780ec19"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bba7c2b5063e8ada5a5477f9fa0c01710645426b5a8628ec50d558542a0a292e"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d97c9fe13aacfbefded6861f5200dcb8e837da7391a9bdeb44ccb133705990af"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d2194ae959630aae4176a4b75bd320b3274c20bef2a5ca6b8d6fc96d3c608edf"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0be639f5b2e1999a4b4a82a0f4633969f97336f052d0c131627983589af52f56"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6753e50904e05c27915933da41518ecd7a8ca4dd3602112ba44920c6e353a455"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:706d07106af91c343150be86caeae1ea3851b74aa0730fcbbf8cd089e817f818"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd4310ea8ddc49cc8872e083737cf806299b17f93159a1f354d59aa08993e876"}, + {file = "libcst-1.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:51bbafdd847529e8a16d1965814ed17831af61452ee31943c414cb23451de926"}, + {file = "libcst-1.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:4f14f5045766646ed9e8826b959c6d07194788babed1e0ba08c94ea4f39517e3"}, + {file = "libcst-1.8.2.tar.gz", hash = "sha256:66e82cedba95a6176194a817be4232c720312f8be6d2c8f3847f3317d95a0c7f"}, ] [[package]] name = "markdown-it-py" version = "3.0.0" -requires_python = ">=3.8" -summary = "Python port of markdown-it. Markdown parsing, done right!" -groups = ["default", "pdm"] +summary = "" dependencies = [ - "mdurl~=0.1", + "mdurl", ] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, @@ -498,9 +407,7 @@ files = [ [[package]] name = "markupsafe" version = "3.0.2" -requires_python = ">=3.9" -summary = "Safely add untrusted strings to HTML/XML markup." -groups = ["conan"] +summary = "" files = [ {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, @@ -528,285 +435,214 @@ files = [ [[package]] name = "mdurl" version = "0.1.2" -requires_python = ">=3.7" -summary = "Markdown URL utilities" -groups = ["default", "pdm"] +summary = "" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[[package]] -name = "msgpack" -version = "1.1.0" -requires_python = ">=3.8" -summary = "MessagePack serializer" -groups = ["pdm"] -files = [ - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, -] - -[[package]] -name = "mypy" -version = "1.15.0" -requires_python = ">=3.9" -summary = "Optional static typing for Python" -groups = ["lint"] -dependencies = [ - "mypy-extensions>=1.0.0", - "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.6.0", -] -files = [ - {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, - {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, - {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, - {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, - {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" -summary = "Type system extensions for programs checked with the mypy type checker." -groups = ["lint"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "packaging" -version = "24.2" -requires_python = ">=3.8" -summary = "Core utilities for Python packages" -groups = ["default", "pdm", "pytest", "test"] +version = "25.0" +summary = "" files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "patch-ng" version = "1.18.1" -requires_python = ">=3.6" -summary = "Library to parse and apply unified diffs." -groups = ["conan"] +summary = "" files = [ {file = "patch-ng-1.18.1.tar.gz", hash = "sha256:52fd46ee46f6c8667692682c1fd7134edc65a2d2d084ebec1d295a6087fc0291"}, ] [[package]] name = "pbs-installer" -version = "2025.4.9" -requires_python = ">=3.8" -summary = "Installer for Python Build Standalone" -groups = ["pdm"] +version = "2025.7.12" +summary = "" files = [ - {file = "pbs_installer-2025.4.9-py3-none-any.whl", hash = "sha256:af110b398248584422f46760ce1e3793622fe3fbcde47aacd22e35baf8c3db1d"}, - {file = "pbs_installer-2025.4.9.tar.gz", hash = "sha256:15755bc94769a544af5dda155f973c70caf76f0e70b21f3c8a8ed506f102f88f"}, + {file = "pbs_installer-2025.7.12-py3-none-any.whl", hash = "sha256:d73414224fceb60d4a07bea97facd9acc05de792dd7becc90a7f22383e7c1cab"}, + {file = "pbs_installer-2025.7.12.tar.gz", hash = "sha256:343b8905e1da3cd4b03b68d630086330dde1814294963b77d2664b18b5002ac6"}, ] [[package]] name = "pdm" -version = "2.23.1" -requires_python = ">=3.9" -summary = "A modern Python package and dependency manager supporting the latest PEP standards" -groups = ["pdm"] +version = "2.25.4" +summary = "" dependencies = [ "blinker", - "certifi>=2024.8.30", - "dep-logic>=0.4.4", - "filelock>=3.13", - "findpython<1.0.0a0,>=0.6.0", - "hishel>=0.0.32", - "httpcore>=1.0.6", - "httpx[socks]<1,>0.20", - "id>=1.5.0", - "importlib-metadata>=3.6; python_version < \"3.10\"", - "installer<0.8,>=0.7", - "msgpack>=1.0", - "packaging!=22.0,>=20.9", - "pbs-installer>=2024.4.18", + "certifi", + "dep-logic", + "filelock", + "findpython", + "hishel", + "httpcore", + "httpx[socks]", + "id", + "installer", + "packaging", + "pbs-installer", "platformdirs", "pyproject-hooks", - "python-dotenv>=0.15", - "resolvelib>=1.1", - "rich>=12.3.0", - "shellingham>=1.3.2", - "tomli>=1.1.0; python_version < \"3.11\"", - "tomlkit<1,>=0.11.1", - "truststore>=0.9; python_version >= \"3.10\"", - "unearth>=0.17.5", - "virtualenv>=20", + "python-dotenv", + "resolvelib", + "rich", + "shellingham", + "tomlkit", + "truststore", + "unearth", + "virtualenv", ] files = [ - {file = "pdm-2.23.1-py3-none-any.whl", hash = "sha256:202c217eef140f7fe933f2025cc2a7a7cc0638abe43c51c4a74d3c1aa686d6f7"}, - {file = "pdm-2.23.1.tar.gz", hash = "sha256:cf12cd2201df78a6543a5485ac1d7015a73b3a6b10c98b66dfae6ebd481386fe"}, + {file = "pdm-2.25.4-py3-none-any.whl", hash = "sha256:3efab7367cb5d9d6e4ef9db6130e4f5620c247343c8e95e18bd0d76b201ff7da"}, + {file = "pdm-2.25.4.tar.gz", hash = "sha256:bd655d789429928d6e27ff6693c19c82bc81aa75ba51d7b1c6102d039c8f211c"}, ] [[package]] name = "platformdirs" -version = "4.3.7" -requires_python = ">=3.9" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["pdm"] +version = "4.3.8" +summary = "" files = [ - {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, - {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [[package]] name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" -groups = ["pytest", "test"] +version = "1.6.0" +summary = "" files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [[package]] name = "pydantic" -version = "2.11.3" -requires_python = ">=3.9" -summary = "Data validation using Python type hints" -groups = ["default"] +version = "2.11.7" +summary = "" dependencies = [ - "annotated-types>=0.6.0", - "pydantic-core==2.33.1", - "typing-extensions>=4.12.2", - "typing-inspection>=0.4.0", + "annotated-types", + "pydantic-core", + "typing-extensions", + "typing-inspection", ] files = [ - {file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"}, - {file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [[package]] name = "pydantic-core" -version = "2.33.1" -requires_python = ">=3.9" -summary = "Core functionality for Pydantic validation and serialization" -groups = ["default"] +version = "2.33.2" +summary = "" dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a"}, - {file = "pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18"}, - {file = "pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df"}, + "typing-extensions", +] +files = [ + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [[package]] name = "pygments" -version = "2.19.1" -requires_python = ">=3.8" -summary = "Pygments is a syntax highlighting package written in Python." -groups = ["default", "pdm"] +version = "2.19.2" +summary = "" files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [[package]] name = "pyproject-hooks" version = "1.2.0" -requires_python = ">=3.7" -summary = "Wrappers to call pyproject.toml-based build backend hooks." -groups = ["pdm"] +summary = "" files = [ {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, ] +[[package]] +name = "pyrefly" +version = "0.23.1" +summary = "" +files = [ + {file = "pyrefly-0.23.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a25506700f179004438221aa50aa107f70dc52d08ee538150ef1c3789544f921"}, + {file = "pyrefly-0.23.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5307f3184b69effbb867be07c09a0181347b76b1723f3ed246030fb253dde4f2"}, + {file = "pyrefly-0.23.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f59e2c0cf65d1f10e9a0b9c7de63c677a17c3634d60bfa3a3426cd0184e73b4"}, + {file = "pyrefly-0.23.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2cd03be65aa0b527e29855a42354641f612885587cb40e6ae8bb91b739a4fca6"}, + {file = "pyrefly-0.23.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d841cb1cd5164407482cb30b09fc25ceaa113809d7715e454dd3cd47faf140"}, + {file = "pyrefly-0.23.1-py3-none-win32.whl", hash = "sha256:b077ba0c832a3994e5f457066bd391eaecbfd2332e15beb4dd42658e39371d93"}, + {file = "pyrefly-0.23.1-py3-none-win_amd64.whl", hash = "sha256:c6f621d22e528904b9253bd378b45e475f2b4e0e43bf85088654383ec42b98c8"}, + {file = "pyrefly-0.23.1-py3-none-win_arm64.whl", hash = "sha256:e567ad4e1001040cfca7418b1bc2ec21c4c6f96fd1102e848ad8ced0bd5dcdb7"}, + {file = "pyrefly-0.23.1.tar.gz", hash = "sha256:7032d97dfdf885e8309e9d78bd70b332649544e1f36905082f703e133f575aaa"}, +] + [[package]] name = "pytest" -version = "8.3.5" -requires_python = ">=3.8" -summary = "pytest: simple powerful testing with Python" -groups = ["pytest", "test"] +version = "8.4.1" +summary = "" dependencies = [ "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", "iniconfig", "packaging", - "pluggy<2,>=1.5", - "tomli>=1; python_version < \"3.11\"", + "pluggy", + "pygments", ] files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [[package]] name = "pytest-cov" -version = "6.1.1" -requires_python = ">=3.9" -summary = "Pytest plugin for measuring coverage." -groups = ["test"] +version = "6.2.1" +summary = "" dependencies = [ - "coverage[toml]>=7.5", - "pytest>=4.6", + "coverage", + "pluggy", + "pytest", ] files = [ - {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, - {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [[package]] name = "pytest-mock" -version = "3.14.0" -requires_python = ">=3.8" -summary = "Thin-wrapper around the mock package for easier use with pytest" -groups = ["pytest", "test"] +version = "3.14.1" +summary = "" dependencies = [ - "pytest>=6.2.5", + "pytest", ] files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, + {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, + {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["conan"] +summary = "" dependencies = [ - "six>=1.5", + "six", ] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, @@ -815,21 +651,17 @@ files = [ [[package]] name = "python-dotenv" -version = "1.1.0" -requires_python = ">=3.9" -summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["pdm"] +version = "1.1.1" +summary = "" files = [ - {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, - {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, ] [[package]] name = "pyyaml" version = "6.0.2" -requires_python = ">=3.8" -summary = "YAML parser and emitter for Python" -groups = ["conan"] +summary = "" files = [ {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, @@ -843,44 +675,61 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +summary = "" +files = [ + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, + {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +] + [[package]] name = "requests" -version = "2.32.3" -requires_python = ">=3.8" -summary = "Python HTTP for Humans." -groups = ["default", "conan", "pdm"] +version = "2.32.4" +summary = "" dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", + "certifi", + "charset-normalizer", + "idna", + "urllib3", ] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [[package]] name = "resolvelib" -version = "1.1.0" -requires_python = ">=3.7" -summary = "Resolve abstract dependencies into concrete ones" -groups = ["pdm"] +version = "1.2.0" +summary = "" files = [ - {file = "resolvelib-1.1.0-py2.py3-none-any.whl", hash = "sha256:f80de38ae744bcf4e918e27a681a5c6cb63a08d9a926c0989c0730bcdd089049"}, - {file = "resolvelib-1.1.0.tar.gz", hash = "sha256:b68591ef748f58c1e2a2ac28d0961b3586ae8b25f60b0ba9a5e4f3d87c1d6a79"}, + {file = "resolvelib-1.2.0-py3-none-any.whl", hash = "sha256:8e3e2000beaf53fdfd8772fda1a7b1df97e803ab7c8925621bbb87c4d187a94d"}, + {file = "resolvelib-1.2.0.tar.gz", hash = "sha256:c27fbb5098acd7dfc01fb2be3724bd0881168edc2bd3b4dc876ca3f46b8e4a3d"}, ] [[package]] name = "rich" version = "14.0.0" -requires_python = ">=3.8.0" -summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -groups = ["default", "pdm"] +summary = "" dependencies = [ - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", + "markdown-it-py", + "pygments", ] files = [ {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, @@ -889,37 +738,33 @@ files = [ [[package]] name = "ruff" -version = "0.11.5" -requires_python = ">=3.7" -summary = "An extremely fast Python linter and code formatter, written in Rust." -groups = ["lint"] -files = [ - {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, - {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, - {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, - {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, - {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, - {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, - {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, +version = "0.12.3" +summary = "" +files = [ + {file = "ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2"}, + {file = "ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041"}, + {file = "ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f"}, + {file = "ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d"}, + {file = "ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7"}, + {file = "ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1"}, + {file = "ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77"}, ] [[package]] name = "shellingham" version = "1.5.4" -requires_python = ">=3.7" -summary = "Tool to Detect Surrounding Shell" -groups = ["default", "pdm"] +summary = "" files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, @@ -928,9 +773,7 @@ files = [ [[package]] name = "six" version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["conan"] +summary = "" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -939,9 +782,7 @@ files = [ [[package]] name = "sniffio" version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["pdm"] +summary = "" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -950,9 +791,7 @@ files = [ [[package]] name = "socksio" version = "1.0.0" -requires_python = ">=3.6" -summary = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." -groups = ["pdm"] +summary = "" files = [ {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, @@ -960,22 +799,17 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.2" -requires_python = ">=3.8" -summary = "Style preserving TOML library" -groups = ["pdm"] +version = "0.13.3" +summary = "" files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] [[package]] name = "truststore" version = "0.10.1" -requires_python = ">=3.10" -summary = "Verify certificates using native system trust stores" -groups = ["pdm"] -marker = "python_version >= \"3.10\"" +summary = "" files = [ {file = "truststore-0.10.1-py3-none-any.whl", hash = "sha256:b64e6025a409a43ebdd2807b0c41c8bff49ea7ae6550b5087ac6df6619352d4c"}, {file = "truststore-0.10.1.tar.gz", hash = "sha256:eda021616b59021812e800fa0a071e51b266721bef3ce092db8a699e21c63539"}, @@ -983,69 +817,59 @@ files = [ [[package]] name = "typer" -version = "0.15.2" -requires_python = ">=3.7" -summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." -groups = ["default"] +version = "0.16.0" +summary = "" dependencies = [ - "click>=8.0.0", - "rich>=10.11.0", - "shellingham>=1.3.0", - "typing-extensions>=3.7.4.3", + "click", + "rich", + "shellingham", + "typing-extensions", ] files = [ - {file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"}, - {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"}, + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, ] [[package]] name = "types-requests" -version = "2.32.0.20250328" -requires_python = ">=3.9" -summary = "Typing stubs for requests" -groups = ["default"] +version = "2.32.4.20250611" +summary = "" dependencies = [ - "urllib3>=2", + "urllib3", ] files = [ - {file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"}, - {file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"}, + {file = "types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072"}, + {file = "types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826"}, ] [[package]] name = "typing-extensions" -version = "4.13.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["default", "lint"] +version = "4.14.1" +summary = "" files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] [[package]] name = "typing-inspection" -version = "0.4.0" -requires_python = ">=3.9" -summary = "Runtime typing introspection tools" -groups = ["default"] +version = "0.4.1" +summary = "" dependencies = [ - "typing-extensions>=4.12.0", + "typing-extensions", ] files = [ - {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, - {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, ] [[package]] name = "unearth" version = "0.17.5" -requires_python = ">=3.8" -summary = "A utility to fetch and download python packages" -groups = ["pdm"] +summary = "" dependencies = [ - "httpx<1,>=0.27.0", - "packaging>=20", + "httpx", + "packaging", ] files = [ {file = "unearth-0.17.5-py3-none-any.whl", hash = "sha256:9963e66b14f0484644c9b45b517e530befb2de6a8da4b06a9a38bed2d086dfe6"}, @@ -1055,9 +879,7 @@ files = [ [[package]] name = "urllib3" version = "2.0.7" -requires_python = ">=3.7" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["default", "conan", "git", "pdm"] +summary = "" files = [ {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, @@ -1065,17 +887,14 @@ files = [ [[package]] name = "virtualenv" -version = "20.30.0" -requires_python = ">=3.8" -summary = "Virtual Python Environment builder" -groups = ["pdm"] +version = "20.31.2" +summary = "" dependencies = [ - "distlib<1,>=0.3.7", - "filelock<4,>=3.12.2", - "importlib-metadata>=6.6; python_version < \"3.8\"", - "platformdirs<5,>=3.9.1", + "distlib", + "filelock", + "platformdirs", ] files = [ - {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, - {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, + {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, + {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, ] diff --git a/pyproject.toml b/pyproject.toml index 420fa119..0d0cc422 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,23 +12,19 @@ dynamic = ["version"] requires-python = ">=3.13" dependencies = [ - "typer>=0.15.2", - "pydantic>=2.11.3", - "packaging>=24.2", - "requests>=2.32.3", - "types-requests>=2.32.0.20250328", + "typer>=0.16.0", + "pydantic>=2.11.7", + "packaging>=25.0", + "requests>=2.32.4", + "types-requests>=2.32.4.20250611", ] [project.optional-dependencies] -pytest = ["pytest>=8.3.5", "pytest-mock>=3.14.0"] - -git = ["dulwich>=0.22.8"] - -pdm = ["pdm>=2.23.1"] - -cmake = ["cmake>=4.0.0"] - -conan = ["conan>=2.15.1", "libcst>=1.7.0"] +pytest = ["pytest>=8.4.1", "pytest-mock>=3.14.1"] +git = ["dulwich>=0.23.2"] +pdm = ["pdm>=2.25.4"] +cmake = ["cmake>=4.0.3"] +conan = ["conan>=2.18.1", "libcst>=1.8.2"] [project.urls] homepage = "https://github.com/Synodic-Software/CPPython" @@ -51,8 +47,8 @@ cppython = "cppython.plugins.pdm.plugin:CPPythonPlugin" cppython = "cppython.test.pytest.fixtures" [dependency-groups] -lint = ["ruff>=0.11.5", "mypy>=1.15.0"] -test = ["pytest>=8.3.5", "pytest-cov>=6.1.1", "pytest-mock>=3.14.0"] +lint = ["ruff>=0.12.3", "pyrefly>=0.23.1"] +test = ["pytest>=8.4.1", "pytest-cov>=6.2.1", "pytest-mock>=3.14.1"] [project.scripts] cppython = "cppython.console.entry:app" @@ -62,11 +58,6 @@ addopts = ["--color=yes"] log_cli = true testpaths = ["tests"] -[tool.mypy] -exclude = "__pypackages__" -plugins = ["pydantic.mypy"] -strict = true - [tool.ruff] line-length = 120 @@ -98,10 +89,6 @@ skip_empty = true [tool.pdm] plugins = ["-e file:///${PROJECT_ROOT}"] -[tool.pdm.options] -install = ["-G:all"] -update = ["--update-all"] - [tool.pdm.version] source = "scm" @@ -110,7 +97,7 @@ analyze = { shell = "ruff check cppython tests" } format = { shell = "ruff format" } lint = { composite = ["analyze", "format", "type-check"] } test = { shell = "pytest --cov=cppython --verbose tests" } -type-check = { shell = "mypy ." } +type-check = { shell = "pyrefly check" } [build-system] build-backend = "pdm.backend" From 953006689a81cf1e6df29e4b853b7d3bf401a0e2 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 11 Jul 2025 15:39:25 -0400 Subject: [PATCH 06/16] Fix and Organize Temp Dir Tests --- cppython/plugins/cmake/builder.py | 13 +++-- cppython/plugins/cmake/plugin.py | 4 +- cppython/plugins/cmake/schema.py | 4 ++ cppython/plugins/vcpkg/plugin.py | 14 ++++-- cppython/test/pytest/base_classes.py | 33 +++--------- cppython/test/pytest/classes.py | 6 +-- cppython/test/pytest/fixtures.py | 11 ++-- .../integration/examples/test_conan_cmake.py | 4 +- tests/unit/core/test_resolution.py | 5 -- tests/unit/plugins/cmake/test_presets.py | 50 ++++++++++++------- tests/unit/test_data.py | 1 - 11 files changed, 74 insertions(+), 71 deletions(-) diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index 2df251ba..55454be5 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -114,13 +114,16 @@ def write_cppython_preset( return cppython_preset_file @staticmethod - def generate_root_preset(preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData) -> CMakePresets: + def generate_root_preset( + preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData, build_directory: Path + ) -> CMakePresets: """Generates the top level root preset with the include reference. Args: preset_file: Preset file to modify cppython_preset_file: Path to the cppython preset file to include cmake_data: The CMake data to use + build_directory: The build directory to use Returns: A CMakePresets object @@ -128,6 +131,7 @@ def generate_root_preset(preset_file: Path, cppython_preset_file: Path, cmake_da default_configure_preset = ConfigurePreset( name=cmake_data.configuration_name, inherits='cppython', + binaryDir=build_directory.as_posix(), ) if preset_file.exists(): @@ -170,7 +174,9 @@ def generate_root_preset(preset_file: Path, cppython_preset_file: Path, cmake_da return root_preset @staticmethod - def write_root_presets(preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData) -> None: + def write_root_presets( + preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData, build_directory: Path + ) -> None: """Read the top level json file and insert the include reference. Receives a relative path to the tool cmake json file @@ -182,6 +188,7 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path, cmake_data preset_file: Preset file to modify cppython_preset_file: Path to the cppython preset file to include cmake_data: The CMake data to use + build_directory: The build directory to use """ initial_root_preset = None @@ -190,7 +197,7 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path, cmake_data initial_json = file.read() initial_root_preset = CMakePresets.model_validate_json(initial_json) - root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data) + root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data, build_directory) # Only write the file if the data has changed if root_preset != initial_root_preset: diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index e321e554..052ef7a4 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -71,6 +71,8 @@ def sync(self, sync_data: SyncData) -> None: self._cppython_preset_directory, self._provider_directory, sync_data ) - self.builder.write_root_presets(self.data.preset_file, cppython_preset_file, self.data) + self.builder.write_root_presets( + self.data.preset_file, cppython_preset_file, self.data, self.core_data.cppython_data.build_path + ) case _: raise ValueError('Unsupported sync data type') diff --git a/cppython/plugins/cmake/schema.py b/cppython/plugins/cmake/schema.py index 90f0f41b..eac1ed6d 100644 --- a/cppython/plugins/cmake/schema.py +++ b/cppython/plugins/cmake/schema.py @@ -54,6 +54,10 @@ class ConfigurePreset(CPPythonModel, extra='allow'): inherits: Annotated[ str | list[str] | None, Field(description='The inherits field allows inheriting from other presets.') ] = None + binaryDir: Annotated[ + str | None, + Field(description='The path to the output binary directory.'), + ] = None cacheVariables: dict[str, None | bool | str | CacheVariable] | None = None diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index 3055f578..b31abe96 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -198,11 +198,14 @@ def install(self) -> None: file.write(serialized) executable = self.core_data.cppython_data.install_path / 'vcpkg' + install_directory = self.data.install_directory + build_path = self.core_data.cppython_data.build_path + logger = getLogger('cppython.vcpkg') try: subprocess.run( - [str(executable), 'install', f'--x-install-root={self.data.install_directory}'], - cwd=self.core_data.cppython_data.build_path, + [str(executable), 'install', f'--x-install-root={str(install_directory)}'], + cwd=str(build_path), check=True, capture_output=True, ) @@ -222,11 +225,14 @@ def update(self) -> None: file.write(serialized) executable = self.core_data.cppython_data.install_path / 'vcpkg' + install_directory = self.data.install_directory + build_path = self.core_data.cppython_data.build_path + logger = getLogger('cppython.vcpkg') try: subprocess.run( - [str(executable), 'install', f'--x-install-root={self.data.install_directory}'], - cwd=self.core_data.cppython_data.build_path, + [str(executable), 'install', f'--x-install-root={str(install_directory)}'], + cwd=str(build_path), check=True, capture_output=True, ) diff --git a/cppython/test/pytest/base_classes.py b/cppython/test/pytest/base_classes.py index a675bd4b..ce9a0f5d 100644 --- a/cppython/test/pytest/base_classes.py +++ b/cppython/test/pytest/base_classes.py @@ -46,7 +46,6 @@ def fixture_plugin_type(self) -> type[T]: @staticmethod @pytest.fixture( name='cppython_plugin_data', - scope='session', ) def fixture_cppython_plugin_data(cppython_data: CPPythonData, plugin_type: type[T]) -> CPPythonPluginData: """Fixture for created the plugin CPPython table @@ -63,7 +62,6 @@ def fixture_cppython_plugin_data(cppython_data: CPPythonData, plugin_type: type[ @staticmethod @pytest.fixture( name='core_plugin_data', - scope='session', ) def fixture_core_plugin_data( cppython_plugin_data: CPPythonPluginData, project_data: ProjectData, pep621_data: PEP621Data @@ -160,7 +158,6 @@ class PluginTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): @staticmethod @pytest.fixture( name='plugin', - scope='session', ) def fixture_plugin( plugin_type: type[T], @@ -194,7 +191,6 @@ class DataPluginTests[T: DataPlugin](BaseTests[T], metaclass=ABCMeta): @staticmethod @pytest.fixture( name='plugin', - scope='session', ) def fixture_plugin( plugin_type: type[T], @@ -251,25 +247,19 @@ def fixture_plugin_configuration_type() -> type[ProviderPluginGroupData]: return ProviderPluginGroupData @staticmethod - @pytest.fixture(name='plugin_group_data', scope='session') + @pytest.fixture(name='plugin_group_data') def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData, tmp_path_factory: pytest.TempPathFactory + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData ) -> ProviderPluginGroupData: """Generates plugin configuration data generation from environment configuration Args: project_data: The project data fixture cppython_plugin_data:The plugin configuration fixture - tmp_path_factory: The temporary path factory Returns: The plugin configuration """ - workspace_path = tmp_path_factory.mktemp('workspace-') - project_data = ProjectData(project_root=workspace_path, verbosity=project_data.verbosity) - # Install path is already pinned to a temp directory to share downloaded resources - cppython_plugin_data.build_path = project_data.project_root / 'build' - cppython_plugin_data.tool_path = project_data.project_root / 'tool' return resolve_provider(project_data=project_data, cppython_data=cppython_plugin_data) @staticmethod @@ -342,25 +332,19 @@ def fixture_plugin_configuration_type() -> type[GeneratorPluginGroupData]: return GeneratorPluginGroupData @staticmethod - @pytest.fixture(name='plugin_group_data', scope='session') + @pytest.fixture(name='plugin_group_data') def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData, tmp_path_factory: pytest.TempPathFactory + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData ) -> GeneratorPluginGroupData: """Generates plugin configuration data generation from environment configuration Args: project_data: The project data fixture cppython_plugin_data:The plugin configuration fixture - tmp_path_factory: The temporary path factory Returns: The plugin configuration """ - workspace_path = tmp_path_factory.mktemp('workspace-') - project_data = ProjectData(project_root=workspace_path, verbosity=project_data.verbosity) - # Install path is already pinned to a temp directory to share downloaded resources - cppython_plugin_data.build_path = project_data.project_root / 'build' - cppython_plugin_data.tool_path = project_data.project_root / 'tool' return resolve_generator(project_data=project_data, cppython_data=cppython_plugin_data) @staticmethod @@ -432,9 +416,9 @@ def fixture_plugin_configuration_type() -> type[SCMPluginGroupData]: return SCMPluginGroupData @staticmethod - @pytest.fixture(name='plugin_group_data', scope='session') + @pytest.fixture(name='plugin_group_data') def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData, tmp_path_factory: pytest.TempPathFactory + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData ) -> SCMPluginGroupData: """Generates plugin configuration data generation from environment configuration @@ -446,11 +430,6 @@ def fixture_plugin_group_data( Returns: The plugin configuration """ - workspace_path = tmp_path_factory.mktemp('workspace-') - project_data = ProjectData(project_root=workspace_path, verbosity=project_data.verbosity) - # Install path is already pinned to a temp directory to share downloaded resources - cppython_plugin_data.build_path = project_data.project_root / 'build' - cppython_plugin_data.tool_path = project_data.project_root / 'tool' return resolve_scm(project_data=project_data, cppython_data=cppython_plugin_data) @staticmethod diff --git a/cppython/test/pytest/classes.py b/cppython/test/pytest/classes.py index 10238823..ecbe1aed 100644 --- a/cppython/test/pytest/classes.py +++ b/cppython/test/pytest/classes.py @@ -26,12 +26,12 @@ class ProviderIntegrationTests[T: Provider](DataPluginIntegrationTests[T], Provi @staticmethod @pytest.fixture(autouse=True, scope='session') - def _fixture_install_dependency(plugin: T, install_path: Path) -> None: + def _fixture_install_dependency(plugin_type: type[T], install_path: Path) -> None: """Forces the download to only happen once per test session""" - path = install_path / canonicalize_type(type(plugin)).name + path = install_path / canonicalize_type(plugin_type).name path.mkdir(parents=True, exist_ok=True) - asyncio.run(plugin.download_tooling(path)) + asyncio.run(plugin_type.download_tooling(path)) @staticmethod def test_install(plugin: T) -> None: diff --git a/cppython/test/pytest/fixtures.py b/cppython/test/pytest/fixtures.py index 754d4c24..5fa0175b 100644 --- a/cppython/test/pytest/fixtures.py +++ b/cppython/test/pytest/fixtures.py @@ -63,7 +63,6 @@ def fixture_pep621_configuration() -> PEP621Configuration: @pytest.fixture( name='pep621_data', - scope='session', ) def fixture_pep621_data( pep621_configuration: PEP621Configuration, project_configuration: ProjectConfiguration @@ -82,7 +81,6 @@ def fixture_pep621_data( @pytest.fixture( name='cppython_local_configuration', - scope='session', ) def fixture_cppython_local_configuration(install_path: Path) -> CPPythonLocalConfiguration: """Fixture defining all testable variations of CPPythonData @@ -102,7 +100,6 @@ def fixture_cppython_local_configuration(install_path: Path) -> CPPythonLocalCon @pytest.fixture( name='cppython_global_configuration', - scope='session', ) def fixture_cppython_global_configuration() -> CPPythonGlobalConfiguration: """Fixture defining all testable variations of CPPythonData @@ -161,7 +158,6 @@ def fixture_plugin_cppython_data( @pytest.fixture( name='cppython_data', - scope='session', ) def fixture_cppython_data( cppython_local_configuration: CPPythonLocalConfiguration, @@ -206,9 +202,8 @@ def fixture_core_data(cppython_data: CPPythonData, project_data: ProjectData) -> @pytest.fixture( name='project_configuration', - scope='session', ) -def fixture_project_configuration() -> ProjectConfiguration: +def fixture_project_configuration(tmp_path_factory: pytest.TempPathFactory) -> ProjectConfiguration: """Project configuration fixture. Here we provide overrides on the input variants so that we can use a temporary directory for testing purposes. @@ -216,12 +211,12 @@ def fixture_project_configuration() -> ProjectConfiguration: Returns: Configuration with temporary directory capabilities """ - return ProjectConfiguration(project_root=Path(), version='0.1.0') + workspace_path = tmp_path_factory.mktemp('workspace-') + return ProjectConfiguration(project_root=workspace_path, version='0.1.0') @pytest.fixture( name='project_data', - scope='session', ) def fixture_project_data(project_configuration: ProjectConfiguration) -> ProjectData: """Fixture that creates a project space at 'workspace/test_project/pyproject.toml' diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index 7da30663..dfef95f7 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -28,5 +28,7 @@ def test_simple(example_runner: CliRunner) -> None: assert result.returncode == 0, f'Cmake failed: {result.stderr}' + path = Path('build').absolute() + # Verify that the build directory contains the expected files - assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found' + assert (path / 'CMakeCache.txt').exists(), f'{path / "CMakeCache.txt"} not found' diff --git a/tests/unit/core/test_resolution.py b/tests/unit/core/test_resolution.py index d7565753..43b6f393 100644 --- a/tests/unit/core/test_resolution.py +++ b/tests/unit/core/test_resolution.py @@ -1,6 +1,5 @@ """Test data resolution""" -from pathlib import Path from typing import Annotated import pytest @@ -27,7 +26,6 @@ CPPythonModel, PEP621Configuration, ProjectConfiguration, - ProjectData, ) from cppython.utility.utility import TypeName @@ -90,7 +88,6 @@ class MockModel(CPPythonModel): @staticmethod def test_generator_resolve(project_configuration: ProjectConfiguration) -> None: """Test generator resolution""" - project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() @@ -114,7 +111,6 @@ class MockGenerator(Generator): @staticmethod def test_provider_resolve(project_configuration: ProjectConfiguration) -> None: """Test provider resolution""" - project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() @@ -138,7 +134,6 @@ class MockProvider(Provider): @staticmethod def test_scm_resolve(project_configuration: ProjectConfiguration) -> None: """Test scm resolution""" - project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() diff --git a/tests/unit/plugins/cmake/test_presets.py b/tests/unit/plugins/cmake/test_presets.py index da0c2afb..253592bc 100644 --- a/tests/unit/plugins/cmake/test_presets.py +++ b/tests/unit/plugins/cmake/test_presets.py @@ -2,6 +2,7 @@ from pathlib import Path +from cppython.core.schema import ProjectData from cppython.plugins.cmake.builder import Builder from cppython.plugins.cmake.schema import CMakeData, CMakePresets, CMakeSyncData from cppython.utility.utility import TypeName @@ -11,15 +12,17 @@ class TestBuilder: """Tests for the CMakePresets class""" @staticmethod - def test_generate_root_preset_new(tmp_path: Path) -> None: + def test_generate_root_preset_new(project_data: ProjectData) -> None: """Test generate_root_preset when the preset file does not exist""" builder = Builder() - preset_file = tmp_path / 'CMakePresets.json' - cppython_preset_file = tmp_path / 'cppython.json' + preset_file = project_data.project_root / 'CMakePresets.json' + cppython_preset_file = project_data.project_root / 'cppython.json' cmake_data = CMakeData(preset_file=preset_file, configuration_name='test-configuration') + build_directory = project_data.project_root / 'build' + # The function should create a new preset with the correct name and inheritance - result = builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data) + result = builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data, build_directory) assert result.configurePresets is not None assert any(p.name == 'test-configuration' for p in result.configurePresets) @@ -27,11 +30,11 @@ def test_generate_root_preset_new(tmp_path: Path) -> None: assert preset.inherits == 'cppython' @staticmethod - def test_generate_root_preset_existing(tmp_path: Path) -> None: + def test_generate_root_preset_existing(project_data: ProjectData) -> None: """Test generate_root_preset when the preset file already exists""" builder = Builder() - preset_file = tmp_path / 'CMakePresets.json' - cppython_preset_file = tmp_path / 'cppython.json' + preset_file = project_data.project_root / 'CMakePresets.json' + cppython_preset_file = project_data.project_root / 'cppython.json' cmake_data = CMakeData(preset_file=preset_file, configuration_name='test-configuration') # Create an initial preset file with a different preset @@ -39,8 +42,10 @@ def test_generate_root_preset_existing(tmp_path: Path) -> None: with open(preset_file, 'w', encoding='utf-8') as f: f.write(initial_presets.model_dump_json(exclude_none=True, by_alias=False, indent=4)) + build_directory = project_data.project_root / 'build' + # Should add the new preset and include - result = builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data) + result = builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data, build_directory) assert result.configurePresets is not None assert any(p.name == 'test-configuration' for p in result.configurePresets) @@ -86,15 +91,15 @@ def test_cppython_write(tmp_path: Path) -> None: builder.write_cppython_preset(tmp_path, provider_directory, data) @staticmethod - def test_root_write(tmp_path: Path) -> None: + def test_root_write(project_data: ProjectData) -> None: """Verifies that the root preset writing works as intended Args: - tmp_path: The input path the use + project_data: The project data with a temporary workspace """ builder = Builder() - cppython_preset_directory = tmp_path / 'cppython' + cppython_preset_directory = project_data.project_root / 'cppython' cppython_preset_directory.mkdir(parents=True, exist_ok=True) provider_directory = cppython_preset_directory / 'providers' @@ -104,7 +109,7 @@ def test_root_write(tmp_path: Path) -> None: with includes_file.open('w', encoding='utf-8') as file: file.write('example contents') - root_file = tmp_path / 'CMakePresets.json' + root_file = project_data.project_root / 'CMakePresets.json' presets = CMakePresets() serialized = presets.model_dump_json(exclude_none=True, by_alias=False, indent=4) @@ -116,20 +121,24 @@ def test_root_write(tmp_path: Path) -> None: cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + build_directory = project_data.project_root / 'build' builder.write_root_presets( - root_file, cppython_preset_file, CMakeData(preset_file=root_file, configuration_name='default') + root_file, + cppython_preset_file, + CMakeData(preset_file=root_file, configuration_name='default'), + build_directory, ) @staticmethod - def test_relative_root_write(tmp_path: Path) -> None: + def test_relative_root_write(project_data: ProjectData) -> None: """Verifies that the root preset writing works as intended Args: - tmp_path: The input path the use + project_data: The project data with a temporary workspace """ builder = Builder() - cppython_preset_directory = tmp_path / 'tool' / 'cppython' + cppython_preset_directory = project_data.project_root / 'tool' / 'cppython' cppython_preset_directory.mkdir(parents=True, exist_ok=True) provider_directory = cppython_preset_directory / 'providers' @@ -139,7 +148,7 @@ def test_relative_root_write(tmp_path: Path) -> None: with includes_file.open('w', encoding='utf-8') as file: file.write('example contents') - relative_indirection = tmp_path / 'nested' + relative_indirection = project_data.project_root / 'nested' relative_indirection.mkdir(parents=True, exist_ok=True) root_file = relative_indirection / 'CMakePresets.json' @@ -152,6 +161,11 @@ def test_relative_root_write(tmp_path: Path) -> None: builder.write_provider_preset(provider_directory, data) cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + + build_directory = project_data.project_root / 'build' builder.write_root_presets( - root_file, cppython_preset_file, CMakeData(preset_file=root_file, configuration_name='default') + root_file, + cppython_preset_file, + CMakeData(preset_file=root_file, configuration_name='default'), + build_directory, ) diff --git a/tests/unit/test_data.py b/tests/unit/test_data.py index 0357d6d4..d62efd22 100644 --- a/tests/unit/test_data.py +++ b/tests/unit/test_data.py @@ -23,7 +23,6 @@ class TestData: @staticmethod @pytest.fixture( name='data', - scope='session', ) def fixture_data( project_configuration: ProjectConfiguration, From eda5a4072af3317653ff82c5fdf22ca50bdbdb75 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 11 Jul 2025 21:19:59 -0400 Subject: [PATCH 07/16] Hook `cppython` App to `pdm cpp`` --- cppython/console/entry.py | 2 +- cppython/plugins/pdm/plugin.py | 48 +++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/cppython/console/entry.py b/cppython/console/entry.py index 3a2f0d34..fc2f36b5 100644 --- a/cppython/console/entry.py +++ b/cppython/console/entry.py @@ -10,7 +10,7 @@ from cppython.core.schema import ProjectConfiguration from cppython.project import Project -app = typer.Typer() +app = typer.Typer(no_args_is_help=True) def _find_pyproject_file() -> Path: diff --git a/cppython/plugins/pdm/plugin.py b/cppython/plugins/pdm/plugin.py index e38cbb25..b1e28037 100644 --- a/cppython/plugins/pdm/plugin.py +++ b/cppython/plugins/pdm/plugin.py @@ -1,12 +1,15 @@ """Implementation of the PDM Interface Plugin""" +from argparse import Namespace from logging import getLogger from typing import Any +from pdm.cli.commands.base import BaseCommand from pdm.core import Core from pdm.project.core import Project from pdm.signals import post_install +from cppython.console.entry import app from cppython.core.schema import Interface, ProjectConfiguration from cppython.project import Project as CPPythonProject @@ -14,11 +17,14 @@ class CPPythonPlugin(Interface): """Implementation of the PDM Interface Plugin""" - def __init__(self, _: Core) -> None: + def __init__(self, core: Core) -> None: """Initializes the plugin""" post_install.connect(self.on_post_install, weak=False) self.logger = getLogger('cppython.interface.pdm') + # Register the cpp command + register_commands(core) + def write_pyproject(self) -> None: """Write to file""" @@ -51,3 +57,43 @@ def on_post_install(self, project: Project, dry_run: bool, **_kwargs: Any) -> No if not dry_run: cppython_project.install() + + +class CPPythonCommand(BaseCommand): + """PDM command to invoke CPPython directly""" + + name = 'cpp' + description = 'Run CPPython commands' + + def add_arguments(self, parser) -> None: + """Add command arguments - delegate to Typer for argument parsing""" + # Add a catch-all for remaining arguments to pass to Typer + parser.add_argument('args', nargs='*', help='CPPython command arguments') + + def handle(self, project: Project, options: Namespace) -> None: + """Handle the command by delegating to the Typer app + + Args: + project: The PDM project + options: Command line options + """ + # Get the command arguments from options + args = getattr(options, 'args', []) + + try: + # Invoke cppython directly with the provided arguments + app(args) + except SystemExit: + # Typer/Click uses SystemExit for normal completion, don't propagate it + pass + except Exception as e: + project.core.ui.echo(f'Error running CPPython command: {e}', style='error') + + +def register_commands(core: Core) -> None: + """Register the CPPython command with PDM + + Args: + core: The PDM core instance + """ + core.register_command(CPPythonCommand) From 8ba7b05924e6b2a7ada2ad65dd98a5ec933e6b02 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 11 Jul 2025 21:38:55 -0400 Subject: [PATCH 08/16] Publish Implementation --- cppython/console/entry.py | 49 +- cppython/core/plugin_schema/provider.py | 5 + cppython/plugins/conan/plugin.py | 84 +++ cppython/plugins/conan/resolution.py | 7 +- cppython/plugins/conan/schema.py | 10 + cppython/plugins/vcpkg/plugin.py | 8 + cppython/project.py | 8 + cppython/test/mock/provider.py | 4 + cppython/test/pytest/__init__.py | 8 +- cppython/test/pytest/base_classes.py | 488 ------------------ cppython/test/pytest/classes.py | 108 ---- cppython/test/pytest/contracts.py | 314 +++++++++++ cppython/test/pytest/mixins.py | 145 ++++++ pdm.toml | 1 + .../plugins/cmake/test_generator.py | 4 +- .../plugins/conan/test_interface.py | 4 +- .../integration/plugins/git/test_interface.py | 4 +- .../plugins/vcpkg/test_provider.py | 4 +- tests/integration/test/test_generator.py | 4 +- tests/integration/test/test_provider.py | 10 +- tests/integration/test/test_scm.py | 4 +- tests/unit/plugins/cmake/test_generator.py | 4 +- .../{test_interface.py => test_provider.py} | 4 +- tests/unit/plugins/conan/test_publish.py | 340 ++++++++++++ .../unit/plugins/git/test_version_control.py | 4 +- tests/unit/plugins/vcpkg/test_provider.py | 4 +- tests/unit/test/test_generator.py | 4 +- tests/unit/test/test_provider.py | 10 +- tests/unit/test/test_scm.py | 4 +- 29 files changed, 996 insertions(+), 651 deletions(-) delete mode 100644 cppython/test/pytest/base_classes.py delete mode 100644 cppython/test/pytest/classes.py create mode 100644 cppython/test/pytest/contracts.py create mode 100644 cppython/test/pytest/mixins.py create mode 100644 pdm.toml rename tests/unit/plugins/conan/{test_interface.py => test_provider.py} (82%) create mode 100644 tests/unit/plugins/conan/test_publish.py diff --git a/cppython/console/entry.py b/cppython/console/entry.py index fc2f36b5..13d11f17 100644 --- a/cppython/console/entry.py +++ b/cppython/console/entry.py @@ -5,6 +5,7 @@ from typing import Annotated import typer +from rich import print from cppython.console.schema import ConsoleConfiguration, ConsoleInterface from cppython.core.schema import ProjectConfiguration @@ -13,6 +14,22 @@ app = typer.Typer(no_args_is_help=True) +def get_enabled_project(context: typer.Context) -> Project: + """Helper to load and validate an enabled Project from CLI context.""" + configuration = context.find_object(ConsoleConfiguration) + if configuration is None: + raise ValueError('The configuration object is missing') + + path = configuration.project_configuration.project_root / 'pyproject.toml' + pyproject_data = loads(path.read_text(encoding='utf-8')) + + project = Project(configuration.project_configuration, configuration.interface, pyproject_data) + if not project.enabled: + print('[bold red]Error[/bold red]: Project is not enabled. Please check your pyproject.toml configuration.') + raise typer.Exit(code=1) + return project + + def _find_pyproject_file() -> Path: """Searches upward for a pyproject.toml file @@ -75,13 +92,7 @@ def install( Raises: ValueError: If the configuration object is missing """ - if (configuration := context.find_object(ConsoleConfiguration)) is None: - raise ValueError('The configuration object is missing') - - path = configuration.project_configuration.project_root / 'pyproject.toml' - pyproject_data = loads(path.read_text(encoding='utf-8')) - - project = Project(configuration.project_configuration, configuration.interface, pyproject_data) + project = get_enabled_project(context) project.install() @@ -97,13 +108,7 @@ def update( Raises: ValueError: If the configuration object is missing """ - if (configuration := context.find_object(ConsoleConfiguration)) is None: - raise ValueError('The configuration object is missing') - - path = configuration.project_configuration.project_root / 'pyproject.toml' - pyproject_data = loads(path.read_text(encoding='utf-8')) - - project = Project(configuration.project_configuration, configuration.interface, pyproject_data) + project = get_enabled_project(context) project.update() @@ -112,3 +117,19 @@ def list_command( _: typer.Context, ) -> None: """Prints project information""" + + +@app.command() +def publish( + context: typer.Context, +) -> None: + """Publish API call + + Args: + context: The CLI configuration object + + Raises: + ValueError: If the configuration object is missing + """ + project = get_enabled_project(context) + project.publish() diff --git a/cppython/core/plugin_schema/provider.py b/cppython/core/plugin_schema/provider.py index 3c34c562..518fe405 100644 --- a/cppython/core/plugin_schema/provider.py +++ b/cppython/core/plugin_schema/provider.py @@ -88,3 +88,8 @@ def install(self) -> None: def update(self) -> None: """Called when dependencies need to be updated and written to the lock file.""" raise NotImplementedError + + @abstractmethod + def publish(self) -> None: + """Called when the project needs to be published.""" + raise NotImplementedError diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 5a3ee74d..18f7adde 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -9,6 +9,8 @@ from typing import Any import requests +from conan.api.conan_api import ConanAPI +from conan.api.model import ListPattern from cppython.core.plugin_schema.generator import SyncConsumer from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures @@ -120,3 +122,85 @@ def sync_data(self, consumer: SyncConsumer) -> SyncData: async def download_tooling(cls, directory: Path) -> None: """Downloads the conan provider file""" cls._download_file(cls._provider_url, directory / 'conan_provider.cmake') + + def publish(self) -> None: + """Publishes the package using conan create workflow.""" + # Get the project root directory where conanfile.py should be located + project_root = self.core_data.project_data.project_root + conanfile_path = project_root / 'conanfile.py' + + if not conanfile_path.exists(): + raise FileNotFoundError(f'conanfile.py not found at {conanfile_path}') + + # Initialize Conan API + conan_api = ConanAPI() + + # Step 1: Export the recipe to the cache + # This is equivalent to the export part of `conan create` + ref, conanfile = conan_api.export.export( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + lockfile=None, + remotes=conan_api.remotes.list(), + ) + + # Step 2: Get default profiles + profile_host, profile_build = conan_api.profiles.get_profiles_from_args([]) + + # Step 3: Build dependency graph for the package + deps_graph = conan_api.graph.load_graph_consumer( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + profile_host=profile_host, + profile_build=profile_build, + lockfile=None, + remotes=conan_api.remotes.list(), + update=None, + check_updates=False, + is_build_require=False, + ) + + # Step 4: Analyze binaries and install/build them if needed + conan_api.graph.analyze_binaries( + graph=deps_graph, + build_mode=['*'], # Build from source (equivalent to the create behavior) + remotes=conan_api.remotes.list(), + update=None, + lockfile=None, + ) + + # Step 5: Install all dependencies and build the package + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=conan_api.remotes.list()) + + # If not local, upload the package + if not self.data.local: + # Get all packages matching the created reference + ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False) + package_list = conan_api.list.select(ref_pattern) + + if package_list.recipes: + # Get the first configured remote or raise an error + remotes = conan_api.remotes.list() + if not remotes: + raise RuntimeError('No remotes configured for upload') + + remote = remotes[0] # Use first remote + + # Upload the package + conan_api.upload.upload_full( + package_list=package_list, + remote=remote, + enabled_remotes=remotes, + check_integrity=False, + force=False, + metadata=None, + dry_run=False, + ) + else: + raise RuntimeError('No packages found to upload') diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index 0abcb59e..f7e80d4e 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -6,7 +6,7 @@ from cppython.core.exception import ConfigException from cppython.core.schema import CorePluginData -from cppython.plugins.conan.schema import ConanData, ConanDependency +from cppython.plugins.conan.schema import ConanConfiguration, ConanData, ConanDependency def resolve_conan_dependency(requirement: Requirement) -> ConanDependency: @@ -41,7 +41,6 @@ def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> Conan Returns: The resolved conan data """ - # parsed_data = ConanConfiguration(**data) - # root_directory = core_data.project_data.pyproject_file.parent.absolute() + parsed_data = ConanConfiguration(**data) - return ConanData() + return ConanData(local=parsed_data.local) diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py index a2919382..256a8c22 100644 --- a/cppython/plugins/conan/schema.py +++ b/cppython/plugins/conan/schema.py @@ -5,6 +5,10 @@ provide structured configuration and data needed by the Conan Provider. """ +from typing import Annotated + +from pydantic import Field + from cppython.core.schema import CPPythonModel @@ -26,6 +30,12 @@ def requires(self) -> str: class ConanData(CPPythonModel): """Resolved conan data""" + local: bool + class ConanConfiguration(CPPythonModel): """Raw conan data""" + + local: Annotated[bool, Field(description='Whether to publish packages locally without uploading to a remote')] = ( + False + ) diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index b31abe96..07eb209a 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -239,3 +239,11 @@ def update(self) -> None: except subprocess.CalledProcessError as e: logger.exception('Unable to install project dependencies: %s', e.stderr.decode() if e.stderr else str(e)) raise + + def publish(self) -> None: + """Called when the project needs to be published. + + Raises: + NotImplementedError: vcpkg does not support publishing + """ + raise NotImplementedError('vcpkg does not support publishing') diff --git a/cppython/project.py b/cppython/project.py index c3404b1a..411b0236 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -99,3 +99,11 @@ def update(self) -> None: raise SystemExit('Error: An unexpected error occurred during update.') from None self._data.sync() + + def publish(self) -> None: + """Publishes the project""" + try: + self._data.plugins.provider.publish() + except Exception as exception: + self.logger.error('Unexpected error during publish: %s', str(exception)) + raise SystemExit('Error: An unexpected error occurred during publish.') from None diff --git a/cppython/test/mock/provider.py b/cppython/test/mock/provider.py index 091a9d59..50c58b1c 100644 --- a/cppython/test/mock/provider.py +++ b/cppython/test/mock/provider.py @@ -90,3 +90,7 @@ def install(self) -> None: def update(self) -> None: """Updates the provider""" pass + + def publish(self) -> None: + """Updates the provider""" + pass diff --git a/cppython/test/pytest/__init__.py b/cppython/test/pytest/__init__.py index a312d6a0..85c2f098 100644 --- a/cppython/test/pytest/__init__.py +++ b/cppython/test/pytest/__init__.py @@ -1,7 +1 @@ -"""Test harness for CPPython plugins using pytest. - -This module provides a test harness for CPPython plugins, enabling them to be -tested using pytest. It includes shared test types, fixtures, and utilities -that facilitate the testing of plugin interfaces, project configurations, and -plugin-specific features. -""" +"""Pytest integration and testing framework for CPPython.""" diff --git a/cppython/test/pytest/base_classes.py b/cppython/test/pytest/base_classes.py deleted file mode 100644 index ce9a0f5d..00000000 --- a/cppython/test/pytest/base_classes.py +++ /dev/null @@ -1,488 +0,0 @@ -"""Composable test types""" - -from abc import ABCMeta, abstractmethod -from importlib.metadata import entry_points -from typing import Any, LiteralString, cast - -import pytest - -from cppython.core.plugin_schema.generator import Generator, GeneratorPluginGroupData -from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData -from cppython.core.plugin_schema.scm import SCM, SCMPluginGroupData -from cppython.core.resolution import ( - resolve_cppython_plugin, - resolve_generator, - resolve_provider, - resolve_scm, -) -from cppython.core.schema import ( - CorePluginData, - CPPythonData, - CPPythonPluginData, - DataPlugin, - DataPluginGroupData, - PEP621Data, - Plugin, - PluginGroupData, - ProjectConfiguration, - ProjectData, -) -from cppython.test.data.mocks import ( - generator_variants, - provider_variants, - scm_variants, -) - - -class BaseTests[T: Plugin](metaclass=ABCMeta): - """Shared testing information for all plugin test classes.""" - - @abstractmethod - @pytest.fixture(name='plugin_type', scope='session') - def fixture_plugin_type(self) -> type[T]: - """A required testing hook that allows type generation""" - raise NotImplementedError('Override this fixture') - - @staticmethod - @pytest.fixture( - name='cppython_plugin_data', - ) - def fixture_cppython_plugin_data(cppython_data: CPPythonData, plugin_type: type[T]) -> CPPythonPluginData: - """Fixture for created the plugin CPPython table - - Args: - cppython_data: The CPPython table to help the resolve - plugin_type: The data plugin type - - Returns: - The plugin specific CPPython table information - """ - return resolve_cppython_plugin(cppython_data, plugin_type) - - @staticmethod - @pytest.fixture( - name='core_plugin_data', - ) - def fixture_core_plugin_data( - cppython_plugin_data: CPPythonPluginData, project_data: ProjectData, pep621_data: PEP621Data - ) -> CorePluginData: - """Fixture for creating the wrapper CoreData type - - Args: - cppython_plugin_data: CPPython data - project_data: The project data - pep621_data: Project table data - - Returns: - Wrapper Core Type - """ - return CorePluginData(cppython_data=cppython_plugin_data, project_data=project_data, pep621_data=pep621_data) - - @staticmethod - @pytest.fixture(name='plugin_group_name', scope='session') - def fixture_plugin_group_name() -> LiteralString: - """A required testing hook that allows plugin group name generation - - Returns: - The plugin group name - """ - return 'cppython' - - -class BaseIntegrationTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): - """Integration testing information for all plugin test classes""" - - @staticmethod - def test_entry_point(plugin_type: type[T], plugin_group_name: LiteralString) -> None: - """Verify that the plugin was registered - - Args: - plugin_type: The type to register - plugin_group_name: The group name for the plugin type - """ - # We only require the entry point to be registered if the plugin is not a Mocked type - if plugin_type.name() == 'mock': - pytest.skip('Mocked plugin type') - - types = [] - for entry in list(entry_points(group=f'{plugin_group_name}.{plugin_type.group()}')): - types.append(entry.load()) - - assert plugin_type in types - - @staticmethod - def test_name(plugin_type: type[Plugin]) -> None: - """Verifies the the class name allows name extraction - - Args: - plugin_type: The type to register - """ - assert plugin_type.group() - assert len(plugin_type.group()) - - assert plugin_type.name() - assert len(plugin_type.name()) - - -class BaseUnitTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): - """Unit testing information for all plugin test classes""" - - @staticmethod - def test_feature_extraction(plugin_type: type[T], project_configuration: ProjectConfiguration) -> None: - """Test the feature extraction of a plugin. - - This method tests the feature extraction functionality of a plugin by asserting that the features - returned by the plugin are correct for the given project configuration. - - Args: - plugin_type: The type of plugin to test. - project_configuration: The project configuration to use for testing. - """ - assert plugin_type.features(project_configuration.project_root) - - @staticmethod - def test_information(plugin_type: type[T]) -> None: - """Test the information method of a plugin. - - This method asserts that the `information` method of the given plugin type returns a value. - - Args: - plugin_type: The type of the plugin to test. - """ - assert plugin_type.information() - - -class PluginTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): - """Testing information for basic plugin test classes.""" - - @staticmethod - @pytest.fixture( - name='plugin', - ) - def fixture_plugin( - plugin_type: type[T], - plugin_group_data: PluginGroupData, - ) -> T: - """Overridden plugin generator for creating a populated data plugin type - - Args: - plugin_type: Plugin type - plugin_group_data: The data group configuration - - Returns: - A newly constructed provider - """ - plugin = plugin_type(plugin_group_data) - - return plugin - - -class PluginIntegrationTests[T: Plugin](BaseIntegrationTests[T], metaclass=ABCMeta): - """Integration testing information for basic plugin test classes""" - - -class PluginUnitTests[T: Plugin](BaseUnitTests[T], metaclass=ABCMeta): - """Unit testing information for basic plugin test classes""" - - -class DataPluginTests[T: DataPlugin](BaseTests[T], metaclass=ABCMeta): - """Shared testing information for all data plugin test classes.""" - - @staticmethod - @pytest.fixture( - name='plugin', - ) - def fixture_plugin( - plugin_type: type[T], - plugin_group_data: DataPluginGroupData, - core_plugin_data: CorePluginData, - plugin_data: dict[str, Any], - ) -> T: - """Overridden plugin generator for creating a populated data plugin type - - Args: - plugin_type: Plugin type - plugin_group_data: The data group configuration - core_plugin_data: The core metadata - plugin_data: The data table - - Returns: - A newly constructed provider - """ - plugin = plugin_type(plugin_group_data, core_plugin_data, plugin_data) - - return plugin - - -class DataPluginIntegrationTests[T: DataPlugin](BaseIntegrationTests[T], metaclass=ABCMeta): - """Integration testing information for all data plugin test classes""" - - -class DataPluginUnitTests[T: DataPlugin](BaseUnitTests[T], metaclass=ABCMeta): - """Unit testing information for all data plugin test classes""" - - @staticmethod - def test_empty_data( - plugin_type: type[T], - plugin_group_data: DataPluginGroupData, - core_plugin_data: CorePluginData, - ) -> None: - """All data plugins should be able to be constructed with empty data""" - plugin = plugin_type(plugin_group_data, core_plugin_data, {}) - - assert plugin, 'The plugin should be able to be constructed with empty data' - - -class ProviderTests[T: Provider](DataPluginTests[T], metaclass=ABCMeta): - """Shared functionality between the different Provider testing categories""" - - @staticmethod - @pytest.fixture(name='plugin_configuration_type', scope='session') - def fixture_plugin_configuration_type() -> type[ProviderPluginGroupData]: - """A required testing hook that allows plugin configuration data generation - - Returns: - The configuration type - """ - return ProviderPluginGroupData - - @staticmethod - @pytest.fixture(name='plugin_group_data') - def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData - ) -> ProviderPluginGroupData: - """Generates plugin configuration data generation from environment configuration - - Args: - project_data: The project data fixture - cppython_plugin_data:The plugin configuration fixture - - Returns: - The plugin configuration - """ - return resolve_provider(project_data=project_data, cppython_data=cppython_plugin_data) - - @staticmethod - @pytest.fixture( - name='provider_type', - scope='session', - params=provider_variants, - ) - def fixture_provider_type(plugin_type: type[T]) -> type[T]: - """Fixture defining all testable variations mock Providers - - Args: - plugin_type: Plugin type - - Returns: - Variation of a Provider - """ - return plugin_type - - @staticmethod - @pytest.fixture( - name='generator_type', - scope='session', - params=generator_variants, - ) - def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: - """Fixture defining all testable variations mock Generator - - Args: - request: Parameterization list - - Returns: - Variation of a Generator - """ - generator_type = cast(type[Generator], request.param) - - return generator_type - - @staticmethod - @pytest.fixture( - name='scm_type', - scope='session', - params=scm_variants, - ) - def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: - """Fixture defining all testable variations mock Generator - - Args: - request: Parameterization list - - Returns: - Variation of a Generator - """ - scm_type = cast(type[SCM], request.param) - - return scm_type - - -class GeneratorTests[T: Generator](DataPluginTests[T], metaclass=ABCMeta): - """Shared functionality between the different Generator testing categories""" - - @staticmethod - @pytest.fixture(name='plugin_configuration_type', scope='session') - def fixture_plugin_configuration_type() -> type[GeneratorPluginGroupData]: - """A required testing hook that allows plugin configuration data generation - - Returns: - The configuration type - """ - return GeneratorPluginGroupData - - @staticmethod - @pytest.fixture(name='plugin_group_data') - def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData - ) -> GeneratorPluginGroupData: - """Generates plugin configuration data generation from environment configuration - - Args: - project_data: The project data fixture - cppython_plugin_data:The plugin configuration fixture - - Returns: - The plugin configuration - """ - return resolve_generator(project_data=project_data, cppython_data=cppython_plugin_data) - - @staticmethod - @pytest.fixture( - name='provider_type', - scope='session', - params=provider_variants, - ) - def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: - """Fixture defining all testable variations mock Providers - - Args: - request: Parameterization list - - Returns: - Variation of a Provider - """ - provider_type = cast(type[Provider], request.param) - - return provider_type - - @staticmethod - @pytest.fixture( - name='generator_type', - scope='session', - ) - def fixture_generator_type(plugin_type: type[T]) -> type[T]: - """Override - - Args: - plugin_type: Plugin type - - Returns: - Plugin type - """ - return plugin_type - - @staticmethod - @pytest.fixture( - name='scm_type', - scope='session', - params=scm_variants, - ) - def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: - """Fixture defining all testable variations mock Generator - - Args: - request: Parameterization list - - Returns: - Variation of a Generator - """ - scm_type = cast(type[SCM], request.param) - - return scm_type - - -class SCMTests[T: SCM](PluginTests[T], metaclass=ABCMeta): - """Shared functionality between the different SCM testing categories""" - - @staticmethod - @pytest.fixture(name='plugin_configuration_type', scope='session') - def fixture_plugin_configuration_type() -> type[SCMPluginGroupData]: - """A required testing hook that allows plugin configuration data generation - - Returns: - The configuration type - """ - return SCMPluginGroupData - - @staticmethod - @pytest.fixture(name='plugin_group_data') - def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData - ) -> SCMPluginGroupData: - """Generates plugin configuration data generation from environment configuration - - Args: - project_data: The project data fixture - cppython_plugin_data:The plugin configuration fixture - tmp_path_factory: The temporary path factory - - Returns: - The plugin configuration - """ - return resolve_scm(project_data=project_data, cppython_data=cppython_plugin_data) - - @staticmethod - @pytest.fixture( - name='provider_type', - scope='session', - params=provider_variants, - ) - def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: - """Fixture defining all testable variations mock Providers - - Args: - request: Parameterization list - - Returns: - Variation of a Provider - """ - provider_type = cast(type[Provider], request.param) - - return provider_type - - @staticmethod - @pytest.fixture( - name='generator_type', - scope='session', - params=generator_variants, - ) - def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: - """Fixture defining all testable variations mock Generator - - Args: - request: Parameterization list - - Returns: - Variation of a Generator - """ - generator_type = cast(type[Generator], request.param) - - return generator_type - - @staticmethod - @pytest.fixture( - name='scm_type', - scope='session', - params=scm_variants, - ) - def fixture_scm_type(plugin_type: type[T]) -> type[SCM]: - """Fixture defining all testable variations mock Generator - - Args: - plugin_type: Parameterization list - - Returns: - Variation of a Generator - """ - return plugin_type diff --git a/cppython/test/pytest/classes.py b/cppython/test/pytest/classes.py deleted file mode 100644 index ecbe1aed..00000000 --- a/cppython/test/pytest/classes.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Types to inherit from""" - -import asyncio -from abc import ABCMeta -from pathlib import Path - -import pytest - -from cppython.core.plugin_schema.generator import Generator -from cppython.core.plugin_schema.provider import Provider -from cppython.core.plugin_schema.scm import SCM -from cppython.test.pytest.base_classes import ( - DataPluginIntegrationTests, - DataPluginUnitTests, - GeneratorTests, - PluginIntegrationTests, - PluginUnitTests, - ProviderTests, - SCMTests, -) -from cppython.utility.utility import canonicalize_type - - -class ProviderIntegrationTests[T: Provider](DataPluginIntegrationTests[T], ProviderTests[T], metaclass=ABCMeta): - """Base class for all provider integration tests that test plugin agnostic behavior""" - - @staticmethod - @pytest.fixture(autouse=True, scope='session') - def _fixture_install_dependency(plugin_type: type[T], install_path: Path) -> None: - """Forces the download to only happen once per test session""" - path = install_path / canonicalize_type(plugin_type).name - path.mkdir(parents=True, exist_ok=True) - - asyncio.run(plugin_type.download_tooling(path)) - - @staticmethod - def test_install(plugin: T) -> None: - """Ensure that the vanilla install command functions - - Args: - plugin: A newly constructed provider - """ - plugin.install() - - @staticmethod - def test_update(plugin: T) -> None: - """Ensure that the vanilla update command functions - - Args: - plugin: A newly constructed provider - """ - plugin.update() - - @staticmethod - def test_group_name(plugin_type: type[T]) -> None: - """Verifies that the group name is the same as the plugin type - - Args: - plugin_type: The type to register - """ - assert canonicalize_type(plugin_type).group == 'provider' - - -class ProviderUnitTests[T: Provider](DataPluginUnitTests[T], ProviderTests[T], metaclass=ABCMeta): - """Base class for all provider unit tests that test plugin agnostic behavior. - - Custom implementations of the Provider class should inherit from this class for its tests. - """ - - -class GeneratorIntegrationTests[T: Generator](DataPluginIntegrationTests[T], GeneratorTests[T], metaclass=ABCMeta): - """Base class for all scm integration tests that test plugin agnostic behavior""" - - @staticmethod - def test_group_name(plugin_type: type[T]) -> None: - """Verifies that the group name is the same as the plugin type - - Args: - plugin_type: The type to register - """ - assert canonicalize_type(plugin_type).group == 'generator' - - -class GeneratorUnitTests[T: Generator](DataPluginUnitTests[T], GeneratorTests[T], metaclass=ABCMeta): - """Base class for all Generator unit tests that test plugin agnostic behavior. - - Custom implementations of the Generator class should inherit from this class for its tests. - """ - - -class SCMIntegrationTests[T: SCM](PluginIntegrationTests[T], SCMTests[T], metaclass=ABCMeta): - """Base class for all generator integration tests that test plugin agnostic behavior""" - - @staticmethod - def test_group_name(plugin_type: type[T]) -> None: - """Verifies that the group name is the same as the plugin type - - Args: - plugin_type: The type to register - """ - assert canonicalize_type(plugin_type).group == 'scm' - - -class SCMUnitTests[T: SCM](PluginUnitTests[T], SCMTests[T], metaclass=ABCMeta): - """Base class for all Generator unit tests that test plugin agnostic behavior. - - Custom implementations of the Generator class should inherit from this class for its tests. - """ diff --git a/cppython/test/pytest/contracts.py b/cppython/test/pytest/contracts.py new file mode 100644 index 00000000..bf63c804 --- /dev/null +++ b/cppython/test/pytest/contracts.py @@ -0,0 +1,314 @@ +"""Plugin test contracts that define standard test requirements. + +This module contains abstract base classes that define the testing contracts +for each plugin type. Each plugin implementation should inherit from the +appropriate contract class exactly once to ensure they fulfill the required +testing obligations. + +These contracts combine the core fixtures with plugin-type-specific requirements. +""" + +import asyncio +from abc import ABCMeta +from importlib.metadata import entry_points +from pathlib import Path +from typing import Any, LiteralString + +import pytest + +from cppython.core.plugin_schema.generator import Generator, GeneratorPluginGroupData +from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData +from cppython.core.plugin_schema.scm import SCM, SCMPluginGroupData +from cppython.core.resolution import resolve_generator, resolve_provider, resolve_scm +from cppython.core.schema import ( + CorePluginData, + CPPythonPluginData, + DataPluginGroupData, + Plugin, + ProjectConfiguration, + ProjectData, +) +from cppython.test.data.mocks import generator_variants, provider_variants, scm_variants +from cppython.test.pytest.mixins import ( + DataPluginTestMixin, + PluginTestMixin, +) +from cppython.utility.utility import canonicalize_type + + +class PluginTestValidation: + """Common validation tests that can be applied to any plugin. + + These are generic tests that validate basic plugin behavior regardless + of the specific plugin type. Test classes can inherit this to get + standard validation tests. + """ + + @staticmethod + def test_feature_extraction(plugin_type: type[Plugin], project_configuration: ProjectConfiguration) -> None: + """Test the feature extraction of a plugin. + + Args: + plugin_type: The type of plugin to test. + project_configuration: The project configuration to use for testing. + """ + assert plugin_type.features(project_configuration.project_root) + + @staticmethod + def test_information(plugin_type: type[Plugin]) -> None: + """Test the information method of a plugin. + + Args: + plugin_type: The type of the plugin to test. + """ + assert plugin_type.information() + + @staticmethod + def test_plugin_name_extraction(plugin_type: type[Plugin]) -> None: + """Verifies the class name allows name extraction + + Args: + plugin_type: The type to register + """ + assert plugin_type.group() + assert len(plugin_type.group()) + assert plugin_type.name() + assert len(plugin_type.name()) + + +class DataPluginTestValidation(PluginTestValidation): + """Validation tests specific to data plugins. + + These tests validate that data plugins can handle various configuration + scenarios properly. + """ + + @staticmethod + def test_empty_data_construction( + plugin_type: type[Any], + plugin_group_data: DataPluginGroupData, + core_plugin_data: CorePluginData, + ) -> None: + """All data plugins should be able to be constructed with empty data + + Args: + plugin_type: The plugin type to test + plugin_group_data: Plugin group configuration + core_plugin_data: Core plugin data + """ + plugin = plugin_type(plugin_group_data, core_plugin_data, {}) + assert plugin, 'The plugin should be able to be constructed with empty data' + + +class ProviderTestContract[T: Provider](DataPluginTestMixin[T], DataPluginTestValidation, metaclass=ABCMeta): + """Test contract for Provider plugins. + + Each Provider plugin should have exactly one test class that inherits from this + to ensure it fulfills all Provider testing requirements. + """ + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[ProviderPluginGroupData]: + """Required hook for Provider plugin configuration data generation""" + return ProviderPluginGroupData + + @staticmethod + @pytest.fixture(name='plugin_group_data') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> ProviderPluginGroupData: + """Generate Provider plugin configuration data""" + return resolve_provider(project_data=project_data, cppython_data=cppython_plugin_data) + + # Cross-plugin testing fixtures for ensuring compatibility + @staticmethod + @pytest.fixture(name='provider_type', scope='session', params=provider_variants) + def fixture_provider_type(plugin_type: type[T]) -> type[T]: + """Return this provider type for cross-plugin testing""" + return plugin_type + + @staticmethod + @pytest.fixture(name='generator_type', scope='session', params=generator_variants) + def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: + """Provide generator variants for cross-plugin testing""" + return request.param + + @staticmethod + @pytest.fixture(name='scm_type', scope='session', params=scm_variants) + def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: + """Provide SCM variants for cross-plugin testing""" + return request.param + + +class ProviderIntegrationTestContract[T: Provider](ProviderTestContract[T], metaclass=ABCMeta): + """Integration test contract for Provider plugins. + + Providers that need integration testing should inherit from this contract. + This includes tests that require actual tool installation and execution. + """ + + @staticmethod + @pytest.fixture(autouse=True, scope='session') + def _fixture_install_dependency(plugin_type: type[T], install_path: Path) -> None: + """Forces the provider tool download to only happen once per test session""" + path = install_path / canonicalize_type(plugin_type).name + path.mkdir(parents=True, exist_ok=True) + asyncio.run(plugin_type.download_tooling(path)) + + @staticmethod + def test_entry_point_registration(plugin_type: type[T], plugin_group_name: LiteralString) -> None: + """Verify that the provider plugin was registered with entry points""" + if plugin_type.name() == 'mock': + pytest.skip('Mocked plugin type') + + registered_types = [] + for entry in list(entry_points(group=f'{plugin_group_name}.{plugin_type.group()}')): + registered_types.append(entry.load()) + + assert plugin_type in registered_types + + @staticmethod + def test_install(plugin: T) -> None: + """Ensure that the provider install command functions""" + plugin.install() + + @staticmethod + def test_update(plugin: T) -> None: + """Ensure that the provider update command functions""" + plugin.update() + + @staticmethod + def test_group_name(plugin_type: type[T]) -> None: + """Verify that the provider group name is correct""" + assert canonicalize_type(plugin_type).group == 'provider' + + +class GeneratorTestContract[T: Generator](DataPluginTestMixin[T], DataPluginTestValidation, metaclass=ABCMeta): + """Test contract for Generator plugins. + + Each Generator plugin should have exactly one test class that inherits from this + to ensure it fulfills all Generator testing requirements. + """ + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[GeneratorPluginGroupData]: + """Required hook for Generator plugin configuration data generation""" + return GeneratorPluginGroupData + + @staticmethod + @pytest.fixture(name='plugin_group_data') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> GeneratorPluginGroupData: + """Generate Generator plugin configuration data""" + return resolve_generator(project_data=project_data, cppython_data=cppython_plugin_data) + + # Cross-plugin testing fixtures for ensuring compatibility + @staticmethod + @pytest.fixture(name='provider_type', scope='session', params=provider_variants) + def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: + """Provide provider variants for cross-plugin testing""" + return request.param + + @staticmethod + @pytest.fixture(name='generator_type', scope='session') + def fixture_generator_type(plugin_type: type[T]) -> type[T]: + """Return this generator type for cross-plugin testing""" + return plugin_type + + @staticmethod + @pytest.fixture(name='scm_type', scope='session', params=scm_variants) + def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: + """Provide SCM variants for cross-plugin testing""" + return request.param + + +class GeneratorIntegrationTestContract[T: Generator](GeneratorTestContract[T], metaclass=ABCMeta): + """Integration test contract for Generator plugins. + + Generators that need integration testing should inherit from this contract. + """ + + @staticmethod + def test_entry_point_registration(plugin_type: type[T], plugin_group_name: LiteralString) -> None: + """Verify that the generator plugin was registered with entry points""" + if plugin_type.name() == 'mock': + pytest.skip('Mocked plugin type') + + registered_types = [] + for entry in list(entry_points(group=f'{plugin_group_name}.{plugin_type.group()}')): + registered_types.append(entry.load()) + + assert plugin_type in registered_types + + @staticmethod + def test_group_name(plugin_type: type[T]) -> None: + """Verify that the generator group name is correct""" + assert canonicalize_type(plugin_type).group == 'generator' + + +class SCMTestContract[T: SCM](PluginTestMixin[T], PluginTestValidation, metaclass=ABCMeta): + """Test contract for SCM plugins. + + Each SCM plugin should have exactly one test class that inherits from this + to ensure it fulfills all SCM testing requirements. + """ + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[SCMPluginGroupData]: + """Required hook for SCM plugin configuration data generation""" + return SCMPluginGroupData + + @staticmethod + @pytest.fixture(name='plugin_group_data') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> SCMPluginGroupData: + """Generate SCM plugin configuration data""" + return resolve_scm(project_data=project_data, cppython_data=cppython_plugin_data) + + # Cross-plugin testing fixtures for ensuring compatibility + @staticmethod + @pytest.fixture(name='provider_type', scope='session', params=provider_variants) + def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: + """Provide provider variants for cross-plugin testing""" + return request.param + + @staticmethod + @pytest.fixture(name='generator_type', scope='session', params=generator_variants) + def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: + """Provide generator variants for cross-plugin testing""" + return request.param + + @staticmethod + @pytest.fixture(name='scm_type', scope='session', params=scm_variants) + def fixture_scm_type(plugin_type: type[T]) -> type[T]: + """Return this SCM type for cross-plugin testing""" + return plugin_type + + +class SCMIntegrationTestContract[T: SCM](SCMTestContract[T], metaclass=ABCMeta): + """Integration test contract for SCM plugins. + + SCM plugins that need integration testing should inherit from this contract. + """ + + @staticmethod + def test_entry_point_registration(plugin_type: type[T], plugin_group_name: LiteralString) -> None: + """Verify that the SCM plugin was registered with entry points""" + if plugin_type.name() == 'mock': + pytest.skip('Mocked plugin type') + + registered_types = [] + for entry in list(entry_points(group=f'{plugin_group_name}.{plugin_type.group()}')): + registered_types.append(entry.load()) + + assert plugin_type in registered_types + + @staticmethod + def test_group_name(plugin_type: type[T]) -> None: + """Verify that the SCM group name is correct""" + assert canonicalize_type(plugin_type).group == 'scm' diff --git a/cppython/test/pytest/mixins.py b/cppython/test/pytest/mixins.py new file mode 100644 index 00000000..a51c2f8c --- /dev/null +++ b/cppython/test/pytest/mixins.py @@ -0,0 +1,145 @@ +"""Core test mixins and utilities that can be used by any test class. + +This module provides the foundational testing infrastructure that all test classes +can inherit from or use directly. These are meant to be mixed into test classes +as needed, not inherited in a strict hierarchy. +""" + +from abc import ABCMeta, abstractmethod +from typing import Any, LiteralString + +import pytest + +from cppython.core.resolution import resolve_cppython_plugin +from cppython.core.schema import ( + CorePluginData, + CPPythonData, + CPPythonPluginData, + DataPlugin, + DataPluginGroupData, + PEP621Data, + Plugin, + PluginGroupData, + ProjectData, +) + + +class TestMixin[T: Plugin](metaclass=ABCMeta): + """Core mixin that provides basic plugin construction capabilities. + + Any test class can inherit from this to get access to standard plugin + construction fixtures. This is the base layer that provides the minimal + infrastructure needed for plugin testing. + """ + + @abstractmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type(self) -> type[T]: + """A required testing hook that allows type generation + + This must be implemented by any concrete test class to specify + which plugin type is being tested. + """ + raise NotImplementedError('Override this fixture') + + @abstractmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data(self) -> dict[str, Any]: + """A required testing hook that allows plugin configuration data generation + + This must be implemented by any concrete test class to provide + the configuration data for the plugin being tested. + """ + raise NotImplementedError('Override this fixture') + + @staticmethod + @pytest.fixture(name='plugin_group_name', scope='session') + def fixture_plugin_group_name() -> LiteralString: + """A required testing hook that allows plugin group name generation + + Returns: + The plugin group name + """ + return 'cppython' + + @staticmethod + @pytest.fixture(name='cppython_plugin_data') + def fixture_cppython_plugin_data(cppython_data: CPPythonData, plugin_type: type[T]) -> CPPythonPluginData: + """Fixture for created the plugin CPPython table + + Args: + cppython_data: The CPPython table to help the resolve + plugin_type: The data plugin type + + Returns: + The plugin specific CPPython table information + """ + return resolve_cppython_plugin(cppython_data, plugin_type) + + @staticmethod + @pytest.fixture(name='core_plugin_data') + def fixture_core_plugin_data( + cppython_plugin_data: CPPythonPluginData, project_data: ProjectData, pep621_data: PEP621Data + ) -> CorePluginData: + """Fixture for creating the wrapper CoreData type + + Args: + cppython_plugin_data: CPPython data + project_data: The project data + pep621_data: Project table data + + Returns: + Wrapper Core Type + """ + return CorePluginData(cppython_data=cppython_plugin_data, project_data=project_data, pep621_data=pep621_data) + + +class PluginTestMixin[T: Plugin](TestMixin[T], metaclass=ABCMeta): + """Plugin construction mixin for simple plugins. + + Provides plugin instance creation for plugins that don't need complex + configuration data (like SCM plugins). + """ + + @staticmethod + @pytest.fixture(name='plugin') + def fixture_plugin(plugin_type: type[T], plugin_group_data: PluginGroupData) -> T: + """Create a basic plugin instance + + Args: + plugin_type: Plugin type + plugin_group_data: The data group configuration + + Returns: + A newly constructed plugin + """ + return plugin_type(plugin_group_data) + + +class DataPluginTestMixin[T: DataPlugin](TestMixin[T], metaclass=ABCMeta): + """Data plugin construction mixin for complex plugins. + + Provides plugin instance creation for plugins that need rich configuration + data (like Provider and Generator plugins). + """ + + @staticmethod + @pytest.fixture(name='plugin') + def fixture_plugin( + plugin_type: type[T], + plugin_group_data: DataPluginGroupData, + core_plugin_data: CorePluginData, + plugin_data: dict[str, Any], + ) -> T: + """Create a data plugin instance + + Args: + plugin_type: Plugin type + plugin_group_data: The data group configuration + core_plugin_data: The core metadata + plugin_data: The data table + + Returns: + A newly constructed provider + """ + return plugin_type(plugin_group_data, core_plugin_data, plugin_data) diff --git a/pdm.toml b/pdm.toml new file mode 100644 index 00000000..71854f2a --- /dev/null +++ b/pdm.toml @@ -0,0 +1 @@ +use_uv = true diff --git a/tests/integration/plugins/cmake/test_generator.py b/tests/integration/plugins/cmake/test_generator.py index 5ffe801c..92a308ec 100644 --- a/tests/integration/plugins/cmake/test_generator.py +++ b/tests/integration/plugins/cmake/test_generator.py @@ -6,12 +6,12 @@ from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import CMakeConfiguration -from cppython.test.pytest.classes import GeneratorIntegrationTests +from cppython.test.pytest.contracts import GeneratorIntegrationTestContract pytest_plugins = ['tests.fixtures.cmake'] -class TestCPPythonGenerator(GeneratorIntegrationTests[CMakeGenerator]): +class TestCPPythonGenerator(GeneratorIntegrationTestContract[CMakeGenerator]): """The tests for the CMake generator""" @staticmethod diff --git a/tests/integration/plugins/conan/test_interface.py b/tests/integration/plugins/conan/test_interface.py index fd01cf67..78d988e3 100644 --- a/tests/integration/plugins/conan/test_interface.py +++ b/tests/integration/plugins/conan/test_interface.py @@ -5,10 +5,10 @@ import pytest from cppython.plugins.conan.plugin import ConanProvider -from cppython.test.pytest.classes import ProviderIntegrationTests +from cppython.test.pytest.contracts import ProviderIntegrationTestContract -class TestCPPythonProvider(ProviderIntegrationTests[ConanProvider]): +class TestCPPythonProvider(ProviderIntegrationTestContract[ConanProvider]): """The tests for the conan provider""" @staticmethod diff --git a/tests/integration/plugins/git/test_interface.py b/tests/integration/plugins/git/test_interface.py index cb3eca61..c67c2239 100644 --- a/tests/integration/plugins/git/test_interface.py +++ b/tests/integration/plugins/git/test_interface.py @@ -3,10 +3,10 @@ import pytest from cppython.plugins.git.plugin import GitSCM -from cppython.test.pytest.classes import SCMIntegrationTests +from cppython.test.pytest.contracts import SCMIntegrationTestContract -class TestGitInterface(SCMIntegrationTests[GitSCM]): +class TestGitInterface(SCMIntegrationTestContract[GitSCM]): """Integration tests for the Git SCM plugin""" @staticmethod diff --git a/tests/integration/plugins/vcpkg/test_provider.py b/tests/integration/plugins/vcpkg/test_provider.py index feafdde2..4d28cd73 100644 --- a/tests/integration/plugins/vcpkg/test_provider.py +++ b/tests/integration/plugins/vcpkg/test_provider.py @@ -5,10 +5,10 @@ import pytest from cppython.plugins.vcpkg.plugin import VcpkgProvider -from cppython.test.pytest.classes import ProviderIntegrationTests +from cppython.test.pytest.contracts import ProviderIntegrationTestContract -class TestCPPythonProvider(ProviderIntegrationTests[VcpkgProvider]): +class TestCPPythonProvider(ProviderIntegrationTestContract[VcpkgProvider]): """The tests for the vcpkg provider""" @staticmethod diff --git a/tests/integration/test/test_generator.py b/tests/integration/test/test_generator.py index 3bdf3027..7e75f97a 100644 --- a/tests/integration/test/test_generator.py +++ b/tests/integration/test/test_generator.py @@ -5,10 +5,10 @@ import pytest from cppython.test.mock.generator import MockGenerator -from cppython.test.pytest.classes import GeneratorIntegrationTests +from cppython.test.pytest.contracts import GeneratorIntegrationTestContract -class TestCPPythonGenerator(GeneratorIntegrationTests[MockGenerator]): +class TestCPPythonGenerator(GeneratorIntegrationTestContract[MockGenerator]): """The tests for the Mock generator""" @staticmethod diff --git a/tests/integration/test/test_provider.py b/tests/integration/test/test_provider.py index d79c6d3f..0aaae822 100644 --- a/tests/integration/test/test_provider.py +++ b/tests/integration/test/test_provider.py @@ -1,14 +1,18 @@ -"""Test the integrations related to the internal provider implementation and the 'Provider' interface itself""" +"""Test integrations related to the internal provider implementation. + +Test integrations related to the internal provider implementation and the +'Provider' interface itself. +""" from typing import Any import pytest from cppython.test.mock.provider import MockProvider -from cppython.test.pytest.classes import ProviderIntegrationTests +from cppython.test.pytest.contracts import ProviderIntegrationTestContract -class TestMockProvider(ProviderIntegrationTests[MockProvider]): +class TestMockProvider(ProviderIntegrationTestContract[MockProvider]): """The tests for our Mock provider""" @staticmethod diff --git a/tests/integration/test/test_scm.py b/tests/integration/test/test_scm.py index da69979c..f5db6094 100644 --- a/tests/integration/test/test_scm.py +++ b/tests/integration/test/test_scm.py @@ -5,10 +5,10 @@ import pytest from cppython.test.mock.scm import MockSCM -from cppython.test.pytest.classes import SCMIntegrationTests +from cppython.test.pytest.contracts import SCMIntegrationTestContract -class TestCPPythonSCM(SCMIntegrationTests[MockSCM]): +class TestCPPythonSCM(SCMIntegrationTestContract[MockSCM]): """The tests for the Mock version control""" @staticmethod diff --git a/tests/unit/plugins/cmake/test_generator.py b/tests/unit/plugins/cmake/test_generator.py index 407a6452..a640efb7 100644 --- a/tests/unit/plugins/cmake/test_generator.py +++ b/tests/unit/plugins/cmake/test_generator.py @@ -8,12 +8,12 @@ from cppython.plugins.cmake.schema import ( CMakeConfiguration, ) -from cppython.test.pytest.classes import GeneratorUnitTests +from cppython.test.pytest.contracts import GeneratorTestContract pytest_plugins = ['tests.fixtures.cmake'] -class TestCPPythonGenerator(GeneratorUnitTests[CMakeGenerator]): +class TestCPPythonGenerator(GeneratorTestContract[CMakeGenerator]): """The tests for the CMake generator""" @staticmethod diff --git a/tests/unit/plugins/conan/test_interface.py b/tests/unit/plugins/conan/test_provider.py similarity index 82% rename from tests/unit/plugins/conan/test_interface.py rename to tests/unit/plugins/conan/test_provider.py index 47c42586..7de6b9dd 100644 --- a/tests/unit/plugins/conan/test_interface.py +++ b/tests/unit/plugins/conan/test_provider.py @@ -5,10 +5,10 @@ import pytest from cppython.plugins.conan.plugin import ConanProvider -from cppython.test.pytest.classes import ProviderUnitTests +from cppython.test.pytest.contracts import ProviderTestContract -class TestCPPythonProvider(ProviderUnitTests[ConanProvider]): +class TestConanProvider(ProviderTestContract[ConanProvider]): """The tests for the Conan Provider""" @staticmethod diff --git a/tests/unit/plugins/conan/test_publish.py b/tests/unit/plugins/conan/test_publish.py new file mode 100644 index 00000000..2423232c --- /dev/null +++ b/tests/unit/plugins/conan/test_publish.py @@ -0,0 +1,340 @@ +"""Unit tests for the conan plugin publish functionality""" + +from typing import Any +from unittest.mock import MagicMock, Mock + +import pytest +from pytest_mock import MockerFixture + +from cppython.plugins.conan.plugin import ConanProvider +from cppython.test.pytest.mixins import ProviderPluginTestMixin + + +class TestConanPublish(ProviderPluginTestMixin[ConanProvider]): + """Tests for the Conan provider publish functionality""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """A required testing hook that allows data generation + + Returns: + The constructed plugin data + """ + return { + 'local': False, + } + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[ConanProvider]: + """A required testing hook that allows type generation + + Returns: + The type of the Provider + """ + return ConanProvider + + @staticmethod + @pytest.fixture(name='mock_conan_api') + def fixture_mock_conan_api(mocker: MockerFixture) -> Mock: + """Creates a mock ConanAPI instance + + Args: + mocker: Pytest mocker fixture + + Returns: + Mock ConanAPI instance + """ + mock_api = mocker.Mock() + + # Mock export module - export returns a tuple (ref, conanfile) + mock_ref = mocker.Mock() + mock_ref.name = 'test_package' + mock_conanfile = mocker.Mock() + mock_api.export.export = mocker.Mock(return_value=(mock_ref, mock_conanfile)) + + # Mock graph module + mock_api.graph.load_graph_consumer = mocker.Mock() + mock_api.graph.analyze_binaries = mocker.Mock() + + # Mock install module + mock_api.install.install_binaries = mocker.Mock() + + # Mock list module + mock_select_result = mocker.Mock() + mock_select_result.recipes = ['some_package/1.0@user/channel'] + mock_api.list.select = mocker.Mock(return_value=mock_select_result) + + # Mock remotes module + mock_remote = mocker.Mock() + mock_remote.name = 'origin' + mock_api.remotes.list = mocker.Mock(return_value=[mock_remote]) + + # Mock upload module + mock_api.upload.upload_full = mocker.Mock() + + # Mock profiles module + mock_profile = mocker.Mock() + mock_api.profiles.get_profiles_from_args = mocker.Mock(return_value=(mock_profile, mock_profile)) + + return mock_api + + @staticmethod + @pytest.fixture(name='temp_conanfile') + def fixture_temp_conanfile(plugin: ConanProvider) -> None: + """Creates a temporary conanfile.py for testing + + Args: + plugin: The plugin instance + """ + project_root = plugin.core_data.project_data.project_root + conanfile_path = project_root / 'conanfile.py' + conanfile_path.write_text( + 'from conan import ConanFile\n\n' + 'class TestConan(ConanFile):\n' + ' name = "test_package"\n' + ' version = "1.0"\n' + ) + + def test_publish_local_only( + self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + ) -> None: + """Test that publish with local=True only exports and builds locally + + Args: + plugin: The plugin instance + mock_conan_api: Mock ConanAPI + temp_conanfile: Fixture to create conanfile.py + mocker: Pytest mocker fixture + """ + # Set plugin to local mode + plugin.data.local = True + + # Mock the necessary imports and API creation + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + + # Mock the dependencies graph + mock_graph = mocker.Mock() + mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + + # Execute publish + plugin.publish() + + # Verify export was called + mock_conan_api.export.export.assert_called_once() + + # Verify graph loading and analysis + mock_conan_api.graph.load_graph_consumer.assert_called_once() + mock_conan_api.graph.analyze_binaries.assert_called_once_with( + graph=mock_graph, + build_mode=['*'], + remotes=mock_conan_api.remotes.list(), + update=None, + lockfile=None, + ) + + # Verify install was called + mock_conan_api.install.install_binaries.assert_called_once_with( + deps_graph=mock_graph, remotes=mock_conan_api.remotes.list() + ) + + # Verify upload was NOT called for local mode + mock_conan_api.upload.upload_full.assert_not_called() + + def test_publish_with_upload( + self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + ) -> None: + """Test that publish with local=False exports, builds, and uploads + + Args: + plugin: The plugin instance + mock_conan_api: Mock ConanAPI + temp_conanfile: Fixture to create conanfile.py + mocker: Pytest mocker fixture + """ + # Set plugin to upload mode + plugin.data.local = False + + # Mock the necessary imports and API creation + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + + # Mock the dependencies graph + mock_graph = mocker.Mock() + mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + + # Execute publish + plugin.publish() + + # Verify all steps were called + mock_conan_api.export.export.assert_called_once() + mock_conan_api.graph.load_graph_consumer.assert_called_once() + mock_conan_api.graph.analyze_binaries.assert_called_once() + mock_conan_api.install.install_binaries.assert_called_once() + + # Verify upload was called + mock_conan_api.list.select.assert_called_once() + mock_conan_api.upload.upload_full.assert_called_once() + + def test_publish_no_remotes_configured( + self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + ) -> None: + """Test that publish raises error when no remotes are configured for upload + + Args: + plugin: The plugin instance + mock_conan_api: Mock ConanAPI + temp_conanfile: Fixture to create conanfile.py + mocker: Pytest mocker fixture + """ + # Set plugin to upload mode + plugin.data.local = False + + # Mock the necessary imports and API creation + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + + # Mock the dependencies graph + mock_graph = mocker.Mock() + mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + + # Mock no remotes configured + mock_conan_api.remotes.list.return_value = [] + + # Execute publish and expect RuntimeError + with pytest.raises(RuntimeError, match='No remotes configured for upload'): + plugin.publish() + + def test_publish_no_packages_found( + self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + ) -> None: + """Test that publish raises error when no packages are found to upload + + Args: + plugin: The plugin instance + mock_conan_api: Mock ConanAPI + temp_conanfile: Fixture to create conanfile.py + mocker: Pytest mocker fixture + """ + # Set plugin to upload mode + plugin.data.local = False + + # Mock the necessary imports and API creation + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + + # Mock the dependencies graph + mock_graph = mocker.Mock() + mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + + # Mock empty package list + mock_select_result = mocker.Mock() + mock_select_result.recipes = [] + mock_conan_api.list.select.return_value = mock_select_result + + # Execute publish and expect RuntimeError + with pytest.raises(RuntimeError, match='No packages found to upload'): + plugin.publish() + + def test_publish_uses_default_profiles( + self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + ) -> None: + """Test that publish uses default profiles from API + + Args: + plugin: The plugin instance + mock_conan_api: Mock ConanAPI + temp_conanfile: Fixture to create conanfile.py + mocker: Pytest mocker fixture + """ + # Set plugin to local mode + plugin.data.local = True + + # Mock the necessary imports and API creation + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + + # Mock the dependencies graph + mock_graph = mocker.Mock() + mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + + # Execute publish + plugin.publish() + + # Verify profiles were obtained from API + mock_conan_api.profiles.get_profiles_from_args.assert_called_once_with([]) + + def test_publish_upload_parameters( + self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + ) -> None: + """Test that publish upload is called with correct parameters + + Args: + plugin: The plugin instance + mock_conan_api: Mock ConanAPI + temp_conanfile: Fixture to create conanfile.py + mocker: Pytest mocker fixture + """ + # Set plugin to upload mode + plugin.data.local = False + + # Mock the necessary imports and API creation + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + + # Mock the dependencies graph + mock_graph = mocker.Mock() + mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + + # Mock remotes and package list + mock_remote = MagicMock() + mock_remote.name = 'origin' + remotes = [mock_remote] + mock_conan_api.remotes.list.return_value = remotes + + mock_package_list = MagicMock() + mock_package_list.recipes = ['test_package/1.0@user/channel'] + mock_conan_api.list.select.return_value = mock_package_list + + # Execute publish + plugin.publish() + + # Verify upload_full was called with correct parameters + mock_conan_api.upload.upload_full.assert_called_once_with( + package_list=mock_package_list, + remote=mock_remote, + enabled_remotes=remotes, + check_integrity=False, + force=False, + metadata=None, + dry_run=False, + ) + + def test_publish_list_pattern_creation( + self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + ) -> None: + """Test that publish creates correct ListPattern for package selection + + Args: + plugin: The plugin instance + mock_conan_api: Mock ConanAPI + temp_conanfile: Fixture to create conanfile.py + mocker: Pytest mocker fixture + """ + # Set plugin to upload mode + plugin.data.local = False + + # Mock the necessary imports and API creation + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mock_list_pattern = mocker.patch('cppython.plugins.conan.plugin.ListPattern') + + # Mock the dependencies graph + mock_graph = mocker.Mock() + mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + + # Execute publish + plugin.publish() + + # Get the ref from the export call to verify ListPattern creation + # The export call returns (ref, conanfile) - we need the ref.name + export_return = mock_conan_api.export.export.return_value + ref = export_return[0] # First element of the tuple + + # Verify ListPattern was created with correct reference pattern + mock_list_pattern.assert_called_once_with(f'{ref.name}/*', package_id='*', only_recipe=False) diff --git a/tests/unit/plugins/git/test_version_control.py b/tests/unit/plugins/git/test_version_control.py index 0d5cbc03..1c8c69c8 100644 --- a/tests/unit/plugins/git/test_version_control.py +++ b/tests/unit/plugins/git/test_version_control.py @@ -3,10 +3,10 @@ import pytest from cppython.plugins.git.plugin import GitSCM -from cppython.test.pytest.classes import SCMUnitTests +from cppython.test.pytest.contracts import SCMTestContract -class TestGitInterface(SCMUnitTests[GitSCM]): +class TestGitInterface(SCMTestContract[GitSCM]): """Unit tests for the Git SCM plugin""" @staticmethod diff --git a/tests/unit/plugins/vcpkg/test_provider.py b/tests/unit/plugins/vcpkg/test_provider.py index 7c117346..58c0fc4f 100644 --- a/tests/unit/plugins/vcpkg/test_provider.py +++ b/tests/unit/plugins/vcpkg/test_provider.py @@ -5,10 +5,10 @@ import pytest from cppython.plugins.vcpkg.plugin import VcpkgProvider -from cppython.test.pytest.classes import ProviderUnitTests +from cppython.test.pytest.contracts import ProviderTestContract -class TestCPPythonProvider(ProviderUnitTests[VcpkgProvider]): +class TestCPPythonProvider(ProviderTestContract[VcpkgProvider]): """The tests for the vcpkg Provider""" @staticmethod diff --git a/tests/unit/test/test_generator.py b/tests/unit/test/test_generator.py index a9b9b952..55c24b54 100644 --- a/tests/unit/test/test_generator.py +++ b/tests/unit/test/test_generator.py @@ -5,10 +5,10 @@ import pytest from cppython.test.mock.generator import MockGenerator -from cppython.test.pytest.classes import GeneratorUnitTests +from cppython.test.pytest.contracts import GeneratorTestContract -class TestCPPythonGenerator(GeneratorUnitTests[MockGenerator]): +class TestCPPythonGenerator(GeneratorTestContract[MockGenerator]): """The tests for the Mock generator""" @staticmethod diff --git a/tests/unit/test/test_provider.py b/tests/unit/test/test_provider.py index 73297bf7..72d7fb55 100644 --- a/tests/unit/test/test_provider.py +++ b/tests/unit/test/test_provider.py @@ -1,4 +1,8 @@ -"""Test the functions related to the internal provider implementation and the 'Provider' interface itself""" +"""Test functions related to the internal provider implementation. + +Test functions related to the internal provider implementation and the +'Provider' interface itself. +""" from typing import Any @@ -7,10 +11,10 @@ from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider -from cppython.test.pytest.classes import ProviderUnitTests +from cppython.test.pytest.contracts import ProviderTestContract -class TestMockProvider(ProviderUnitTests[MockProvider]): +class TestMockProvider(ProviderTestContract[MockProvider]): """The tests for our Mock provider""" @staticmethod diff --git a/tests/unit/test/test_scm.py b/tests/unit/test/test_scm.py index ef07354b..a05ece9d 100644 --- a/tests/unit/test/test_scm.py +++ b/tests/unit/test/test_scm.py @@ -5,10 +5,10 @@ import pytest from cppython.test.mock.scm import MockSCM -from cppython.test.pytest.classes import SCMUnitTests +from cppython.test.pytest.contracts import SCMTestContract -class TestCPPythonSCM(SCMUnitTests[MockSCM]): +class TestCPPythonSCM(SCMTestContract[MockSCM]): """The tests for the Mock version control""" @staticmethod From e603c78b021cfb5fa07efd367b41c40acafcc655 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 12 Jul 2025 20:43:01 -0400 Subject: [PATCH 09/16] Test Cleanup --- tests/unit/plugins/cmake/test_generator.py | 4 ++-- tests/unit/plugins/conan/test_provider.py | 4 ++-- tests/unit/plugins/git/test_version_control.py | 4 ++-- tests/unit/plugins/vcpkg/test_provider.py | 4 ++-- tests/unit/test/test_generator.py | 4 ++-- tests/unit/test/test_provider.py | 4 ++-- tests/unit/test/test_scm.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/unit/plugins/cmake/test_generator.py b/tests/unit/plugins/cmake/test_generator.py index a640efb7..c6e42215 100644 --- a/tests/unit/plugins/cmake/test_generator.py +++ b/tests/unit/plugins/cmake/test_generator.py @@ -8,12 +8,12 @@ from cppython.plugins.cmake.schema import ( CMakeConfiguration, ) -from cppython.test.pytest.contracts import GeneratorTestContract +from cppython.test.pytest.contracts import GeneratorUnitTestContract pytest_plugins = ['tests.fixtures.cmake'] -class TestCPPythonGenerator(GeneratorTestContract[CMakeGenerator]): +class TestCPPythonGenerator(GeneratorUnitTestContract[CMakeGenerator]): """The tests for the CMake generator""" @staticmethod diff --git a/tests/unit/plugins/conan/test_provider.py b/tests/unit/plugins/conan/test_provider.py index 7de6b9dd..28ebc094 100644 --- a/tests/unit/plugins/conan/test_provider.py +++ b/tests/unit/plugins/conan/test_provider.py @@ -5,10 +5,10 @@ import pytest from cppython.plugins.conan.plugin import ConanProvider -from cppython.test.pytest.contracts import ProviderTestContract +from cppython.test.pytest.contracts import ProviderUnitTestContract -class TestConanProvider(ProviderTestContract[ConanProvider]): +class TestConanProvider(ProviderUnitTestContract[ConanProvider]): """The tests for the Conan Provider""" @staticmethod diff --git a/tests/unit/plugins/git/test_version_control.py b/tests/unit/plugins/git/test_version_control.py index 1c8c69c8..e0c85cfd 100644 --- a/tests/unit/plugins/git/test_version_control.py +++ b/tests/unit/plugins/git/test_version_control.py @@ -3,10 +3,10 @@ import pytest from cppython.plugins.git.plugin import GitSCM -from cppython.test.pytest.contracts import SCMTestContract +from cppython.test.pytest.contracts import SCMUnitTestContract -class TestGitInterface(SCMTestContract[GitSCM]): +class TestGitInterface(SCMUnitTestContract[GitSCM]): """Unit tests for the Git SCM plugin""" @staticmethod diff --git a/tests/unit/plugins/vcpkg/test_provider.py b/tests/unit/plugins/vcpkg/test_provider.py index 58c0fc4f..a31d56e7 100644 --- a/tests/unit/plugins/vcpkg/test_provider.py +++ b/tests/unit/plugins/vcpkg/test_provider.py @@ -5,10 +5,10 @@ import pytest from cppython.plugins.vcpkg.plugin import VcpkgProvider -from cppython.test.pytest.contracts import ProviderTestContract +from cppython.test.pytest.contracts import ProviderUnitTestContract -class TestCPPythonProvider(ProviderTestContract[VcpkgProvider]): +class TestCPPythonProvider(ProviderUnitTestContract[VcpkgProvider]): """The tests for the vcpkg Provider""" @staticmethod diff --git a/tests/unit/test/test_generator.py b/tests/unit/test/test_generator.py index 55c24b54..ca2caa44 100644 --- a/tests/unit/test/test_generator.py +++ b/tests/unit/test/test_generator.py @@ -5,10 +5,10 @@ import pytest from cppython.test.mock.generator import MockGenerator -from cppython.test.pytest.contracts import GeneratorTestContract +from cppython.test.pytest.contracts import GeneratorUnitTestContract -class TestCPPythonGenerator(GeneratorTestContract[MockGenerator]): +class TestCPPythonGenerator(GeneratorUnitTestContract[MockGenerator]): """The tests for the Mock generator""" @staticmethod diff --git a/tests/unit/test/test_provider.py b/tests/unit/test/test_provider.py index 72d7fb55..2c56ede6 100644 --- a/tests/unit/test/test_provider.py +++ b/tests/unit/test/test_provider.py @@ -11,10 +11,10 @@ from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider -from cppython.test.pytest.contracts import ProviderTestContract +from cppython.test.pytest.contracts import ProviderUnitTestContract -class TestMockProvider(ProviderTestContract[MockProvider]): +class TestMockProvider(ProviderUnitTestContract[MockProvider]): """The tests for our Mock provider""" @staticmethod diff --git a/tests/unit/test/test_scm.py b/tests/unit/test/test_scm.py index a05ece9d..00135d67 100644 --- a/tests/unit/test/test_scm.py +++ b/tests/unit/test/test_scm.py @@ -5,10 +5,10 @@ import pytest from cppython.test.mock.scm import MockSCM -from cppython.test.pytest.contracts import SCMTestContract +from cppython.test.pytest.contracts import SCMUnitTestContract -class TestCPPythonSCM(SCMTestContract[MockSCM]): +class TestCPPythonSCM(SCMUnitTestContract[MockSCM]): """The tests for the Mock version control""" @staticmethod From de65a6a7b57579f3c0f7dea7eb3032b1c103240b Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 12 Jul 2025 20:56:00 -0400 Subject: [PATCH 10/16] Update Provider Usage --- cppython/plugins/conan/plugin.py | 74 ++- cppython/plugins/vcpkg/plugin.py | 29 +- cppython/project.py | 33 +- cppython/test/pytest/contracts.py | 132 +---- cppython/test/pytest/mixins.py | 235 +++++++- cppython/utility/exception.py | 152 +++++ examples/conan_cmake/simple/pdm.lock | 537 ++++++++++++++++++ tests/fixtures/conan.py | 165 ++++++ .../integration/examples/test_conan_cmake.py | 23 +- .../{test_interface.py => test_provider.py} | 0 tests/unit/plugins/conan/test_install.py | 202 +++++++ tests/unit/plugins/conan/test_publish.py | 186 +++--- tests/unit/plugins/conan/test_update.py | 41 ++ 13 files changed, 1516 insertions(+), 293 deletions(-) create mode 100644 examples/conan_cmake/simple/pdm.lock create mode 100644 tests/fixtures/conan.py rename tests/integration/plugins/conan/{test_interface.py => test_provider.py} (100%) create mode 100644 tests/unit/plugins/conan/test_install.py create mode 100644 tests/unit/plugins/conan/test_update.py diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 18f7adde..6c01c3b0 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -20,7 +20,7 @@ from cppython.plugins.conan.builder import Builder from cppython.plugins.conan.resolution import resolve_conan_data, resolve_conan_dependency from cppython.plugins.conan.schema import ConanData -from cppython.utility.exception import NotSupportedError +from cppython.utility.exception import NotSupportedError, ProviderConfigurationError, ProviderInstallationError from cppython.utility.utility import TypeName @@ -69,21 +69,64 @@ def information() -> Information: """ return Information() - def install(self) -> None: - """Installs the provider""" - resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] + def _install_dependencies(self, *, update: bool = False) -> None: + """Common implementation for installing/updating dependencies. + + Args: + update: If True, check remotes for newer versions/revisions and install those. + If False, use cached versions when available. + """ + try: + resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] + + self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies) + + self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True) + + # Install/update dependencies using Conan API + project_root = self.core_data.project_data.project_root + conanfile_path = project_root / 'conanfile.py' + + if conanfile_path.exists(): + # Initialize Conan API + conan_api = ConanAPI() + + # Get default profiles + profile_host_path = conan_api.profiles.get_default_host() + profile_build_path = conan_api.profiles.get_default_build() + profile_host = conan_api.profiles.get_profile([profile_host_path]) + profile_build = conan_api.profiles.get_profile([profile_build_path]) + + # Build dependency graph for the package + deps_graph = conan_api.graph.load_graph_consumer( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + profile_host=profile_host, + profile_build=profile_build, + lockfile=None, + remotes=conan_api.remotes.list(), + update=update, + check_updates=update, + is_build_require=False, + ) - self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies) + # Install dependencies + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=conan_api.remotes.list()) + except Exception as e: + operation = 'update' if update else 'install' + error_msg = str(e) + raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {error_msg}', e) from e - self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True) + def install(self) -> None: + """Installs the provider""" + self._install_dependencies(update=False) def update(self) -> None: """Updates the provider""" - resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] - - self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies) - - self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True) + self._install_dependencies(update=True) @staticmethod def supported_sync_type(sync_type: type[SyncData]) -> bool: @@ -148,7 +191,10 @@ def publish(self) -> None: ) # Step 2: Get default profiles - profile_host, profile_build = conan_api.profiles.get_profiles_from_args([]) + profile_host_path = conan_api.profiles.get_default_host() + profile_build_path = conan_api.profiles.get_default_build() + profile_host = conan_api.profiles.get_profile([profile_host_path]) + profile_build = conan_api.profiles.get_profile([profile_build_path]) # Step 3: Build dependency graph for the package deps_graph = conan_api.graph.load_graph_consumer( @@ -188,7 +234,7 @@ def publish(self) -> None: # Get the first configured remote or raise an error remotes = conan_api.remotes.list() if not remotes: - raise RuntimeError('No remotes configured for upload') + raise ProviderConfigurationError('conan', 'No remotes configured for upload', 'remotes') remote = remotes[0] # Use first remote @@ -203,4 +249,4 @@ def publish(self) -> None: dry_run=False, ) else: - raise RuntimeError('No packages found to upload') + raise ProviderInstallationError('conan', 'No packages found to upload') diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index 07eb209a..06522209 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -17,7 +17,7 @@ from cppython.plugins.cmake.schema import CMakeSyncData from cppython.plugins.vcpkg.resolution import generate_manifest, resolve_vcpkg_data from cppython.plugins.vcpkg.schema import VcpkgData -from cppython.utility.exception import NotSupportedError +from cppython.utility.exception import NotSupportedError, ProviderInstallationError, ProviderToolingError from cppython.utility.utility import TypeName @@ -92,10 +92,9 @@ def _update_provider(cls, path: Path) -> None: capture_output=True, ) except subprocess.CalledProcessError as e: - logger.error( - 'Unable to bootstrap the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e), exc_info=True - ) - raise + error_msg = e.stderr.decode() if e.stderr else str(e) + logger.error('Unable to bootstrap the vcpkg repository: %s', error_msg, exc_info=True) + raise ProviderToolingError('vcpkg', 'bootstrap', error_msg, e) from e def sync_data(self, consumer: SyncConsumer) -> SyncData: """Gathers a data object for the given generator @@ -167,8 +166,9 @@ async def download_tooling(cls, directory: Path) -> None: capture_output=True, ) except subprocess.CalledProcessError as e: - logger.exception('Unable to update the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e)) - raise + error_msg = e.stderr.decode() if e.stderr else str(e) + logger.error('Unable to update the vcpkg repository: %s', error_msg, exc_info=True) + raise ProviderToolingError('vcpkg', 'update', error_msg, e) from e else: try: logger.debug("Cloning the vcpkg repository to '%s'", directory.absolute()) @@ -182,8 +182,9 @@ async def download_tooling(cls, directory: Path) -> None: ) except subprocess.CalledProcessError as e: - logger.exception('Unable to clone the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e)) - raise + error_msg = e.stderr.decode() if e.stderr else str(e) + logger.error('Unable to clone the vcpkg repository: %s', error_msg, exc_info=True) + raise ProviderToolingError('vcpkg', 'clone', error_msg, e) from e cls._update_provider(directory) @@ -210,8 +211,9 @@ def install(self) -> None: capture_output=True, ) except subprocess.CalledProcessError as e: - logger.exception('Unable to install project dependencies: %s', e.stderr.decode() if e.stderr else str(e)) - raise + error_msg = e.stderr.decode() if e.stderr else str(e) + logger.error('Unable to install project dependencies: %s', error_msg, exc_info=True) + raise ProviderInstallationError('vcpkg', error_msg, e) from e def update(self) -> None: """Called when dependencies need to be updated and written to the lock file.""" @@ -237,8 +239,9 @@ def update(self) -> None: capture_output=True, ) except subprocess.CalledProcessError as e: - logger.exception('Unable to install project dependencies: %s', e.stderr.decode() if e.stderr else str(e)) - raise + error_msg = e.stderr.decode() if e.stderr else str(e) + logger.error('Unable to update project dependencies: %s', error_msg, exc_info=True) + raise ProviderInstallationError('vcpkg', error_msg, e) from e def publish(self) -> None: """Called when the project needs to be published. diff --git a/cppython/project.py b/cppython/project.py index 411b0236..0ac3c9ea 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -56,7 +56,7 @@ def install(self) -> None: """Installs project dependencies Raises: - Exception: Raised if failed + Exception: Provider-specific exceptions are propagated with full context """ if not self._enabled: self.logger.info('Skipping install because the project is not enabled') @@ -68,19 +68,15 @@ def install(self) -> None: self.logger.info('Installing project') self.logger.info('Installing %s provider', self._data.plugins.provider.name()) - try: - self._data.plugins.provider.install() - except Exception as exception: - self.logger.error('Unexpected error during installation: %s', str(exception)) - raise SystemExit('Error: An unexpected error occurred during installation.') from None - + # Let provider handle its own exceptions for better error context + self._data.plugins.provider.install() self._data.sync() def update(self) -> None: """Updates project dependencies Raises: - Exception: Raised if failed + Exception: Provider-specific exception """ if not self._enabled: self.logger.info('Skipping update because the project is not enabled') @@ -92,18 +88,15 @@ def update(self) -> None: self.logger.info('Updating project') self.logger.info('Updating %s provider', self._data.plugins.provider.name()) - try: - self._data.plugins.provider.update() - except Exception as exception: - self.logger.error('Unexpected error during update: %s', str(exception)) - raise SystemExit('Error: An unexpected error occurred during update.') from None - + # Let provider handle its own exceptions for better error context + self._data.plugins.provider.update() self._data.sync() def publish(self) -> None: - """Publishes the project""" - try: - self._data.plugins.provider.publish() - except Exception as exception: - self.logger.error('Unexpected error during publish: %s', str(exception)) - raise SystemExit('Error: An unexpected error occurred during publish.') from None + """Publishes the project + + Raises: + Exception: Provider-specific exception + """ + # Let provider handle its own exceptions for better error context + self._data.plugins.provider.publish() diff --git a/cppython/test/pytest/contracts.py b/cppython/test/pytest/contracts.py index bf63c804..2c6f0f53 100644 --- a/cppython/test/pytest/contracts.py +++ b/cppython/test/pytest/contracts.py @@ -9,34 +9,31 @@ """ import asyncio -from abc import ABCMeta +from abc import ABC from importlib.metadata import entry_points from pathlib import Path from typing import Any, LiteralString import pytest -from cppython.core.plugin_schema.generator import Generator, GeneratorPluginGroupData -from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData -from cppython.core.plugin_schema.scm import SCM, SCMPluginGroupData -from cppython.core.resolution import resolve_generator, resolve_provider, resolve_scm +from cppython.core.plugin_schema.generator import Generator +from cppython.core.plugin_schema.provider import Provider +from cppython.core.plugin_schema.scm import SCM from cppython.core.schema import ( CorePluginData, - CPPythonPluginData, DataPluginGroupData, Plugin, ProjectConfiguration, - ProjectData, ) -from cppython.test.data.mocks import generator_variants, provider_variants, scm_variants from cppython.test.pytest.mixins import ( - DataPluginTestMixin, - PluginTestMixin, + GeneratorPluginTestMixin, + ProviderPluginTestMixin, + SCMPluginTestMixin, ) from cppython.utility.utility import canonicalize_type -class PluginTestValidation: +class _PluginValidation: """Common validation tests that can be applied to any plugin. These are generic tests that validate basic plugin behavior regardless @@ -76,7 +73,7 @@ def test_plugin_name_extraction(plugin_type: type[Plugin]) -> None: assert len(plugin_type.name()) -class DataPluginTestValidation(PluginTestValidation): +class _DataPluginValidation(_PluginValidation): """Validation tests specific to data plugins. These tests validate that data plugins can handle various configuration @@ -100,48 +97,15 @@ def test_empty_data_construction( assert plugin, 'The plugin should be able to be constructed with empty data' -class ProviderTestContract[T: Provider](DataPluginTestMixin[T], DataPluginTestValidation, metaclass=ABCMeta): +class ProviderUnitTestContract[T: Provider](ProviderPluginTestMixin[T], _DataPluginValidation, ABC): """Test contract for Provider plugins. Each Provider plugin should have exactly one test class that inherits from this to ensure it fulfills all Provider testing requirements. """ - @staticmethod - @pytest.fixture(name='plugin_configuration_type', scope='session') - def fixture_plugin_configuration_type() -> type[ProviderPluginGroupData]: - """Required hook for Provider plugin configuration data generation""" - return ProviderPluginGroupData - - @staticmethod - @pytest.fixture(name='plugin_group_data') - def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData - ) -> ProviderPluginGroupData: - """Generate Provider plugin configuration data""" - return resolve_provider(project_data=project_data, cppython_data=cppython_plugin_data) - - # Cross-plugin testing fixtures for ensuring compatibility - @staticmethod - @pytest.fixture(name='provider_type', scope='session', params=provider_variants) - def fixture_provider_type(plugin_type: type[T]) -> type[T]: - """Return this provider type for cross-plugin testing""" - return plugin_type - - @staticmethod - @pytest.fixture(name='generator_type', scope='session', params=generator_variants) - def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: - """Provide generator variants for cross-plugin testing""" - return request.param - - @staticmethod - @pytest.fixture(name='scm_type', scope='session', params=scm_variants) - def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: - """Provide SCM variants for cross-plugin testing""" - return request.param - -class ProviderIntegrationTestContract[T: Provider](ProviderTestContract[T], metaclass=ABCMeta): +class ProviderIntegrationTestContract[T: Provider](ProviderPluginTestMixin[T], ABC): """Integration test contract for Provider plugins. Providers that need integration testing should inherit from this contract. @@ -184,48 +148,15 @@ def test_group_name(plugin_type: type[T]) -> None: assert canonicalize_type(plugin_type).group == 'provider' -class GeneratorTestContract[T: Generator](DataPluginTestMixin[T], DataPluginTestValidation, metaclass=ABCMeta): +class GeneratorUnitTestContract[T: Generator](GeneratorPluginTestMixin[T], _DataPluginValidation, ABC): """Test contract for Generator plugins. Each Generator plugin should have exactly one test class that inherits from this to ensure it fulfills all Generator testing requirements. """ - @staticmethod - @pytest.fixture(name='plugin_configuration_type', scope='session') - def fixture_plugin_configuration_type() -> type[GeneratorPluginGroupData]: - """Required hook for Generator plugin configuration data generation""" - return GeneratorPluginGroupData - @staticmethod - @pytest.fixture(name='plugin_group_data') - def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData - ) -> GeneratorPluginGroupData: - """Generate Generator plugin configuration data""" - return resolve_generator(project_data=project_data, cppython_data=cppython_plugin_data) - - # Cross-plugin testing fixtures for ensuring compatibility - @staticmethod - @pytest.fixture(name='provider_type', scope='session', params=provider_variants) - def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: - """Provide provider variants for cross-plugin testing""" - return request.param - - @staticmethod - @pytest.fixture(name='generator_type', scope='session') - def fixture_generator_type(plugin_type: type[T]) -> type[T]: - """Return this generator type for cross-plugin testing""" - return plugin_type - - @staticmethod - @pytest.fixture(name='scm_type', scope='session', params=scm_variants) - def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: - """Provide SCM variants for cross-plugin testing""" - return request.param - - -class GeneratorIntegrationTestContract[T: Generator](GeneratorTestContract[T], metaclass=ABCMeta): +class GeneratorIntegrationTestContract[T: Generator](GeneratorPluginTestMixin[T], ABC): """Integration test contract for Generator plugins. Generators that need integration testing should inherit from this contract. @@ -249,48 +180,15 @@ def test_group_name(plugin_type: type[T]) -> None: assert canonicalize_type(plugin_type).group == 'generator' -class SCMTestContract[T: SCM](PluginTestMixin[T], PluginTestValidation, metaclass=ABCMeta): +class SCMUnitTestContract[T: SCM](SCMPluginTestMixin[T], _PluginValidation, ABC): """Test contract for SCM plugins. Each SCM plugin should have exactly one test class that inherits from this to ensure it fulfills all SCM testing requirements. """ - @staticmethod - @pytest.fixture(name='plugin_configuration_type', scope='session') - def fixture_plugin_configuration_type() -> type[SCMPluginGroupData]: - """Required hook for SCM plugin configuration data generation""" - return SCMPluginGroupData - - @staticmethod - @pytest.fixture(name='plugin_group_data') - def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData - ) -> SCMPluginGroupData: - """Generate SCM plugin configuration data""" - return resolve_scm(project_data=project_data, cppython_data=cppython_plugin_data) - - # Cross-plugin testing fixtures for ensuring compatibility - @staticmethod - @pytest.fixture(name='provider_type', scope='session', params=provider_variants) - def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: - """Provide provider variants for cross-plugin testing""" - return request.param - - @staticmethod - @pytest.fixture(name='generator_type', scope='session', params=generator_variants) - def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: - """Provide generator variants for cross-plugin testing""" - return request.param - - @staticmethod - @pytest.fixture(name='scm_type', scope='session', params=scm_variants) - def fixture_scm_type(plugin_type: type[T]) -> type[T]: - """Return this SCM type for cross-plugin testing""" - return plugin_type - -class SCMIntegrationTestContract[T: SCM](SCMTestContract[T], metaclass=ABCMeta): +class SCMIntegrationTestContract[T: SCM](SCMPluginTestMixin[T], ABC): """Integration test contract for SCM plugins. SCM plugins that need integration testing should inherit from this contract. diff --git a/cppython/test/pytest/mixins.py b/cppython/test/pytest/mixins.py index a51c2f8c..87fc628a 100644 --- a/cppython/test/pytest/mixins.py +++ b/cppython/test/pytest/mixins.py @@ -5,12 +5,20 @@ as needed, not inherited in a strict hierarchy. """ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from typing import Any, LiteralString import pytest -from cppython.core.resolution import resolve_cppython_plugin +from cppython.core.plugin_schema.generator import Generator, GeneratorPluginGroupData +from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData +from cppython.core.plugin_schema.scm import SCM, SCMPluginGroupData +from cppython.core.resolution import ( + resolve_cppython_plugin, + resolve_generator, + resolve_provider, + resolve_scm, +) from cppython.core.schema import ( CorePluginData, CPPythonData, @@ -22,9 +30,10 @@ PluginGroupData, ProjectData, ) +from cppython.test.data.mocks import generator_variants, provider_variants, scm_variants -class TestMixin[T: Plugin](metaclass=ABCMeta): +class TestMixin[T: Plugin](ABC): """Core mixin that provides basic plugin construction capabilities. Any test class can inherit from this to get access to standard plugin @@ -94,7 +103,7 @@ def fixture_core_plugin_data( return CorePluginData(cppython_data=cppython_plugin_data, project_data=project_data, pep621_data=pep621_data) -class PluginTestMixin[T: Plugin](TestMixin[T], metaclass=ABCMeta): +class PluginTestMixin[T: Plugin](TestMixin[T], ABC): """Plugin construction mixin for simple plugins. Provides plugin instance creation for plugins that don't need complex @@ -116,7 +125,7 @@ def fixture_plugin(plugin_type: type[T], plugin_group_data: PluginGroupData) -> return plugin_type(plugin_group_data) -class DataPluginTestMixin[T: DataPlugin](TestMixin[T], metaclass=ABCMeta): +class DataPluginTestMixin[T: DataPlugin](TestMixin[T], ABC): """Data plugin construction mixin for complex plugins. Provides plugin instance creation for plugins that need rich configuration @@ -143,3 +152,219 @@ def fixture_plugin( A newly constructed provider """ return plugin_type(plugin_group_data, core_plugin_data, plugin_data) + + +class ProviderPluginTestMixin[T: Provider](DataPluginTestMixin[T], ABC): + """Data plugin construction mixin specifically for Provider plugins. + + Provides all necessary fixtures for Provider plugin testing, including + the plugin_group_data fixture that creates ProviderPluginGroupData. + """ + + @staticmethod + @pytest.fixture(name='plugin_group_data') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> ProviderPluginGroupData: + """Generate Provider plugin configuration data + + Args: + project_data: The project data + cppython_plugin_data: CPPython plugin data + + Returns: + Provider plugin group data + """ + return resolve_provider(project_data=project_data, cppython_data=cppython_plugin_data) + + # Cross-plugin testing fixtures for ensuring compatibility + @staticmethod + @pytest.fixture(name='provider_type', scope='session') + def fixture_provider_type(plugin_type: type[T]) -> type[T]: + """Return this provider type for cross-plugin testing + + Args: + plugin_type: The provider plugin type being tested + + Returns: + The same provider type + """ + return plugin_type + + @staticmethod + @pytest.fixture(name='generator_type', scope='session') + def fixture_generator_type(request: 'pytest.FixtureRequest') -> type[Generator]: + """Provide generator variants for cross-plugin testing + + Args: + request: Pytest fixture request + + Returns: + Generator type for testing + """ + # Use the first generator variant for testing + return generator_variants[0] + + @staticmethod + @pytest.fixture(name='scm_type', scope='session') + def fixture_scm_type(request: 'pytest.FixtureRequest') -> type[SCM]: + """Provide SCM variants for cross-plugin testing + + Args: + request: Pytest fixture request + + Returns: + SCM type for testing + """ + # Use the first SCM variant for testing + return scm_variants[0] + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[ProviderPluginGroupData]: + """Required hook for Provider plugin configuration data generation""" + return ProviderPluginGroupData + + +class GeneratorPluginTestMixin[T: Generator](DataPluginTestMixin[T], ABC): + """Data plugin construction mixin specifically for Generator plugins. + + Provides all necessary fixtures for Generator plugin testing, including + the plugin_group_data fixture that creates GeneratorPluginGroupData. + """ + + @staticmethod + @pytest.fixture(name='plugin_group_data') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> GeneratorPluginGroupData: + """Generate Generator plugin configuration data + + Args: + project_data: The project data + cppython_plugin_data: CPPython plugin data + + Returns: + Generator plugin group data + """ + return resolve_generator(project_data=project_data, cppython_data=cppython_plugin_data) + + # Cross-plugin testing fixtures for ensuring compatibility + @staticmethod + @pytest.fixture(name='provider_type', scope='session') + def fixture_provider_type(request: 'pytest.FixtureRequest') -> type[Provider]: + """Provide provider variants for cross-plugin testing + + Args: + request: Pytest fixture request + + Returns: + Provider type for testing + """ + # Use the first provider variant for testing + return provider_variants[0] + + @staticmethod + @pytest.fixture(name='generator_type', scope='session') + def fixture_generator_type(plugin_type: type[T]) -> type[T]: + """Return this generator type for cross-plugin testing + + Args: + plugin_type: The generator plugin type being tested + + Returns: + The same generator type + """ + return plugin_type + + @staticmethod + @pytest.fixture(name='scm_type', scope='session') + def fixture_scm_type(request: 'pytest.FixtureRequest') -> type[SCM]: + """Provide SCM variants for cross-plugin testing + + Args: + request: Pytest fixture request + + Returns: + SCM type for testing + """ + # Use the first SCM variant for testing + return scm_variants[0] + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[GeneratorPluginGroupData]: + """Required hook for Generator plugin configuration data generation""" + return GeneratorPluginGroupData + + +class SCMPluginTestMixin[T: SCM](PluginTestMixin[T], ABC): + """Plugin construction mixin specifically for SCM plugins. + + Provides all necessary fixtures for SCM plugin testing, including + the plugin_group_data fixture that creates SCMPluginGroupData. + """ + + @staticmethod + @pytest.fixture(name='plugin_group_data') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> SCMPluginGroupData: + """Generate SCM plugin configuration data + + Args: + project_data: The project data + cppython_plugin_data: CPPython plugin data + + Returns: + SCM plugin group data + """ + return resolve_scm(project_data=project_data, cppython_data=cppython_plugin_data) + + # Cross-plugin testing fixtures for ensuring compatibility + @staticmethod + @pytest.fixture(name='provider_type', scope='session') + def fixture_provider_type(request: 'pytest.FixtureRequest') -> type[Provider]: + """Provide provider variants for cross-plugin testing + + Args: + request: Pytest fixture request + + Returns: + Provider type for testing + """ + # Use the first provider variant for testing + return provider_variants[0] + + @staticmethod + @pytest.fixture(name='generator_type', scope='session') + def fixture_generator_type(request: 'pytest.FixtureRequest') -> type[Generator]: + """Provide generator variants for cross-plugin testing + + Args: + request: Pytest fixture request + + Returns: + Generator type for testing + """ + # Use the first generator variant for testing + return generator_variants[0] + + @staticmethod + @pytest.fixture(name='scm_type', scope='session') + def fixture_scm_type(plugin_type: type[T]) -> type[T]: + """Return this SCM type for cross-plugin testing + + Args: + plugin_type: The SCM plugin type being tested + + Returns: + The same SCM type + """ + return plugin_type + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[SCMPluginGroupData]: + """Required hook for SCM plugin configuration data generation""" + return SCMPluginGroupData diff --git a/cppython/utility/exception.py b/cppython/utility/exception.py index 3f90fb7f..bee69599 100644 --- a/cppython/utility/exception.py +++ b/cppython/utility/exception.py @@ -45,3 +45,155 @@ def error(self) -> str: str -- The underlying error """ return self._error + + +class ProviderInstallationError(Exception): + """Raised when provider installation fails""" + + def __init__(self, provider_name: str, error: str, original_error: Exception | None = None) -> None: + """Initializes the error + + Args: + provider_name: The name of the provider that failed + error: The error message + original_error: The original exception that caused this error + """ + self._provider_name = provider_name + self._error = error + self._original_error = original_error + + message = f"Provider '{provider_name}' installation failed: {error}" + super().__init__(message) + + @property + def provider_name(self) -> str: + """Returns the provider name + + Returns: + str -- The provider name + """ + return self._provider_name + + @property + def error(self) -> str: + """Returns the underlying error + + Returns: + str -- The underlying error + """ + return self._error + + @property + def original_error(self) -> Exception | None: + """Returns the original exception + + Returns: + Exception | None -- The original exception if available + """ + return self._original_error + + +class ProviderConfigurationError(Exception): + """Raised when provider configuration is invalid""" + + def __init__(self, provider_name: str, error: str, configuration_key: str | None = None) -> None: + """Initializes the error + + Args: + provider_name: The name of the provider with invalid configuration + error: The error message + configuration_key: The specific configuration key that caused the error + """ + self._provider_name = provider_name + self._error = error + self._configuration_key = configuration_key + + message = f"Provider '{provider_name}' configuration error" + if configuration_key: + message += f" in '{configuration_key}'" + message += f': {error}' + super().__init__(message) + + @property + def provider_name(self) -> str: + """Returns the provider name + + Returns: + str -- The provider name + """ + return self._provider_name + + @property + def error(self) -> str: + """Returns the underlying error + + Returns: + str -- The underlying error + """ + return self._error + + @property + def configuration_key(self) -> str | None: + """Returns the configuration key + + Returns: + str | None -- The configuration key if available + """ + return self._configuration_key + + +class ProviderToolingError(Exception): + """Raised when provider tooling operations fail""" + + def __init__(self, provider_name: str, operation: str, error: str, original_error: Exception | None = None) -> None: + """Initializes the error + + Args: + provider_name: The name of the provider that failed + operation: The operation that failed (e.g., 'download', 'bootstrap', 'install') + error: The error message + original_error: The original exception that caused this error + """ + self._provider_name = provider_name + self._operation = operation + self._error = error + self._original_error = original_error + + message = f"Provider '{provider_name}' {operation} failed: {error}" + super().__init__(message) + + @property + def provider_name(self) -> str: + """Returns the provider name + + Returns: + str -- The provider name + """ + return self._provider_name + + @property + def operation(self) -> str: + """Returns the operation that failed + + Returns: + str -- The operation name + """ + return self._operation + + @property + def error(self) -> str: + """Returns the underlying error + + Returns: + str -- The underlying error + """ + return self._error + + @property + def original_error(self) -> Exception | None: + """Returns the original exception + + Returns: + Exception | None -- The original exception if available + """ + return self._original_error diff --git a/examples/conan_cmake/simple/pdm.lock b/examples/conan_cmake/simple/pdm.lock new file mode 100644 index 00000000..1eb1bc96 --- /dev/null +++ b/examples/conan_cmake/simple/pdm.lock @@ -0,0 +1,537 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = [] +lock_version = "4.5.0" +content_hash = "sha256:618cf02c62d23783da0e2d36b9ea92bec82152cbc38a811e71f5dcf02c5eeffc" + +[[metadata.targets]] +requires_python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +summary = "" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "certifi" +version = "2025.7.9" +summary = "" +files = [ + {file = "certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39"}, + {file = "certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +summary = "" +files = [ + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] + +[[package]] +name = "click" +version = "8.2.1" +summary = "" +dependencies = [ + "colorama; sys_platform == \"win32\"", +] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[[package]] +name = "cmake" +version = "4.0.3" +summary = "" +files = [ + {file = "cmake-4.0.3-py3-none-macosx_10_10_universal2.whl", hash = "sha256:f2adfb459747025f40f9d3bdd1f3a485d43e866c0c4eb66373d1fcd666b13e4a"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04c40c92fdcaa96c66a5731b5b3fbbdf87da99cc68fdd30ff30b90c34d222986"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d41b83d061bcc375a7a5f2942ba523a7563368d296d91260f9d8a53a10f5e5e5"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:434f84fdf1e21578974876b8414dc47afeaea62027d9adc37a943a6bb08eb053"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beec48371a4b906fe398758ded5df57fc16e9bb14fd34244d9d66ee35862fb9f"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47dc28bee6cfb4de00c7cf7e87d565b5c86eb4088da81b60a49e214fcdd4ffda"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10fdc972b3211915b65cc89e8cd24e1a26c9bd684ee71c3f369fb488f2c4388"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d840e780c48c5df1330879d50615176896e8e6eee554507d21ce8e2f1a5f0ff8"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:6ef63bbabcbe3b89c1d80547913b6caceaad57987a27e7afc79ebc88ecd829e4"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:67103f2bcce8f57b8705ba8e353f18fdc3684a346eee97dc5f94d11575a424c6"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:880a1e1ae26d440d7e4f604fecbf839728ca7b096c870f2e7359855cc4828532"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:c403b660bbff1fd4d7f1c5d9e015ea27566e49ca9461e260c9758f2fd4e5e813"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:2a66ecdd4c3238484cb0c377d689c086a9b8b533e25329f73d21bd1c38f1ae86"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:004e58b1a1a384c2ca799c9c41ac4ed86ac3b80129462992c43c1121f8729ffd"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:133dbc33f995cb97a4456d83d67fa0a7a798f53f979454359140588baa928f43"}, + {file = "cmake-4.0.3-py3-none-win32.whl", hash = "sha256:3e07bdd14e69ea67d1e67a4f5225ac2fd91ee9e349c440143cdddd7368be1f46"}, + {file = "cmake-4.0.3-py3-none-win_amd64.whl", hash = "sha256:9a349ff2b4a7c63c896061676bc0f4e6994f373d54314d79ba3608ee7fa75442"}, + {file = "cmake-4.0.3-py3-none-win_arm64.whl", hash = "sha256:94a52e67b264a51089907c9e74ca5a9e2f3e65c57c457e0f40f02629a0de74d8"}, + {file = "cmake-4.0.3.tar.gz", hash = "sha256:215732f09ea8a7088fe1ab46bbd61669437217278d709fd3851bf8211e8c59e3"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +summary = "" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "conan" +version = "2.18.1" +summary = "" +dependencies = [ + "colorama", + "distro; platform_system == \"FreeBSD\" or sys_platform == \"linux\"", + "fasteners", + "jinja2", + "patch-ng", + "python-dateutil", + "pyyaml", + "requests", + "urllib3", +] +files = [ + {file = "conan-2.18.1.tar.gz", hash = "sha256:5d8e9fac7614de9297933f65de8f17db14851a871cebc962f4856b7c294f43c5"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +summary = "" +dependencies = [ + "packaging", + "pydantic", + "requests", + "typer", + "types-requests", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +extras = ["cmake"] +summary = "" +dependencies = [ + "cmake", + "cppython==0.9.2", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +extras = ["conan"] +summary = "" +dependencies = [ + "conan", + "cppython==0.9.2", + "libcst", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +extras = ["git"] +summary = "" +dependencies = [ + "cppython==0.9.2", + "dulwich", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "distro" +version = "1.8.0" +summary = "" +files = [ + {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, + {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, +] + +[[package]] +name = "dulwich" +version = "0.23.2" +summary = "" +dependencies = [ + "urllib3", +] +files = [ + {file = "dulwich-0.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e44dec7e36bc035da0ec3df6c1564810699e319ba41b71d17750dd7452e1b2fc"}, + {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:398ba1c0e1581071cdcb38a681e0ff1e046aa8f31bad3bc368266f499c4ddf9e"}, + {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:432c6eeac5edf97ff7090fbac7cda708167ee90e5afa78652d252e87e397f425"}, + {file = "dulwich-0.23.2-cp313-cp313-win32.whl", hash = "sha256:8555980e8509d7f76e80de58d1eb7bd2c1c317942b7a3c9c113d81dfc287f4c0"}, + {file = "dulwich-0.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:2b042dca31de4d4a0e88e4dbe20afe804a640c8882eec0de5093bffb34b75370"}, + {file = "dulwich-0.23.2-py3-none-any.whl", hash = "sha256:0b0439d309cf808f7955f74776981d9ac9dc1ec715aa39798de9b22bb95ac163"}, + {file = "dulwich-0.23.2.tar.gz", hash = "sha256:a152ebb0e95bc0f23768be563f80ff1e719bf5c4f5c2696be4fa8ab625a39879"}, +] + +[[package]] +name = "fasteners" +version = "0.19" +summary = "" +files = [ + {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, + {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, +] + +[[package]] +name = "idna" +version = "3.10" +summary = "" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +summary = "" +dependencies = [ + "markupsafe", +] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[[package]] +name = "libcst" +version = "1.8.2" +summary = "" +dependencies = [ + "pyyaml-ft", +] +files = [ + {file = "libcst-1.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08e9dca4ab6f8551794ce7ec146f86def6a82da41750cbed2c07551345fa10d3"}, + {file = "libcst-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8310521f2ccb79b5c4345750d475b88afa37bad930ab5554735f85ad5e3add30"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:da2d8b008aff72acd5a4a588491abdda1b446f17508e700f26df9be80d8442ae"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:be821d874ce8b26cbadd7277fa251a9b37f6d2326f8b5682b6fc8966b50a3a59"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f74b0bc7378ad5afcf25ac9d0367b4dbba50f6f6468faa41f5dfddcf8bf9c0f8"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b68ea4a6018abfea1f68d50f74de7d399172684c264eb09809023e2c8696fc23"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e264307ec49b2c72480422abafe80457f90b4e6e693b7ddf8a23d24b5c24001"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5d5519962ce7c72d81888fb0c09e58e308ba4c376e76bcd853b48151063d6a8"}, + {file = "libcst-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:b62aa11d6b74ed5545e58ac613d3f63095e5fd0254b3e0d1168fda991b9a6b41"}, + {file = "libcst-1.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9c2bd4ac288a9cdb7ffc3229a9ce8027a66a3fd3f2ab9e13da60f5fbfe91f3b2"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:08a8c7d9922ca6eed24e2c13a3c552b3c186af8fc78e5d4820b58487d780ec19"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bba7c2b5063e8ada5a5477f9fa0c01710645426b5a8628ec50d558542a0a292e"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d97c9fe13aacfbefded6861f5200dcb8e837da7391a9bdeb44ccb133705990af"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d2194ae959630aae4176a4b75bd320b3274c20bef2a5ca6b8d6fc96d3c608edf"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0be639f5b2e1999a4b4a82a0f4633969f97336f052d0c131627983589af52f56"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6753e50904e05c27915933da41518ecd7a8ca4dd3602112ba44920c6e353a455"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:706d07106af91c343150be86caeae1ea3851b74aa0730fcbbf8cd089e817f818"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd4310ea8ddc49cc8872e083737cf806299b17f93159a1f354d59aa08993e876"}, + {file = "libcst-1.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:51bbafdd847529e8a16d1965814ed17831af61452ee31943c414cb23451de926"}, + {file = "libcst-1.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:4f14f5045766646ed9e8826b959c6d07194788babed1e0ba08c94ea4f39517e3"}, + {file = "libcst-1.8.2.tar.gz", hash = "sha256:66e82cedba95a6176194a817be4232c720312f8be6d2c8f3847f3317d95a0c7f"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +summary = "" +dependencies = [ + "mdurl", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +summary = "" +files = [ + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +summary = "" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "packaging" +version = "25.0" +summary = "" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "patch-ng" +version = "1.18.1" +summary = "" +files = [ + {file = "patch-ng-1.18.1.tar.gz", hash = "sha256:52fd46ee46f6c8667692682c1fd7134edc65a2d2d084ebec1d295a6087fc0291"}, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +summary = "" +dependencies = [ + "annotated-types", + "pydantic-core", + "typing-extensions", + "typing-inspection", +] +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +summary = "" +dependencies = [ + "typing-extensions", +] +files = [ + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +summary = "" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +summary = "" +dependencies = [ + "six", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +summary = "" +files = [ + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +summary = "" +files = [ + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, + {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +summary = "" +dependencies = [ + "certifi", + "charset-normalizer", + "idna", + "urllib3", +] +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[[package]] +name = "rich" +version = "14.0.0" +summary = "" +dependencies = [ + "markdown-it-py", + "pygments", +] +files = [ + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +summary = "" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +summary = "" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "typer" +version = "0.16.0" +summary = "" +dependencies = [ + "click", + "rich", + "shellingham", + "typing-extensions", +] +files = [ + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +summary = "" +dependencies = [ + "urllib3", +] +files = [ + {file = "types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072"}, + {file = "types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +summary = "" +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +summary = "" +dependencies = [ + "typing-extensions", +] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +summary = "" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py new file mode 100644 index 00000000..7286ebe4 --- /dev/null +++ b/tests/fixtures/conan.py @@ -0,0 +1,165 @@ +"""Shared fixtures for Conan plugin tests""" + +from pathlib import Path +from unittest.mock import Mock + +import pytest +from packaging.requirements import Requirement +from pytest_mock import MockerFixture + +from cppython.plugins.conan.plugin import ConanProvider +from cppython.plugins.conan.schema import ConanDependency + + +@pytest.fixture(name='conan_mock_api') +def fixture_conan_mock_api(mocker: MockerFixture) -> Mock: + """Creates a mock ConanAPI instance for install/update operations + + Args: + mocker: Pytest mocker fixture + + Returns: + Mock ConanAPI instance + """ + mock_api = mocker.Mock() + + # Mock graph module + mock_deps_graph = mocker.Mock() + mock_api.graph.load_graph_consumer = mocker.Mock(return_value=mock_deps_graph) + + # Mock install module + mock_api.install.install_binaries = mocker.Mock() + + # Mock remotes module + mock_remote = mocker.Mock() + mock_remote.name = 'conancenter' + mock_api.remotes.list = mocker.Mock(return_value=[mock_remote]) + + # Mock profiles module + mock_profile_host = mocker.Mock() + mock_profile_build = mocker.Mock() + mock_api.profiles.get_default_host = mocker.Mock(return_value='/path/to/default/host') + mock_api.profiles.get_default_build = mocker.Mock(return_value='/path/to/default/build') + mock_api.profiles.get_profile = mocker.Mock( + side_effect=lambda paths: mock_profile_host if 'host' in paths[0] else mock_profile_build + ) + + return mock_api + + +@pytest.fixture(name='conan_mock_api_publish') +def fixture_conan_mock_api_publish(mocker: MockerFixture) -> Mock: + """Creates a mock ConanAPI instance for publish operations + + Args: + mocker: Pytest mocker fixture + + Returns: + Mock ConanAPI instance configured for publish operations + """ + mock_api = mocker.Mock() + + # Mock export module - export returns a tuple (ref, conanfile) + mock_ref = mocker.Mock() + mock_ref.name = 'test_package' + mock_conanfile = mocker.Mock() + mock_api.export.export = mocker.Mock(return_value=(mock_ref, mock_conanfile)) + + # Mock graph module + mock_api.graph.load_graph_consumer = mocker.Mock() + mock_api.graph.analyze_binaries = mocker.Mock() + + # Mock install module + mock_api.install.install_binaries = mocker.Mock() + + # Mock list module + mock_select_result = mocker.Mock() + mock_select_result.recipes = ['some_package/1.0@user/channel'] + mock_api.list.select = mocker.Mock(return_value=mock_select_result) + + # Mock remotes module + mock_remote = mocker.Mock() + mock_remote.name = 'origin' + mock_api.remotes.list = mocker.Mock(return_value=[mock_remote]) + + # Mock upload module + mock_api.upload.upload_full = mocker.Mock() + + # Mock profiles module + mock_profile = mocker.Mock() + mock_api.profiles.get_default_host = mocker.Mock(return_value='/path/to/default/host') + mock_api.profiles.get_default_build = mocker.Mock(return_value='/path/to/default/build') + mock_api.profiles.get_profile = mocker.Mock(return_value=mock_profile) + + return mock_api + + +@pytest.fixture(name='conan_temp_conanfile') +def fixture_conan_temp_conanfile(plugin: ConanProvider) -> Path: + """Creates a temporary conanfile.py for testing + + Args: + plugin: The plugin instance + + Returns: + Path to the created conanfile.py + """ + project_root = plugin.core_data.project_data.project_root + conanfile_path = project_root / 'conanfile.py' + conanfile_path.write_text( + 'from conan import ConanFile\n\nclass TestConan(ConanFile):\n name = "test_package"\n version = "1.0"\n' + ) + return conanfile_path + + +@pytest.fixture(name='conan_mock_dependencies') +def fixture_conan_mock_dependencies() -> list[Requirement]: + """Creates mock dependencies for testing + + Returns: + List of mock requirements + """ + return [ + Requirement('boost>=1.70.0'), + Requirement('zlib>=1.2.11'), + ] + + +@pytest.fixture(name='conan_setup_mocks') +def fixture_conan_setup_mocks( + plugin: ConanProvider, + conan_mock_api: Mock, + mocker: MockerFixture, +) -> dict[str, Mock]: + """Sets up all mocks for testing install/update operations + + Args: + plugin: The plugin instance + conan_mock_api: Mock ConanAPI instance + mocker: Pytest mocker fixture + + Returns: + Dictionary containing all mocks + """ + # Mock builder + mock_builder = mocker.Mock() + mock_builder.generate_conanfile = mocker.Mock() + # Set the builder attribute on the plugin + plugin.builder = mock_builder # type: ignore[attr-defined] + + # Mock ConanAPI constructor + mock_conan_api_constructor = mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api) + + # Mock resolve_conan_dependency + def mock_resolve(requirement: Requirement) -> ConanDependency: + return ConanDependency(name=requirement.name, version_ge=None) + + mock_resolve_conan_dependency = mocker.patch( + 'cppython.plugins.conan.plugin.resolve_conan_dependency', side_effect=mock_resolve + ) + + return { + 'builder': mock_builder, + 'conan_api_constructor': mock_conan_api_constructor, + 'resolve_conan_dependency': mock_resolve_conan_dependency, + } diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index dfef95f7..2d9ab68b 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -6,9 +6,14 @@ import subprocess from pathlib import Path +from tomllib import loads from typer.testing import CliRunner +from cppython.console.schema import ConsoleInterface +from cppython.core.schema import ProjectConfiguration +from cppython.project import Project + pytest_plugins = ['tests.fixtures.example'] @@ -18,10 +23,22 @@ class TestConanCMake: @staticmethod def test_simple(example_runner: CliRunner) -> None: """Simple project""" - # By nature of running the test, we require PDM to develop the project and so it will be installed - result = subprocess.run(['pdm', 'install'], capture_output=True, text=True, check=False) + # Create project configuration + project_root = Path.cwd() + project_configuration = ProjectConfiguration(project_root=project_root, version=None) + + # Create console interface + interface = ConsoleInterface() + + # Load pyproject.toml data + pyproject_path = project_root / 'pyproject.toml' + pyproject_data = loads(pyproject_path.read_text(encoding='utf-8')) + + # Create and use the project directly + project = Project(project_configuration, interface, pyproject_data) - assert result.returncode == 0, f'PDM install failed: {result.stderr}' + # Call install directly to get structured results + project.install() # Run the CMake configuration command result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) diff --git a/tests/integration/plugins/conan/test_interface.py b/tests/integration/plugins/conan/test_provider.py similarity index 100% rename from tests/integration/plugins/conan/test_interface.py rename to tests/integration/plugins/conan/test_provider.py diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py new file mode 100644 index 00000000..ac9b28f1 --- /dev/null +++ b/tests/unit/plugins/conan/test_install.py @@ -0,0 +1,202 @@ +"""Unit tests for the conan plugin install functionality""" + +from pathlib import Path +from typing import Any +from unittest.mock import Mock + +import pytest +from packaging.requirements import Requirement + +from cppython.plugins.conan.plugin import ConanProvider +from cppython.test.pytest.mixins import ProviderPluginTestMixin + +# Use shared fixtures +pytest_plugins = ['tests.fixtures.conan'] + +# Constants for test verification +EXPECTED_DEPENDENCY_COUNT = 2 + + +class TestConanInstall(ProviderPluginTestMixin[ConanProvider]): + """Tests for the Conan provider install functionality""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """A required testing hook that allows data generation + + Returns: + The constructed plugin data + """ + return { + 'local': True, + } + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[ConanProvider]: + """A required testing hook that allows type generation + + Returns: + The type of the Provider + """ + return ConanProvider + + def test_install_with_dependencies( + self, + plugin: ConanProvider, + conan_mock_api: Mock, + conan_temp_conanfile: Path, + conan_mock_dependencies: list[Requirement], + conan_setup_mocks: dict[str, Mock], + ) -> None: + """Test install method with dependencies and existing conanfile + + Args: + plugin: The plugin instance + conan_mock_api: Mock ConanAPI instance + conan_temp_conanfile: Path to temporary conanfile.py + conan_mock_dependencies: List of mock dependencies + conan_setup_mocks: Dictionary containing all mocks + """ + # Setup dependencies + plugin.core_data.cppython_data.dependencies = conan_mock_dependencies + + # Execute + plugin.install() + + # Verify builder was called + conan_setup_mocks['builder'].generate_conanfile.assert_called_once() + assert ( + conan_setup_mocks['builder'].generate_conanfile.call_args[0][0] + == plugin.core_data.project_data.project_root + ) + assert len(conan_setup_mocks['builder'].generate_conanfile.call_args[0][1]) == EXPECTED_DEPENDENCY_COUNT + + # Verify dependency resolution was called + assert conan_setup_mocks['resolve_conan_dependency'].call_count == EXPECTED_DEPENDENCY_COUNT + + # Verify build path was created + assert plugin.core_data.cppython_data.build_path.exists() + + # Verify Conan API calls + conan_mock_api.profiles.get_default_host.assert_called_once() + conan_mock_api.profiles.get_default_build.assert_called_once() + conan_mock_api.profiles.get_profile.assert_called() + conan_mock_api.graph.load_graph_consumer.assert_called_once() + conan_mock_api.install.install_binaries.assert_called_once() + + # Verify graph consumer call arguments + load_graph_args = conan_mock_api.graph.load_graph_consumer.call_args + assert load_graph_args[1]['path'] == str(conan_temp_conanfile) + assert load_graph_args[1]['update'] is False + assert load_graph_args[1]['check_updates'] is False + + def test_install_without_conanfile( + self, + plugin: ConanProvider, + conan_mock_api: Mock, + conan_mock_dependencies: list[Requirement], + conan_setup_mocks: dict[str, Mock], + ) -> None: + """Test install method when conanfile.py doesn't exist + + Args: + plugin: The plugin instance + conan_mock_api: Mock ConanAPI instance + conan_mock_dependencies: List of mock dependencies + conan_setup_mocks: Dictionary containing all mocks + """ + # Setup dependencies + plugin.core_data.cppython_data.dependencies = [conan_mock_dependencies[0]] + + # Execute + plugin.install() + + # Verify builder was called + conan_setup_mocks['builder'].generate_conanfile.assert_called_once() + + # Verify build path was created + assert plugin.core_data.cppython_data.build_path.exists() + + # Verify Conan API calls were NOT made (no conanfile.py) + conan_mock_api.profiles.get_default_host.assert_not_called() + conan_mock_api.profiles.get_default_build.assert_not_called() + conan_mock_api.profiles.get_profile.assert_not_called() + conan_mock_api.graph.load_graph_consumer.assert_not_called() + conan_mock_api.install.install_binaries.assert_not_called() + + def test_install_no_dependencies( + self, + plugin: ConanProvider, + conan_mock_api: Mock, + conan_temp_conanfile: Path, + conan_setup_mocks: dict[str, Mock], + ) -> None: + """Test install method with no dependencies + + Args: + plugin: The plugin instance + conan_mock_api: Mock ConanAPI instance + conan_temp_conanfile: Path to temporary conanfile.py + conan_setup_mocks: Dictionary containing all mocks + """ + # No dependencies + plugin.core_data.cppython_data.dependencies = [] + + # Execute + plugin.install() + + # Verify builder was called with empty dependencies + conan_setup_mocks['builder'].generate_conanfile.assert_called_once() + assert len(conan_setup_mocks['builder'].generate_conanfile.call_args[0][1]) == 0 + + # Verify dependency resolution was not called + conan_setup_mocks['resolve_conan_dependency'].assert_not_called() + + # Verify build path was created + assert plugin.core_data.cppython_data.build_path.exists() + + # Verify Conan API calls were still made (conanfile.py exists) + conan_mock_api.profiles.get_default_host.assert_called_once() + conan_mock_api.profiles.get_default_build.assert_called_once() + conan_mock_api.profiles.get_profile.assert_called() + conan_mock_api.graph.load_graph_consumer.assert_called_once() + conan_mock_api.install.install_binaries.assert_called_once() + + def test_install_conan_api_failure( + self, + plugin: ConanProvider, + conan_mock_api: Mock, + conan_temp_conanfile: Path, + conan_mock_dependencies: list[Requirement], + conan_setup_mocks: dict[str, Mock], + ) -> None: + """Test install method when Conan API calls fail + + Args: + plugin: The plugin instance + conan_mock_api: Mock ConanAPI instance + conan_temp_conanfile: Path to temporary conanfile.py + conan_mock_dependencies: List of mock dependencies + conan_setup_mocks: Dictionary containing all mocks + """ + # Make API call fail + conan_mock_api.graph.load_graph_consumer.side_effect = Exception('Conan graph load failed') + + # Add a dependency + plugin.core_data.cppython_data.dependencies = [conan_mock_dependencies[0]] + + # Execute and verify exception is raised + with pytest.raises(Exception, match='Conan graph load failed'): + plugin.install() + + # Verify builder was still called + conan_setup_mocks['builder'].generate_conanfile.assert_called_once() + + # Verify API was called up to the point of failure + conan_mock_api.profiles.get_default_host.assert_called_once() + conan_mock_api.profiles.get_default_build.assert_called_once() + conan_mock_api.profiles.get_profile.assert_called() + conan_mock_api.graph.load_graph_consumer.assert_called_once() + conan_mock_api.install.install_binaries.assert_not_called() diff --git a/tests/unit/plugins/conan/test_publish.py b/tests/unit/plugins/conan/test_publish.py index 2423232c..f8f57276 100644 --- a/tests/unit/plugins/conan/test_publish.py +++ b/tests/unit/plugins/conan/test_publish.py @@ -8,6 +8,10 @@ from cppython.plugins.conan.plugin import ConanProvider from cppython.test.pytest.mixins import ProviderPluginTestMixin +from cppython.utility.exception import ProviderConfigurationError, ProviderInstallationError + +# Use shared fixtures +pytest_plugins = ['tests.fixtures.conan'] class TestConanPublish(ProviderPluginTestMixin[ConanProvider]): @@ -35,268 +39,208 @@ def fixture_plugin_type() -> type[ConanProvider]: """ return ConanProvider - @staticmethod - @pytest.fixture(name='mock_conan_api') - def fixture_mock_conan_api(mocker: MockerFixture) -> Mock: - """Creates a mock ConanAPI instance - - Args: - mocker: Pytest mocker fixture - - Returns: - Mock ConanAPI instance - """ - mock_api = mocker.Mock() - - # Mock export module - export returns a tuple (ref, conanfile) - mock_ref = mocker.Mock() - mock_ref.name = 'test_package' - mock_conanfile = mocker.Mock() - mock_api.export.export = mocker.Mock(return_value=(mock_ref, mock_conanfile)) - - # Mock graph module - mock_api.graph.load_graph_consumer = mocker.Mock() - mock_api.graph.analyze_binaries = mocker.Mock() - - # Mock install module - mock_api.install.install_binaries = mocker.Mock() - - # Mock list module - mock_select_result = mocker.Mock() - mock_select_result.recipes = ['some_package/1.0@user/channel'] - mock_api.list.select = mocker.Mock(return_value=mock_select_result) - - # Mock remotes module - mock_remote = mocker.Mock() - mock_remote.name = 'origin' - mock_api.remotes.list = mocker.Mock(return_value=[mock_remote]) - - # Mock upload module - mock_api.upload.upload_full = mocker.Mock() - - # Mock profiles module - mock_profile = mocker.Mock() - mock_api.profiles.get_profiles_from_args = mocker.Mock(return_value=(mock_profile, mock_profile)) - - return mock_api - - @staticmethod - @pytest.fixture(name='temp_conanfile') - def fixture_temp_conanfile(plugin: ConanProvider) -> None: - """Creates a temporary conanfile.py for testing - - Args: - plugin: The plugin instance - """ - project_root = plugin.core_data.project_data.project_root - conanfile_path = project_root / 'conanfile.py' - conanfile_path.write_text( - 'from conan import ConanFile\n\n' - 'class TestConan(ConanFile):\n' - ' name = "test_package"\n' - ' version = "1.0"\n' - ) - def test_publish_local_only( - self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish with local=True only exports and builds locally Args: plugin: The plugin instance - mock_conan_api: Mock ConanAPI - temp_conanfile: Fixture to create conanfile.py + conan_mock_api_publish: Mock ConanAPI for publish operations + conan_temp_conanfile: Fixture to create conanfile.py mocker: Pytest mocker fixture """ # Set plugin to local mode plugin.data.local = True # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) # Mock the dependencies graph mock_graph = mocker.Mock() - mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph # Execute publish plugin.publish() # Verify export was called - mock_conan_api.export.export.assert_called_once() + conan_mock_api_publish.export.export.assert_called_once() # Verify graph loading and analysis - mock_conan_api.graph.load_graph_consumer.assert_called_once() - mock_conan_api.graph.analyze_binaries.assert_called_once_with( + conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() + conan_mock_api_publish.graph.analyze_binaries.assert_called_once_with( graph=mock_graph, build_mode=['*'], - remotes=mock_conan_api.remotes.list(), + remotes=conan_mock_api_publish.remotes.list(), update=None, lockfile=None, ) # Verify install was called - mock_conan_api.install.install_binaries.assert_called_once_with( - deps_graph=mock_graph, remotes=mock_conan_api.remotes.list() + conan_mock_api_publish.install.install_binaries.assert_called_once_with( + deps_graph=mock_graph, remotes=conan_mock_api_publish.remotes.list() ) # Verify upload was NOT called for local mode - mock_conan_api.upload.upload_full.assert_not_called() + conan_mock_api_publish.upload.upload_full.assert_not_called() def test_publish_with_upload( - self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish with local=False exports, builds, and uploads Args: plugin: The plugin instance - mock_conan_api: Mock ConanAPI - temp_conanfile: Fixture to create conanfile.py + conan_mock_api_publish: Mock ConanAPI for publish operations + conan_temp_conanfile: Fixture to create conanfile.py mocker: Pytest mocker fixture """ # Set plugin to upload mode plugin.data.local = False # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) # Mock the dependencies graph mock_graph = mocker.Mock() - mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph # Execute publish plugin.publish() # Verify all steps were called - mock_conan_api.export.export.assert_called_once() - mock_conan_api.graph.load_graph_consumer.assert_called_once() - mock_conan_api.graph.analyze_binaries.assert_called_once() - mock_conan_api.install.install_binaries.assert_called_once() + conan_mock_api_publish.export.export.assert_called_once() + conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() + conan_mock_api_publish.graph.analyze_binaries.assert_called_once() + conan_mock_api_publish.install.install_binaries.assert_called_once() # Verify upload was called - mock_conan_api.list.select.assert_called_once() - mock_conan_api.upload.upload_full.assert_called_once() + conan_mock_api_publish.list.select.assert_called_once() + conan_mock_api_publish.upload.upload_full.assert_called_once() def test_publish_no_remotes_configured( - self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish raises error when no remotes are configured for upload Args: plugin: The plugin instance - mock_conan_api: Mock ConanAPI - temp_conanfile: Fixture to create conanfile.py + conan_mock_api_publish: Mock ConanAPI for publish operations + conan_temp_conanfile: Fixture to create conanfile.py mocker: Pytest mocker fixture """ # Set plugin to upload mode plugin.data.local = False # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) # Mock the dependencies graph mock_graph = mocker.Mock() - mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph # Mock no remotes configured - mock_conan_api.remotes.list.return_value = [] + conan_mock_api_publish.remotes.list.return_value = [] - # Execute publish and expect RuntimeError - with pytest.raises(RuntimeError, match='No remotes configured for upload'): + # Execute publish and expect ProviderConfigurationError + with pytest.raises(ProviderConfigurationError, match='No remotes configured for upload'): plugin.publish() def test_publish_no_packages_found( - self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish raises error when no packages are found to upload Args: plugin: The plugin instance - mock_conan_api: Mock ConanAPI - temp_conanfile: Fixture to create conanfile.py + conan_mock_api_publish: Mock ConanAPI for publish operations + conan_temp_conanfile: Fixture to create conanfile.py mocker: Pytest mocker fixture """ # Set plugin to upload mode plugin.data.local = False # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) # Mock the dependencies graph mock_graph = mocker.Mock() - mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph # Mock empty package list mock_select_result = mocker.Mock() mock_select_result.recipes = [] - mock_conan_api.list.select.return_value = mock_select_result + conan_mock_api_publish.list.select.return_value = mock_select_result - # Execute publish and expect RuntimeError - with pytest.raises(RuntimeError, match='No packages found to upload'): + # Execute publish and expect ProviderInstallationError + with pytest.raises(ProviderInstallationError, match='No packages found to upload'): plugin.publish() def test_publish_uses_default_profiles( - self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish uses default profiles from API Args: plugin: The plugin instance - mock_conan_api: Mock ConanAPI - temp_conanfile: Fixture to create conanfile.py + conan_mock_api_publish: Mock ConanAPI for publish operations + conan_temp_conanfile: Fixture to create conanfile.py mocker: Pytest mocker fixture """ # Set plugin to local mode plugin.data.local = True # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) # Mock the dependencies graph mock_graph = mocker.Mock() - mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph # Execute publish plugin.publish() # Verify profiles were obtained from API - mock_conan_api.profiles.get_profiles_from_args.assert_called_once_with([]) + conan_mock_api_publish.profiles.get_default_host.assert_called_once() + conan_mock_api_publish.profiles.get_default_build.assert_called_once() + conan_mock_api_publish.profiles.get_profile.assert_called() def test_publish_upload_parameters( - self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish upload is called with correct parameters Args: plugin: The plugin instance - mock_conan_api: Mock ConanAPI - temp_conanfile: Fixture to create conanfile.py + conan_mock_api_publish: Mock ConanAPI for publish operations + conan_temp_conanfile: Fixture to create conanfile.py mocker: Pytest mocker fixture """ # Set plugin to upload mode plugin.data.local = False # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) # Mock the dependencies graph mock_graph = mocker.Mock() - mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph # Mock remotes and package list mock_remote = MagicMock() mock_remote.name = 'origin' remotes = [mock_remote] - mock_conan_api.remotes.list.return_value = remotes + conan_mock_api_publish.remotes.list.return_value = remotes mock_package_list = MagicMock() mock_package_list.recipes = ['test_package/1.0@user/channel'] - mock_conan_api.list.select.return_value = mock_package_list + conan_mock_api_publish.list.select.return_value = mock_package_list # Execute publish plugin.publish() # Verify upload_full was called with correct parameters - mock_conan_api.upload.upload_full.assert_called_once_with( + conan_mock_api_publish.upload.upload_full.assert_called_once_with( package_list=mock_package_list, remote=mock_remote, enabled_remotes=remotes, @@ -307,33 +251,33 @@ def test_publish_upload_parameters( ) def test_publish_list_pattern_creation( - self, plugin: ConanProvider, mock_conan_api: Mock, temp_conanfile: None, mocker: MockerFixture + self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish creates correct ListPattern for package selection Args: plugin: The plugin instance - mock_conan_api: Mock ConanAPI - temp_conanfile: Fixture to create conanfile.py + conan_mock_api_publish: Mock ConanAPI for publish operations + conan_temp_conanfile: Fixture to create conanfile.py mocker: Pytest mocker fixture """ # Set plugin to upload mode plugin.data.local = False # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=mock_conan_api) + mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) mock_list_pattern = mocker.patch('cppython.plugins.conan.plugin.ListPattern') # Mock the dependencies graph mock_graph = mocker.Mock() - mock_conan_api.graph.load_graph_consumer.return_value = mock_graph + conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph # Execute publish plugin.publish() # Get the ref from the export call to verify ListPattern creation # The export call returns (ref, conanfile) - we need the ref.name - export_return = mock_conan_api.export.export.return_value + export_return = conan_mock_api_publish.export.export.return_value ref = export_return[0] # First element of the tuple # Verify ListPattern was created with correct reference pattern diff --git a/tests/unit/plugins/conan/test_update.py b/tests/unit/plugins/conan/test_update.py new file mode 100644 index 00000000..5582cff8 --- /dev/null +++ b/tests/unit/plugins/conan/test_update.py @@ -0,0 +1,41 @@ +"""Unit tests for the conan plugin update functionality + +This module tests the update-specific behavior and differences from install. +The core installation functionality is tested in test_install.py since both +install() and update() now share the same underlying implementation. +""" + +from typing import Any + +import pytest + +from cppython.plugins.conan.plugin import ConanProvider +from cppython.test.pytest.mixins import ProviderPluginTestMixin + +pytest_plugins = ['tests.fixtures.conan'] + + +class TestConanUpdate(ProviderPluginTestMixin[ConanProvider]): + """Tests for the Conan provider update-specific functionality""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """A required testing hook that allows data generation + + Returns: + The constructed plugin data + """ + return { + 'local': True, + } + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[ConanProvider]: + """A required testing hook that allows type generation + + Returns: + The type of the Provider + """ + return ConanProvider From 9fb413e969b7b3521ce96e8674b970cecb99f11b Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sun, 13 Jul 2025 06:02:46 -0400 Subject: [PATCH 11/16] Add Install Tests w/ Logging Traces --- cppython/plugins/conan/plugin.py | 82 +++++++---- examples/conan_cmake/simple/pyproject.toml | 2 +- .../integration/examples/test_conan_cmake.py | 2 +- tests/unit/plugins/conan/test_install.py | 133 +++++------------- 4 files changed, 91 insertions(+), 128 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 6c01c3b0..8303d8dd 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -5,6 +5,8 @@ installation, and synchronization with other tools. """ +import logging +import subprocess from pathlib import Path from typing import Any @@ -70,51 +72,73 @@ def information() -> Information: return Information() def _install_dependencies(self, *, update: bool = False) -> None: - """Common implementation for installing/updating dependencies. + """Install/update dependencies using conan CLI command. Args: update: If True, check remotes for newer versions/revisions and install those. If False, use cached versions when available. """ try: + logger = logging.getLogger('cppython.conan') + logger.debug('Starting dependency installation/update (update=%s)', update) + resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] + logger.debug( + 'Resolved %d dependencies: %s', len(resolved_dependencies), [str(dep) for dep in resolved_dependencies] + ) + # Generate conanfile.py self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies) + logger.debug('Generated conanfile.py at %s', self.core_data.project_data.project_root) + # Ensure build directory exists self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True) + logger.debug('Created build path: %s', self.core_data.cppython_data.build_path) - # Install/update dependencies using Conan API + # Build conan install command project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' - if conanfile_path.exists(): - # Initialize Conan API - conan_api = ConanAPI() - - # Get default profiles - profile_host_path = conan_api.profiles.get_default_host() - profile_build_path = conan_api.profiles.get_default_build() - profile_host = conan_api.profiles.get_profile([profile_host_path]) - profile_build = conan_api.profiles.get_profile([profile_build_path]) - - # Build dependency graph for the package - deps_graph = conan_api.graph.load_graph_consumer( - path=str(conanfile_path), - name=None, - version=None, - user=None, - channel=None, - profile_host=profile_host, - profile_build=profile_build, - lockfile=None, - remotes=conan_api.remotes.list(), - update=update, - check_updates=update, - is_build_require=False, - ) + if not conanfile_path.exists(): + raise ProviderInstallationError('conan', 'Generated conanfile.py not found') + + # Prepare conan install command + cmd = [ + 'conan', + 'install', + str(conanfile_path), + '--output-folder', + str(self.core_data.cppython_data.build_path), + '--build', + 'missing', + ] + + if update: + cmd.extend(['--update']) + + logger.debug('Running conan command: %s', ' '.join(cmd)) - # Install dependencies - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=conan_api.remotes.list()) + # Execute conan install command + result = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, check=False) + + # Log output for debugging + if result.stdout: + logger.debug('Conan stdout:\n%s', result.stdout) + if result.stderr: + logger.debug('Conan stderr:\n%s', result.stderr) + + # Check for success + if result.returncode != 0: + error_msg = f'Conan install failed with return code {result.returncode}' + if result.stderr: + error_msg += f': {result.stderr}' + raise ProviderInstallationError('conan', error_msg) + + logger.debug('Successfully installed dependencies using conan CLI') + + except subprocess.SubprocessError as e: + operation = 'update' if update else 'install' + raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {e}', e) from e except Exception as e: operation = 'update' if update else 'install' error_msg = str(e) diff --git a/examples/conan_cmake/simple/pyproject.toml b/examples/conan_cmake/simple/pyproject.toml index d97b190c..6803860d 100644 --- a/examples/conan_cmake/simple/pyproject.toml +++ b/examples/conan_cmake/simple/pyproject.toml @@ -18,7 +18,7 @@ provider-name = "conan" install-path = "install" -dependencies = ["fmt>=11.0.1"] +dependencies = ["fmt>=11.2.0"] [tool.cppython.generator] diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index 2d9ab68b..a9a8f6cc 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -25,7 +25,7 @@ def test_simple(example_runner: CliRunner) -> None: """Simple project""" # Create project configuration project_root = Path.cwd() - project_configuration = ProjectConfiguration(project_root=project_root, version=None) + project_configuration = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True) # Create console interface interface = ConsoleInterface() diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py index ac9b28f1..fda25da3 100644 --- a/tests/unit/plugins/conan/test_install.py +++ b/tests/unit/plugins/conan/test_install.py @@ -6,9 +6,11 @@ import pytest from packaging.requirements import Requirement +from pytest_mock import MockerFixture from cppython.plugins.conan.plugin import ConanProvider from cppython.test.pytest.mixins import ProviderPluginTestMixin +from cppython.utility.exception import ProviderInstallationError # Use shared fixtures pytest_plugins = ['tests.fixtures.conan'] @@ -44,8 +46,8 @@ def fixture_plugin_type() -> type[ConanProvider]: def test_install_with_dependencies( self, + mocker: MockerFixture, plugin: ConanProvider, - conan_mock_api: Mock, conan_temp_conanfile: Path, conan_mock_dependencies: list[Requirement], conan_setup_mocks: dict[str, Mock], @@ -53,12 +55,18 @@ def test_install_with_dependencies( """Test install method with dependencies and existing conanfile Args: + mocker: Pytest mocker fixture plugin: The plugin instance - conan_mock_api: Mock ConanAPI instance conan_temp_conanfile: Path to temporary conanfile.py conan_mock_dependencies: List of mock dependencies conan_setup_mocks: Dictionary containing all mocks """ + # Setup subprocess mock to return success + mock_subprocess = mocker.patch('cppython.plugins.conan.plugin.subprocess.run') + mock_subprocess.return_value.returncode = 0 + mock_subprocess.return_value.stdout = 'Install completed successfully' + mock_subprocess.return_value.stderr = '' + # Setup dependencies plugin.core_data.cppython_data.dependencies = conan_mock_dependencies @@ -79,124 +87,55 @@ def test_install_with_dependencies( # Verify build path was created assert plugin.core_data.cppython_data.build_path.exists() - # Verify Conan API calls - conan_mock_api.profiles.get_default_host.assert_called_once() - conan_mock_api.profiles.get_default_build.assert_called_once() - conan_mock_api.profiles.get_profile.assert_called() - conan_mock_api.graph.load_graph_consumer.assert_called_once() - conan_mock_api.install.install_binaries.assert_called_once() - - # Verify graph consumer call arguments - load_graph_args = conan_mock_api.graph.load_graph_consumer.call_args - assert load_graph_args[1]['path'] == str(conan_temp_conanfile) - assert load_graph_args[1]['update'] is False - assert load_graph_args[1]['check_updates'] is False - - def test_install_without_conanfile( - self, - plugin: ConanProvider, - conan_mock_api: Mock, - conan_mock_dependencies: list[Requirement], - conan_setup_mocks: dict[str, Mock], - ) -> None: - """Test install method when conanfile.py doesn't exist - - Args: - plugin: The plugin instance - conan_mock_api: Mock ConanAPI instance - conan_mock_dependencies: List of mock dependencies - conan_setup_mocks: Dictionary containing all mocks - """ - # Setup dependencies - plugin.core_data.cppython_data.dependencies = [conan_mock_dependencies[0]] - - # Execute - plugin.install() - - # Verify builder was called - conan_setup_mocks['builder'].generate_conanfile.assert_called_once() - - # Verify build path was created - assert plugin.core_data.cppython_data.build_path.exists() - - # Verify Conan API calls were NOT made (no conanfile.py) - conan_mock_api.profiles.get_default_host.assert_not_called() - conan_mock_api.profiles.get_default_build.assert_not_called() - conan_mock_api.profiles.get_profile.assert_not_called() - conan_mock_api.graph.load_graph_consumer.assert_not_called() - conan_mock_api.install.install_binaries.assert_not_called() - - def test_install_no_dependencies( - self, - plugin: ConanProvider, - conan_mock_api: Mock, - conan_temp_conanfile: Path, - conan_setup_mocks: dict[str, Mock], - ) -> None: - """Test install method with no dependencies + # Verify subprocess was called with correct command + mock_subprocess.assert_called_once() + call_args = mock_subprocess.call_args + cmd = call_args[0][0] - Args: - plugin: The plugin instance - conan_mock_api: Mock ConanAPI instance - conan_temp_conanfile: Path to temporary conanfile.py - conan_setup_mocks: Dictionary containing all mocks - """ - # No dependencies - plugin.core_data.cppython_data.dependencies = [] - - # Execute - plugin.install() - - # Verify builder was called with empty dependencies - conan_setup_mocks['builder'].generate_conanfile.assert_called_once() - assert len(conan_setup_mocks['builder'].generate_conanfile.call_args[0][1]) == 0 - - # Verify dependency resolution was not called - conan_setup_mocks['resolve_conan_dependency'].assert_not_called() - - # Verify build path was created - assert plugin.core_data.cppython_data.build_path.exists() + # Check command structure + assert cmd[0] == 'conan' + assert cmd[1] == 'install' + assert cmd[2] == str(conan_temp_conanfile) + assert '--output-folder' in cmd + assert '--build' in cmd + assert 'missing' in cmd + assert '--update' not in cmd # install mode, not update - # Verify Conan API calls were still made (conanfile.py exists) - conan_mock_api.profiles.get_default_host.assert_called_once() - conan_mock_api.profiles.get_default_build.assert_called_once() - conan_mock_api.profiles.get_profile.assert_called() - conan_mock_api.graph.load_graph_consumer.assert_called_once() - conan_mock_api.install.install_binaries.assert_called_once() + # Check working directory + assert call_args[1]['cwd'] == str(plugin.core_data.project_data.project_root) - def test_install_conan_api_failure( + def test_install_conan_command_failure( self, + mocker: MockerFixture, plugin: ConanProvider, - conan_mock_api: Mock, conan_temp_conanfile: Path, conan_mock_dependencies: list[Requirement], conan_setup_mocks: dict[str, Mock], ) -> None: - """Test install method when Conan API calls fail + """Test install method when conan command fails Args: + mocker: Pytest mocker fixture plugin: The plugin instance - conan_mock_api: Mock ConanAPI instance conan_temp_conanfile: Path to temporary conanfile.py conan_mock_dependencies: List of mock dependencies conan_setup_mocks: Dictionary containing all mocks """ - # Make API call fail - conan_mock_api.graph.load_graph_consumer.side_effect = Exception('Conan graph load failed') + # Make subprocess return failure + mock_subprocess = mocker.patch('cppython.plugins.conan.plugin.subprocess.run') + mock_subprocess.return_value.returncode = 1 + mock_subprocess.return_value.stdout = '' + mock_subprocess.return_value.stderr = 'Conan install failed: package not found' # Add a dependency plugin.core_data.cppython_data.dependencies = [conan_mock_dependencies[0]] # Execute and verify exception is raised - with pytest.raises(Exception, match='Conan graph load failed'): + with pytest.raises(ProviderInstallationError, match='Conan install failed with return code 1'): plugin.install() # Verify builder was still called conan_setup_mocks['builder'].generate_conanfile.assert_called_once() - # Verify API was called up to the point of failure - conan_mock_api.profiles.get_default_host.assert_called_once() - conan_mock_api.profiles.get_default_build.assert_called_once() - conan_mock_api.profiles.get_profile.assert_called() - conan_mock_api.graph.load_graph_consumer.assert_called_once() - conan_mock_api.install.install_binaries.assert_not_called() + # Verify subprocess was called + mock_subprocess.assert_called_once() From 43f4fbfb387070c2a81c129445f4b941b07a6c02 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sun, 13 Jul 2025 15:59:30 -0400 Subject: [PATCH 12/16] Initial Remote Specification --- cppython/plugins/conan/plugin.py | 42 ++++++++++++++++-------- cppython/plugins/conan/resolution.py | 2 +- cppython/plugins/conan/schema.py | 14 +++++--- tests/fixtures/conan.py | 2 +- tests/unit/plugins/conan/test_install.py | 2 +- tests/unit/plugins/conan/test_publish.py | 24 +++++++------- tests/unit/plugins/conan/test_update.py | 2 +- 7 files changed, 54 insertions(+), 34 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 8303d8dd..f2ce80ec 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -202,6 +202,24 @@ def publish(self) -> None: # Initialize Conan API conan_api = ConanAPI() + # Get configured remotes from Conan API and filter by our configuration + # TODO: We want to replace the global conan remotes with the ones configured in CPPython. + all_remotes = conan_api.remotes.list() + if not self.data.local_only: + # Filter remotes to only include those specified in configuration + configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes] + + if not configured_remotes: + available_remotes = [remote.name for remote in all_remotes] + raise ProviderConfigurationError( + 'conan', + f'No configured remotes found. Available remotes: {available_remotes}, ' + f'Configured remotes: {self.data.remotes}', + 'remotes', + ) + else: + configured_remotes = [] + # Step 1: Export the recipe to the cache # This is equivalent to the export part of `conan create` ref, conanfile = conan_api.export.export( @@ -211,7 +229,7 @@ def publish(self) -> None: user=None, channel=None, lockfile=None, - remotes=conan_api.remotes.list(), + remotes=all_remotes, # Use all remotes for dependency resolution during export ) # Step 2: Get default profiles @@ -230,7 +248,7 @@ def publish(self) -> None: profile_host=profile_host, profile_build=profile_build, lockfile=None, - remotes=conan_api.remotes.list(), + remotes=all_remotes, # Use all remotes for dependency resolution update=None, check_updates=False, is_build_require=False, @@ -240,33 +258,29 @@ def publish(self) -> None: conan_api.graph.analyze_binaries( graph=deps_graph, build_mode=['*'], # Build from source (equivalent to the create behavior) - remotes=conan_api.remotes.list(), + remotes=all_remotes, # Use all remotes for dependency resolution update=None, lockfile=None, ) # Step 5: Install all dependencies and build the package - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=conan_api.remotes.list()) + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) - # If not local, upload the package - if not self.data.local: + # If not local only, upload the package + if not self.data.local_only: # Get all packages matching the created reference ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False) package_list = conan_api.list.select(ref_pattern) if package_list.recipes: - # Get the first configured remote or raise an error - remotes = conan_api.remotes.list() - if not remotes: - raise ProviderConfigurationError('conan', 'No remotes configured for upload', 'remotes') - - remote = remotes[0] # Use first remote + # Use the first configured remote for upload + remote = configured_remotes[0] - # Upload the package + # Upload the package to configured remotes conan_api.upload.upload_full( package_list=package_list, remote=remote, - enabled_remotes=remotes, + enabled_remotes=configured_remotes, # Only upload to configured remotes check_integrity=False, force=False, metadata=None, diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index f7e80d4e..a287b803 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -43,4 +43,4 @@ def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> Conan """ parsed_data = ConanConfiguration(**data) - return ConanData(local=parsed_data.local) + return ConanData(remotes=parsed_data.remotes) diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py index 256a8c22..9f30206d 100644 --- a/cppython/plugins/conan/schema.py +++ b/cppython/plugins/conan/schema.py @@ -30,12 +30,18 @@ def requires(self) -> str: class ConanData(CPPythonModel): """Resolved conan data""" - local: bool + remotes: list[str] + + @property + def local_only(self) -> bool: + """Check if publishing should be local-only.""" + return len(self.remotes) == 0 class ConanConfiguration(CPPythonModel): """Raw conan data""" - local: Annotated[bool, Field(description='Whether to publish packages locally without uploading to a remote')] = ( - False - ) + remotes: Annotated[ + list[str], + Field(description='List of remotes to upload to. Empty list means the local conan cache will be used.'), + ] = ['conancenter'] diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py index 7286ebe4..330022d3 100644 --- a/tests/fixtures/conan.py +++ b/tests/fixtures/conan.py @@ -79,7 +79,7 @@ def fixture_conan_mock_api_publish(mocker: MockerFixture) -> Mock: # Mock remotes module mock_remote = mocker.Mock() - mock_remote.name = 'origin' + mock_remote.name = 'conancenter' mock_api.remotes.list = mocker.Mock(return_value=[mock_remote]) # Mock upload module diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py index fda25da3..4316a1cf 100644 --- a/tests/unit/plugins/conan/test_install.py +++ b/tests/unit/plugins/conan/test_install.py @@ -31,7 +31,7 @@ def fixture_plugin_data() -> dict[str, Any]: The constructed plugin data """ return { - 'local': True, + 'remotes': [], } @staticmethod diff --git a/tests/unit/plugins/conan/test_publish.py b/tests/unit/plugins/conan/test_publish.py index f8f57276..fe6194c7 100644 --- a/tests/unit/plugins/conan/test_publish.py +++ b/tests/unit/plugins/conan/test_publish.py @@ -26,7 +26,7 @@ def fixture_plugin_data() -> dict[str, Any]: The constructed plugin data """ return { - 'local': False, + 'remotes': ['conancenter'], } @staticmethod @@ -42,7 +42,7 @@ def fixture_plugin_type() -> type[ConanProvider]: def test_publish_local_only( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: - """Test that publish with local=True only exports and builds locally + """Test that publish with remotes=[] only exports and builds locally Args: plugin: The plugin instance @@ -51,7 +51,7 @@ def test_publish_local_only( mocker: Pytest mocker fixture """ # Set plugin to local mode - plugin.data.local = True + plugin.data.remotes = [] # Mock the necessary imports and API creation mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) @@ -87,7 +87,7 @@ def test_publish_local_only( def test_publish_with_upload( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: - """Test that publish with local=False exports, builds, and uploads + """Test that publish with remotes=['conancenter'] exports, builds, and uploads Args: plugin: The plugin instance @@ -96,7 +96,7 @@ def test_publish_with_upload( mocker: Pytest mocker fixture """ # Set plugin to upload mode - plugin.data.local = False + plugin.data.remotes = ['conancenter'] # Mock the necessary imports and API creation mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) @@ -130,7 +130,7 @@ def test_publish_no_remotes_configured( mocker: Pytest mocker fixture """ # Set plugin to upload mode - plugin.data.local = False + plugin.data.remotes = ['conancenter'] # Mock the necessary imports and API creation mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) @@ -143,7 +143,7 @@ def test_publish_no_remotes_configured( conan_mock_api_publish.remotes.list.return_value = [] # Execute publish and expect ProviderConfigurationError - with pytest.raises(ProviderConfigurationError, match='No remotes configured for upload'): + with pytest.raises(ProviderConfigurationError, match='No configured remotes found'): plugin.publish() def test_publish_no_packages_found( @@ -158,7 +158,7 @@ def test_publish_no_packages_found( mocker: Pytest mocker fixture """ # Set plugin to upload mode - plugin.data.local = False + plugin.data.remotes = ['conancenter'] # Mock the necessary imports and API creation mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) @@ -188,7 +188,7 @@ def test_publish_uses_default_profiles( mocker: Pytest mocker fixture """ # Set plugin to local mode - plugin.data.local = True + plugin.data.remotes = [] # Mock the necessary imports and API creation mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) @@ -217,7 +217,7 @@ def test_publish_upload_parameters( mocker: Pytest mocker fixture """ # Set plugin to upload mode - plugin.data.local = False + plugin.data.remotes = ['conancenter'] # Mock the necessary imports and API creation mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) @@ -228,7 +228,7 @@ def test_publish_upload_parameters( # Mock remotes and package list mock_remote = MagicMock() - mock_remote.name = 'origin' + mock_remote.name = 'conancenter' remotes = [mock_remote] conan_mock_api_publish.remotes.list.return_value = remotes @@ -262,7 +262,7 @@ def test_publish_list_pattern_creation( mocker: Pytest mocker fixture """ # Set plugin to upload mode - plugin.data.local = False + plugin.data.remotes = ['conancenter'] # Mock the necessary imports and API creation mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) diff --git a/tests/unit/plugins/conan/test_update.py b/tests/unit/plugins/conan/test_update.py index 5582cff8..d8622028 100644 --- a/tests/unit/plugins/conan/test_update.py +++ b/tests/unit/plugins/conan/test_update.py @@ -27,7 +27,7 @@ def fixture_plugin_data() -> dict[str, Any]: The constructed plugin data """ return { - 'local': True, + 'remotes': [], } @staticmethod From 5d51200ab884d3748beeec233a0a0e9db3a6c166 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sun, 13 Jul 2025 20:10:56 -0400 Subject: [PATCH 13/16] Delete pdm.toml Revert "Delete pdm.toml" This reverts commit a782f6c01f6b71324e39105566687424d9597b0a. Reapply "Delete pdm.toml" This reverts commit 80a36ecd9c220613e6efeca28892bfe00b91e8eb. --- pdm.toml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 pdm.toml diff --git a/pdm.toml b/pdm.toml deleted file mode 100644 index 71854f2a..00000000 --- a/pdm.toml +++ /dev/null @@ -1 +0,0 @@ -use_uv = true From 5020ad0dc02b36385df7214a3b09c2636774e3f4 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sun, 13 Jul 2025 23:19:21 -0400 Subject: [PATCH 14/16] Update Conan Installation Process --- cppython/plugins/conan/plugin.py | 141 +++++++++++------ tests/fixtures/conan.py | 16 +- tests/unit/plugins/conan/test_install.py | 193 ++++++++++++++++++----- 3 files changed, 259 insertions(+), 91 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index f2ce80ec..fd3c6d7e 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -6,7 +6,6 @@ """ import logging -import subprocess from pathlib import Path from typing import Any @@ -72,7 +71,7 @@ def information() -> Information: return Information() def _install_dependencies(self, *, update: bool = False) -> None: - """Install/update dependencies using conan CLI command. + """Install/update dependencies using Conan API. Args: update: If True, check remotes for newer versions/revisions and install those. @@ -95,50 +94,85 @@ def _install_dependencies(self, *, update: bool = False) -> None: self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True) logger.debug('Created build path: %s', self.core_data.cppython_data.build_path) - # Build conan install command + # Initialize Conan API + conan_api = ConanAPI() + + # Get project paths project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' if not conanfile_path.exists(): raise ProviderInstallationError('conan', 'Generated conanfile.py not found') - # Prepare conan install command - cmd = [ - 'conan', - 'install', - str(conanfile_path), - '--output-folder', - str(self.core_data.cppython_data.build_path), - '--build', - 'missing', - ] - - if update: - cmd.extend(['--update']) - - logger.debug('Running conan command: %s', ' '.join(cmd)) - - # Execute conan install command - result = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, check=False) - - # Log output for debugging - if result.stdout: - logger.debug('Conan stdout:\n%s', result.stdout) - if result.stderr: - logger.debug('Conan stderr:\n%s', result.stderr) - - # Check for success - if result.returncode != 0: - error_msg = f'Conan install failed with return code {result.returncode}' - if result.stderr: - error_msg += f': {result.stderr}' - raise ProviderInstallationError('conan', error_msg) - - logger.debug('Successfully installed dependencies using conan CLI') - - except subprocess.SubprocessError as e: - operation = 'update' if update else 'install' - raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {e}', e) from e + # Get all remotes + all_remotes = conan_api.remotes.list() + logger.debug('Available remotes: %s', [remote.name for remote in all_remotes]) + + # Get default profiles, handle case when no default profile exists + try: + profile_host_path = conan_api.profiles.get_default_host() + profile_build_path = conan_api.profiles.get_default_build() + + # Ensure we have valid profile paths + if profile_host_path is None: + # Create a minimal default profile if none exists + profile_host = conan_api.profiles.get_profile([]) + else: + profile_host = conan_api.profiles.get_profile([profile_host_path]) + + if profile_build_path is None: + # Create a minimal default profile if none exists + profile_build = conan_api.profiles.get_profile([]) + else: + profile_build = conan_api.profiles.get_profile([profile_build_path]) + + except Exception: + # If profile operations fail, create minimal default profiles + profile_host = conan_api.profiles.get_profile([]) + profile_build = conan_api.profiles.get_profile([]) + + logger.debug('Using profiles: host=%s, build=%s', profile_host, profile_build) + + # Build dependency graph + deps_graph = conan_api.graph.load_graph_consumer( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + profile_host=profile_host, + profile_build=profile_build, + lockfile=None, + remotes=all_remotes, + update=None if not update else True, + check_updates=update, + is_build_require=False, + ) + + logger.debug('Dependency graph loaded with %d nodes', len(deps_graph.nodes)) + + # Analyze binaries to determine what needs to be built/downloaded + conan_api.graph.analyze_binaries( + graph=deps_graph, + build_mode=['missing'], # Only build what's missing + remotes=all_remotes, + update=None if not update else True, + lockfile=None, + ) + + # Install all dependencies + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + + # Generate files for the consumer (conandata.yml, conan_toolchain.cmake, etc.) + conan_api.install.install_consumer( + deps_graph=deps_graph, + generators=['CMakeToolchain', 'CMakeDeps'], + source_folder=str(project_root), + output_folder=str(self.core_data.cppython_data.build_path), + ) + + logger.debug('Successfully installed dependencies using Conan API') + except Exception as e: operation = 'update' if update else 'install' error_msg = str(e) @@ -232,11 +266,28 @@ def publish(self) -> None: remotes=all_remotes, # Use all remotes for dependency resolution during export ) - # Step 2: Get default profiles - profile_host_path = conan_api.profiles.get_default_host() - profile_build_path = conan_api.profiles.get_default_build() - profile_host = conan_api.profiles.get_profile([profile_host_path]) - profile_build = conan_api.profiles.get_profile([profile_build_path]) + # Step 2: Get default profiles, handle case when no default profile exists + try: + profile_host_path = conan_api.profiles.get_default_host() + profile_build_path = conan_api.profiles.get_default_build() + + # Ensure we have valid profile paths + if profile_host_path is None: + # Create a minimal default profile if none exists + profile_host = conan_api.profiles.get_profile([]) + else: + profile_host = conan_api.profiles.get_profile([profile_host_path]) + + if profile_build_path is None: + # Create a minimal default profile if none exists + profile_build = conan_api.profiles.get_profile([]) + else: + profile_build = conan_api.profiles.get_profile([profile_build_path]) + + except Exception: + # If profile operations fail, create minimal default profiles + profile_host = conan_api.profiles.get_profile([]) + profile_build = conan_api.profiles.get_profile([]) # Step 3: Build dependency graph for the package deps_graph = conan_api.graph.load_graph_consumer( diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py index 330022d3..b02d783d 100644 --- a/tests/fixtures/conan.py +++ b/tests/fixtures/conan.py @@ -25,24 +25,24 @@ def fixture_conan_mock_api(mocker: MockerFixture) -> Mock: # Mock graph module mock_deps_graph = mocker.Mock() + mock_deps_graph.nodes = [] mock_api.graph.load_graph_consumer = mocker.Mock(return_value=mock_deps_graph) + mock_api.graph.analyze_binaries = mocker.Mock() # Mock install module mock_api.install.install_binaries = mocker.Mock() + mock_api.install.install_consumer = mocker.Mock() # Mock remotes module mock_remote = mocker.Mock() mock_remote.name = 'conancenter' mock_api.remotes.list = mocker.Mock(return_value=[mock_remote]) - # Mock profiles module - mock_profile_host = mocker.Mock() - mock_profile_build = mocker.Mock() - mock_api.profiles.get_default_host = mocker.Mock(return_value='/path/to/default/host') - mock_api.profiles.get_default_build = mocker.Mock(return_value='/path/to/default/build') - mock_api.profiles.get_profile = mocker.Mock( - side_effect=lambda paths: mock_profile_host if 'host' in paths[0] else mock_profile_build - ) + # Mock profiles module - simulate no default profile by default + mock_profile = mocker.Mock() + mock_api.profiles.get_default_host = mocker.Mock(return_value=None) + mock_api.profiles.get_default_build = mocker.Mock(return_value=None) + mock_api.profiles.get_profile = mocker.Mock(return_value=mock_profile) return mock_api diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py index 4316a1cf..6f345b92 100644 --- a/tests/unit/plugins/conan/test_install.py +++ b/tests/unit/plugins/conan/test_install.py @@ -9,6 +9,7 @@ from pytest_mock import MockerFixture from cppython.plugins.conan.plugin import ConanProvider +from cppython.plugins.conan.schema import ConanDependency from cppython.test.pytest.mixins import ProviderPluginTestMixin from cppython.utility.exception import ProviderInstallationError @@ -17,6 +18,7 @@ # Constants for test verification EXPECTED_DEPENDENCY_COUNT = 2 +EXPECTED_PROFILE_CALLS = 2 class TestConanInstall(ProviderPluginTestMixin[ConanProvider]): @@ -46,7 +48,6 @@ def fixture_plugin_type() -> type[ConanProvider]: def test_install_with_dependencies( self, - mocker: MockerFixture, plugin: ConanProvider, conan_temp_conanfile: Path, conan_mock_dependencies: list[Requirement], @@ -55,18 +56,11 @@ def test_install_with_dependencies( """Test install method with dependencies and existing conanfile Args: - mocker: Pytest mocker fixture plugin: The plugin instance conan_temp_conanfile: Path to temporary conanfile.py conan_mock_dependencies: List of mock dependencies conan_setup_mocks: Dictionary containing all mocks """ - # Setup subprocess mock to return success - mock_subprocess = mocker.patch('cppython.plugins.conan.plugin.subprocess.run') - mock_subprocess.return_value.returncode = 0 - mock_subprocess.return_value.stdout = 'Install completed successfully' - mock_subprocess.return_value.stderr = '' - # Setup dependencies plugin.core_data.cppython_data.dependencies = conan_mock_dependencies @@ -87,55 +81,178 @@ def test_install_with_dependencies( # Verify build path was created assert plugin.core_data.cppython_data.build_path.exists() - # Verify subprocess was called with correct command - mock_subprocess.assert_called_once() - call_args = mock_subprocess.call_args - cmd = call_args[0][0] - - # Check command structure - assert cmd[0] == 'conan' - assert cmd[1] == 'install' - assert cmd[2] == str(conan_temp_conanfile) - assert '--output-folder' in cmd - assert '--build' in cmd - assert 'missing' in cmd - assert '--update' not in cmd # install mode, not update - - # Check working directory - assert call_args[1]['cwd'] == str(plugin.core_data.project_data.project_root) + # Verify ConanAPI constructor was called + conan_setup_mocks['conan_api_constructor'].assert_called_once() def test_install_conan_command_failure( self, - mocker: MockerFixture, plugin: ConanProvider, conan_temp_conanfile: Path, conan_mock_dependencies: list[Requirement], - conan_setup_mocks: dict[str, Mock], + conan_mock_api: Mock, + mocker: MockerFixture, ) -> None: - """Test install method when conan command fails + """Test install method when conan API operations fail Args: - mocker: Pytest mocker fixture plugin: The plugin instance conan_temp_conanfile: Path to temporary conanfile.py conan_mock_dependencies: List of mock dependencies - conan_setup_mocks: Dictionary containing all mocks + conan_mock_api: Mock ConanAPI instance + mocker: Pytest mocker fixture """ - # Make subprocess return failure - mock_subprocess = mocker.patch('cppython.plugins.conan.plugin.subprocess.run') - mock_subprocess.return_value.returncode = 1 - mock_subprocess.return_value.stdout = '' - mock_subprocess.return_value.stderr = 'Conan install failed: package not found' + # Mock builder + mock_builder = mocker.Mock() + mock_builder.generate_conanfile = mocker.Mock() + plugin.builder = mock_builder # type: ignore[attr-defined] + + # Configure the API mock to fail on graph loading + conan_mock_api.graph.load_graph_consumer.side_effect = Exception('Conan API error: package not found') + + # Mock ConanAPI constructor to return our configured mock + mock_conan_api_constructor = mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api) + + # Mock resolve_conan_dependency + def mock_resolve(requirement: Requirement) -> ConanDependency: + return ConanDependency(name=requirement.name, version_ge=None) + + mocker.patch('cppython.plugins.conan.plugin.resolve_conan_dependency', side_effect=mock_resolve) # Add a dependency plugin.core_data.cppython_data.dependencies = [conan_mock_dependencies[0]] # Execute and verify exception is raised - with pytest.raises(ProviderInstallationError, match='Conan install failed with return code 1'): + with pytest.raises( + ProviderInstallationError, + match='Failed to install dependencies: Conan API error: package not found' + ): plugin.install() # Verify builder was still called - conan_setup_mocks['builder'].generate_conanfile.assert_called_once() + mock_builder.generate_conanfile.assert_called_once() + + # Verify Conan API was attempted + mock_conan_api_constructor.assert_called_once() + + def test_install_with_profile_exception_fallback( + self, + plugin: ConanProvider, + conan_temp_conanfile: Path, + conan_mock_dependencies: list[Requirement], + conan_setup_mocks: dict[str, Mock], + conan_mock_api: Mock, + ) -> None: + """Test install method when profile operations throw exceptions + + Args: + plugin: The plugin instance + conan_temp_conanfile: Path to temporary conanfile.py + conan_mock_dependencies: List of mock dependencies + conan_setup_mocks: Dictionary containing all mocks + conan_mock_api: Mock ConanAPI instance + """ + # Configure the API mock to throw exception on first profile call + conan_mock_api.profiles.get_default_host.side_effect = Exception('Profile not found') + + # Setup dependencies + plugin.core_data.cppython_data.dependencies = conan_mock_dependencies + + # Execute - should not throw an exception despite profile operations failing + plugin.install() + + # Verify that despite the exceptions, the fallback was used + conan_setup_mocks['conan_api_constructor'].assert_called_once() + conan_mock_api.profiles.get_default_host.assert_called_once() + # Note: get_default_build is not called because get_default_host throws exception first + + # Verify fallback profile creation was called (should be called twice for empty profiles) + assert conan_mock_api.profiles.get_profile.call_count >= EXPECTED_PROFILE_CALLS + + # Verify the rest of the process continued + conan_mock_api.graph.load_graph_consumer.assert_called_once() + conan_mock_api.install.install_binaries.assert_called_once() + conan_mock_api.install.install_consumer.assert_called_once() + + def test_publish_with_profile_exception_fallback( + self, + plugin: ConanProvider, + conan_temp_conanfile: Path, + conan_mock_api_publish: Mock, + mocker: MockerFixture, + ) -> None: + """Test publish method when profile operations throw exceptions + + Args: + plugin: The plugin instance + conan_temp_conanfile: Path to temporary conanfile.py + conan_mock_api_publish: Mock ConanAPI instance configured for publish + mocker: Pytest mocker fixture + """ + # Set plugin to local mode to avoid remote upload complications + plugin.data = plugin.data.__class__( + remotes=[], + ) + + # Configure the API mock to throw exception on first profile call + conan_mock_api_publish.profiles.get_default_host.side_effect = Exception('Profile configuration error') + + # Mock ConanAPI constructor to return our configured mock + mock_conan_api_constructor = mocker.patch( + 'cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish + ) + + # Execute - should not throw an exception despite profile operations failing + plugin.publish() + + # Verify that despite the exceptions, the fallback was used + mock_conan_api_constructor.assert_called_once() + conan_mock_api_publish.profiles.get_default_host.assert_called_once() + # Note: get_default_build is not called because get_default_host throws exception first + + # Verify fallback profile creation was called (should be called twice for empty profiles) + assert conan_mock_api_publish.profiles.get_profile.call_count >= EXPECTED_PROFILE_CALLS + + # Verify the rest of the process continued + conan_mock_api_publish.export.export.assert_called_once() + conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() + conan_mock_api_publish.install.install_binaries.assert_called_once() + + def test_install_with_second_profile_exception_fallback( + self, + plugin: ConanProvider, + conan_temp_conanfile: Path, + conan_mock_dependencies: list[Requirement], + conan_setup_mocks: dict[str, Mock], + conan_mock_api: Mock, + ) -> None: + """Test install method when the second profile operation throws exception + + Args: + plugin: The plugin instance + conan_temp_conanfile: Path to temporary conanfile.py + conan_mock_dependencies: List of mock dependencies + conan_setup_mocks: Dictionary containing all mocks + conan_mock_api: Mock ConanAPI instance + """ + # Configure the API mock: first call succeeds, second one fails + conan_mock_api.profiles.get_default_host.return_value = '/path/to/host' + conan_mock_api.profiles.get_default_build.side_effect = Exception('Build profile not found') + + # Setup dependencies + plugin.core_data.cppython_data.dependencies = conan_mock_dependencies + + # Execute - should not throw an exception despite profile operations failing + plugin.install() - # Verify subprocess was called - mock_subprocess.assert_called_once() + # Verify that despite the exceptions, the fallback was used + conan_setup_mocks['conan_api_constructor'].assert_called_once() + conan_mock_api.profiles.get_default_host.assert_called_once() + conan_mock_api.profiles.get_default_build.assert_called_once() + + # Verify fallback profile creation was called (should be called twice for empty profiles) + assert conan_mock_api.profiles.get_profile.call_count >= EXPECTED_PROFILE_CALLS + + # Verify the rest of the process continued + conan_mock_api.graph.load_graph_consumer.assert_called_once() + conan_mock_api.install.install_binaries.assert_called_once() + conan_mock_api.install.install_consumer.assert_called_once() From b57e1d77b2764e4278e89b2e93ef94196c98f215 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sun, 13 Jul 2025 23:37:40 -0400 Subject: [PATCH 15/16] Update Profile Detection --- cppython/plugins/conan/plugin.py | 147 +++++++++++------- tests/fixtures/conan.py | 2 + .../plugins/conan/test_provider.py | 20 ++- tests/unit/plugins/conan/test_install.py | 109 ++----------- tests/unit/plugins/conan/test_publish.py | 3 + 5 files changed, 126 insertions(+), 155 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index fd3c6d7e..a30d4b2c 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -12,6 +12,7 @@ import requests from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern +from conan.internal.model.profile import Profile from cppython.core.plugin_schema.generator import SyncConsumer from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures @@ -108,45 +109,27 @@ def _install_dependencies(self, *, update: bool = False) -> None: all_remotes = conan_api.remotes.list() logger.debug('Available remotes: %s', [remote.name for remote in all_remotes]) - # Get default profiles, handle case when no default profile exists - try: - profile_host_path = conan_api.profiles.get_default_host() - profile_build_path = conan_api.profiles.get_default_build() - - # Ensure we have valid profile paths - if profile_host_path is None: - # Create a minimal default profile if none exists - profile_host = conan_api.profiles.get_profile([]) - else: - profile_host = conan_api.profiles.get_profile([profile_host_path]) - - if profile_build_path is None: - # Create a minimal default profile if none exists - profile_build = conan_api.profiles.get_profile([]) - else: - profile_build = conan_api.profiles.get_profile([profile_build_path]) - - except Exception: - # If profile operations fail, create minimal default profiles - profile_host = conan_api.profiles.get_profile([]) - profile_build = conan_api.profiles.get_profile([]) - - logger.debug('Using profiles: host=%s, build=%s', profile_host, profile_build) - - # Build dependency graph + # Get profiles with fallback to auto-detection + profile_host, profile_build = self._get_profiles(conan_api) + + path = str(conanfile_path) + remotes = all_remotes + update_flag = None if not update else True + check_updates_flag = update + deps_graph = conan_api.graph.load_graph_consumer( - path=str(conanfile_path), + path=path, name=None, version=None, user=None, channel=None, - profile_host=profile_host, - profile_build=profile_build, lockfile=None, - remotes=all_remotes, - update=None if not update else True, - check_updates=update, + remotes=remotes, + update=update_flag, + check_updates=check_updates_flag, is_build_require=False, + profile_host=profile_host, + profile_build=profile_build, ) logger.debug('Dependency graph loaded with %d nodes', len(deps_graph.nodes)) @@ -266,43 +249,26 @@ def publish(self) -> None: remotes=all_remotes, # Use all remotes for dependency resolution during export ) - # Step 2: Get default profiles, handle case when no default profile exists - try: - profile_host_path = conan_api.profiles.get_default_host() - profile_build_path = conan_api.profiles.get_default_build() + # Step 2: Get profiles with fallback to auto-detection + profile_host, profile_build = self._get_profiles(conan_api) - # Ensure we have valid profile paths - if profile_host_path is None: - # Create a minimal default profile if none exists - profile_host = conan_api.profiles.get_profile([]) - else: - profile_host = conan_api.profiles.get_profile([profile_host_path]) - - if profile_build_path is None: - # Create a minimal default profile if none exists - profile_build = conan_api.profiles.get_profile([]) - else: - profile_build = conan_api.profiles.get_profile([profile_build_path]) + # Step 3: Build dependency graph for the package - prepare parameters + path = str(conanfile_path) + remotes = all_remotes # Use all remotes for dependency resolution - except Exception: - # If profile operations fail, create minimal default profiles - profile_host = conan_api.profiles.get_profile([]) - profile_build = conan_api.profiles.get_profile([]) - - # Step 3: Build dependency graph for the package deps_graph = conan_api.graph.load_graph_consumer( - path=str(conanfile_path), + path=path, name=None, version=None, user=None, channel=None, - profile_host=profile_host, - profile_build=profile_build, lockfile=None, - remotes=all_remotes, # Use all remotes for dependency resolution + remotes=remotes, update=None, check_updates=False, is_build_require=False, + profile_host=profile_host, + profile_build=profile_build, ) # Step 4: Analyze binaries and install/build them if needed @@ -339,3 +305,68 @@ def publish(self) -> None: ) else: raise ProviderInstallationError('conan', 'No packages found to upload') + + def _apply_profile_processing(self, profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any) -> None: + """Apply profile plugin and settings processing to a list of profiles. + + Args: + profiles: List of profiles to process + conan_api: The Conan API instance + cache_settings: The settings configuration + """ + logger = logging.getLogger('cppython.conan') + + # Apply profile plugin processing + try: + profile_plugin = conan_api.profiles._load_profile_plugin() + if profile_plugin is not None: + for profile in profiles: + try: + profile_plugin(profile) + except Exception as plugin_error: + logger.warning('Profile plugin failed for profile: %s', str(plugin_error)) + except (AttributeError, Exception): + logger.debug('Profile plugin not available or failed to load') + + # Process settings to initialize processed_settings + for profile in profiles: + try: + profile.process_settings(cache_settings) + except (AttributeError, Exception) as settings_error: + logger.debug('Settings processing failed for profile: %s', str(settings_error)) + + def _get_profiles(self, conan_api: ConanAPI) -> tuple[Profile, Profile]: + """Get Conan profiles with fallback to auto-detection. + + Args: + conan_api: The Conan API instance + + Returns: + A tuple of (profile_host, profile_build) objects + """ + logger = logging.getLogger('cppython.conan') + + try: + # Gather default profile paths, these can raise exceptions if not available + profile_host_path = conan_api.profiles.get_default_host() + profile_build_path = conan_api.profiles.get_default_build() + + # Load the actual profile objects, can raise if data is invalid + profile_host = conan_api.profiles.get_profile([profile_host_path]) + profile_build = conan_api.profiles.get_profile([profile_build_path]) + + logger.debug('Using existing default profiles') + return profile_host, profile_build + + except Exception as e: + logger.warning('Default profiles not available, using auto-detection. Conan message: %s', str(e)) + + # Create auto-detected profiles + profiles = [conan_api.profiles.detect(), conan_api.profiles.detect()] + cache_settings = conan_api.config.settings_yml + + # Apply profile plugin processing to both profiles + self._apply_profile_processing(profiles, conan_api, cache_settings) + + logger.debug('Auto-detected profiles with plugin processing applied') + return profiles[0], profiles[1] diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py index b02d783d..9571c24f 100644 --- a/tests/fixtures/conan.py +++ b/tests/fixtures/conan.py @@ -43,6 +43,7 @@ def fixture_conan_mock_api(mocker: MockerFixture) -> Mock: mock_api.profiles.get_default_host = mocker.Mock(return_value=None) mock_api.profiles.get_default_build = mocker.Mock(return_value=None) mock_api.profiles.get_profile = mocker.Mock(return_value=mock_profile) + mock_api.profiles.detect = mocker.Mock(return_value=mock_profile) return mock_api @@ -90,6 +91,7 @@ def fixture_conan_mock_api_publish(mocker: MockerFixture) -> Mock: mock_api.profiles.get_default_host = mocker.Mock(return_value='/path/to/default/host') mock_api.profiles.get_default_build = mocker.Mock(return_value='/path/to/default/build') mock_api.profiles.get_profile = mocker.Mock(return_value=mock_profile) + mock_api.profiles.detect = mocker.Mock(return_value=mock_profile) return mock_api diff --git a/tests/integration/plugins/conan/test_provider.py b/tests/integration/plugins/conan/test_provider.py index 78d988e3..c0ba43e0 100644 --- a/tests/integration/plugins/conan/test_provider.py +++ b/tests/integration/plugins/conan/test_provider.py @@ -1,5 +1,6 @@ """Integration tests for the provider""" +from pathlib import Path from typing import Any import pytest @@ -8,7 +9,24 @@ from cppython.test.pytest.contracts import ProviderIntegrationTestContract -class TestCPPythonProvider(ProviderIntegrationTestContract[ConanProvider]): +@pytest.fixture(autouse=True) +def clean_conan_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Sets CONAN_HOME to a temporary directory for each test. + + This ensures all tests run with a clean Conan cache. + + Args: + tmp_path: Pytest temporary directory fixture + monkeypatch: Pytest monkeypatch fixture for environment variable manipulation + """ + conan_home = tmp_path / 'conan_home' + conan_home.mkdir() + + # Set CONAN_HOME to the temporary directory + monkeypatch.setenv('CONAN_HOME', str(conan_home)) + + +class TestConanProvider(ProviderIntegrationTestContract[ConanProvider]): """The tests for the conan provider""" @staticmethod diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py index 6f345b92..04a157cd 100644 --- a/tests/unit/plugins/conan/test_install.py +++ b/tests/unit/plugins/conan/test_install.py @@ -13,12 +13,15 @@ from cppython.test.pytest.mixins import ProviderPluginTestMixin from cppython.utility.exception import ProviderInstallationError +# Constants for test assertions +EXPECTED_PROFILE_CALLS = 2 +EXPECTED_GET_PROFILE_CALLS = 2 + # Use shared fixtures pytest_plugins = ['tests.fixtures.conan'] # Constants for test verification EXPECTED_DEPENDENCY_COUNT = 2 -EXPECTED_PROFILE_CALLS = 2 class TestConanInstall(ProviderPluginTestMixin[ConanProvider]): @@ -108,7 +111,7 @@ def test_install_conan_command_failure( # Configure the API mock to fail on graph loading conan_mock_api.graph.load_graph_consumer.side_effect = Exception('Conan API error: package not found') - + # Mock ConanAPI constructor to return our configured mock mock_conan_api_constructor = mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api) @@ -123,8 +126,7 @@ def mock_resolve(requirement: Requirement) -> ConanDependency: # Execute and verify exception is raised with pytest.raises( - ProviderInstallationError, - match='Failed to install dependencies: Conan API error: package not found' + ProviderInstallationError, match='Failed to install dependencies: Conan API error: package not found' ): plugin.install() @@ -134,7 +136,7 @@ def mock_resolve(requirement: Requirement) -> ConanDependency: # Verify Conan API was attempted mock_conan_api_constructor.assert_called_once() - def test_install_with_profile_exception_fallback( + def test_install_with_profile_exception( self, plugin: ConanProvider, conan_temp_conanfile: Path, @@ -142,7 +144,7 @@ def test_install_with_profile_exception_fallback( conan_setup_mocks: dict[str, Mock], conan_mock_api: Mock, ) -> None: - """Test install method when profile operations throw exceptions + """Test install method when profile operations throw exceptions but detect() works Args: plugin: The plugin instance @@ -151,107 +153,22 @@ def test_install_with_profile_exception_fallback( conan_setup_mocks: Dictionary containing all mocks conan_mock_api: Mock ConanAPI instance """ - # Configure the API mock to throw exception on first profile call + # Configure the API mock to throw exception on profile calls but detect() works conan_mock_api.profiles.get_default_host.side_effect = Exception('Profile not found') # Setup dependencies plugin.core_data.cppython_data.dependencies = conan_mock_dependencies - # Execute - should not throw an exception despite profile operations failing + # Execute - should succeed using fallback detect profiles plugin.install() - # Verify that despite the exceptions, the fallback was used + # Verify that the fallback was used conan_setup_mocks['conan_api_constructor'].assert_called_once() conan_mock_api.profiles.get_default_host.assert_called_once() - # Note: get_default_build is not called because get_default_host throws exception first - - # Verify fallback profile creation was called (should be called twice for empty profiles) - assert conan_mock_api.profiles.get_profile.call_count >= EXPECTED_PROFILE_CALLS - - # Verify the rest of the process continued - conan_mock_api.graph.load_graph_consumer.assert_called_once() - conan_mock_api.install.install_binaries.assert_called_once() - conan_mock_api.install.install_consumer.assert_called_once() - - def test_publish_with_profile_exception_fallback( - self, - plugin: ConanProvider, - conan_temp_conanfile: Path, - conan_mock_api_publish: Mock, - mocker: MockerFixture, - ) -> None: - """Test publish method when profile operations throw exceptions - - Args: - plugin: The plugin instance - conan_temp_conanfile: Path to temporary conanfile.py - conan_mock_api_publish: Mock ConanAPI instance configured for publish - mocker: Pytest mocker fixture - """ - # Set plugin to local mode to avoid remote upload complications - plugin.data = plugin.data.__class__( - remotes=[], - ) - - # Configure the API mock to throw exception on first profile call - conan_mock_api_publish.profiles.get_default_host.side_effect = Exception('Profile configuration error') - - # Mock ConanAPI constructor to return our configured mock - mock_conan_api_constructor = mocker.patch( - 'cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish - ) - - # Execute - should not throw an exception despite profile operations failing - plugin.publish() - # Verify that despite the exceptions, the fallback was used - mock_conan_api_constructor.assert_called_once() - conan_mock_api_publish.profiles.get_default_host.assert_called_once() - # Note: get_default_build is not called because get_default_host throws exception first - - # Verify fallback profile creation was called (should be called twice for empty profiles) - assert conan_mock_api_publish.profiles.get_profile.call_count >= EXPECTED_PROFILE_CALLS - - # Verify the rest of the process continued - conan_mock_api_publish.export.export.assert_called_once() - conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() - conan_mock_api_publish.install.install_binaries.assert_called_once() + # Verify detect was called for fallback (should be called twice for fallback) + assert conan_mock_api.profiles.detect.call_count >= EXPECTED_PROFILE_CALLS - def test_install_with_second_profile_exception_fallback( - self, - plugin: ConanProvider, - conan_temp_conanfile: Path, - conan_mock_dependencies: list[Requirement], - conan_setup_mocks: dict[str, Mock], - conan_mock_api: Mock, - ) -> None: - """Test install method when the second profile operation throws exception - - Args: - plugin: The plugin instance - conan_temp_conanfile: Path to temporary conanfile.py - conan_mock_dependencies: List of mock dependencies - conan_setup_mocks: Dictionary containing all mocks - conan_mock_api: Mock ConanAPI instance - """ - # Configure the API mock: first call succeeds, second one fails - conan_mock_api.profiles.get_default_host.return_value = '/path/to/host' - conan_mock_api.profiles.get_default_build.side_effect = Exception('Build profile not found') - - # Setup dependencies - plugin.core_data.cppython_data.dependencies = conan_mock_dependencies - - # Execute - should not throw an exception despite profile operations failing - plugin.install() - - # Verify that despite the exceptions, the fallback was used - conan_setup_mocks['conan_api_constructor'].assert_called_once() - conan_mock_api.profiles.get_default_host.assert_called_once() - conan_mock_api.profiles.get_default_build.assert_called_once() - - # Verify fallback profile creation was called (should be called twice for empty profiles) - assert conan_mock_api.profiles.get_profile.call_count >= EXPECTED_PROFILE_CALLS - # Verify the rest of the process continued conan_mock_api.graph.load_graph_consumer.assert_called_once() conan_mock_api.install.install_binaries.assert_called_once() diff --git a/tests/unit/plugins/conan/test_publish.py b/tests/unit/plugins/conan/test_publish.py index fe6194c7..b21f8f77 100644 --- a/tests/unit/plugins/conan/test_publish.py +++ b/tests/unit/plugins/conan/test_publish.py @@ -13,6 +13,9 @@ # Use shared fixtures pytest_plugins = ['tests.fixtures.conan'] +# Constants for test assertions +EXPECTED_PROFILE_CALLS = 2 + class TestConanPublish(ProviderPluginTestMixin[ConanProvider]): """Tests for the Conan provider publish functionality""" From 2f89faba4ae8397d8fa26c7a2d1c5f7423f91f05 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 14 Jul 2025 04:30:52 -0400 Subject: [PATCH 16/16] Add SingleConfig Compat --- cppython/plugins/cmake/builder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index 55454be5..659a79b0 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -132,6 +132,9 @@ def generate_root_preset( name=cmake_data.configuration_name, inherits='cppython', binaryDir=build_directory.as_posix(), + cacheVariables={ + 'CMAKE_BUILD_TYPE': 'Release' # Ensure compatibility for single-config and multi-config generators + }, ) if preset_file.exists():