diff --git a/.github/workflows/documentation-trigger.yml b/.github/workflows/documentation-trigger.yml new file mode 100644 index 0000000..7b7e565 --- /dev/null +++ b/.github/workflows/documentation-trigger.yml @@ -0,0 +1,16 @@ +name: Build Documentation +on: + push: + paths: + - docs/** + branches: + - '**' + +jobs: + publish_release: + if: github.repository_owner == 'synodic' + uses: synodic/.github/.github/workflows/documentation-trigger.yml@stable + with: + repository: synodic/CPPython-Website + secrets: + PAT: ${{ secrets.CPPYTHON_DOC_PAT }} \ No newline at end of file diff --git a/.github/workflows/python-check.yml b/.github/workflows/python-check.yml index 9e3ec6a..019e747 100644 --- a/.github/workflows/python-check.yml +++ b/.github/workflows/python-check.yml @@ -4,4 +4,4 @@ on: jobs: check: - uses: synodic-software/.github/.github/workflows/python-check.yml@stable + uses: synodic/.github/.github/workflows/python-check.yml@stable diff --git a/.github/workflows/python-merge.yml b/.github/workflows/python-merge.yml index b5e1c0b..3cfac8b 100644 --- a/.github/workflows/python-merge.yml +++ b/.github/workflows/python-merge.yml @@ -6,8 +6,8 @@ on: jobs: publish_release: - if: github.repository_owner == 'Synodic-Software' - uses: synodic-software/.github/.github/workflows/python-merge.yml@stable + if: github.repository_owner == 'synodic' + uses: synodic/.github/.github/workflows/python-merge.yml@stable with: repository_url: https://upload.pypi.org/legacy/ secrets: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index fa978b6..83bc92b 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -5,8 +5,8 @@ on: jobs: publish_release: - if: github.repository_owner == 'Synodic-Software' - uses: synodic-software/.github/.github/workflows/python-publish.yml@stable + if: github.repository_owner == 'synodic' + uses: synodic/.github/.github/workflows/python-publish.yml@stable with: repository_url: https://upload.pypi.org/legacy/ secrets: diff --git a/.gitignore b/.gitignore index 416d732..c140dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,4 @@ __pypackages__/ !.vscode/launch.json !.vscode/extensions.json /.mypy_cache -node_modules/ -build/ \ No newline at end of file +node_modules/ \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 89243e9..ac5b79c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { "recommendations": [ "ms-python.mypy-type-checker", - "ms-python.black-formatter", "asciidoctor.asciidoctor-vscode", "charliermarsh.ruff" ] diff --git a/.vscode/settings.json b/.vscode/settings.json index 33312c0..e7cf253 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,4 +8,7 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, + "mypy-type-checker.reportingScope": "workspace", + "mypy-type-checker.preferDaemon": true, + "mypy-type-checker.importStrategy": "fromEnvironment", } \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 4eb00b8..aecae73 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Asher Norland +Copyright (c) 2025 Synodic Software Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/cppython/builder.py b/cppython/builder.py index adaad8e..4a2019d 100644 --- a/cppython/builder.py +++ b/cppython/builder.py @@ -132,8 +132,10 @@ def find_generators(self) -> list[type[Generator]]: group_name = 'generator' plugin_types: list[type[Generator]] = [] + entries = entry_points(group=f'cppython.{group_name}') + # Filter entries by type - for entry_point in list(entry_points(group=f'cppython.{group_name}')): + for entry_point in list(entries): loaded_type = entry_point.load() if not issubclass(loaded_type, Generator): self._logger.warning( @@ -141,7 +143,7 @@ def find_generators(self) -> list[type[Generator]]: f" '{group_name}'" ) else: - self._logger.warning(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') + self._logger.info(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') plugin_types.append(loaded_type) if not plugin_types: @@ -161,8 +163,10 @@ def find_providers(self) -> list[type[Provider]]: group_name = 'provider' plugin_types: list[type[Provider]] = [] + entries = entry_points(group=f'cppython.{group_name}') + # Filter entries by type - for entry_point in list(entry_points(group=f'cppython.{group_name}')): + for entry_point in list(entries): loaded_type = entry_point.load() if not issubclass(loaded_type, Provider): self._logger.warning( @@ -170,7 +174,7 @@ def find_providers(self) -> list[type[Provider]]: f" '{group_name}'" ) else: - self._logger.warning(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') + self._logger.info(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') plugin_types.append(loaded_type) if not plugin_types: @@ -190,8 +194,10 @@ def find_source_managers(self) -> list[type[SCM]]: group_name = 'scm' plugin_types: list[type[SCM]] = [] + entries = entry_points(group=f'cppython.{group_name}') + # Filter entries by type - for entry_point in list(entry_points(group=f'cppython.{group_name}')): + for entry_point in list(entries): loaded_type = entry_point.load() if not issubclass(loaded_type, SCM): self._logger.warning( @@ -199,7 +205,7 @@ def find_source_managers(self) -> list[type[SCM]]: f" '{group_name}'" ) else: - self._logger.warning(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') + self._logger.info(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') plugin_types.append(loaded_type) if not plugin_types: @@ -227,20 +233,16 @@ def filter_plugins[T: DataPlugin]( if pinned_name is not None: for loaded_type in plugin_types: if loaded_type.name() == pinned_name: - self._logger.warning( - f'Using {group_name} plugin: {loaded_type.name()} from {getmodule(loaded_type)}' - ) + self._logger.info(f'Using {group_name} plugin: {loaded_type.name()} from {getmodule(loaded_type)}') return [loaded_type] - self._logger.warning(f"'{group_name}_name' was empty. Trying to deduce {group_name}s") + self._logger.info(f"'{group_name}_name' was empty. Trying to deduce {group_name}s") supported_types: list[type[T]] = [] # Deduce types for loaded_type in plugin_types: - self._logger.warning( - f'A {group_name} plugin is supported: {loaded_type.name()} from {getmodule(loaded_type)}' - ) + self._logger.info(f'A {group_name} plugin is supported: {loaded_type.name()} from {getmodule(loaded_type)}') supported_types.append(loaded_type) # Fail @@ -260,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.pyproject_file.parent).repository: + if scm_type.features(project_data.project_root).repository: return scm_type self._logger.info('No SCM plugin was found that supports the given path') @@ -342,7 +344,7 @@ def create_generator( generator_data = resolve_generator(core_data.project_data, cppython_plugin_data) if not generator_configuration: - self._logger.error( + self._logger.info( "The pyproject.toml table 'tool.cppython.generator' does not exist. Sending generator empty data", ) @@ -377,7 +379,7 @@ def create_provider( provider_data = resolve_provider(core_data.project_data, cppython_plugin_data) if not provider_configuration: - self._logger.error( + self._logger.info( "The pyproject.toml table 'tool.cppython.provider' does not exist. Sending provider empty data", ) @@ -393,17 +395,16 @@ def create_provider( class Builder: """Helper class for building CPPython projects""" + levels = [logging.WARNING, logging.INFO, logging.DEBUG] + def __init__(self, project_configuration: ProjectConfiguration, logger: Logger) -> None: """Initializes the builder""" self._project_configuration = project_configuration self._logger = logger - # Default logging levels - levels = [logging.WARNING, logging.INFO, logging.DEBUG] - # Add default output stream self._logger.addHandler(logging.StreamHandler()) - self._logger.setLevel(levels[project_configuration.verbosity]) + self._logger.setLevel(Builder.levels[project_configuration.verbosity]) self._logger.info('Logging setup complete') diff --git a/cppython/console/entry.py b/cppython/console/entry.py index fcdd209..3a2f0d3 100644 --- a/cppython/console/entry.py +++ b/cppython/console/entry.py @@ -49,9 +49,8 @@ def main( debug: Debug mode """ path = _find_pyproject_file() - file_path = path / 'pyproject.toml' - project_configuration = ProjectConfiguration(verbosity=verbose, debug=debug, pyproject_file=file_path, version=None) + project_configuration = ProjectConfiguration(verbosity=verbose, debug=debug, project_root=path, version=None) interface = ConsoleInterface() context.obj = ConsoleConfiguration(project_configuration=project_configuration, interface=interface) @@ -79,7 +78,7 @@ def install( if (configuration := context.find_object(ConsoleConfiguration)) is None: raise ValueError('The configuration object is missing') - path = configuration.project_configuration.pyproject_file + 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) @@ -101,7 +100,7 @@ def update( if (configuration := context.find_object(ConsoleConfiguration)) is None: raise ValueError('The configuration object is missing') - path = configuration.project_configuration.pyproject_file + 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) diff --git a/cppython/core/resolution.py b/cppython/core/resolution.py index 03a873c..0c7c09d 100644 --- a/cppython/core/resolution.py +++ b/cppython/core/resolution.py @@ -1,11 +1,13 @@ """Data conversion routines""" +import logging from pathlib import Path from typing import Any, cast +from packaging.requirements import InvalidRequirement, Requirement from pydantic import BaseModel, DirectoryPath, ValidationError -from cppython.core.exception import ConfigError, ConfigException +from cppython.core.exception import ConfigException 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 @@ -33,7 +35,7 @@ def resolve_project_configuration(project_configuration: ProjectConfiguration) - Returns: The resolved data """ - return ProjectData(pyproject_file=project_configuration.pyproject_file, verbosity=project_configuration.verbosity) + return ProjectData(project_root=project_configuration.project_root, verbosity=project_configuration.verbosity) def resolve_pep621( @@ -58,7 +60,7 @@ def resolve_pep621( if project_configuration.version is not None: modified_version = project_configuration.version elif scm is not None: - modified_version = scm.version(project_configuration.pyproject_file.parent) + modified_version = scm.version(project_configuration.project_root) else: raise ValueError("Version can't be resolved. No SCM") @@ -110,9 +112,18 @@ def resolve_cppython( Returns: An instance of the resolved type """ - root_directory = project_data.pyproject_file.parent.absolute() + root_directory = project_data.project_root.absolute() # Add the base path to all relative paths + modified_configuration_path = local_configuration.configuration_path + + # TODO: Grab configuration from the project, user, or system + if modified_configuration_path is None: + modified_configuration_path = root_directory / 'cppython.json' + + if not modified_configuration_path.is_absolute(): + modified_configuration_path = root_directory / modified_configuration_path + modified_install_path = local_configuration.install_path if not modified_install_path.is_absolute(): @@ -128,11 +139,6 @@ def resolve_cppython( if not modified_build_path.is_absolute(): modified_build_path = root_directory / modified_build_path - # Create directories if they do not exist - modified_install_path.mkdir(parents=True, exist_ok=True) - modified_tool_path.mkdir(parents=True, exist_ok=True) - modified_build_path.mkdir(parents=True, exist_ok=True) - modified_provider_name = local_configuration.provider_name modified_generator_name = local_configuration.generator_name @@ -144,7 +150,21 @@ def resolve_cppython( modified_scm_name = plugin_build_data.scm_name + # Construct dependencies from the local configuration only + dependencies: list[Requirement] = [] + invalid_requirements: list[str] = [] + if local_configuration.dependencies: + for dependency in local_configuration.dependencies: + try: + dependencies.append(Requirement(dependency)) + except InvalidRequirement as error: + invalid_requirements.append(f"Invalid requirement '{dependency}': {error}") + + if invalid_requirements: + raise ConfigException('\n'.join(invalid_requirements), []) + cppython_data = CPPythonData( + configuration_path=modified_configuration_path, install_path=modified_install_path, tool_path=modified_tool_path, build_path=modified_build_path, @@ -152,6 +172,7 @@ def resolve_cppython( provider_name=modified_provider_name, generator_name=modified_generator_name, scm_name=modified_scm_name, + dependencies=dependencies, ) return cppython_data @@ -168,9 +189,9 @@ def resolve_cppython_plugin(cppython_data: CPPythonData, plugin_type: type[Plugi """ # Add plugin specific paths to the base path modified_install_path = cppython_data.install_path / plugin_type.name() - modified_install_path.mkdir(parents=True, exist_ok=True) plugin_data = CPPythonData( + configuration_path=cppython_data.configuration_path, install_path=modified_install_path, tool_path=cppython_data.tool_path, build_path=cppython_data.build_path, @@ -178,6 +199,7 @@ def resolve_cppython_plugin(cppython_data: CPPythonData, plugin_type: type[Plugi provider_name=cppython_data.provider_name, generator_name=cppython_data.generator_name, scm_name=cppython_data.scm_name, + dependencies=cppython_data.dependencies, ) return cast(CPPythonPluginData, plugin_data) @@ -194,7 +216,6 @@ def _write_tool_directory(cppython_data: CPPythonData, directory: Path) -> Direc The written path """ plugin_directory = cppython_data.tool_path / 'cppython' / directory - plugin_directory.mkdir(parents=True, exist_ok=True) return plugin_directory @@ -209,7 +230,7 @@ def resolve_generator(project_data: ProjectData, cppython_data: CPPythonPluginDa Returns: The plugin specific configuration """ - root_directory = project_data.pyproject_file.parent + root_directory = project_data.project_root tool_directory = _write_tool_directory(cppython_data, Path('generators') / cppython_data.generator_name) configuration = GeneratorPluginGroupData(root_directory=root_directory, tool_directory=tool_directory) return configuration @@ -225,7 +246,7 @@ def resolve_provider(project_data: ProjectData, cppython_data: CPPythonPluginDat Returns: The plugin specific configuration """ - root_directory = project_data.pyproject_file.parent + root_directory = project_data.project_root tool_directory = _write_tool_directory(cppython_data, Path('providers') / cppython_data.provider_name) configuration = ProviderPluginGroupData(root_directory=root_directory, tool_directory=tool_directory) return configuration @@ -241,7 +262,7 @@ def resolve_scm(project_data: ProjectData, cppython_data: CPPythonPluginData) -> Returns: The plugin specific configuration """ - root_directory = project_data.pyproject_file.parent + root_directory = project_data.project_root tool_directory = _write_tool_directory(cppython_data, Path('managers') / cppython_data.scm_name) configuration = SCMPluginGroupData(root_directory=root_directory, tool_directory=tool_directory) return configuration @@ -264,7 +285,15 @@ def resolve_model[T: BaseModel](model: type[T], data: dict[str, Any]) -> T: # BaseModel is setup to ignore extra fields return model(**data) except ValidationError as e: - new_errors: list[ConfigError] = [] - for error in e.errors(): - new_errors.append(ConfigError(message=error['msg'])) - raise ConfigException('The input project failed', new_errors) from e + # Log the raw ValidationError for debugging + logging.getLogger('cppython').debug('ValidationError details: %s', e.errors()) + + if e.errors(): + formatted_errors = '\n'.join( + f"Field '{'.'.join(map(str, error['loc']))}': {error['msg']}" + for error in e.errors(include_input=True, include_context=True) + ) + else: + formatted_errors = 'An unknown validation error occurred.' + + raise ConfigException(f'The input project failed validation:\n{formatted_errors}', []) from e diff --git a/cppython/core/schema.py b/cppython/core/schema.py index e5c566d..db0dd2a 100644 --- a/cppython/core/schema.py +++ b/cppython/core/schema.py @@ -4,8 +4,9 @@ from pathlib import Path from typing import Annotated, Any, NewType, Protocol, runtime_checkable -from pydantic import BaseModel, Field, field_validator, model_validator -from pydantic.types import DirectoryPath, FilePath +from packaging.requirements import Requirement +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic.types import DirectoryPath from cppython.utility.plugin import Plugin as SynodicPlugin from cppython.utility.utility import TypeName @@ -14,20 +15,20 @@ class CPPythonModel(BaseModel): """The base model to use for all CPPython models""" - model_config = {'populate_by_name': False} + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True, arbitrary_types_allowed=True) class ProjectData(CPPythonModel, extra='forbid'): """Resolved data of 'ProjectConfiguration'""" - pyproject_file: Annotated[FilePath, Field(description='The path where the pyproject.toml exists')] + project_root: Annotated[Path, Field(description='The path where the pyproject.toml exists')] verbosity: Annotated[int, Field(description='The verbosity level as an integer [0,2]')] = 0 class ProjectConfiguration(CPPythonModel, extra='forbid'): """Project-wide configuration""" - pyproject_file: Annotated[FilePath, Field(description='The path where the pyproject.toml exists')] + project_root: Annotated[Path, Field(description='The path where the pyproject.toml exists')] version: Annotated[ str | None, Field( @@ -56,25 +57,6 @@ def min_max(cls, value: int) -> int: """ return min(max(value, 0), 2) - @field_validator('pyproject_file') - @classmethod - def pyproject_name(cls, value: FilePath) -> FilePath: - """Validator that verifies the name of the file - - Args: - value: Input to validate - - Raises: - ValueError: The given filepath is not named "pyproject.toml" - - Returns: - The file path - """ - if value.name != 'pyproject.toml': - raise ValueError('The given file is not named "pyproject.toml"') - - return value - class PEP621Data(CPPythonModel): """Resolved PEP621Configuration data""" @@ -110,7 +92,7 @@ def dynamic_data(cls, model: 'PEP621Configuration') -> 'PEP621Configuration': Returns: The data """ - for field in model.model_fields: + for field in PEP621Configuration.model_fields: if field == 'dynamic': continue value = getattr(model, field) @@ -123,24 +105,22 @@ def dynamic_data(cls, model: 'PEP621Configuration') -> 'PEP621Configuration': return model -def _default_install_location() -> Path: - return Path.home() / '.cppython' - - class CPPythonData(CPPythonModel, extra='forbid'): """Resolved CPPython data with local and global configuration""" - install_path: DirectoryPath - tool_path: DirectoryPath - build_path: DirectoryPath + configuration_path: Path + install_path: Path + tool_path: Path + build_path: Path current_check: bool provider_name: TypeName generator_name: TypeName scm_name: TypeName + dependencies: list[Requirement] - @field_validator('install_path', 'tool_path', 'build_path') + @field_validator('configuration_path', 'install_path', 'tool_path', 'build_path') @classmethod - def validate_absolute_path(cls, value: DirectoryPath) -> DirectoryPath: + def validate_absolute_path(cls, value: Path) -> Path: """Enforce the input is an absolute path Args: @@ -184,7 +164,7 @@ class PluginGroupData(CPPythonModel, extra='forbid'): root_directory: Annotated[DirectoryPath, Field(description='The directory of the project')] tool_directory: Annotated[ - DirectoryPath, + Path, Field( description=( 'Points to the project plugin directory within the tool directory. ' @@ -287,20 +267,40 @@ class CPPythonGlobalConfiguration(CPPythonModel, extra='forbid'): class CPPythonLocalConfiguration(CPPythonModel, extra='forbid'): """Data required by the tool""" + configuration_path: Annotated[ + Path | None, + Field( + description='The path to the configuration override file. If present, configuration found in the given' + ' directory will be preferred' + ), + ] = None + install_path: Annotated[ Path, Field( alias='install-path', - description='The global install path for the project', + description='The global install path for the project. Provider and generator plugins will be' + ' installed here.', ), - ] = _default_install_location() - tool_path: Annotated[Path, Field(alias='tool-path', description='The local tooling path for the project')] = Path( - 'tool' - ) + ] = Path.home() / '.cppython' - build_path: Annotated[Path, Field(alias='build-path', description='The local build path for the project')] = Path( - 'build' - ) + tool_path: Annotated[ + Path, + Field( + alias='tool-path', + description='The local tooling path for the project. If the provider or generator need additional file' + ' support, this directory will be used', + ), + ] = Path('tool') + + build_path: Annotated[ + Path, + Field( + alias='build-path', + description='The local build path for the project. This is where the artifacts of the local C++ build' + ' process will be generated.', + ), + ] = Path('build') provider: Annotated[ProviderData, Field(description="Provider plugin data associated with 'provider_name")] = ( ProviderData({}) @@ -326,6 +326,14 @@ class CPPythonLocalConfiguration(CPPythonModel, extra='forbid'): ), ] = None + dependencies: Annotated[ + list[str] | None, + Field( + description='A list of dependencies that will be installed. This is a list of pip compatible requirements' + ' strings', + ), + ] = None + class ToolData(CPPythonModel): """Tool entry of pyproject.toml""" diff --git a/cppython/core/utility.py b/cppython/core/utility.py deleted file mode 100644 index b361baf..0000000 --- a/cppython/core/utility.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Core Utilities""" - -import json -from pathlib import Path -from typing import Any - -from pydantic import BaseModel - - -def read_json(path: Path) -> Any: - """Reading routine - - Args: - path: The json file to read - - Returns: - The json data - """ - with open(path, encoding='utf-8') as file: - return json.load(file) - - -def write_model_json(path: Path, model: BaseModel) -> None: - """Writing routine. Only writes model data - - Args: - path: The json file to write - model: The model to write into a json - """ - serialized = json.loads(model.model_dump_json(exclude_none=True)) - with open(path, 'w', encoding='utf8') as file: - json.dump(serialized, file, ensure_ascii=False, indent=4) - - -def write_json(path: Path, data: Any) -> None: - """Writing routine - - Args: - path: The json to write - data: The data to write into json - """ - with open(path, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index ae385ec..15e7dcf 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -1,9 +1,9 @@ """Plugin builder""" +import json from copy import deepcopy from pathlib import Path -from cppython.core.utility import read_json, write_json, write_model_json from cppython.plugins.cmake.schema import CMakePresets, CMakeSyncData, ConfigurePreset @@ -23,7 +23,9 @@ def write_provider_preset(provider_directory: Path, data: CMakeSyncData) -> None json_path = provider_directory / f'{data.provider_name}.json' - write_model_json(json_path, presets) + serialized = json.loads(presets.model_dump_json(exclude_none=True, by_alias=False)) + with open(json_path, 'w', encoding='utf8') as file: + json.dump(serialized, file, ensure_ascii=False, indent=4) @staticmethod def write_cppython_preset( @@ -42,7 +44,10 @@ def write_cppython_preset( cppython_json_path = cppython_preset_directory / 'cppython.json' - write_model_json(cppython_json_path, presets) + serialized = json.loads(presets.model_dump_json(exclude_none=True, by_alias=False)) + with open(cppython_json_path, 'w', encoding='utf8') as file: + json.dump(serialized, file, ensure_ascii=False, indent=4) + return cppython_json_path @staticmethod @@ -57,7 +62,9 @@ def write_root_presets(preset_file: Path, _: Path) -> None: Args: preset_file: Preset file to modify """ - initial_root_preset = read_json(preset_file) + with open(preset_file, encoding='utf-8') as file: + initial_root_preset = json.load(file) if (root_preset := deepcopy(initial_root_preset)) != initial_root_preset: - write_json(preset_file, root_preset) + with open(preset_file, 'w', encoding='utf-8') as file: + json.dump(root_preset, file, ensure_ascii=False, indent=4) diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index 0fbb419..15fdc62 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -24,6 +24,9 @@ def __init__(self, group_data: GeneratorPluginGroupData, core_data: CorePluginDa self.data = resolve_cmake_data(data, core_data) self.builder = Builder() + self._cppython_preset_directory = self.core_data.cppython_data.tool_path / 'cppython' + self._provider_directory = self._cppython_preset_directory / 'providers' + @staticmethod def features(_: Path) -> SupportedGeneratorFeatures: """Queries if CMake is supported @@ -57,17 +60,17 @@ def sync(self, sync_data: SyncData) -> None: Args: sync_data: The input data """ - if isinstance(sync_data, CMakeSyncData): - cppython_preset_directory = self.core_data.cppython_data.tool_path / 'cppython' - cppython_preset_directory.mkdir(parents=True, exist_ok=True) - - provider_directory = cppython_preset_directory / 'providers' - provider_directory.mkdir(parents=True, exist_ok=True) + match sync_data: + case CMakeSyncData(): + self._cppython_preset_directory.mkdir(parents=True, exist_ok=True) + self._provider_directory.mkdir(parents=True, exist_ok=True) - self.builder.write_provider_preset(provider_directory, sync_data) + self.builder.write_provider_preset(self._provider_directory, sync_data) - cppython_preset_file = self.builder.write_cppython_preset( - cppython_preset_directory, provider_directory, sync_data - ) + cppython_preset_file = self.builder.write_cppython_preset( + self._cppython_preset_directory, self._provider_directory, sync_data + ) - self.builder.write_root_presets(self.data.preset_file, cppython_preset_file) + self.builder.write_root_presets(self.data.preset_file, cppython_preset_file) + case _: + raise ValueError('Unsupported sync data type') diff --git a/cppython/plugins/cmake/resolution.py b/cppython/plugins/cmake/resolution.py index ddae351..c3ad547 100644 --- a/cppython/plugins/cmake/resolution.py +++ b/cppython/plugins/cmake/resolution.py @@ -1,9 +1,10 @@ """Builder to help resolve cmake state""" +import json from typing import Any from cppython.core.schema import CorePluginData -from cppython.plugins.cmake.schema import CMakeConfiguration, CMakeData +from cppython.plugins.cmake.schema import CMakeConfiguration, CMakeData, CMakePresets def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMakeData: @@ -18,10 +19,17 @@ def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMake """ parsed_data = CMakeConfiguration(**data) - root_directory = core_data.project_data.pyproject_file.parent.absolute() + root_directory = core_data.project_data.project_root.absolute() - modified_preset = parsed_data.preset_file - if not modified_preset.is_absolute(): - modified_preset = root_directory / modified_preset + modified_preset_dir = parsed_data.preset_file + if not modified_preset_dir.is_absolute(): + modified_preset_dir = root_directory / modified_preset_dir - return CMakeData(preset_file=modified_preset, configuration_name=parsed_data.configuration_name) + # If the user hasn't specified a preset file, we need to create one + if not modified_preset_dir.exists(): + modified_preset_dir.parent.mkdir(parents=True, exist_ok=True) + with modified_preset_dir.open('w', encoding='utf-8') as file: + presets_dict = CMakePresets().model_dump_json(exclude_none=True) + json.dump(presets_dict, file, ensure_ascii=False, indent=4) + + return CMakeData(preset_file=modified_preset_dir, configuration_name=parsed_data.configuration_name) diff --git a/cppython/plugins/cmake/schema.py b/cppython/plugins/cmake/schema.py index 2e7cabb..b310fb7 100644 --- a/cppython/plugins/cmake/schema.py +++ b/cppython/plugins/cmake/schema.py @@ -66,9 +66,12 @@ class CMakeConfiguration(CPPythonModel): """Configuration""" preset_file: Annotated[ - FilePath, + Path, Field( - description="The CMakePreset.json file that will be searched for the given 'configuration_name'", + description='The CMakePreset.json file that will be managed by CPPython. Will' + " be searched for the given 'configuration_name'", ), ] = Path('CMakePresets.json') - configuration_name: Annotated[str, Field(description='The CMake configuration preset to look for and override')] + configuration_name: Annotated[ + str, Field(description='The CMake configuration preset to look for and override inside the given `preset_file`') + ] = 'cppython' diff --git a/cppython/plugins/conan/__init__.py b/cppython/plugins/conan/__init__.py new file mode 100644 index 0000000..cd8a30d --- /dev/null +++ b/cppython/plugins/conan/__init__.py @@ -0,0 +1 @@ +"""The Conan provider plugin for CPPython.""" diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py new file mode 100644 index 0000000..db441d3 --- /dev/null +++ b/cppython/plugins/conan/plugin.py @@ -0,0 +1,101 @@ +"""_summary_""" + +from pathlib import Path +from typing import Any + +import requests + +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.plugins.cmake.plugin import CMakeGenerator +from cppython.plugins.cmake.schema import CMakeSyncData +from cppython.plugins.conan.resolution import resolve_conan_data +from cppython.plugins.conan.schema import ConanData +from cppython.utility.exception import NotSupportedError +from cppython.utility.utility import TypeName + + +class ConanProvider(Provider): + """Conan Provider""" + + _provider_url = 'https://raw.githubusercontent.com/conan-io/cmake-conan/refs/heads/develop2/conan_provider.cmake' + + def __init__( + self, group_data: ProviderPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any] + ) -> None: + """Initializes the provider""" + self.group_data: ProviderPluginGroupData = group_data + self.core_data: CorePluginData = core_data + self.data: ConanData = resolve_conan_data(configuration_data, core_data) + + @staticmethod + def _download_file(url: str, file: Path) -> None: + """Replaces the given file with the contents of the url""" + file.parent.mkdir(parents=True, exist_ok=True) + + with open(file, 'wb') as out_file: + content = requests.get(url, stream=True).content + out_file.write(content) + + @staticmethod + def features(directory: Path) -> SupportedProviderFeatures: + """Queries conan support + + Args: + directory: The directory to query + + Returns: + Supported features + """ + return SupportedProviderFeatures() + + @staticmethod + def information() -> Information: + """Returns plugin information + + Returns: + Plugin information + """ + return Information() + + def install(self) -> None: + """Installs the provider""" + + def update(self) -> None: + """Updates the provider""" + + @staticmethod + def supported_sync_type(sync_type: type[SyncData]) -> bool: + """_summary_ + + Args: + sync_type: _description_ + + Returns: + _description_ + """ + return sync_type in CMakeGenerator.sync_types() + + def sync_data(self, consumer: SyncConsumer) -> SyncData: + """_summary_ + + Args: + consumer: _description_ + + Returns: + _description_ + """ + for sync_type in consumer.sync_types(): + if sync_type == CMakeSyncData: + return CMakeSyncData( + provider_name=TypeName('conan'), + top_level_includes=self.core_data.cppython_data.tool_path / 'conan' / 'conan_provider.cmake', + ) + + raise NotSupportedError('OOF') + + @classmethod + async def download_tooling(cls, directory: Path) -> None: + """Downloads the conan provider file""" + cls._download_file(cls._provider_url, directory / 'conan_provider.cmake') diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py new file mode 100644 index 0000000..b006062 --- /dev/null +++ b/cppython/plugins/conan/resolution.py @@ -0,0 +1,22 @@ +"""_summary_""" + +from typing import Any + +from cppython.core.schema import CorePluginData +from cppython.plugins.conan.schema import ConanData + + +def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> ConanData: + """Resolves the conan data + + Args: + data: The data to resolve + core_data: The core plugin data + + Returns: + The resolved conan data + """ + # parsed_data = ConanConfiguration(**data) + # root_directory = core_data.project_data.pyproject_file.parent.absolute() + + return ConanData() diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py new file mode 100644 index 0000000..6d751a2 --- /dev/null +++ b/cppython/plugins/conan/schema.py @@ -0,0 +1,11 @@ +"""TODO""" + +from cppython.core.schema import CPPythonModel + + +class ConanData(CPPythonModel): + """Resolved conan data""" + + +class ConanConfiguration(CPPythonModel): + """Raw conan data""" diff --git a/cppython/plugins/pdm/plugin.py b/cppython/plugins/pdm/plugin.py index eca64fe..e38cbb2 100644 --- a/cppython/plugins/pdm/plugin.py +++ b/cppython/plugins/pdm/plugin.py @@ -33,15 +33,13 @@ def on_post_install(self, project: Project, dry_run: bool, **_kwargs: Any) -> No dry_run: If true, won't perform any actions _kwargs: Sink for unknown arguments """ - pyproject_file = project.root.absolute() / project.PYPROJECT_FILENAME + root = project.root.absolute() # Attach configuration for CPPythonPlugin callbacks version = project.pyproject.metadata.get('version') verbosity = project.core.ui.verbosity - project_configuration = ProjectConfiguration( - pyproject_file=pyproject_file, verbosity=verbosity, version=version - ) + project_configuration = ProjectConfiguration(project_root=root, verbosity=verbosity, version=version) self.logger.info("CPPython: Entered 'on_post_install'") diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index f5464ab..1fef385 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -18,7 +18,7 @@ from cppython.plugins.vcpkg.resolution import generate_manifest, resolve_vcpkg_data from cppython.plugins.vcpkg.schema import VcpkgData from cppython.utility.exception import NotSupportedError, ProcessError -from cppython.utility.subprocess import call as subprocess_call +from cppython.utility.subprocess import invoke as subprocess_call from cppython.utility.utility import TypeName @@ -77,15 +77,22 @@ def _update_provider(cls, path: Path) -> None: try: if system_name == 'nt': - subprocess_call([str(WindowsPath('bootstrap-vcpkg.bat'))], logger=logger, cwd=path, shell=True) + subprocess_call( + str(WindowsPath('bootstrap-vcpkg.bat')), ['-disableMetrics'], logger=logger, cwd=path, shell=True + ) elif system_name == 'posix': - subprocess_call(['./' + str(PosixPath('bootstrap-vcpkg.sh'))], logger=logger, cwd=path, shell=True) + subprocess_call( + './' + str(PosixPath('bootstrap-vcpkg.sh')), + ['-disableMetrics'], + logger=logger, + cwd=path, + shell=True, + ) except ProcessError: logger.error('Unable to bootstrap the vcpkg repository', exc_info=True) raise - @staticmethod - def sync_data(consumer: SyncConsumer) -> SyncData: + def sync_data(self, consumer: SyncConsumer) -> SyncData: """Gathers a data object for the given generator Args: @@ -99,8 +106,10 @@ def sync_data(consumer: SyncConsumer) -> SyncData: """ for sync_type in consumer.sync_types(): if sync_type == CMakeSyncData: - # toolchain_file = self.core_data.cppython_data.install_path / "scripts/buildsystems/vcpkg.cmake" - return CMakeSyncData(provider_name=TypeName('vcpkg'), top_level_includes=Path('test')) + return CMakeSyncData( + provider_name=TypeName('vcpkg'), + top_level_includes=self.core_data.cppython_data.install_path / 'scripts/buildsystems/vcpkg.cmake', + ) raise NotSupportedError('OOF') @@ -122,7 +131,8 @@ def tooling_downloaded(cls, path: Path) -> bool: try: # Hide output, given an error output is a logic conditional subprocess_call( - ['git', 'rev-parse', '--is-inside-work-tree'], + 'git', + ['rev-parse', '--is-inside-work-tree'], logger=logger, suppress=True, cwd=path, @@ -150,8 +160,8 @@ async def download_tooling(cls, directory: Path) -> None: logger.debug("Updating the vcpkg repository at '%s'", directory.absolute()) # The entire history is need for vcpkg 'baseline' information - subprocess_call(['git', 'fetch', 'origin'], logger=logger, cwd=directory) - subprocess_call(['git', 'pull'], logger=logger, cwd=directory) + subprocess_call('git', ['fetch', 'origin'], logger=logger, cwd=directory) + subprocess_call('git', ['pull'], logger=logger, cwd=directory) except ProcessError: logger.exception('Unable to update the vcpkg repository') raise @@ -161,7 +171,8 @@ async def download_tooling(cls, directory: Path) -> None: # The entire history is need for vcpkg 'baseline' information subprocess_call( - ['git', 'clone', 'https://github.com/microsoft/vcpkg', '.'], + 'git', + ['clone', 'https://github.com/microsoft/vcpkg', '.'], logger=logger, cwd=directory, ) @@ -178,7 +189,7 @@ def install(self) -> None: Raises: ProcessError: Failed vcpkg calls """ - manifest_directory = self.core_data.project_data.pyproject_file.parent + manifest_directory = self.core_data.project_data.project_root manifest = generate_manifest(self.core_data, self.data) # Write out the manifest @@ -190,8 +201,8 @@ def install(self) -> None: logger = getLogger('cppython.vcpkg') try: subprocess_call( + executable, [ - executable, 'install', f'--x-install-root={self.data.install_directory}', ], @@ -208,7 +219,8 @@ def update(self) -> None: Raises: ProcessError: Failed vcpkg calls """ - manifest_directory = self.core_data.project_data.pyproject_file.parent + manifest_directory = self.core_data.project_data.project_root + manifest = generate_manifest(self.core_data, self.data) # Write out the manifest @@ -220,8 +232,8 @@ def update(self) -> None: logger = getLogger('cppython.vcpkg') try: subprocess_call( + executable, [ - executable, 'install', f'--x-install-root={self.data.install_directory}', ], diff --git a/cppython/plugins/vcpkg/resolution.py b/cppython/plugins/vcpkg/resolution.py index 61229c8..4dcfde5 100644 --- a/cppython/plugins/vcpkg/resolution.py +++ b/cppython/plugins/vcpkg/resolution.py @@ -1,7 +1,11 @@ """Builder to help build vcpkg state""" +from subprocess import CalledProcessError, check_output from typing import Any +from packaging.requirements import Requirement + +from cppython.core.exception import ConfigException from cppython.core.schema import CorePluginData from cppython.plugins.vcpkg.schema import ( Manifest, @@ -21,13 +25,23 @@ def generate_manifest(core_data: CorePluginData, data: VcpkgData) -> Manifest: Returns: The manifest """ - manifest = { - 'name': core_data.pep621_data.name, - 'version_string': core_data.pep621_data.version, - 'dependencies': data.dependencies, - } - - return Manifest(**manifest) + # If builtin_baseline is None, we set it to the current commit of the cloned vcpkg repository + if data.builtin_baseline is None: + try: + cwd = core_data.cppython_data.install_path + + # Get the current commit hash from the vcpkg repository + result = check_output(['git', 'rev-parse', 'HEAD'], cwd=cwd) + data.builtin_baseline = result.decode('utf-8').strip() + except (CalledProcessError, FileNotFoundError) as e: + raise ConfigException('Failed to get the current commit hash from the vcpkg repository.', []) from e + + return Manifest( + name=core_data.pep621_data.name, + version_string=core_data.pep621_data.version, + dependencies=data.dependencies, + builtin_baseline=data.builtin_baseline, + ) def resolve_vcpkg_data(data: dict[str, Any], core_data: CorePluginData) -> VcpkgData: @@ -42,7 +56,7 @@ def resolve_vcpkg_data(data: dict[str, Any], core_data: CorePluginData) -> Vcpkg """ parsed_data = VcpkgConfiguration(**data) - root_directory = core_data.project_data.pyproject_file.parent.absolute() + root_directory = core_data.project_data.project_root.absolute() modified_install_directory = parsed_data.install_directory @@ -54,11 +68,45 @@ def resolve_vcpkg_data(data: dict[str, Any], core_data: CorePluginData) -> Vcpkg modified_install_directory.mkdir(parents=True, exist_ok=True) vcpkg_dependencies: list[VcpkgDependency] = [] - for dependency in parsed_data.dependencies: - vcpkg_dependency = VcpkgDependency(name=dependency.name) - vcpkg_dependencies.append(vcpkg_dependency) + for requirement in core_data.cppython_data.dependencies: + resolved_dependency = resolve_vcpkg_dependency(requirement) + vcpkg_dependencies.append(resolved_dependency) return VcpkgData( install_directory=modified_install_directory, dependencies=vcpkg_dependencies, + builtin_baseline=parsed_data.builtin_baseline, + ) + + +def resolve_vcpkg_dependency(requirement: Requirement) -> VcpkgDependency: + """Resolve a VcpkgDependency from a packaging requirement. + + Args: + requirement: A packaging requirement object. + + Returns: + A resolved VcpkgDependency object. + """ + specifiers = requirement.specifier + + # If the length of specifiers is greater than one, raise a configuration error + if len(specifiers) > 1: + raise ConfigException('Multiple specifiers are not supported. Please provide a single specifier.', []) + + # Extract the version from the single specifier + min_version = None + if len(specifiers) == 1: + specifier = next(iter(specifiers)) + if specifier.operator != '>=': + raise ConfigException(f"Unsupported specifier '{specifier.operator}'. Only '>=' is supported.", []) + min_version = specifier.version + + return VcpkgDependency( + name=requirement.name, + default_features=True, + features=[], + version_ge=min_version, + platform=None, + host=False, ) diff --git a/cppython/plugins/vcpkg/schema.py b/cppython/plugins/vcpkg/schema.py index ef34113..662dcb5 100644 --- a/cppython/plugins/vcpkg/schema.py +++ b/cppython/plugins/vcpkg/schema.py @@ -4,7 +4,6 @@ from typing import Annotated from pydantic import Field, HttpUrl -from pydantic.types import DirectoryPath from cppython.core.schema import CPPythonModel @@ -12,14 +11,41 @@ class VcpkgDependency(CPPythonModel): """Vcpkg dependency type""" - name: str + name: Annotated[str, Field(description='The name of the dependency.')] + default_features: Annotated[ + bool, + Field( + alias='default-features', + description='Whether to use the default features of the dependency. Defaults to true.', + ), + ] = True + features: Annotated[ + list[str], + Field(description='A list of additional features to require for the dependency.'), + ] = [] + version_ge: Annotated[ + str | None, + Field( + alias='version>=', + description='The minimum required version of the dependency, optionally with a port-version suffix.', + ), + ] = None + platform: Annotated[ + str | None, + Field(description='A platform expression specifying the platforms where the dependency applies.'), + ] = None + host: Annotated[ + bool, + Field(description='Whether the dependency is required for the host machine instead of the target.'), + ] = False class VcpkgData(CPPythonModel): """Resolved vcpkg data""" - install_directory: DirectoryPath + install_directory: Path dependencies: list[VcpkgDependency] + builtin_baseline: str | None class VcpkgConfiguration(CPPythonModel): @@ -33,9 +59,13 @@ class VcpkgConfiguration(CPPythonModel): ), ] = Path('build') - dependencies: Annotated[ - list[VcpkgDependency], Field(description='The directory to store the manifest file, vcpkg.json') - ] = [] + builtin_baseline: Annotated[ + str | None, + Field( + alias='builtin-baseline', + description='A shortcut for specifying the baseline for version resolution in the default registry.', + ), + ] = None class Manifest(CPPythonModel): @@ -43,7 +73,9 @@ class Manifest(CPPythonModel): name: Annotated[str, Field(description='The project name')] - version_string: Annotated[str, Field(alias='version-string', description='The arbitrary version string')] = '' + version_string: Annotated[str, Field(alias='version-string', description='The arbitrary version string')] + description: Annotated[str, Field(description='The project description')] = '' homepage: Annotated[HttpUrl | None, Field(description='Homepage URL')] = None dependencies: Annotated[list[VcpkgDependency], Field(description='List of dependencies')] = [] + builtin_baseline: Annotated[str, Field(alias='builtin-baseline', description='The arbitrary version string')] diff --git a/cppython/project.py b/cppython/project.py index 40c871f..075c0a2 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -9,6 +9,7 @@ from cppython.core.resolution import resolve_model from cppython.core.schema import Interface, ProjectConfiguration, PyProject from cppython.schema import API +from cppython.utility.exception import ProcessError class Project(API): @@ -29,11 +30,12 @@ def __init__( try: pyproject = resolve_model(PyProject, pyproject_data) except ConfigException as error: - self.logger.error(error, exc_info=True) - return + # Log the exception message explicitly + self.logger.error('Configuration error:\n%s', error, exc_info=False) + raise SystemExit('Error: Invalid configuration. Please check your pyproject.toml.') from None if not pyproject.tool or not pyproject.tool.cppython: - self.logger.warning("The pyproject.toml file doesn't contain the `tool.cppython` table") + self.logger.info("The pyproject.toml file doesn't contain the `tool.cppython` table") return self._data = builder.build(pyproject.project, pyproject.tool.cppython) @@ -69,9 +71,12 @@ def install(self) -> None: try: self._data.plugins.provider.install() + except ProcessError as error: + self.logger.error('Installation failed: %s', error.error) + raise SystemExit('Error: Provider installation failed. Please check the logs.') from None except Exception as exception: - self.logger.error('Provider %s failed to install', self._data.plugins.provider.name()) - raise exception + self.logger.error('Unexpected error during installation: %s', str(exception)) + raise SystemExit('Error: An unexpected error occurred during installation.') from None self._data.sync() @@ -93,8 +98,11 @@ def update(self) -> None: try: self._data.plugins.provider.update() + except ProcessError as error: + self.logger.error('Update failed: %s', error.error) + raise SystemExit('Error: Provider update failed. Please check the logs.') from None except Exception as exception: - self.logger.error('Provider %s failed to update', self._data.plugins.provider.name()) - raise exception + self.logger.error('Unexpected error during update: %s', str(exception)) + raise SystemExit('Error: An unexpected error occurred during update.') from None self._data.sync() diff --git a/cppython/test/data/__init__.py b/cppython/test/data/__init__.py new file mode 100644 index 0000000..071166f --- /dev/null +++ b/cppython/test/data/__init__.py @@ -0,0 +1 @@ +"""Data for testing plugins""" diff --git a/cppython/test/data/mocks.py b/cppython/test/data/mocks.py new file mode 100644 index 0000000..cbb7126 --- /dev/null +++ b/cppython/test/data/mocks.py @@ -0,0 +1,57 @@ +"""Mocked types and data for testing plugins""" + +from collections.abc import Sequence + +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.mock.generator import MockGenerator +from cppython.test.mock.provider import MockProvider +from cppython.test.mock.scm import MockSCM + + +def _mock_provider_list() -> Sequence[type[Provider]]: + """Mocked list of providers + + Returns: + A list of mock providers + """ + variants = [] + + # Default + variants.append(MockProvider) + + return variants + + +def _mock_generator_list() -> Sequence[type[Generator]]: + """Mocked list of generators + + Returns: + List of mock generators + """ + variants = [] + + # Default + variants.append(MockGenerator) + + return variants + + +def _mock_scm_list() -> Sequence[type[SCM]]: + """Mocked list of SCMs + + Returns: + List of mock SCMs + """ + variants = [] + + # Default + variants.append(MockSCM) + + return variants + + +provider_variants = _mock_provider_list() +generator_variants = _mock_generator_list() +scm_variants = _mock_scm_list() diff --git a/cppython/test/data/variants.py b/cppython/test/data/variants.py new file mode 100644 index 0000000..9a0d807 --- /dev/null +++ b/cppython/test/data/variants.py @@ -0,0 +1,93 @@ +"""Data definitions""" + +from pathlib import Path + +from cppython.core.schema import ( + CPPythonGlobalConfiguration, + CPPythonLocalConfiguration, + PEP621Configuration, + ProjectConfiguration, +) +from cppython.test.schema import Variant, Variants + + +def _pep621_configuration_list() -> Variants[PEP621Configuration]: + """Creates a list of mocked configuration types + + Returns: + A list of variants to test + """ + data = Variants[PEP621Configuration]() + + # Default + default = PEP621Configuration(name='default-test', version='1.0.0') + default_variant = Variant[PEP621Configuration](configuration=default) + + data.variants.append(default_variant) + + return data + + +def _cppython_local_configuration_list() -> Variants[CPPythonLocalConfiguration]: + """Mocked list of local configuration data + + Returns: + A list of variants to test + """ + data = Variants[CPPythonLocalConfiguration]() + + # Default + default = CPPythonLocalConfiguration() + default_variant = Variant[CPPythonLocalConfiguration](configuration=default) + + data.variants.append(default_variant) + + return data + + +def _cppython_global_configuration_list() -> Variants[CPPythonGlobalConfiguration]: + """Mocked list of global configuration data + + Returns: + A list of variants to test + """ + data = Variants[CPPythonGlobalConfiguration]() + + # Default + default = CPPythonGlobalConfiguration() + default_variant = Variant[CPPythonGlobalConfiguration](configuration=default) + + # Check off + check_off_data = {'current-check': False} + check_off = CPPythonGlobalConfiguration(**check_off_data) + check_off_variant = Variant[CPPythonGlobalConfiguration](configuration=check_off) + + data.variants.append(default_variant) + data.variants.append(check_off_variant) + + return data + + +def _project_configuration_list() -> Variants[ProjectConfiguration]: + """Mocked list of project configuration data + + Returns: + A list of variants to test + """ + data = Variants[ProjectConfiguration]() + + # NOTE: pyproject_file will be overridden by fixture + + # Default + default = ProjectConfiguration(project_root=Path(), version='0.1.0') + default_variant = Variant[ProjectConfiguration](configuration=default) + + data.variants.append(default_variant) + + return data + + +pep621_variants = _pep621_configuration_list() +cppython_local_variants = _cppython_local_configuration_list() +cppython_global_variants = _cppython_global_configuration_list() +project_variants = _project_configuration_list() diff --git a/tests/conftest.py b/cppython/test/pytest/fixtures.py similarity index 62% rename from tests/conftest.py rename to cppython/test/pytest/fixtures.py index 2e1f173..194720e 100644 --- a/tests/conftest.py +++ b/cppython/test/pytest/fixtures.py @@ -1,4 +1,4 @@ -"""Data variations for testing""" +"""Global fixtures for the test suite""" # from pathlib import Path from pathlib import Path @@ -28,29 +28,13 @@ PyProject, ToolData, ) -from cppython.plugins.cmake.schema import CMakeConfiguration -from cppython.test.pytest.variants import ( +from cppython.test.data.variants import ( cppython_global_variants, cppython_local_variants, pep621_variants, project_variants, ) - - -def _cmake_data_list() -> list[CMakeConfiguration]: - """Creates a list of mocked configuration types - - Returns: - A list of variants to test - """ - variants = [] - - # Default - variants.append(CMakeConfiguration(configuration_name='default')) - - # variants.append(CMakeConfiguration(preset_file=Path("inner/CMakePresets.json"), configuration_name="default")) - - return variants +from cppython.test.schema import Variant @pytest.fixture( @@ -74,9 +58,9 @@ def fixture_install_path(tmp_path_factory: pytest.TempPathFactory) -> Path: @pytest.fixture( name='pep621_configuration', scope='session', - params=pep621_variants, + params=pep621_variants.variants, ) -def fixture_pep621_configuration(request: pytest.FixtureRequest) -> PEP621Configuration: +def fixture_pep621_configuration(request: pytest.FixtureRequest) -> Variant[PEP621Configuration]: """Fixture defining all testable variations of PEP621 Args: @@ -85,7 +69,7 @@ def fixture_pep621_configuration(request: pytest.FixtureRequest) -> PEP621Config Returns: PEP621 variant """ - return cast(PEP621Configuration, request.param) + return cast(Variant[PEP621Configuration], request.param) @pytest.fixture( @@ -93,7 +77,7 @@ def fixture_pep621_configuration(request: pytest.FixtureRequest) -> PEP621Config scope='session', ) def fixture_pep621_data( - pep621_configuration: PEP621Configuration, project_configuration: ProjectConfiguration + pep621_configuration: Variant[PEP621Configuration], project_configuration: Variant[ProjectConfiguration] ) -> PEP621Data: """Resolved project table fixture @@ -104,17 +88,17 @@ def fixture_pep621_data( Returns: The resolved project table """ - return resolve_pep621(pep621_configuration, project_configuration, None) + return resolve_pep621(pep621_configuration.configuration, project_configuration.configuration, None) @pytest.fixture( name='cppython_local_configuration', scope='session', - params=cppython_local_variants, + params=cppython_local_variants.variants, ) def fixture_cppython_local_configuration( request: pytest.FixtureRequest, install_path: Path -) -> CPPythonLocalConfiguration: +) -> Variant[CPPythonLocalConfiguration]: """Fixture defining all testable variations of CPPythonData Args: @@ -124,9 +108,9 @@ def fixture_cppython_local_configuration( Returns: Variation of CPPython data """ - cppython_local_configuration = cast(CPPythonLocalConfiguration, request.param) + cppython_local_configuration = cast(Variant[CPPythonLocalConfiguration], request.param) - data = cppython_local_configuration.model_dump(by_alias=True) + data = cppython_local_configuration.configuration.model_dump(by_alias=True) # Pin the install location to the base temporary directory data['install-path'] = install_path @@ -135,15 +119,16 @@ def fixture_cppython_local_configuration( data['provider-name'] = 'mock' data['generator-name'] = 'mock' - return CPPythonLocalConfiguration(**data) + new_configuration = CPPythonLocalConfiguration(**data) + return Variant[CPPythonLocalConfiguration](configuration=new_configuration) @pytest.fixture( name='cppython_global_configuration', scope='session', - params=cppython_global_variants, + params=cppython_global_variants.variants, ) -def fixture_cppython_global_configuration(request: pytest.FixtureRequest) -> CPPythonGlobalConfiguration: +def fixture_cppython_global_configuration(request: pytest.FixtureRequest) -> Variant[CPPythonGlobalConfiguration]: """Fixture defining all testable variations of CPPythonData Args: @@ -152,7 +137,7 @@ def fixture_cppython_global_configuration(request: pytest.FixtureRequest) -> CPP Returns: Variation of CPPython data """ - cppython_global_configuration = cast(CPPythonGlobalConfiguration, request.param) + cppython_global_configuration = cast(Variant[CPPythonGlobalConfiguration], request.param) return cppython_global_configuration @@ -208,8 +193,8 @@ def fixture_plugin_cppython_data( scope='session', ) def fixture_cppython_data( - cppython_local_configuration: CPPythonLocalConfiguration, - cppython_global_configuration: CPPythonGlobalConfiguration, + cppython_local_configuration: Variant[CPPythonLocalConfiguration], + cppython_global_configuration: Variant[CPPythonGlobalConfiguration], project_data: ProjectData, plugin_cppython_data: PluginCPPythonData, ) -> CPPythonData: @@ -225,7 +210,10 @@ def fixture_cppython_data( The resolved CPPython table """ return resolve_cppython( - cppython_local_configuration, cppython_global_configuration, project_data, plugin_cppython_data + cppython_local_configuration.configuration, + cppython_global_configuration.configuration, + project_data, + plugin_cppython_data, ) @@ -248,11 +236,9 @@ def fixture_core_data(cppython_data: CPPythonData, project_data: ProjectData) -> @pytest.fixture( name='project_configuration', scope='session', - params=project_variants, + params=project_variants.variants, ) -def fixture_project_configuration( - request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory -) -> ProjectConfiguration: +def fixture_project_configuration(request: pytest.FixtureRequest) -> Variant[ProjectConfiguration]: """Project configuration fixture. Here we provide overrides on the input variants so that we can use a temporary directory for testing purposes. @@ -264,17 +250,7 @@ def fixture_project_configuration( Returns: Configuration with temporary directory capabilities """ - tmp_path = tmp_path_factory.mktemp('workspace-') - configuration = cast(ProjectConfiguration, request.param) - - pyproject_file = tmp_path / 'pyproject.toml' - - # Write a dummy file to satisfy the config constraints - with open(pyproject_file, 'w', encoding='utf-8'): - pass - - configuration.pyproject_file = pyproject_file - + configuration = cast(Variant[ProjectConfiguration], request.param) return configuration @@ -282,7 +258,7 @@ def fixture_project_configuration( name='project_data', scope='session', ) -def fixture_project_data(project_configuration: ProjectConfiguration) -> ProjectData: +def fixture_project_data(project_configuration: Variant[ProjectConfiguration]) -> ProjectData: """Fixture that creates a project space at 'workspace/test_project/pyproject.toml' Args: @@ -291,12 +267,13 @@ def fixture_project_data(project_configuration: ProjectConfiguration) -> Project Returns: A project data object that has populated a function level temporary directory """ - return resolve_project_configuration(project_configuration) + return resolve_project_configuration(project_configuration.configuration) @pytest.fixture(name='project') def fixture_project( - cppython_local_configuration: CPPythonLocalConfiguration, pep621_configuration: PEP621Configuration + cppython_local_configuration: Variant[CPPythonLocalConfiguration], + pep621_configuration: Variant[PEP621Configuration], ) -> PyProject: """Parameterized construction of PyProject data @@ -307,37 +284,5 @@ def fixture_project( Returns: All the data as one object """ - tool = ToolData(cppython=cppython_local_configuration) - return PyProject(project=pep621_configuration, tool=tool) - - -@pytest.fixture( - name='cmake_data', - scope='session', - params=_cmake_data_list(), -) -def fixture_cmake_data(request: pytest.FixtureRequest) -> CMakeConfiguration: - """A fixture to provide a list of configuration types - - Args: - request: Parameterization list - - Returns: - A configuration type instance - """ - return cast(CMakeConfiguration, request.param) - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Provides custom parameterization for dynamic fixture names. - - Args: - metafunc: Pytest hook data - """ - for fixture in metafunc.fixturenames: - match fixture.split('_', 1): - case ['build', directory]: - # Parameterizes the paths under tests/build/ where is the fixture suffix - - build_data_path = metafunc.config.rootpath / 'tests' / 'build' / directory - metafunc.parametrize(fixture, [build_data_path], scope='session') + tool = ToolData(cppython=cppython_local_configuration.configuration) + return PyProject(project=pep621_configuration.configuration, tool=tool) diff --git a/cppython/test/pytest/shared.py b/cppython/test/pytest/shared.py index 3363357..cf07e07 100644 --- a/cppython/test/pytest/shared.py +++ b/cppython/test/pytest/shared.py @@ -27,11 +27,12 @@ ProjectConfiguration, ProjectData, ) -from cppython.test.pytest.variants import ( +from cppython.test.data.mocks import ( generator_variants, provider_variants, scm_variants, ) +from cppython.test.schema import Variant class BaseTests[T: Plugin](metaclass=ABCMeta): @@ -130,7 +131,7 @@ 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: + def test_feature_extraction(plugin_type: type[T], project_configuration: Variant[ProjectConfiguration]) -> None: """Test the feature extraction of a plugin. This method tests the feature extraction functionality of a plugin by asserting that the features @@ -140,7 +141,7 @@ def test_feature_extraction(plugin_type: type[T], project_configuration: Project plugin_type: The type of plugin to test. project_configuration: The project configuration to use for testing. """ - assert plugin_type.features(project_configuration.pyproject_file.parent) + assert plugin_type.features(project_configuration.configuration.project_root) @staticmethod def test_information(plugin_type: type[T]) -> None: @@ -225,6 +226,17 @@ class DataPluginIntegrationTests[T: DataPlugin](BaseIntegrationTests[T], metacla 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""" @@ -242,17 +254,22 @@ def fixture_plugin_configuration_type() -> type[ProviderPluginGroupData]: @staticmethod @pytest.fixture(name='plugin_group_data', scope='session') def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData, tmp_path_factory: pytest.TempPathFactory ) -> 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 """ + project_data.project_root = tmp_path_factory.mktemp('workspace-') + # 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 @@ -327,17 +344,22 @@ def fixture_plugin_configuration_type() -> type[GeneratorPluginGroupData]: @staticmethod @pytest.fixture(name='plugin_group_data', scope='session') def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData, tmp_path_factory: pytest.TempPathFactory ) -> 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 """ + project_data.project_root = tmp_path_factory.mktemp('workspace-') + # 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 @@ -411,17 +433,22 @@ def fixture_plugin_configuration_type() -> type[SCMPluginGroupData]: @staticmethod @pytest.fixture(name='plugin_group_data', scope='session') def fixture_plugin_group_data( - project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData, tmp_path_factory: pytest.TempPathFactory ) -> 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 """ + project_data.project_root = tmp_path_factory.mktemp('workspace-') + # 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/variants.py b/cppython/test/pytest/variants.py deleted file mode 100644 index 3c16992..0000000 --- a/cppython/test/pytest/variants.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Data definitions""" - -from collections.abc import Sequence -from pathlib import Path - -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 ( - CPPythonGlobalConfiguration, - CPPythonLocalConfiguration, - PEP621Configuration, - ProjectConfiguration, -) -from cppython.test.mock.generator import MockGenerator -from cppython.test.mock.provider import MockProvider -from cppython.test.mock.scm import MockSCM - - -def _pep621_configuration_list() -> list[PEP621Configuration]: - """Creates a list of mocked configuration types - - Returns: - A list of variants to test - """ - variants = [] - - # Default - variants.append(PEP621Configuration(name='default-test', version='1.0.0')) - - return variants - - -def _cppython_local_configuration_list() -> list[CPPythonLocalConfiguration]: - """Mocked list of local configuration data - - Returns: - A list of variants to test - """ - variants = [] - - # Default - variants.append(CPPythonLocalConfiguration()) - - return variants - - -def _cppython_global_configuration_list() -> list[CPPythonGlobalConfiguration]: - """Mocked list of global configuration data - - Returns: - A list of variants to test - """ - variants = [] - - data = {'current-check': False} - - # Default - variants.append(CPPythonGlobalConfiguration()) - - # Check off - variants.append(CPPythonGlobalConfiguration(**data)) - - return variants - - -def _project_configuration_list() -> list[ProjectConfiguration]: - """Mocked list of project configuration data - - Returns: - A list of variants to test - """ - variants = [] - - # NOTE: pyproject_file will be overridden by fixture - - # Default - variants.append(ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0')) - - return variants - - -def _mock_provider_list() -> Sequence[type[Provider]]: - """Mocked list of providers - - Returns: - A list of mock providers - """ - variants = [] - - # Default - variants.append(MockProvider) - - return variants - - -def _mock_generator_list() -> Sequence[type[Generator]]: - """Mocked list of generators - - Returns: - List of mock generators - """ - variants = [] - - # Default - variants.append(MockGenerator) - - return variants - - -def _mock_scm_list() -> Sequence[type[SCM]]: - """Mocked list of SCMs - - Returns: - List of mock SCMs - """ - variants = [] - - # Default - variants.append(MockSCM) - - return variants - - -pep621_variants = _pep621_configuration_list() -cppython_local_variants = _cppython_local_configuration_list() -cppython_global_variants = _cppython_global_configuration_list() -project_variants = _project_configuration_list() -provider_variants = _mock_provider_list() -generator_variants = _mock_generator_list() -scm_variants = _mock_scm_list() diff --git a/cppython/test/schema.py b/cppython/test/schema.py new file mode 100644 index 0000000..b010726 --- /dev/null +++ b/cppython/test/schema.py @@ -0,0 +1,24 @@ +"""Data schemas for plugin testing""" + +from pathlib import Path +from typing import Annotated + +from pydantic import Field + +from cppython.core.schema import CPPythonModel + + +class Variant[T: CPPythonModel](CPPythonModel): + """A configuration variant for a schema type""" + + configuration: Annotated[T, Field(description='The configuration data')] + directory: Annotated[ + Path | None, + Field(description='The directory to mount alongside the configuration. `tests/build/`'), + ] = None + + +class Variants[T: CPPythonModel](CPPythonModel): + """A group of variants""" + + variants: Annotated[list[Variant[T]], Field(description='Data variants')] = [] diff --git a/cppython/utility/filesystem.py b/cppython/utility/filesystem.py new file mode 100644 index 0000000..36aa17f --- /dev/null +++ b/cppython/utility/filesystem.py @@ -0,0 +1,20 @@ +"""Helpers for working with the filesystem.""" + +import os +import tempfile +from collections.abc import Generator +from contextlib import contextmanager +from pathlib import Path + + +@contextmanager +def isolated_filesystem() -> Generator[Path]: + """Change the current working directory to the given path for the duration of the test.""" + old_cwd = os.getcwd() + + try: + with tempfile.TemporaryDirectory() as temp_directory: + os.chdir(temp_directory) + yield Path(temp_directory) + finally: + os.chdir(old_cwd) diff --git a/cppython/utility/subprocess.py b/cppython/utility/subprocess.py index 6cc0c72..d2d60b6 100644 --- a/cppython/utility/subprocess.py +++ b/cppython/utility/subprocess.py @@ -8,7 +8,8 @@ from cppython.utility.exception import ProcessError -def call( +def invoke( + executable: str | Path, arguments: list[str | Path], logger: logging.Logger, log_level: int = logging.WARNING, @@ -18,6 +19,7 @@ def call( """Executes a subprocess call with logger and utility attachments. Captures STDOUT and STDERR Args: + executable: The executable to call arguments: Arguments to pass to Popen logger: The logger to log the process pipes to log_level: The level to log to. Defaults to logging.WARNING. @@ -27,7 +29,9 @@ def call( Raises: ProcessError: If the underlying process fails """ - with subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, **kwargs) as process: + with subprocess.Popen( + [executable] + arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, **kwargs + ) as process: if process.stdout is None: return diff --git a/docs/antora.yml b/docs/antora.yml index 2212372..3334c3c 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -3,3 +3,4 @@ version: 0.1.0 title: CPPython nav: - modules/ROOT/nav.adoc + - modules/tests/nav.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 99cea0f..e5f79e6 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1 +1,3 @@ +.CPPython +* xref:index.adoc[] * xref:configuration.adoc[] \ No newline at end of file diff --git a/docs/modules/ROOT/pages/configuration.adoc b/docs/modules/ROOT/pages/configuration.adoc index e69de29..6fb6e45 100644 --- a/docs/modules/ROOT/pages/configuration.adoc +++ b/docs/modules/ROOT/pages/configuration.adoc @@ -0,0 +1,16 @@ += Configuration + +== Local Configuration + +Defined within the `pyproject.toml` file, the local configuration is used to customize the behavior of `cppython` for a specific project. The configuration is defined under the `[tool.cppython]` table. + +== Global Configuration + +Configuration exists in four places, each with a different default scope. The order of precedence is as follows: + +1. Configuration file override provided by the CLI or local `pyproject.toml` file. +2. The local configuration file, `cppython.toml`, in the current working directory. +3. The user level configuration file, `cppython.toml`, in the user's home directory. +4. The global configuration file, `cppython.toml`, in the system configuration directory. + +Each configuration file that is found will be merged into the next configuration file found. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 2120a3c..c3caa44 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -1 +1 @@ -= CPPython Website \ No newline at end of file += Documentation for CPPython \ No newline at end of file diff --git a/docs/modules/tests/nav.adoc b/docs/modules/tests/nav.adoc index b08b38f..24495cf 100644 --- a/docs/modules/tests/nav.adoc +++ b/docs/modules/tests/nav.adoc @@ -1 +1,3 @@ +.CPPython +* xref:index.adoc[] * xref:fixtures.adoc[] \ No newline at end of file diff --git a/docs/modules/tests/pages/fixtures.adoc b/docs/modules/tests/pages/fixtures.adoc index 2f5f65d..121803f 100644 --- a/docs/modules/tests/pages/fixtures.adoc +++ b/docs/modules/tests/pages/fixtures.adoc @@ -2,13 +2,38 @@ Fixtures can be accessed by installing the optional `pytest` module, `cppython.pytest`. +== Static Fixtures + +=== `install_path` + +=== `pep621_configuration` + +=== `pep621_data` + +=== `cppython_local_configuration` + +=== `cppython_global_configuration` + +=== `plugin_build_data` + +=== `plugin_cppython_data` + +=== `cppython_data` + +=== `core_data` + +=== `project_configuration` + +=== `project_data` + +=== `project` + == Dynamic Fixtures With the pytest feature of `pytest_generate_tests` we register multiple dynamic fixtures for collecting test data, both for plugins and for the test suite itself. +=== `build_` -=== `build_` Fixture +A Path object representing the directory named `` in the build directory. * Discovered from `tests/build/` -* The contents of the directory are copied to a temporary directory - diff --git a/docs/modules/tests/pages/index.adoc b/docs/modules/tests/pages/index.adoc new file mode 100644 index 0000000..fc10862 --- /dev/null +++ b/docs/modules/tests/pages/index.adoc @@ -0,0 +1 @@ += Test Documentation \ No newline at end of file diff --git a/examples/vcpkg_cmake/simple/CMakeLists.txt b/examples/vcpkg_cmake/simple/CMakeLists.txt new file mode 100644 index 0000000..5526812 --- /dev/null +++ b/examples/vcpkg_cmake/simple/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) + +project(HelloWorld) + +find_package(fmt CONFIG REQUIRED) + +add_executable(HelloWorld helloworld.cpp) + +target_link_libraries(HelloWorld PRIVATE fmt::fmt) \ No newline at end of file diff --git a/examples/vcpkg_cmake/simple/CMakePresets.json b/examples/vcpkg_cmake/simple/CMakePresets.json new file mode 100644 index 0000000..bdec0d0 --- /dev/null +++ b/examples/vcpkg_cmake/simple/CMakePresets.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "vcpkg", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + ] +} \ No newline at end of file diff --git a/examples/vcpkg_cmake/simple/helloworld.cpp b/examples/vcpkg_cmake/simple/helloworld.cpp new file mode 100644 index 0000000..82c4e7a --- /dev/null +++ b/examples/vcpkg_cmake/simple/helloworld.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + fmt::print("Hello World!\n"); + return 0; +} \ No newline at end of file diff --git a/examples/vcpkg_cmake/simple/pyproject.toml b/examples/vcpkg_cmake/simple/pyproject.toml new file mode 100644 index 0000000..a910566 --- /dev/null +++ b/examples/vcpkg_cmake/simple/pyproject.toml @@ -0,0 +1,33 @@ +[project] +description = "A simple project showing how to use vcpkg with CPPython" +name = "cppython-vcpkg-cmake-simple" +version = "1.0.0" + +license = {text = "MIT"} + +authors = [ + {name = "Synodic Software", email = "contact@synodic.software"}, +] + +requires-python = ">=3.13" + +dependencies = [ + "cppython>=0.1.0", +] + +[tool.cppython] +generator-name = "cmake" +provider-name = "vcpkg" + +install-path = "install" + +dependencies = [ + "fmt>=11.0.2", +] + +[tool.cppython.generator] + +[tool.cppython.provider] + +[tool.pdm] +distribution = false diff --git a/pdm.lock b/pdm.lock index 6da657b..2056101 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "git", "lint", "pdm", "pytest", "test"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:56348598f9d12ee9b21d2682e7ec128491f8508f8444494c469381e44908ab15" +content_hash = "sha256:39486bd6dc7dafe2696c974da9b2f015a8f4d918670601b707214a9568f49052" [[metadata.targets]] requires_python = ">=3.13" @@ -26,7 +26,7 @@ files = [ [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.9.0" requires_python = ">=3.9" summary = "High level compatibility layer for multiple asynchronous event loop implementations" groups = ["pdm"] @@ -34,11 +34,11 @@ dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", "sniffio>=1.1", - "typing-extensions>=4.1; python_version < \"3.11\"", + "typing-extensions>=4.5; python_version < \"3.13\"", ] files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [[package]] @@ -54,18 +54,42 @@ files = [ [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.1.31" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." -groups = ["pdm"] +groups = ["default", "pdm"] +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[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"] files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {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"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" requires_python = ">=3.7" summary = "Composable command line interface toolkit" groups = ["default"] @@ -74,8 +98,8 @@ dependencies = [ "importlib-metadata; python_version < \"3.8\"", ] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [[package]] @@ -92,72 +116,74 @@ files = [ [[package]] name = "coverage" -version = "7.6.7" +version = "7.8.0" requires_python = ">=3.9" summary = "Code coverage measurement for Python" groups = ["test"] files = [ - {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, - {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, - {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, - {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, - {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, - {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, + {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.6.7" +version = "7.8.0" extras = ["toml"] requires_python = ">=3.9" summary = "Code coverage measurement for Python" groups = ["test"] dependencies = [ - "coverage==7.6.7", + "coverage==7.8.0", "tomli; python_full_version <= \"3.11.0a6\"", ] files = [ - {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, - {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, - {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, - {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, - {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, - {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, + {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 = "dep-logic" -version = "0.4.9" +version = "0.4.11" requires_python = ">=3.8" summary = "Python dependency specifications supporting logical operations" groups = ["pdm"] @@ -165,8 +191,8 @@ dependencies = [ "packaging>=22", ] files = [ - {file = "dep_logic-0.4.9-py3-none-any.whl", hash = "sha256:06faa33814e5ff881922f644284a608d7da7946462760f710217d829ae864a0e"}, - {file = "dep_logic-0.4.9.tar.gz", hash = "sha256:5d455ea2a3da4fea2be6186d886905c57eeeebe3ea7fa967f599cb8e0f01d5c9"}, + {file = "dep_logic-0.4.11-py3-none-any.whl", hash = "sha256:44cf69b3e0e369315e7d4cafa4a5050aa70666753831bf0c27b7f3eadbcf70ce"}, + {file = "dep_logic-0.4.11.tar.gz", hash = "sha256:6084c81ce683943a60394ca0f8531ff803dfd955b5d7f52fb0571b53b930a182"}, ] [[package]] @@ -181,7 +207,7 @@ files = [ [[package]] name = "dulwich" -version = "0.22.6" +version = "0.22.8" requires_python = ">=3.9" summary = "Python Git Library" groups = ["git"] @@ -189,30 +215,30 @@ dependencies = [ "urllib3>=1.25", ] files = [ - {file = "dulwich-0.22.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dff11fe1ecd6f9d915b490c660456a467c9150404de5100b3cf112c6bd5c830d"}, - {file = "dulwich-0.22.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9e75774fe5f219490912931bb8d91a12ec5cc7155f377fd88088fe0f035e1b8"}, - {file = "dulwich-0.22.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb5f8ff0eb0d6759cebd13cb9e6d7029af5d87f89074bd1a702c7a0943a33d6e"}, - {file = "dulwich-0.22.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:096a9fe8754bce136e128b7161fca30ff1b2e5859f2324a7e2c8270dcb2ba672"}, - {file = "dulwich-0.22.6-cp313-cp313-win32.whl", hash = "sha256:115920aff5756cdf02900504fe3c9b941f2f41fab5cea2670130783f02e0ae2c"}, - {file = "dulwich-0.22.6-cp313-cp313-win_amd64.whl", hash = "sha256:4b84f9a849d64201a8e5cde024bbbf9c56d3138f284eb5cc1a937ee3503f18b0"}, - {file = "dulwich-0.22.6-py3-none-any.whl", hash = "sha256:a609c1939b8050c9876d0dd2b15302fef695759f479613c20025fbd4ece32bda"}, - {file = "dulwich-0.22.6.tar.gz", hash = "sha256:c1f44d599fa5dc59ca43e0789f835b8689b4d831d8de5ae009c442192a1408b5"}, + {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"}, ] [[package]] name = "filelock" -version = "3.16.1" -requires_python = ">=3.8" +version = "3.18.0" +requires_python = ">=3.9" summary = "A platform independent file lock." groups = ["pdm"] files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [[package]] name = "findpython" -version = "0.6.2" +version = "0.6.3" requires_python = ">=3.8" summary = "A utility to find python versions on your system" groups = ["pdm"] @@ -220,8 +246,8 @@ dependencies = [ "packaging>=20", ] files = [ - {file = "findpython-0.6.2-py3-none-any.whl", hash = "sha256:bda62477f858ea623ef2269f5e734469a018104a5f6c0fd9317ba238464ddb76"}, - {file = "findpython-0.6.2.tar.gz", hash = "sha256:e0c75ba9f35a7f9bb4423eb31bd17358cccf15761b6837317719177aeff46723"}, + {file = "findpython-0.6.3-py3-none-any.whl", hash = "sha256:a85bb589b559cdf1b87227cc233736eb7cad894b9e68021ee498850611939ebc"}, + {file = "findpython-0.6.3.tar.gz", hash = "sha256:5863ea55556d8aadc693481a14ac4f3624952719efc1c5591abb0b4a9e965c94"}, ] [[package]] @@ -240,17 +266,16 @@ files = [ [[package]] name = "hishel" -version = "0.0.33" -requires_python = ">=3.8" +version = "0.1.2" +requires_python = ">=3.9" summary = "Persistent cache implementation for httpx and httpcore" groups = ["pdm"] dependencies = [ - "httpx>=0.22.0", - "typing-extensions>=4.8.0", + "httpx>=0.28.0", ] files = [ - {file = "hishel-0.0.33-py3-none-any.whl", hash = "sha256:6e6c6cdaf432ff4c4981e7792ef7d1fa4c8ede58b9dbbcefb9ab3fc9770f2a07"}, - {file = "hishel-0.0.33.tar.gz", hash = "sha256:ab5b2661d5e2252f305fd0fb20e8c76bfab3ea73458f20f2591c53c37b270089"}, + {file = "hishel-0.1.2-py3-none-any.whl", hash = "sha256:802b4e446017f4867efdb26d3417670991ad1b4826d24331110871fe8957b5d0"}, + {file = "hishel-0.1.2.tar.gz", hash = "sha256:6643450bfb1cfa2ecd6002769f6f5069d0d048c9c1f1e29a98a48302d5875092"}, ] [[package]] @@ -270,7 +295,7 @@ files = [ [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" requires_python = ">=3.8" summary = "The next generation HTTP client." groups = ["pdm"] @@ -279,27 +304,26 @@ dependencies = [ "certifi", "httpcore==1.*", "idna", - "sniffio", ] files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" extras = ["socks"] requires_python = ">=3.8" summary = "The next generation HTTP client." groups = ["pdm"] dependencies = [ - "httpx==0.27.2", + "httpx==0.28.1", "socksio==1.*", ] files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [[package]] @@ -307,7 +331,7 @@ name = "idna" version = "3.10" requires_python = ">=3.6" summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["pdm"] +groups = ["default", "pdm"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -315,13 +339,13 @@ files = [ [[package]] name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" +version = "2.1.0" +requires_python = ">=3.8" summary = "brain-dead simple config-ini parsing" groups = ["pytest", "test"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -335,17 +359,6 @@ files = [ {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, ] -[[package]] -name = "isort" -version = "5.13.2" -requires_python = ">=3.8.0" -summary = "A Python utility / library to sort Python imports." -groups = ["lint"] -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - [[package]] name = "markdown-it-py" version = "3.0.0" @@ -394,8 +407,8 @@ files = [ [[package]] name = "mypy" -version = "1.13.0" -requires_python = ">=3.8" +version = "1.15.0" +requires_python = ">=3.9" summary = "Optional static typing for Python" groups = ["lint"] dependencies = [ @@ -404,13 +417,14 @@ dependencies = [ "typing-extensions>=4.6.0", ] files = [ - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, + {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]] @@ -437,19 +451,19 @@ files = [ [[package]] name = "pbs-installer" -version = "2024.10.16" +version = "2025.3.17" requires_python = ">=3.8" summary = "Installer for Python Build Standalone" groups = ["pdm"] files = [ - {file = "pbs_installer-2024.10.16-py3-none-any.whl", hash = "sha256:043f157ae0939b403b98b410f92bcc1d52062c8ced46e52ff0fdb45d37320a25"}, - {file = "pbs_installer-2024.10.16.tar.gz", hash = "sha256:d547d9a5bb564791102d138346bff609659c16acc0147fd701755a2eae8f2050"}, + {file = "pbs_installer-2025.3.17-py3-none-any.whl", hash = "sha256:d2b0563b1d5d814e479f3c43d7aee019250f68a0a113d754714fa9a721f83b47"}, + {file = "pbs_installer-2025.3.17.tar.gz", hash = "sha256:dde058f925b989c1d3bd90739c16ffd0e68732f7716e4d1e01ca480d00a67560"}, ] [[package]] name = "pdm" -version = "2.20.1" -requires_python = ">=3.8" +version = "2.23.0" +requires_python = ">=3.9" summary = "A modern Python package and dependency manager supporting the latest PEP standards" groups = ["pdm"] dependencies = [ @@ -458,11 +472,10 @@ dependencies = [ "dep-logic>=0.4.4", "filelock>=3.13", "findpython<1.0.0a0,>=0.6.0", - "hishel<0.1.0,>=0.0.32", + "hishel>=0.0.32", "httpcore>=1.0.6", "httpx[socks]<1,>0.20", "importlib-metadata>=3.6; python_version < \"3.10\"", - "importlib-resources>=5; python_version < \"3.9\"", "installer<0.8,>=0.7", "msgpack>=1.0", "packaging!=22.0,>=20.9", @@ -480,19 +493,19 @@ dependencies = [ "virtualenv>=20", ] files = [ - {file = "pdm-2.20.1-py3-none-any.whl", hash = "sha256:27904e5a703e6ce6598a2a92a6e4c95b2099746b1aba9402154978afce4ed6a7"}, - {file = "pdm-2.20.1.tar.gz", hash = "sha256:5348e9d33de381f998904a63ab18efdd6d1cf6377d45572e8b996d58dfc5b996"}, + {file = "pdm-2.23.0-py3-none-any.whl", hash = "sha256:965087f3ce1ad1f9ef84dc11c499ef5d1c5e0a5f09a1437148d9beaf7502d6c9"}, + {file = "pdm-2.23.0.tar.gz", hash = "sha256:ec5ce439648b42b928f4afa91c2b6251e6a33f40cc83779d899ca970bd9eb131"}, ] [[package]] name = "platformdirs" -version = "4.3.6" -requires_python = ">=3.8" +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"] files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [[package]] @@ -508,55 +521,60 @@ files = [ [[package]] name = "pydantic" -version = "2.9.2" -requires_python = ">=3.8" +version = "2.11.3" +requires_python = ">=3.9" summary = "Data validation using Python type hints" groups = ["default"] dependencies = [ "annotated-types>=0.6.0", - "pydantic-core==2.23.4", - "typing-extensions>=4.12.2; python_version >= \"3.13\"", - "typing-extensions>=4.6.1; python_version < \"3.13\"", + "pydantic-core==2.33.1", + "typing-extensions>=4.12.2", + "typing-inspection>=0.4.0", ] files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, + {file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"}, + {file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"}, ] [[package]] name = "pydantic-core" -version = "2.23.4" -requires_python = ">=3.8" +version = "2.33.1" +requires_python = ">=3.9" summary = "Core functionality for Pydantic validation and serialization" groups = ["default"] dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, + {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"}, ] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" requires_python = ">=3.8" summary = "Pygments is a syntax highlighting package written in Python." groups = ["default", "pdm"] files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [[package]] @@ -572,7 +590,7 @@ files = [ [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.5" requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" groups = ["pytest", "test"] @@ -585,13 +603,13 @@ dependencies = [ "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "6.1.1" requires_python = ">=3.9" summary = "Pytest plugin for measuring coverage." groups = ["test"] @@ -600,8 +618,8 @@ dependencies = [ "pytest>=4.6", ] files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, ] [[package]] @@ -620,13 +638,30 @@ files = [ [[package]] name = "python-dotenv" -version = "1.0.1" -requires_python = ">=3.8" +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"] files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["default"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [[package]] @@ -642,7 +677,7 @@ files = [ [[package]] name = "rich" -version = "13.9.4" +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"] @@ -652,35 +687,35 @@ dependencies = [ "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", ] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, ] [[package]] name = "ruff" -version = "0.7.4" +version = "0.11.4" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["lint"] files = [ - {file = "ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478"}, - {file = "ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63"}, - {file = "ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a"}, - {file = "ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac"}, - {file = "ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6"}, - {file = "ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f"}, - {file = "ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2"}, + {file = "ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2"}, + {file = "ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc"}, + {file = "ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906"}, + {file = "ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f"}, + {file = "ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e"}, + {file = "ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223"}, + {file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e"}, + {file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d"}, + {file = "ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99"}, + {file = "ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222"}, + {file = "ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304"}, + {file = "ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019"}, + {file = "ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896"}, + {file = "ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751"}, + {file = "ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270"}, + {file = "ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb"}, + {file = "ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc"}, + {file = "ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407"}, ] [[package]] @@ -729,19 +764,19 @@ files = [ [[package]] name = "truststore" -version = "0.10.0" +version = "0.10.1" requires_python = ">=3.10" summary = "Verify certificates using native system trust stores" groups = ["pdm"] marker = "python_version >= \"3.10\"" files = [ - {file = "truststore-0.10.0-py3-none-any.whl", hash = "sha256:b3798548e421ffe2ca2a6217cca49e7a17baf40b72d86a5505dc7d701e77d15b"}, - {file = "truststore-0.10.0.tar.gz", hash = "sha256:5da347c665714fdfbd46f738c823fe9f0d8775e41ac5fb94f325749091187896"}, + {file = "truststore-0.10.1-py3-none-any.whl", hash = "sha256:b64e6025a409a43ebdd2807b0c41c8bff49ea7ae6550b5087ac6df6619352d4c"}, + {file = "truststore-0.10.1.tar.gz", hash = "sha256:eda021616b59021812e800fa0a071e51b266721bef3ce092db8a699e21c63539"}, ] [[package]] name = "typer" -version = "0.13.1" +version = "0.15.2" requires_python = ">=3.7" summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." groups = ["default"] @@ -752,24 +787,52 @@ dependencies = [ "typing-extensions>=3.7.4.3", ] files = [ - {file = "typer-0.13.1-py3-none-any.whl", hash = "sha256:5b59580fd925e89463a29d363e0a43245ec02765bde9fb77d39e5d0f29dd7157"}, - {file = "typer-0.13.1.tar.gz", hash = "sha256:9d444cb96cc268ce6f8b94e13b4335084cef4c079998a9f4851a90229a3bd25c"}, + {file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"}, + {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"}, +] + +[[package]] +name = "types-requests" +version = "2.32.0.20250328" +requires_python = ">=3.9" +summary = "Typing stubs for requests" +groups = ["default"] +dependencies = [ + "urllib3>=2", +] +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"}, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.1" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["default", "lint", "pdm"] +groups = ["default", "lint"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69"}, + {file = "typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +requires_python = ">=3.9" +summary = "Runtime typing introspection tools" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.12.0", +] +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"}, ] [[package]] name = "unearth" -version = "0.17.2" +version = "0.17.3" requires_python = ">=3.8" summary = "A utility to fetch and download python packages" groups = ["pdm"] @@ -778,24 +841,24 @@ dependencies = [ "packaging>=20", ] files = [ - {file = "unearth-0.17.2-py3-none-any.whl", hash = "sha256:4d21af1238a583835fca156322f7225382e718cdcc42d6278050a88e605c4ad5"}, - {file = "unearth-0.17.2.tar.gz", hash = "sha256:0b8a2afd3476f1ab6155fc579501ac47fffe43547d88a70e5a5b76a7fe6caa2c"}, + {file = "unearth-0.17.3-py3-none-any.whl", hash = "sha256:654dba44ac6ea9e4a73acface11dafc342fb0a8095e7d63c2d6801a047e96dfc"}, + {file = "unearth-0.17.3.tar.gz", hash = "sha256:32e96c9df63c563a118d411dfb4f9c672f181a410977f6765c0ed430b0d32784"}, ] [[package]] name = "urllib3" -version = "2.2.3" -requires_python = ">=3.8" +version = "2.3.0" +requires_python = ">=3.9" summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["git"] +groups = ["default", "git"] files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [[package]] name = "virtualenv" -version = "20.27.1" +version = "20.30.0" requires_python = ">=3.8" summary = "Virtual Python Environment builder" groups = ["pdm"] @@ -806,6 +869,6 @@ dependencies = [ "platformdirs<5,>=3.9.1", ] files = [ - {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, - {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, ] diff --git a/pyproject.toml b/pyproject.toml index f69a6ec..561a478 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,23 +14,25 @@ dynamic = ["version"] requires-python = ">=3.13" dependencies = [ - "typer>=0.13.1", - "pydantic>=2.8.2", - "packaging>=24.1", + "typer>=0.15.2", + "pydantic>=2.11.3", + "packaging>=24.2", + "requests>=2.32.3", + "types-requests>=2.32.0.20250328", ] [project.optional-dependencies] pytest = [ - "pytest>=8.3.3", + "pytest>=8.3.5", "pytest-mock>=3.14.0", ] git = [ - "dulwich>=0.22.5", + "dulwich>=0.22.8", ] pdm = [ - "pdm>=2.20.1", + "pdm>=2.23.0", ] [project.urls] @@ -43,21 +45,24 @@ git = "cppython.plugins.git.plugin:GitSCM" [project.entry-points."cppython.generator"] cmake = "cppython.plugins.cmake.plugin:CMakeGenerator" +[project.entry-points."cppython.provider"] +conan = "cppython.plugins.conan.plugin:ConanProvider" +vcpkg = "cppython.plugins.vcpkg.plugin:VcpkgProvider" + [project.entry-points.pdm] cppython = "cppython.plugins.pdm.plugin:CPPythonPlugin" -[project.entry-points."cppython.provider"] -vcpkg = "cppython.plugins.vcpkg.plugin:VcpkgProvider" +[project.entry-points.pytest11] +cppython = "cppython.test.pytest.fixtures" [dependency-groups] lint = [ - "ruff>=0.7.4", - "mypy>=1.13", - "isort>=5.13.2", + "ruff>=0.11.4", + "mypy>=1.15.0", ] test = [ - "pytest>=8.3.3", - "pytest-cov>=6.0.0", + "pytest>=8.3.5", + "pytest-cov>=6.1.1", "pytest-mock>=3.14.0", ] @@ -75,9 +80,6 @@ exclude = "__pypackages__" plugins = ["pydantic.mypy"] strict = true -[tool.isort] -profile = "black" - [tool.ruff] line-length = 120 preview = true @@ -114,8 +116,13 @@ quote-style = "single" [tool.coverage.report] skip_empty = true +[tool.pdm] +plugins = [ + "-e file:///${PROJECT_ROOT}", +] + [tool.pdm.options] -update = ["--update-all"] +update = ["--update-all", "--unconstrained"] [tool.pdm.version] source = "scm" @@ -123,8 +130,7 @@ source = "scm" [tool.pdm.scripts] analyze = {shell = "ruff check cppython tests"} format = {shell = "ruff format"} -lint = {composite = ["analyze", "format", "sort-imports", "type-check"]} -sort-imports = {shell = "isort --check-only --diff ."} +lint = {composite = ["analyze", "format", "type-check"]} test = {shell = "pytest --cov=cppython --verbose tests"} type-check = {shell = "mypy ."} diff --git a/tests/build/test_build/build.txt b/tests/build/test_build/build.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..7f38ccd --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1,10 @@ +"""Fixtures for tests. + +`pytest_plugins` is the preferred way to load fixtures, to prevent the overhead of a large root conftest file. +The plugins must be defined at the test module's global scope and not in non-root conftest files. + +ex. +``` +pytest_plugins = ['fixtures.fixture_name'] +``` +""" diff --git a/tests/fixtures/cli.py b/tests/fixtures/cli.py new file mode 100644 index 0000000..25ee2e4 --- /dev/null +++ b/tests/fixtures/cli.py @@ -0,0 +1,15 @@ +"""Fixtures for interfacing with the CLI.""" + +import pytest +from typer.testing import CliRunner + + +@pytest.fixture( + name='typer_runner', + scope='session', +) +def fixture_typer_runner() -> CliRunner: + """Returns a runner setup for the CPPython interface""" + runner = CliRunner() + + return runner diff --git a/tests/fixtures/cmake.py b/tests/fixtures/cmake.py new file mode 100644 index 0000000..4ff4288 --- /dev/null +++ b/tests/fixtures/cmake.py @@ -0,0 +1,48 @@ +"""Fixtures for the cmake plugin""" + +from pathlib import Path +from typing import cast + +import pytest + +from cppython.plugins.cmake.schema import CMakeConfiguration +from cppython.test.schema import Variant, Variants + + +def _cmake_data_list() -> Variants[CMakeConfiguration]: + """Creates a list of mocked configuration types + + Returns: + A list of variants to test + """ + data = Variants[CMakeConfiguration]() + + # Default + default = CMakeConfiguration(configuration_name='default') + default_variant = Variant[CMakeConfiguration](configuration=default) + + # Non-root preset file + config = CMakeConfiguration(preset_file=Path('inner/CMakePresets.json'), configuration_name='default') + config_variant = Variant[CMakeConfiguration](configuration=config, directory=Path('cmake/non-root')) + + data.variants.append(default_variant) + data.variants.append(config_variant) + + return data + + +@pytest.fixture( + name='cmake_data', + scope='session', + params=_cmake_data_list(), +) +def fixture_cmake_data(request: pytest.FixtureRequest) -> Variant[CMakeConfiguration]: + """A fixture to provide a list of configuration types + + Args: + request: Parameterization list + + Returns: + A configuration type instance + """ + return cast(Variant[CMakeConfiguration], request.param) diff --git a/tests/fixtures/example.py b/tests/fixtures/example.py new file mode 100644 index 0000000..b1c8fc9 --- /dev/null +++ b/tests/fixtures/example.py @@ -0,0 +1,68 @@ +"""Fixtures for the cmake plugin""" + +import os +import shutil +from collections.abc import Generator +from pathlib import Path +from typing import cast + +import pytest +from typer.testing import CliRunner + +pytest_plugins = ['tests.fixtures.cli'] + + +def _examples() -> list[Path]: + """Returns the examples directory""" + matching_directories = [] + + for dirpath, _, filenames in os.walk('examples'): + for filename in filenames: + if filename == 'pyproject.toml': + absolute_path = Path(dirpath).absolute() + matching_directories.append(absolute_path) + break + + return matching_directories + + +@pytest.fixture( + name='example_directory', + scope='session', + params=_examples(), +) +def fixture_example_directory( + request: pytest.FixtureRequest, +) -> Path: + """Enumerates folders in the examples directory. + + Parameterizes all directories with a pyproject.toml file within the examples directory. + """ + directory = cast(Path, request.param) + return directory + + +@pytest.fixture( + name='example_runner', +) +def fixture_example_runner( + request: pytest.FixtureRequest, typer_runner: CliRunner, tmp_path: Path +) -> Generator[CliRunner]: + """Sets up an isolated filesystem for an example test.""" + # Get the root directory of the project + root_directory = Path(__file__).parent.parent.parent.absolute() + + # Remove the file extension and required 'test_' prefix from the test's file name + file_name = request.node.fspath.basename[:-3].replace('test_', '') + + # Get the test function name and remove the required 'test_' prefix + test_name = request.node.name.replace('test_', '') + + # Generate the example path from the pytest file and test name + example_path = root_directory / 'examples' / file_name / test_name + + with typer_runner.isolated_filesystem(temp_dir=tmp_path): + # Copy the example directory to the temporary directory + shutil.copytree(example_path, Path(), dirs_exist_ok=True) + + yield typer_runner diff --git a/tests/integration/examples/__init__.py b/tests/integration/examples/__init__.py new file mode 100644 index 0000000..707867c --- /dev/null +++ b/tests/integration/examples/__init__.py @@ -0,0 +1 @@ +"""Integration tests for CPPython examples.""" diff --git a/tests/integration/examples/test_examples.py b/tests/integration/examples/test_examples.py new file mode 100644 index 0000000..e980fab --- /dev/null +++ b/tests/integration/examples/test_examples.py @@ -0,0 +1,18 @@ +"""Example folder tests. + +All examples can be run with the CPPython entry-point, and we use the examples as the test data for the CLI. +""" + +from pathlib import Path + +pytest_plugins = ['tests.fixtures.example'] + + +class TestExamples: + """Verification that the example directory is setup correctly""" + + @staticmethod + def test_example_directory(example_directory: Path) -> None: + """Verify that the fixture is returning the right data""" + assert example_directory.is_dir() + assert (example_directory / 'pyproject.toml').is_file() diff --git a/tests/integration/examples/test_vcpkg_cmake.py b/tests/integration/examples/test_vcpkg_cmake.py new file mode 100644 index 0000000..e1a222c --- /dev/null +++ b/tests/integration/examples/test_vcpkg_cmake.py @@ -0,0 +1,45 @@ +"""TODO""" + +import subprocess + +import pytest +from typer.testing import CliRunner + +from cppython.console.entry import app + +pytest_plugins = ['tests.fixtures.example'] + + +class TestVcpkgCMake: + """Test project variation of vcpkg and CMake""" + + @staticmethod + @pytest.mark.skip(reason='TODO') + def test_simple(example_runner: CliRunner) -> None: + """Simple project""" + result = example_runner.invoke( + app, + [ + 'install', + ], + ) + + assert result.exit_code == 0, result.output + + # Run the CMake configuration command + cmake_result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) + + assert cmake_result.returncode == 0, f'CMake configuration failed: {cmake_result.stderr}' + + # Run the CMake build command + build_result = subprocess.run(['cmake', '--build', 'build'], capture_output=True, text=True, check=False) + + assert build_result.returncode == 0, f'CMake build failed: {build_result.stderr}' + assert 'Build finished successfully' in build_result.stdout, 'CMake build did not finish successfully' + + # Execute the built program and verify the output + program_result = subprocess.run(['build/HelloWorld'], capture_output=True, text=True, check=False) + + assert program_result.returncode == 0, f'Program execution failed: {program_result.stderr}' + + assert 'Hello, World!' in program_result.stdout, 'Program output did not match expected output' diff --git a/tests/integration/plugins/cmake/test_generator.py b/tests/integration/plugins/cmake/test_generator.py index d3d172d..993338c 100644 --- a/tests/integration/plugins/cmake/test_generator.py +++ b/tests/integration/plugins/cmake/test_generator.py @@ -7,6 +7,9 @@ from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import CMakeConfiguration from cppython.test.pytest.tests import GeneratorIntegrationTests +from cppython.test.schema import Variant + +pytest_plugins = ['tests.fixtures.cmake'] class TestCPPythonGenerator(GeneratorIntegrationTests[CMakeGenerator]): @@ -14,7 +17,7 @@ class TestCPPythonGenerator(GeneratorIntegrationTests[CMakeGenerator]): @staticmethod @pytest.fixture(name='plugin_data', scope='session') - def fixture_plugin_data(cmake_data: CMakeConfiguration) -> dict[str, Any]: + def fixture_plugin_data(cmake_data: Variant[CMakeConfiguration]) -> dict[str, Any]: """A required testing hook that allows data generation Args: @@ -23,7 +26,7 @@ def fixture_plugin_data(cmake_data: CMakeConfiguration) -> dict[str, Any]: Returns: The constructed plugin data """ - return cmake_data.model_dump() + return cmake_data.configuration.model_dump() @staticmethod @pytest.fixture(name='plugin_type', scope='session') diff --git a/tests/integration/plugins/conan/__init__.py b/tests/integration/plugins/conan/__init__.py new file mode 100644 index 0000000..12f4562 --- /dev/null +++ b/tests/integration/plugins/conan/__init__.py @@ -0,0 +1 @@ +"""Conan plugin integration tests""" diff --git a/tests/integration/plugins/conan/test_interface.py b/tests/integration/plugins/conan/test_interface.py new file mode 100644 index 0000000..556a0be --- /dev/null +++ b/tests/integration/plugins/conan/test_interface.py @@ -0,0 +1,32 @@ +"""Integration tests for the provider""" + +from typing import Any + +import pytest + +from cppython.plugins.conan.plugin import ConanProvider +from cppython.test.pytest.tests import ProviderIntegrationTests + + +class TestCPPythonProvider(ProviderIntegrationTests[ConanProvider]): + """The tests for the conan provider""" + + @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 {} + + @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 diff --git a/tests/integration/plugins/git/__init__.py b/tests/integration/plugins/git/__init__.py new file mode 100644 index 0000000..5f01350 --- /dev/null +++ b/tests/integration/plugins/git/__init__.py @@ -0,0 +1 @@ +"""Git plugin integration tests""" diff --git a/tests/integration/plugins/git/test_interface.py b/tests/integration/plugins/git/test_interface.py new file mode 100644 index 0000000..3664017 --- /dev/null +++ b/tests/integration/plugins/git/test_interface.py @@ -0,0 +1,20 @@ +"""Integration tests for the cppython SCM plugin""" + +import pytest + +from cppython.plugins.git.plugin import GitSCM +from cppython.test.pytest.tests import SCMIntegrationTests + + +class TestGitInterface(SCMIntegrationTests[GitSCM]): + """Integration tests for the Git SCM plugin""" + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[GitSCM]: + """A required testing hook that allows type generation + + Returns: + The SCM type + """ + return GitSCM diff --git a/tests/unit/core/test_resolution.py b/tests/unit/core/test_resolution.py index d4bf9d0..6583c74 100644 --- a/tests/unit/core/test_resolution.py +++ b/tests/unit/core/test_resolution.py @@ -29,6 +29,7 @@ ProjectConfiguration, ProjectData, ) +from cppython.test.schema import Variant from cppython.utility.utility import TypeName @@ -36,31 +37,28 @@ class TestResolve: """Test resolution of data""" @staticmethod - def test_pep621_resolve() -> None: + def test_pep621_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: """Test the PEP621 schema resolve function""" data = PEP621Configuration(name='pep621-resolve-test', dynamic=['version']) - config = ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0') - resolved = resolve_pep621(data, config, None) + resolved = resolve_pep621(data, project_configuration.configuration, None) class_variables = vars(resolved) - assert len(class_variables) + assert class_variables assert None not in class_variables.values() @staticmethod - def test_project_resolve() -> None: + def test_project_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: """Tests project configuration resolution""" - config = ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0') - assert resolve_project_configuration(config) + assert resolve_project_configuration(project_configuration.configuration) @staticmethod - def test_cppython_resolve() -> None: + def test_cppython_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: """Tests cppython configuration resolution""" cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - config = ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0') - project_data = resolve_project_configuration(config) + project_data = resolve_project_configuration(project_configuration.configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') @@ -83,24 +81,21 @@ class MockModel(CPPythonModel): bad_data = {'field': 4} - with pytest.raises(ConfigException) as error: + with pytest.raises(ConfigException): resolve_model(MockModel, bad_data) - assert error.value.error_count == 1 - good_data = {'field': 'good'} resolve_model(MockModel, good_data) @staticmethod - def test_generator_resolve() -> None: + def test_generator_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: """Test generator resolution""" - project_data = ProjectData(pyproject_file=Path('pyproject.toml')) + project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - config = ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0') - project_data = resolve_project_configuration(config) + project_data = resolve_project_configuration(project_configuration.configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') @@ -117,14 +112,13 @@ def test_generator_resolve() -> None: assert resolve_generator(project_data, cppython_plugin_data) @staticmethod - def test_provider_resolve() -> None: + def test_provider_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: """Test provider resolution""" - project_data = ProjectData(pyproject_file=Path('pyproject.toml')) + project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - config = ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0') - project_data = resolve_project_configuration(config) + project_data = resolve_project_configuration(project_configuration.configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') @@ -141,14 +135,13 @@ def test_provider_resolve() -> None: assert resolve_provider(project_data, cppython_plugin_data) @staticmethod - def test_scm_resolve() -> None: + def test_scm_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: """Test scm resolution""" - project_data = ProjectData(pyproject_file=Path('pyproject.toml')) + project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - config = ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0') - project_data = resolve_project_configuration(config) + project_data = resolve_project_configuration(project_configuration.configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') diff --git a/tests/unit/core/test_schema.py b/tests/unit/core/test_schema.py index 454812d..0b2b515 100644 --- a/tests/unit/core/test_schema.py +++ b/tests/unit/core/test_schema.py @@ -25,7 +25,7 @@ class Model(CPPythonModel): def test_model_construction(self) -> None: """Verifies that the base model type has the expected construction behaviors""" model = self.Model(**{'aliased_variable': True}) - assert model.aliased_variable is False + assert model.aliased_variable is True model = self.Model(**{'aliased-variable': True}) assert model.aliased_variable is True diff --git a/tests/unit/plugins/cmake/test_generator.py b/tests/unit/plugins/cmake/test_generator.py index 9d5c0ce..64197fc 100644 --- a/tests/unit/plugins/cmake/test_generator.py +++ b/tests/unit/plugins/cmake/test_generator.py @@ -1,11 +1,11 @@ """Unit test the provider plugin""" +import json from pathlib import Path from typing import Any import pytest -from cppython.core.utility import write_model_json from cppython.plugins.cmake.builder import Builder from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import ( @@ -14,15 +14,18 @@ CMakeSyncData, ) from cppython.test.pytest.tests import GeneratorUnitTests +from cppython.test.schema import Variant from cppython.utility.utility import TypeName +pytest_plugins = ['tests.fixtures.cmake'] + class TestCPPythonGenerator(GeneratorUnitTests[CMakeGenerator]): """The tests for the CMake generator""" @staticmethod @pytest.fixture(name='plugin_data', scope='session') - def fixture_plugin_data(cmake_data: CMakeConfiguration) -> dict[str, Any]: + def fixture_plugin_data(cmake_data: Variant[CMakeConfiguration]) -> dict[str, Any]: """A required testing hook that allows data generation Args: @@ -31,7 +34,7 @@ def fixture_plugin_data(cmake_data: CMakeConfiguration) -> dict[str, Any]: Returns: The constructed plugin data """ - return cmake_data.model_dump() + return cmake_data.configuration.model_dump() @staticmethod @pytest.fixture(name='plugin_type', scope='session') @@ -101,7 +104,10 @@ def test_root_write(tmp_path: Path) -> None: root_file = tmp_path / 'CMakePresets.json' presets = CMakePresets() - write_model_json(root_file, presets) + + serialized = json.loads(presets.model_dump_json(exclude_none=True, by_alias=False)) + with open(root_file, 'w', encoding='utf8') as file: + json.dump(serialized, file, ensure_ascii=False, indent=4) data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) builder.write_provider_preset(provider_directory, data) @@ -134,7 +140,9 @@ def test_relative_root_write(tmp_path: Path) -> None: root_file = relative_indirection / 'CMakePresets.json' presets = CMakePresets() - write_model_json(root_file, presets) + serialized = json.loads(presets.model_dump_json(exclude_none=True, by_alias=False)) + with open(root_file, 'w', encoding='utf8') as file: + json.dump(serialized, file, ensure_ascii=False, indent=4) data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) builder.write_provider_preset(provider_directory, data) diff --git a/tests/unit/plugins/conan/__init__.py b/tests/unit/plugins/conan/__init__.py new file mode 100644 index 0000000..3a03d8f --- /dev/null +++ b/tests/unit/plugins/conan/__init__.py @@ -0,0 +1 @@ +"""Conan plugin unit tests""" diff --git a/tests/unit/plugins/conan/test_interface.py b/tests/unit/plugins/conan/test_interface.py new file mode 100644 index 0000000..c9b2793 --- /dev/null +++ b/tests/unit/plugins/conan/test_interface.py @@ -0,0 +1,32 @@ +"""Unit test the provider plugin""" + +from typing import Any + +import pytest + +from cppython.plugins.conan.plugin import ConanProvider +from cppython.test.pytest.tests import ProviderUnitTests + + +class TestCPPythonProvider(ProviderUnitTests[ConanProvider]): + """The tests for the Conan Provider""" + + @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 {} + + @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 diff --git a/tests/unit/plugins/vcpkg/test_resolution.py b/tests/unit/plugins/vcpkg/test_resolution.py new file mode 100644 index 0000000..e8df2f1 --- /dev/null +++ b/tests/unit/plugins/vcpkg/test_resolution.py @@ -0,0 +1,23 @@ +"""Unit tests for the Vcpkg resolution plugin.""" + +from packaging.requirements import Requirement + +from cppython.plugins.vcpkg.resolution import resolve_vcpkg_dependency + + +class TestVcpkgResolution: + """Test the resolution of Vcpkg dependencies""" + + @staticmethod + def test_resolve_vcpkg_dependency() -> None: + """Test resolving a VcpkgDependency from a packaging requirement.""" + requirement = Requirement('example-package>=1.2.3') + + dependency = resolve_vcpkg_dependency(requirement) + + assert dependency.name == 'example-package' + assert dependency.version_ge == '1.2.3' + assert dependency.default_features is True + assert dependency.features == [] + assert dependency.platform is None + assert dependency.host is False diff --git a/tests/unit/test/test_fixtures.py b/tests/unit/test/test_fixtures.py deleted file mode 100644 index 2aac2ed..0000000 --- a/tests/unit/test/test_fixtures.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Tests for fixtures""" - -from pathlib import Path - - -class TestFixtures: - """Tests for fixtures""" - - @staticmethod - def test_build_directory(build_test_build: Path) -> None: - """Verifies that the build data provided is the expected path - - Args: - build_test_build: The plugins build folder directory - """ - requirement = build_test_build / 'build.txt' - - assert requirement.exists() diff --git a/tests/unit/test_builder.py b/tests/unit/test_builder.py index befb64f..8b5e647 100644 --- a/tests/unit/test_builder.py +++ b/tests/unit/test_builder.py @@ -15,6 +15,7 @@ from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider from cppython.test.mock.scm import MockSCM +from cppython.test.schema import Variant class TestBuilder: @@ -22,9 +23,9 @@ class TestBuilder: @staticmethod def test_build( - project_configuration: ProjectConfiguration, - pep621_configuration: PEP621Configuration, - cppython_local_configuration: CPPythonLocalConfiguration, + project_configuration: Variant[ProjectConfiguration], + pep621_configuration: Variant[PEP621Configuration], + cppython_local_configuration: Variant[CPPythonLocalConfiguration], mocker: MockerFixture, ) -> None: """Verifies that the builder can build a project with all test variants @@ -36,16 +37,17 @@ def test_build( mocker: Pytest mocker fixture """ logger = logging.getLogger() - builder = Builder(project_configuration, logger) + builder = Builder(project_configuration.configuration, logger) - mocker.patch.object( - metadata, - 'entry_points', + # Insert ourself into the builder and load the mock plugins by returning them directly in the expected order + # they will be built + mocker.patch( + 'cppython.builder.entry_points', return_value=[metadata.EntryPoint(name='mock', value='mock', group='mock')], ) mocker.patch.object(metadata.EntryPoint, 'load', side_effect=[MockGenerator, MockProvider, MockSCM]) - assert builder.build(pep621_configuration, cppython_local_configuration) + assert builder.build(pep621_configuration.configuration, cppython_local_configuration.configuration) class TestResolver: @@ -53,8 +55,8 @@ class TestResolver: @staticmethod def test_generate_plugins( - project_configuration: ProjectConfiguration, - cppython_local_configuration: CPPythonLocalConfiguration, + project_configuration: Variant[ProjectConfiguration], + cppython_local_configuration: Variant[CPPythonLocalConfiguration], project_data: ProjectData, ) -> None: """Verifies that the resolver can generate plugins @@ -65,6 +67,6 @@ def test_generate_plugins( project_data: Variant fixture for the project data """ logger = logging.getLogger() - resolver = Resolver(project_configuration, logger) + resolver = Resolver(project_configuration.configuration, logger) - assert resolver.generate_plugins(cppython_local_configuration, project_data) + assert resolver.generate_plugins(cppython_local_configuration.configuration, project_data) diff --git a/tests/unit/test_console.py b/tests/unit/test_console.py index 0ea412f..1031104 100644 --- a/tests/unit/test_console.py +++ b/tests/unit/test_console.py @@ -8,28 +8,10 @@ class TestConsole: - """Various tests for the typer interface""" + """Various that all the examples are accessible to cppython. The project should be mocked so nothing executes""" @staticmethod - def test_info() -> None: - """Verifies that the info command functions with CPPython hooks""" - result = runner.invoke(app, ['info']) - assert result.exit_code == 0 - - @staticmethod - def test_list() -> None: - """Verifies that the list command functions with CPPython hooks""" - result = runner.invoke(app, ['list']) - assert result.exit_code == 0 - - @staticmethod - def test_update() -> None: - """Verifies that the update command functions with CPPython hooks""" - result = runner.invoke(app, ['update']) - assert result.exit_code == 0 - - @staticmethod - def test_install() -> None: - """Verifies that the install command functions with CPPython hooks""" - result = runner.invoke(app, ['install']) - assert result.exit_code == 0 + def test_entrypoint() -> None: + """Verifies that the entry functions with CPPython hooks""" + with runner.isolated_filesystem(): + runner.invoke(app, []) diff --git a/tests/unit/test_data.py b/tests/unit/test_data.py index 0357d6d..2d58b2e 100644 --- a/tests/unit/test_data.py +++ b/tests/unit/test_data.py @@ -15,6 +15,7 @@ from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider from cppython.test.mock.scm import MockSCM +from cppython.test.schema import Variant class TestData: @@ -26,9 +27,9 @@ class TestData: scope='session', ) def fixture_data( - project_configuration: ProjectConfiguration, - pep621_configuration: PEP621Configuration, - cppython_local_configuration: CPPythonLocalConfiguration, + project_configuration: Variant[ProjectConfiguration], + pep621_configuration: Variant[PEP621Configuration], + cppython_local_configuration: Variant[CPPythonLocalConfiguration], ) -> Data: """Creates a mock plugins fixture. @@ -45,11 +46,13 @@ def fixture_data( """ logger = logging.getLogger() - builder = Builder(project_configuration, logger) + builder = Builder(project_configuration.configuration, logger) plugin_build_data = PluginBuildData(generator_type=MockGenerator, provider_type=MockProvider, scm_type=MockSCM) - return builder.build(pep621_configuration, cppython_local_configuration, plugin_build_data) + return builder.build( + pep621_configuration.configuration, cppython_local_configuration.configuration, plugin_build_data + ) @staticmethod def test_sync(data: Data) -> None: diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 3bc8315..eff1981 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -1,5 +1,6 @@ """Tests the Project type""" +import logging import tomllib from importlib import metadata from pathlib import Path @@ -35,7 +36,7 @@ def test_self_construction(request: pytest.FixtureRequest) -> None: """ # Use the CPPython directory as the test data file = request.config.rootpath / 'pyproject.toml' - project_configuration = ProjectConfiguration(pyproject_file=file, version=None) + project_configuration = ProjectConfiguration(project_root=file.parent, version=None) interface = MockInterface() pyproject_data = tomllib.loads(file.read_text(encoding='utf-8')) @@ -45,57 +46,71 @@ def test_self_construction(request: pytest.FixtureRequest) -> None: assert not project.enabled @staticmethod - def test_missing_tool_table(tmp_path: Path) -> None: + def test_missing_tool_table(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: """The project type should be constructable without the tool table Args: tmp_path: Temporary directory for dummy data + caplog: Pytest fixture for capturing logs """ file_path = tmp_path / 'pyproject.toml' with open(file_path, 'a', encoding='utf8'): pass - project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) + project_configuration = ProjectConfiguration(project_root=file_path.parent, version=None) interface = MockInterface() pyproject = PyProject(project=pep621) - project = Project(project_configuration, interface, pyproject.model_dump(by_alias=True)) + + with caplog.at_level(logging.WARNING): + project = Project(project_configuration, interface, pyproject.model_dump(by_alias=True)) + + # We don't want to have the log of the calling tool polluted with any default logging + assert len(caplog.records) == 0 assert not project.enabled @staticmethod - def test_missing_cppython_table(tmp_path: Path) -> None: + def test_missing_cppython_table(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: """The project type should be constructable without the cppython table Args: tmp_path: Temporary directory for dummy data + caplog: Pytest fixture for capturing logs """ file_path = tmp_path / 'pyproject.toml' with open(file_path, 'a', encoding='utf8'): pass - project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) + project_configuration = ProjectConfiguration(project_root=file_path.parent, version=None) interface = MockInterface() tool_data = ToolData() pyproject = PyProject(project=pep621, tool=tool_data) - project = Project(project_configuration, interface, pyproject.model_dump(by_alias=True)) + + with caplog.at_level(logging.WARNING): + project = Project(project_configuration, interface, pyproject.model_dump(by_alias=True)) + + # We don't want to have the log of the calling tool polluted with any default logging + assert len(caplog.records) == 0 assert not project.enabled @staticmethod - def test_default_cppython_table(tmp_path: Path, mocker: MockerFixture) -> None: + def test_default_cppython_table(tmp_path: Path, mocker: MockerFixture, caplog: pytest.LogCaptureFixture) -> None: """The project type should be constructable with the default cppython table Args: tmp_path: Temporary directory for dummy data mocker: Pytest mocker fixture + caplog: Pytest fixture for capturing logs """ - mocker.patch.object( - metadata, - 'entry_points', + # Insert ourself into the builder and load the mock plugins by returning them directly in the expected order + # they will be built + mocker.patch( + 'cppython.builder.entry_points', return_value=[metadata.EntryPoint(name='mock', value='mock', group='mock')], ) mocker.patch.object(metadata.EntryPoint, 'load', side_effect=[MockGenerator, MockProvider, MockSCM]) @@ -105,12 +120,17 @@ def test_default_cppython_table(tmp_path: Path, mocker: MockerFixture) -> None: with open(file_path, 'a', encoding='utf8'): pass - project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) + project_configuration = ProjectConfiguration(project_root=file_path.parent, version=None) interface = MockInterface() cppython_config = CPPythonLocalConfiguration() tool_data = ToolData(cppython=cppython_config) pyproject = PyProject(project=pep621, tool=tool_data) - project = Project(project_configuration, interface, pyproject.model_dump(by_alias=True)) + + with caplog.at_level(logging.WARNING): + project = Project(project_configuration, interface, pyproject.model_dump(by_alias=True)) + + # We don't want to have the log of the calling tool polluted with any default logging + assert len(caplog.records) == 0 assert project.enabled diff --git a/tests/unit/utility/test_utility.py b/tests/unit/utility/test_utility.py index 93692c5..537bf30 100644 --- a/tests/unit/utility/test_utility.py +++ b/tests/unit/utility/test_utility.py @@ -9,7 +9,7 @@ import pytest from cppython.utility.exception import ProcessError -from cppython.utility.subprocess import call +from cppython.utility.subprocess import invoke from cppython.utility.utility import canonicalize_name cppython_logger = logging.getLogger('cppython') @@ -72,6 +72,7 @@ def test_name_multi_caps() -> None: assert test.name == 'name' +@pytest.mark.skip(reason='Breaks debugging tests') class TestSubprocess: """Subprocess testing""" @@ -85,8 +86,9 @@ def test_subprocess_stdout(caplog: pytest.LogCaptureFixture) -> None: python = Path(executable) with caplog.at_level(logging.INFO): - call( - [python, '-c', "import sys; print('Test Out', file = sys.stdout)"], + invoke( + python, + ['-c', "import sys; print('Test Out', file = sys.stdout)"], cppython_logger, ) @@ -103,8 +105,9 @@ def test_subprocess_stderr(caplog: pytest.LogCaptureFixture) -> None: python = Path(executable) with caplog.at_level(logging.INFO): - call( - [python, '-c', "import sys; print('Test Error', file = sys.stderr)"], + invoke( + python, + ['-c', "import sys; print('Test Error', file = sys.stderr)"], cppython_logger, ) @@ -121,8 +124,9 @@ def test_subprocess_suppression(caplog: pytest.LogCaptureFixture) -> None: python = Path(executable) with caplog.at_level(logging.INFO): - call( - [python, '-c', "import sys; print('Test Out', file = sys.stdout)"], + invoke( + python, + ['-c', "import sys; print('Test Out', file = sys.stdout)"], cppython_logger, suppress=True, ) @@ -138,8 +142,9 @@ def test_subprocess_exit(caplog: pytest.LogCaptureFixture) -> None: python = Path(executable) with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - call( - [python, '-c', "import sys; sys.exit('Test Exit Output')"], + invoke( + python, + ['-c', "import sys; sys.exit('Test Exit Output')"], cppython_logger, ) @@ -158,12 +163,11 @@ def test_subprocess_exception(caplog: pytest.LogCaptureFixture) -> None: python = Path(executable) with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - call( - [python, '-c', "import sys; raise Exception('Test Exception Output')"], + invoke( + python, + ['-c', "import sys; raise Exception('Test Exception Output')"], cppython_logger, ) - assert len(caplog.records) == 1 - assert caplog.records[0].message == 'Test Exception Output' assert 'Subprocess task failed' in str(exec_info.value) @@ -176,9 +180,9 @@ def test_stderr_exception(caplog: pytest.LogCaptureFixture) -> None: """ python = Path(executable) with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - call( + invoke( + python, [ - python, '-c', "import sys; print('Test Out', file = sys.stdout); sys.exit('Test Exit Out')", ], @@ -201,9 +205,9 @@ def test_stdout_exception(caplog: pytest.LogCaptureFixture) -> None: """ python = Path(executable) with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - call( + invoke( + python, [ - python, '-c', "import sys; print('Test Error', file = sys.stderr); sys.exit('Test Exit Error')", ],