diff --git a/.gitignore b/.gitignore index 54f7eb4..416d732 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ __pypackages__/ !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json +/.mypy_cache +node_modules/ +build/ \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2a98eef..89243e9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,8 @@ { "recommendations": [ "ms-python.mypy-type-checker", - "ms-python.pylint", - "ms-python.black-formatter" + "ms-python.black-formatter", + "asciidoctor.asciidoctor-vscode", + "charliermarsh.ruff" ] } \ No newline at end of file diff --git a/cppython/__init__.py b/cppython/__init__.py index 5f28270..6d58322 100644 --- a/cppython/__init__.py +++ b/cppython/__init__.py @@ -1 +1,7 @@ - \ No newline at end of file +"""The CPPython project. + +This module serves as the entry point for the CPPython project, a Python-based +solution for managing C++ dependencies. It includes core functionality, plugin +interfaces, and utility functions that facilitate the integration and management +of various tools and systems. +""" diff --git a/cppython/builder.py b/cppython/builder.py index 9b5bdb8..adaad8e 100644 --- a/cppython/builder.py +++ b/cppython/builder.py @@ -1,16 +1,15 @@ """Defines the data and routines for building a CPPython project type""" import logging -from importlib import metadata +from importlib.metadata import entry_points from inspect import getmodule from logging import Logger from typing import Any -from cppython_core.exceptions import PluginError -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.resolution import ( +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.resolution import ( PluginBuildData, PluginCPPythonData, resolve_cppython, @@ -21,7 +20,7 @@ resolve_provider, resolve_scm, ) -from cppython_core.schema import ( +from cppython.core.schema import ( CoreData, CorePluginData, CPPythonGlobalConfiguration, @@ -32,15 +31,16 @@ ProjectConfiguration, ProjectData, ) - from cppython.data import Data, Plugins +from cppython.defaults import DefaultSCM +from cppython.utility.exception import PluginError class Resolver: """The resolution of data sources for the builder""" def __init__(self, project_configuration: ProjectConfiguration, logger: Logger) -> None: - + """Initializes the resolver""" self._project_configuration = project_configuration self._logger = logger @@ -56,19 +56,18 @@ def generate_plugins( Returns: The resolved plugin data """ - raw_generator_plugins = self.find_generators() generator_plugins = self.filter_plugins( raw_generator_plugins, cppython_local_configuration.generator_name, - "Generator", + 'Generator', ) raw_provider_plugins = self.find_providers() provider_plugins = self.filter_plugins( raw_provider_plugins, cppython_local_configuration.provider_name, - "Provider", + 'Provider', ) scm_plugins = self.find_source_managers() @@ -80,7 +79,8 @@ def generate_plugins( return PluginBuildData(generator_type=generator_type, provider_type=provider_type, scm_type=scm_type) - def generate_cppython_plugin_data(self, plugin_build_data: PluginBuildData) -> PluginCPPythonData: + @staticmethod + def generate_cppython_plugin_data(plugin_build_data: PluginBuildData) -> PluginCPPythonData: """Generates the CPPython plugin data from the resolved plugins Args: @@ -89,15 +89,15 @@ def generate_cppython_plugin_data(self, plugin_build_data: PluginBuildData) -> P Returns: The plugin data used by CPPython """ - return PluginCPPythonData( generator_name=plugin_build_data.generator_type.name(), provider_name=plugin_build_data.provider_type.name(), scm_name=plugin_build_data.scm_type.name(), ) + @staticmethod def generate_pep621_data( - self, pep621_configuration: PEP621Configuration, project_configuration: ProjectConfiguration, scm: SCM | None + pep621_configuration: PEP621Configuration, project_configuration: ProjectConfiguration, scm: SCM | None ) -> PEP621Data: """Generates the PEP621 data from configuration sources @@ -111,13 +111,13 @@ def generate_pep621_data( """ return resolve_pep621(pep621_configuration, project_configuration, scm) - def resolve_global_config(self) -> CPPythonGlobalConfiguration: + @staticmethod + def resolve_global_config() -> CPPythonGlobalConfiguration: """Generates the global configuration object Returns: The global configuration object """ - return CPPythonGlobalConfiguration() def find_generators(self) -> list[type[Generator]]: @@ -129,12 +129,11 @@ def find_generators(self) -> list[type[Generator]]: Returns: The list of generator plugin types """ - - group_name = "generator" + group_name = 'generator' plugin_types: list[type[Generator]] = [] # Filter entries by type - for entry_point in list(metadata.entry_points(group=f"cppython.{group_name}")): + for entry_point in list(entry_points(group=f'cppython.{group_name}')): loaded_type = entry_point.load() if not issubclass(loaded_type, Generator): self._logger.warning( @@ -142,11 +141,11 @@ 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.warning(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') plugin_types.append(loaded_type) if not plugin_types: - raise PluginError(f"No {group_name} plugin was found") + raise PluginError(f'No {group_name} plugin was found') return plugin_types @@ -159,12 +158,11 @@ def find_providers(self) -> list[type[Provider]]: Returns: The list of provider plugin types """ - - group_name = "provider" + group_name = 'provider' plugin_types: list[type[Provider]] = [] # Filter entries by type - for entry_point in list(metadata.entry_points(group=f"cppython.{group_name}")): + for entry_point in list(entry_points(group=f'cppython.{group_name}')): loaded_type = entry_point.load() if not issubclass(loaded_type, Provider): self._logger.warning( @@ -172,11 +170,11 @@ 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.warning(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') plugin_types.append(loaded_type) if not plugin_types: - raise PluginError(f"No {group_name} plugin was found") + raise PluginError(f'No {group_name} plugin was found') return plugin_types @@ -189,12 +187,11 @@ def find_source_managers(self) -> list[type[SCM]]: Returns: The list of source control manager plugin types """ - - group_name = "scm" + group_name = 'scm' plugin_types: list[type[SCM]] = [] # Filter entries by type - for entry_point in list(metadata.entry_points(group=f"cppython.{group_name}")): + for entry_point in list(entry_points(group=f'cppython.{group_name}')): loaded_type = entry_point.load() if not issubclass(loaded_type, SCM): self._logger.warning( @@ -202,17 +199,17 @@ 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.warning(f'{group_name} plugin found: {loaded_type.name()} from {getmodule(loaded_type)}') plugin_types.append(loaded_type) if not plugin_types: - raise PluginError(f"No {group_name} plugin was found") + raise PluginError(f'No {group_name} plugin was found') return plugin_types - def filter_plugins[ - T: DataPlugin - ](self, plugin_types: list[type[T]], pinned_name: str | None, group_name: str) -> list[type[T]]: + def filter_plugins[T: DataPlugin]( + self, plugin_types: list[type[T]], pinned_name: str | None, group_name: str + ) -> list[type[T]]: """Finds and filters data plugins Args: @@ -226,13 +223,12 @@ def filter_plugins[ Returns: The list of applicable plugins """ - # Lookup the requested plugin if given 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)}" + f'Using {group_name} plugin: {loaded_type.name()} from {getmodule(loaded_type)}' ) return [loaded_type] @@ -243,13 +239,13 @@ def filter_plugins[ # 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)}" + f'A {group_name} plugin is supported: {loaded_type.name()} from {getmodule(loaded_type)}' ) supported_types.append(loaded_type) # Fail if supported_types is None: - raise PluginError(f"No {group_name} could be deduced from the root directory.") + raise PluginError(f'No {group_name} could be deduced from the root directory.') return supported_types @@ -260,21 +256,20 @@ def select_scm(self, scm_plugins: list[type[SCM]], project_data: ProjectData) -> scm_plugins: The list of SCM plugin types project_data: The project data - Raises: - PluginError: Raised if no SCM plugin was found that supports the given data - Returns: The selected SCM plugin type """ - for scm_type in scm_plugins: if scm_type.features(project_data.pyproject_file.parent).repository: return scm_type - raise PluginError("No SCM plugin was found that supports the given path") + self._logger.info('No SCM plugin was found that supports the given path') + + return DefaultSCM + @staticmethod def solve( - self, generator_types: list[type[Generator]], provider_types: list[type[Provider]] + generator_types: list[type[Generator]], provider_types: list[type[Provider]] ) -> tuple[type[Generator], type[Provider]]: """Selects the first generator and provider that can work together @@ -288,7 +283,6 @@ def solve( Returns: A tuple of the selected generator and provider plugin types """ - combos: list[tuple[type[Generator], type[Provider]]] = [] for generator_type in generator_types: @@ -300,12 +294,12 @@ def solve( break if not combos: - raise PluginError("No provider that supports a given generator could be deduced") + raise PluginError('No provider that supports a given generator could be deduced') return combos[0] + @staticmethod def create_scm( - self, core_data: CoreData, scm_type: type[SCM], ) -> SCM: @@ -318,7 +312,6 @@ def create_scm( Returns: The constructed source control manager """ - cppython_plugin_data = resolve_cppython_plugin(core_data.cppython_data, scm_type) scm_data = resolve_scm(core_data.project_data, cppython_plugin_data) @@ -344,7 +337,6 @@ def create_generator( Returns: The constructed generator """ - cppython_plugin_data = resolve_cppython_plugin(core_data.cppython_data, generator_type) generator_data = resolve_generator(core_data.project_data, cppython_plugin_data) @@ -380,7 +372,6 @@ def create_provider( Returns: A constructed provider plugins """ - cppython_plugin_data = resolve_cppython_plugin(core_data.cppython_data, provider_type) provider_data = resolve_provider(core_data.project_data, cppython_plugin_data) @@ -403,6 +394,7 @@ class Builder: """Helper class for building CPPython projects""" def __init__(self, project_configuration: ProjectConfiguration, logger: Logger) -> None: + """Initializes the builder""" self._project_configuration = project_configuration self._logger = logger @@ -413,7 +405,7 @@ def __init__(self, project_configuration: ProjectConfiguration, logger: Logger) self._logger.addHandler(logging.StreamHandler()) self._logger.setLevel(levels[project_configuration.verbosity]) - self._logger.info("Logging setup complete") + self._logger.info('Logging setup complete') self._resolver = Resolver(self._project_configuration, self._logger) @@ -428,12 +420,12 @@ def build( Args: pep621_configuration: The PEP621 configuration cppython_local_configuration: The local configuration - plugin_build_data: Plugin override data. If it exists, the build will use the given types instead of resolving them + plugin_build_data: Plugin override data. If it exists, the build will use the given types + instead of resolving them Returns: The built data object """ - project_data = resolve_project_configuration(self._project_configuration) if plugin_build_data is None: diff --git a/cppython/console/__init__.py b/cppython/console/__init__.py index 5f28270..dd8a440 100644 --- a/cppython/console/__init__.py +++ b/cppython/console/__init__.py @@ -1 +1,6 @@ - \ No newline at end of file +"""Console interface for the CPPython project. + +This module provides a command-line interface (CLI) for interacting with the +CPPython project. It includes commands for managing project configurations, +installing dependencies, and updating project data. +""" diff --git a/cppython/console/entry.py b/cppython/console/entry.py new file mode 100644 index 0000000..fcdd209 --- /dev/null +++ b/cppython/console/entry.py @@ -0,0 +1,115 @@ +"""A click CLI for CPPython interfacing""" + +from pathlib import Path +from tomllib import loads +from typing import Annotated + +import typer + +from cppython.console.schema import ConsoleConfiguration, ConsoleInterface +from cppython.core.schema import ProjectConfiguration +from cppython.project import Project + +app = typer.Typer() + + +def _find_pyproject_file() -> Path: + """Searches upward for a pyproject.toml file + + Returns: + The found directory + """ + # Search for a path upward + path = Path.cwd() + + while not path.glob('pyproject.toml'): + if path.is_absolute(): + raise AssertionError( + 'This is not a valid project. No pyproject.toml found in the current directory or any of its parents.' + ) + + path = Path(path) + + return path + + +@app.callback() +def main( + context: typer.Context, + verbose: Annotated[ + int, typer.Option('-v', '--verbose', count=True, min=0, max=2, help='Print additional output') + ] = 0, + debug: Annotated[bool, typer.Option()] = False, +) -> None: + """entry_point group for the CLI commands + + Args: + context: The typer context + verbose: The verbosity level + 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) + + interface = ConsoleInterface() + context.obj = ConsoleConfiguration(project_configuration=project_configuration, interface=interface) + + +@app.command() +def info( + _: typer.Context, +) -> None: + """Prints project information""" + + +@app.command() +def install( + context: typer.Context, +) -> None: + """Install API call + + Args: + context: The CLI configuration object + + Raises: + ValueError: If the configuration object is missing + """ + if (configuration := context.find_object(ConsoleConfiguration)) is None: + raise ValueError('The configuration object is missing') + + path = configuration.project_configuration.pyproject_file + pyproject_data = loads(path.read_text(encoding='utf-8')) + + project = Project(configuration.project_configuration, configuration.interface, pyproject_data) + project.install() + + +@app.command() +def update( + context: typer.Context, +) -> None: + """Update API call + + Args: + context: The CLI configuration object + + Raises: + ValueError: If the configuration object is missing + """ + if (configuration := context.find_object(ConsoleConfiguration)) is None: + raise ValueError('The configuration object is missing') + + path = configuration.project_configuration.pyproject_file + pyproject_data = loads(path.read_text(encoding='utf-8')) + + project = Project(configuration.project_configuration, configuration.interface, pyproject_data) + project.update() + + +@app.command(name='list') +def list_command( + _: typer.Context, +) -> None: + """Prints project information""" diff --git a/cppython/console/interface.py b/cppython/console/interface.py deleted file mode 100644 index 1a5e49b..0000000 --- a/cppython/console/interface.py +++ /dev/null @@ -1,146 +0,0 @@ -"""A click CLI for CPPython interfacing""" - -from logging import getLogger -from pathlib import Path - -import click -import tomlkit -from cppython_core.schema import Interface, ProjectConfiguration - -from cppython.project import Project - - -def _find_pyproject_file() -> Path: - """Searches upward for a pyproject.toml file - - Returns: - The found directory - """ - - # Search for a path upward - path = Path.cwd() - - while not path.glob("pyproject.toml"): - if path.is_absolute(): - assert ( - False - ), "This is not a valid project. No pyproject.toml found in the current directory or any of its parents." - - path = Path(path) - - return path - - -class Configuration: - """Click configuration object""" - - def __init__(self) -> None: - self.interface = ConsoleInterface() - - self.logger = getLogger("cppython.console") - - path = _find_pyproject_file() - file_path = path / "pyproject.toml" - - self.configuration = ProjectConfiguration(pyproject_file=file_path, version=None) - - def query_scm(self) -> str: - """Queries the SCM system for its version - - Returns: - The version - """ - - return "TODO" - - def generate_project(self) -> Project: - """Aids in project generation. Allows deferred configuration from within the "config" object - - Returns: - The constructed Project - """ - - path: Path = self.configuration.pyproject_file - pyproject_data = tomlkit.loads(path.read_text(encoding="utf-8")) - - return Project(self.configuration, self.interface, pyproject_data) - - -# Attach our config object to click's hook -pass_config = click.make_pass_decorator(Configuration, ensure=True) - - -@click.group() -@click.option("-v", "--verbose", count=True, help="Print additional output") -@click.option("--debug/--no-debug", default=False) -@pass_config -def cli(config: Configuration, verbose: int, debug: bool) -> None: - """entry_point group for the CLI commands - - Args: - config: The CLI configuration object - verbose: The verbosity level - debug: Debug mode - """ - config.configuration.verbosity = verbose - config.configuration.debug = debug - - -@cli.command(name="info") -@pass_config -def info_command(config: Configuration) -> None: - """Prints project information - - Args: - config: The CLI configuration object - """ - - version = config.query_scm() - config.logger.info("The SCM project version is: %s", version) - - -@cli.command(name="list") -@pass_config -def list_command(config: Configuration) -> None: - """Prints project information - - Args: - config: The CLI configuration object - """ - - version = config.query_scm() - config.logger.info("The SCM project version is: %s", version) - - -@cli.command(name="install") -@pass_config -def install_command(config: Configuration) -> None: - """Install API call - - Args: - config: The CLI configuration object - """ - project = config.generate_project() - project.install() - - -@cli.command(name="update") -@pass_config -def update_command(config: Configuration) -> None: - """Update API call - - Args: - config: The CLI configuration object - """ - project = config.generate_project() - project.update() - - -class ConsoleInterface(Interface): - """Interface implementation to pass to the project""" - - def write_pyproject(self) -> None: - """Write output""" - - def write_configuration(self) -> None: - """Write output""" diff --git a/cppython/console/schema.py b/cppython/console/schema.py new file mode 100644 index 0000000..263bcd6 --- /dev/null +++ b/cppython/console/schema.py @@ -0,0 +1,24 @@ +"""Data definitions for the console application""" + +from pydantic import ConfigDict + +from cppython.core.schema import CPPythonModel, Interface, ProjectConfiguration + + +class ConsoleInterface(Interface): + """Interface implementation to pass to the project""" + + def write_pyproject(self) -> None: + """Write output""" + + def write_configuration(self) -> None: + """Write output""" + + +class ConsoleConfiguration(CPPythonModel): + """Configuration data for the console application""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + project_configuration: ProjectConfiguration + interface: Interface diff --git a/cppython/core/__init__.py b/cppython/core/__init__.py new file mode 100644 index 0000000..353b88f --- /dev/null +++ b/cppython/core/__init__.py @@ -0,0 +1,6 @@ +"""Core functionality for the CPPython project. + +This module contains the core components and utilities that form the foundation +of the CPPython project. It includes schema definitions, exception handling, +resolution processes, and utility functions. +""" diff --git a/cppython/core/exception.py b/cppython/core/exception.py new file mode 100644 index 0000000..a77bffc --- /dev/null +++ b/cppython/core/exception.py @@ -0,0 +1,28 @@ +"""Custom exceptions used by CPPython""" + +from pydantic import BaseModel + + +class ConfigError(BaseModel): + """Data for ConfigError""" + + message: str + + +class ConfigException(ValueError): + """Raised when there is a configuration error""" + + def __init__(self, message: str, errors: list[ConfigError]): + """Initializes the exception""" + super().__init__(message) + self._errors = errors + + @property + def error_count(self) -> int: + """The number of configuration errors associated with this exception""" + return len(self._errors) + + @property + def errors(self) -> list[ConfigError]: + """The list of configuration errors""" + return self._errors diff --git a/cppython/core/plugin_schema/__init__.py b/cppython/core/plugin_schema/__init__.py new file mode 100644 index 0000000..65baf09 --- /dev/null +++ b/cppython/core/plugin_schema/__init__.py @@ -0,0 +1,7 @@ +"""Schema definitions for CPPython plugins. + +This module defines the schemas and protocols for CPPython plugins, including +generators, providers, and SCMs. It provides the necessary interfaces and data +structures to ensure consistent communication and functionality between the core +CPPython system and its plugins. +""" diff --git a/cppython/core/plugin_schema/generator.py b/cppython/core/plugin_schema/generator.py new file mode 100644 index 0000000..0a81266 --- /dev/null +++ b/cppython/core/plugin_schema/generator.py @@ -0,0 +1,70 @@ +"""Generator data plugin definitions""" + +from abc import abstractmethod +from typing import Any, Protocol, runtime_checkable + +from pydantic.types import DirectoryPath + +from cppython.core.schema import ( + CorePluginData, + DataPlugin, + DataPluginGroupData, + SupportedDataFeatures, + SyncData, +) + + +class GeneratorPluginGroupData(DataPluginGroupData): + """Base class for the configuration data that is set by the project for the generator""" + + +class SupportedGeneratorFeatures(SupportedDataFeatures): + """Generator plugin feature support""" + + +class SyncConsumer(Protocol): + """Interface for consuming synchronization data from providers""" + + @staticmethod + @abstractmethod + def sync_types() -> list[type[SyncData]]: + """Broadcasts supported types + + Returns: + A list of synchronization types that are supported + """ + raise NotImplementedError + + @abstractmethod + def sync(self, sync_data: SyncData) -> None: + """Synchronizes generator files and state with the providers input + + Args: + sync_data: The input data to sync with + """ + raise NotImplementedError + + +@runtime_checkable +class Generator(DataPlugin, SyncConsumer, Protocol): + """Abstract type to be inherited by CPPython Generator plugins""" + + @abstractmethod + def __init__( + self, group_data: GeneratorPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any] + ) -> None: + """Initializes the generator plugin""" + raise NotImplementedError + + @staticmethod + @abstractmethod + def features(directory: DirectoryPath) -> SupportedGeneratorFeatures: + """Broadcasts the shared features of the generator plugin to CPPython + + Args: + directory: The root directory where features are evaluated + + Returns: + The supported features + """ + raise NotImplementedError diff --git a/cppython/core/plugin_schema/provider.py b/cppython/core/plugin_schema/provider.py new file mode 100644 index 0000000..b818976 --- /dev/null +++ b/cppython/core/plugin_schema/provider.py @@ -0,0 +1,89 @@ +"""Provider data plugin definitions""" + +from abc import abstractmethod +from typing import Any, Protocol, runtime_checkable + +from pydantic.types import DirectoryPath + +from cppython.core.plugin_schema.generator import SyncConsumer +from cppython.core.schema import ( + CorePluginData, + DataPlugin, + DataPluginGroupData, + SupportedDataFeatures, + SyncData, +) + + +class ProviderPluginGroupData(DataPluginGroupData): + """Base class for the configuration data that is set by the project for the provider""" + + +class SupportedProviderFeatures(SupportedDataFeatures): + """Provider plugin feature support""" + + +class SyncProducer(Protocol): + """Interface for producing synchronization data with generators""" + + @staticmethod + @abstractmethod + def supported_sync_type(sync_type: type[SyncData]) -> bool: + """Queries for support for a given synchronization type + + Args: + sync_type: The type to query support for + + Returns: + Support + """ + raise NotImplementedError + + @abstractmethod + def sync_data(self, consumer: SyncConsumer) -> SyncData | None: + """Requests generator information from the provider. + + The generator is either defined by a provider specific file or the CPPython configuration table + + Args: + consumer: The consumer + + Returns: + An instantiated data type, or None if no instantiation is made + """ + raise NotImplementedError + + +@runtime_checkable +class Provider(DataPlugin, SyncProducer, Protocol): + """Abstract type to be inherited by CPPython Provider plugins""" + + @abstractmethod + def __init__( + self, group_data: ProviderPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any] + ) -> None: + """Initializes the provider""" + raise NotImplementedError + + @staticmethod + @abstractmethod + def features(directory: DirectoryPath) -> SupportedProviderFeatures: + """Broadcasts the shared features of the Provider plugin to CPPython + + Args: + directory: The root directory where features are evaluated + + Returns: + The supported features + """ + raise NotImplementedError + + @abstractmethod + def install(self) -> None: + """Called when dependencies need to be installed from a lock file.""" + raise NotImplementedError + + @abstractmethod + def update(self) -> None: + """Called when dependencies need to be updated and written to the lock file.""" + raise NotImplementedError diff --git a/cppython/core/plugin_schema/scm.py b/cppython/core/plugin_schema/scm.py new file mode 100644 index 0000000..4e5be7c --- /dev/null +++ b/cppython/core/plugin_schema/scm.py @@ -0,0 +1,62 @@ +"""Version control data plugin definitions""" + +from abc import abstractmethod +from typing import Annotated, Protocol, runtime_checkable + +from pydantic import DirectoryPath, Field + +from cppython.core.schema import Plugin, PluginGroupData, SupportedFeatures + + +class SCMPluginGroupData(PluginGroupData): + """SCM plugin input data""" + + +class SupportedSCMFeatures(SupportedFeatures): + """SCM plugin feature support""" + + repository: Annotated[ + bool, Field(description='True if the directory is a repository for the SCM. False, otherwise') + ] + + +@runtime_checkable +class SCM(Plugin, Protocol): + """Base class for version control systems""" + + @abstractmethod + def __init__(self, group_data: SCMPluginGroupData) -> None: + """Initializes the SCM plugin""" + raise NotImplementedError + + @staticmethod + @abstractmethod + def features(directory: DirectoryPath) -> SupportedSCMFeatures: + """Broadcasts the shared features of the SCM plugin to CPPython + + Args: + directory: The root directory where features are evaluated + + Returns: + The supported features + """ + raise NotImplementedError + + @abstractmethod + def version(self, directory: DirectoryPath) -> str: + """Extracts the system's version metadata + + Args: + directory: The input directory + + Returns: + A version string + """ + raise NotImplementedError + + def description(self) -> str | None: + """Requests extraction of the project description + + Returns: + Returns the project description, or none if unavailable + """ diff --git a/cppython/core/resolution.py b/cppython/core/resolution.py new file mode 100644 index 0000000..03a873c --- /dev/null +++ b/cppython/core/resolution.py @@ -0,0 +1,270 @@ +"""Data conversion routines""" + +from pathlib import Path +from typing import Any, cast + +from pydantic import BaseModel, DirectoryPath, ValidationError + +from cppython.core.exception import ConfigError, 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 +from cppython.core.schema import ( + CPPythonData, + CPPythonGlobalConfiguration, + CPPythonLocalConfiguration, + CPPythonModel, + CPPythonPluginData, + PEP621Configuration, + PEP621Data, + Plugin, + ProjectConfiguration, + ProjectData, +) +from cppython.utility.utility import TypeName + + +def resolve_project_configuration(project_configuration: ProjectConfiguration) -> ProjectData: + """Creates a resolved type + + Args: + project_configuration: Input configuration + + Returns: + The resolved data + """ + return ProjectData(pyproject_file=project_configuration.pyproject_file, verbosity=project_configuration.verbosity) + + +def resolve_pep621( + pep621_configuration: PEP621Configuration, project_configuration: ProjectConfiguration, scm: SCM | None +) -> PEP621Data: + """Creates a resolved type + + Args: + pep621_configuration: Input PEP621 configuration + project_configuration: The input configuration used to aid the resolve + scm: SCM + + Raises: + ConfigError: Raised when the tooling did not satisfy the configuration request + ValueError: Raised if there is a broken schema + + Returns: + The resolved type + """ + # Update the dynamic version + if 'version' in pep621_configuration.dynamic: + 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) + else: + raise ValueError("Version can't be resolved. No SCM") + + elif pep621_configuration.version is not None: + modified_version = pep621_configuration.version + + else: + raise ValueError("Version can't be resolved. Schema error") + + pep621_data = PEP621Data( + name=pep621_configuration.name, version=modified_version, description=pep621_configuration.description + ) + return pep621_data + + +class PluginBuildData(CPPythonModel): + """Data needed to construct CoreData""" + + generator_type: type[Generator] + provider_type: type[Provider] + scm_type: type[SCM] + + +class PluginCPPythonData(CPPythonModel): + """Plugin data needed to construct CPPythonData""" + + generator_name: TypeName + provider_name: TypeName + scm_name: TypeName + + +def resolve_cppython( + local_configuration: CPPythonLocalConfiguration, + global_configuration: CPPythonGlobalConfiguration, + project_data: ProjectData, + plugin_build_data: PluginCPPythonData, +) -> CPPythonData: + """Creates a copy and resolves dynamic attributes + + Args: + local_configuration: Local project configuration + global_configuration: Shared project configuration + project_data: Project information to aid in the resolution + plugin_build_data: Plugin build data + + Raises: + ConfigError: Raised when the tooling did not satisfy the configuration request + + Returns: + An instance of the resolved type + """ + root_directory = project_data.pyproject_file.parent.absolute() + + # Add the base path to all relative paths + modified_install_path = local_configuration.install_path + + if not modified_install_path.is_absolute(): + modified_install_path = root_directory / modified_install_path + + modified_tool_path = local_configuration.tool_path + + if not modified_tool_path.is_absolute(): + modified_tool_path = root_directory / modified_tool_path + + modified_build_path = local_configuration.build_path + + 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 + + if modified_provider_name is None: + modified_provider_name = plugin_build_data.provider_name + + if modified_generator_name is None: + modified_generator_name = plugin_build_data.generator_name + + modified_scm_name = plugin_build_data.scm_name + + cppython_data = CPPythonData( + install_path=modified_install_path, + tool_path=modified_tool_path, + build_path=modified_build_path, + current_check=global_configuration.current_check, + provider_name=modified_provider_name, + generator_name=modified_generator_name, + scm_name=modified_scm_name, + ) + return cppython_data + + +def resolve_cppython_plugin(cppython_data: CPPythonData, plugin_type: type[Plugin]) -> CPPythonPluginData: + """Resolve project configuration for plugins + + Args: + cppython_data: The CPPython data + plugin_type: The plugin type + + Returns: + The resolved type with plugin specific modifications + """ + # 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( + install_path=modified_install_path, + tool_path=cppython_data.tool_path, + build_path=cppython_data.build_path, + current_check=cppython_data.current_check, + provider_name=cppython_data.provider_name, + generator_name=cppython_data.generator_name, + scm_name=cppython_data.scm_name, + ) + + return cast(CPPythonPluginData, plugin_data) + + +def _write_tool_directory(cppython_data: CPPythonData, directory: Path) -> DirectoryPath: + """Creates directories following a certain format + + Args: + cppython_data: The cppython data + directory: The directory to create + + Returns: + The written path + """ + plugin_directory = cppython_data.tool_path / 'cppython' / directory + plugin_directory.mkdir(parents=True, exist_ok=True) + + return plugin_directory + + +def resolve_generator(project_data: ProjectData, cppython_data: CPPythonPluginData) -> GeneratorPluginGroupData: + """Creates an instance from the given project + + Args: + project_data: The input project data + cppython_data: The input cppython data + + Returns: + The plugin specific configuration + """ + root_directory = project_data.pyproject_file.parent + 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 + + +def resolve_provider(project_data: ProjectData, cppython_data: CPPythonPluginData) -> ProviderPluginGroupData: + """Creates an instance from the given project + + Args: + project_data: The input project data + cppython_data: The input cppython data + + Returns: + The plugin specific configuration + """ + root_directory = project_data.pyproject_file.parent + 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 + + +def resolve_scm(project_data: ProjectData, cppython_data: CPPythonPluginData) -> SCMPluginGroupData: + """Creates an instance from the given project + + Args: + project_data: The input project data + cppython_data: The input cppython data + + Returns: + The plugin specific configuration + """ + root_directory = project_data.pyproject_file.parent + 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 + + +def resolve_model[T: BaseModel](model: type[T], data: dict[str, Any]) -> T: + """Wraps the model validation and conversion + + Args: + model: The model to create + data: The input data to create the model from + + Raises: + ConfigException: Raised when the input does not satisfy the given schema + + Returns: + The instance of the model + """ + try: + # 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 diff --git a/cppython/core/schema.py b/cppython/core/schema.py new file mode 100644 index 0000000..e5c566d --- /dev/null +++ b/cppython/core/schema.py @@ -0,0 +1,362 @@ +"""Data types for CPPython that encapsulate the requirements between the plugins and the core library""" + +from abc import abstractmethod +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 cppython.utility.plugin import Plugin as SynodicPlugin +from cppython.utility.utility import TypeName + + +class CPPythonModel(BaseModel): + """The base model to use for all CPPython models""" + + model_config = {'populate_by_name': False} + + +class ProjectData(CPPythonModel, extra='forbid'): + """Resolved data of 'ProjectConfiguration'""" + + pyproject_file: Annotated[FilePath, 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')] + version: Annotated[ + str | None, + Field( + description=( + "The version number a 'dynamic' project version will resolve to. If not provided" + 'a CPPython project will' + ' initialize its SCM plugins to discover any available version' + ) + ), + ] + verbosity: Annotated[int, Field(description='The verbosity level as an integer [0,2]')] = 0 + debug: Annotated[ + bool, Field(description='Debug mode. Additional processing will happen to expose more debug information') + ] = False + + @field_validator('verbosity') + @classmethod + def min_max(cls, value: int) -> int: + """Validator that clamps the input value + + Args: + value: Input to validate + + Returns: + The clamped input value + """ + 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""" + + name: str + version: str + description: str + + +class PEP621Configuration(CPPythonModel): + """CPPython relevant PEP 621 conforming data + + Because only the partial schema is used, we ignore 'extra' attributes + Schema: https://www.python.org/dev/peps/pep-0621/ + """ + + dynamic: Annotated[list[str], Field(description='https://peps.python.org/pep-0621/#dynamic')] = [] + name: Annotated[str, Field(description='https://peps.python.org/pep-0621/#name')] + version: Annotated[str | None, Field(description='https://peps.python.org/pep-0621/#version')] = None + description: Annotated[str, Field(description='https://peps.python.org/pep-0621/#description')] = '' + + @model_validator(mode='after') # type: ignore + @classmethod + def dynamic_data(cls, model: 'PEP621Configuration') -> 'PEP621Configuration': + """Validates that dynamic data is represented correctly + + Args: + model: The input model data + + Raises: + ValueError: If dynamic versioning is incorrect + + Returns: + The data + """ + for field in model.model_fields: + if field == 'dynamic': + continue + value = getattr(model, field) + if field not in model.dynamic: + if value is None: + raise ValueError(f"'{field}' is not a dynamic field. It must be defined") + elif value is not None: + raise ValueError(f"'{field}' is a dynamic field. It must not be defined") + + 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 + current_check: bool + provider_name: TypeName + generator_name: TypeName + scm_name: TypeName + + @field_validator('install_path', 'tool_path', 'build_path') + @classmethod + def validate_absolute_path(cls, value: DirectoryPath) -> DirectoryPath: + """Enforce the input is an absolute path + + Args: + value: The input value + + Raises: + ValueError: Raised if the input is not an absolute path + + Returns: + The validated input value + """ + if not value.is_absolute(): + raise ValueError('Absolute path required') + + return value + + +CPPythonPluginData = NewType('CPPythonPluginData', CPPythonData) + + +class SyncData(CPPythonModel): + """Data that passes in a plugin sync""" + + provider_name: TypeName + + +class SupportedFeatures(CPPythonModel): + """Plugin feature support""" + + initialization: Annotated[ + bool, Field(description='Whether the plugin supports initialization from an empty state') + ] = False + + +class Information(CPPythonModel): + """Plugin information that complements the packaged project metadata""" + + +class PluginGroupData(CPPythonModel, extra='forbid'): + """Plugin group data""" + + root_directory: Annotated[DirectoryPath, Field(description='The directory of the project')] + tool_directory: Annotated[ + DirectoryPath, + Field( + description=( + 'Points to the project plugin directory within the tool directory. ' + 'This directory is for project specific cached data.' + ) + ), + ] + + +class Plugin(SynodicPlugin, Protocol): + """CPPython plugin""" + + @abstractmethod + def __init__(self, group_data: PluginGroupData) -> None: + """Initializes the plugin""" + raise NotImplementedError + + @staticmethod + @abstractmethod + def features(directory: DirectoryPath) -> SupportedFeatures: + """Broadcasts the shared features of the plugin to CPPython + + Args: + directory: The root directory where features are evaluated + + Returns: + The supported features + """ + raise NotImplementedError + + @staticmethod + @abstractmethod + def information() -> Information: + """Retrieves plugin information that complements the packaged project metadata + + Returns: + The plugin's information + """ + raise NotImplementedError + + +class DataPluginGroupData(PluginGroupData): + """Data plugin group data""" + + +class CorePluginData(CPPythonModel): + """Core resolved data that will be passed to data plugins""" + + project_data: ProjectData + pep621_data: PEP621Data + cppython_data: CPPythonPluginData + + +class SupportedDataFeatures(SupportedFeatures): + """Data plugin feature support""" + + +class DataPlugin(Plugin, Protocol): + """Abstract plugin type for internal CPPython data""" + + @abstractmethod + def __init__( + self, group_data: DataPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any] + ) -> None: + """Initializes the data plugin""" + raise NotImplementedError + + @staticmethod + @abstractmethod + def features(directory: DirectoryPath) -> SupportedDataFeatures: + """Broadcasts the shared features of the data plugin to CPPython + + Args: + directory: The root directory where features are evaluated + + Returns: + The supported features + """ + raise NotImplementedError + + @classmethod + async def download_tooling(cls, directory: DirectoryPath) -> None: + """Installs the external tooling required by the plugin. Should be overridden if required + + Args: + directory: The directory to download any extra tooling to + """ + + +class CPPythonGlobalConfiguration(CPPythonModel, extra='forbid'): + """Global data extracted by the tool""" + + current_check: Annotated[bool, Field(alias='current-check', description='Checks for a new CPPython version')] = True + + +ProviderData = NewType('ProviderData', dict[str, Any]) +GeneratorData = NewType('GeneratorData', dict[str, Any]) + + +class CPPythonLocalConfiguration(CPPythonModel, extra='forbid'): + """Data required by the tool""" + + install_path: Annotated[ + Path, + Field( + alias='install-path', + description='The global install path for the project', + ), + ] = _default_install_location() + tool_path: Annotated[Path, Field(alias='tool-path', description='The local tooling path for the project')] = Path( + 'tool' + ) + + build_path: Annotated[Path, Field(alias='build-path', description='The local build path for the project')] = Path( + 'build' + ) + + provider: Annotated[ProviderData, Field(description="Provider plugin data associated with 'provider_name")] = ( + ProviderData({}) + ) + + provider_name: Annotated[ + TypeName | None, + Field( + alias='provider-name', + description='If empty, the provider will be automatically deduced.', + ), + ] = None + + generator: Annotated[GeneratorData, Field(description="Generator plugin data associated with 'generator_name'")] = ( + GeneratorData({}) + ) + + generator_name: Annotated[ + TypeName | None, + Field( + alias='generator-name', + description='If empty, the generator will be automatically deduced.', + ), + ] = None + + +class ToolData(CPPythonModel): + """Tool entry of pyproject.toml""" + + cppython: Annotated[CPPythonLocalConfiguration | None, Field(description='CPPython tool data')] = None + + +class PyProject(CPPythonModel): + """pyproject.toml schema""" + + project: Annotated[PEP621Configuration, Field(description='PEP621: https://www.python.org/dev/peps/pep-0621/')] + tool: Annotated[ToolData | None, Field(description='Tool data')] = None + + +class CoreData(CPPythonModel): + """Core resolved data that will be resolved""" + + project_data: ProjectData + cppython_data: CPPythonData + + +@runtime_checkable +class Interface(Protocol): + """Type for interfaces to allow feedback from CPPython""" + + @abstractmethod + def write_pyproject(self) -> None: + """Called when CPPython requires the interface to write out pyproject.toml changes""" + raise NotImplementedError + + @abstractmethod + def write_configuration(self) -> None: + """Called when CPPython requires the interface to write out configuration changes""" + raise NotImplementedError diff --git a/cppython/core/utility.py b/cppython/core/utility.py new file mode 100644 index 0000000..b361baf --- /dev/null +++ b/cppython/core/utility.py @@ -0,0 +1,43 @@ +"""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/data.py b/cppython/data.py index ad24718..c2c76be 100644 --- a/cppython/data.py +++ b/cppython/data.py @@ -3,11 +3,11 @@ from dataclasses import dataclass from logging import Logger -from cppython_core.exceptions import PluginError -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 CoreData +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 CoreData +from cppython.utility.exception import PluginError @dataclass @@ -23,6 +23,7 @@ class Data: """Contains and manages the project data""" def __init__(self, core_data: CoreData, plugins: Plugins, logger: Logger) -> None: + """Initializes the data""" self._core_data = core_data self._plugins = plugins self.logger = logger @@ -38,7 +39,6 @@ def sync(self) -> None: Raises: PluginError: Plugin error """ - if (sync_data := self.plugins.provider.sync_data(self.plugins.generator)) is None: raise PluginError("The provider doesn't support the generator") @@ -52,5 +52,5 @@ async def download_provider_tools(self) -> None: path.mkdir(parents=True, exist_ok=True) - self.logger.warning("Downloading the %s requirements to %s", self.plugins.provider.name(), path) + self.logger.warning('Downloading the %s requirements to %s', self.plugins.provider.name(), path) await self.plugins.provider.download_tooling(path) diff --git a/cppython/defaults.py b/cppython/defaults.py new file mode 100644 index 0000000..eb00267 --- /dev/null +++ b/cppython/defaults.py @@ -0,0 +1,45 @@ +"""Defines a SCM subclass that is used as the default SCM if no plugin is found or selected""" + +from pydantic import DirectoryPath + +from cppython.core.plugin_schema.scm import ( + SCM, + SCMPluginGroupData, + SupportedSCMFeatures, +) +from cppython.core.schema import Information + + +class DefaultSCM(SCM): + """A default SCM class for when no SCM plugin is selected""" + + def __init__(self, group_data: SCMPluginGroupData) -> None: + """Initializes the default SCM class""" + self.group_data = group_data + + @staticmethod + def features(_: DirectoryPath) -> SupportedSCMFeatures: + """Broadcasts the shared features of the SCM plugin to CPPython + + Returns: + The supported features + """ + return SupportedSCMFeatures(repository=True) + + @staticmethod + def information() -> Information: + """Returns plugin information + + Returns: + The plugin information + """ + return Information() + + @staticmethod + def version(_: DirectoryPath) -> str: + """Extracts the system's version metadata + + Returns: + A version + """ + return '1.0.0' diff --git a/cppython/plugins/__init__.py b/cppython/plugins/__init__.py new file mode 100644 index 0000000..501f55f --- /dev/null +++ b/cppython/plugins/__init__.py @@ -0,0 +1,7 @@ +"""Plugins for the CPPython project. + +This module contains various plugins that extend the functionality of the CPPython +project. Each plugin integrates with different tools and systems to provide +additional capabilities, such as dependency management, build system integration, +and version control. +""" diff --git a/cppython/plugins/cmake/__init__.py b/cppython/plugins/cmake/__init__.py new file mode 100644 index 0000000..41cf2e1 --- /dev/null +++ b/cppython/plugins/cmake/__init__.py @@ -0,0 +1,6 @@ +"""The CMake generator plugin for CPPython. + +This module implements the CMake generator plugin, which integrates CPPython with +the CMake build system. It includes functionality for resolving configuration data, +writing presets, and synchronizing project data. +""" diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py new file mode 100644 index 0000000..ae385ec --- /dev/null +++ b/cppython/plugins/cmake/builder.py @@ -0,0 +1,63 @@ +"""Plugin builder""" + +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 + + +class Builder: + """Aids in building the information needed for the CMake plugin""" + + @staticmethod + def write_provider_preset(provider_directory: Path, data: CMakeSyncData) -> None: + """Writes a provider preset from input sync data + + Args: + provider_directory: The base directory to place the preset files + data: The providers synchronization data + """ + configure_preset = ConfigurePreset(name=data.provider_name, cacheVariables=None) + presets = CMakePresets(configurePresets=[configure_preset]) + + json_path = provider_directory / f'{data.provider_name}.json' + + write_model_json(json_path, presets) + + @staticmethod + def write_cppython_preset( + cppython_preset_directory: Path, _provider_directory: Path, _provider_data: CMakeSyncData + ) -> Path: + """Write the cppython presets which inherit from the provider presets + + Args: + cppython_preset_directory: The tool directory + + Returns: + A file path to the written data + """ + configure_preset = ConfigurePreset(name='cppython', cacheVariables=None) + presets = CMakePresets(configurePresets=[configure_preset]) + + cppython_json_path = cppython_preset_directory / 'cppython.json' + + write_model_json(cppython_json_path, presets) + return cppython_json_path + + @staticmethod + def write_root_presets(preset_file: Path, _: Path) -> None: + """Read the top level json file and insert the include reference. + + Receives a relative path to the tool cmake json file + + Raises: + ConfigError: If key files do not exists + + Args: + preset_file: Preset file to modify + """ + initial_root_preset = read_json(preset_file) + + if (root_preset := deepcopy(initial_root_preset)) != initial_root_preset: + write_json(preset_file, root_preset) diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py new file mode 100644 index 0000000..0fbb419 --- /dev/null +++ b/cppython/plugins/cmake/plugin.py @@ -0,0 +1,73 @@ +"""The CMake generator implementation""" + +from pathlib import Path +from typing import Any + +from cppython.core.plugin_schema.generator import ( + Generator, + GeneratorPluginGroupData, + SupportedGeneratorFeatures, +) +from cppython.core.schema import CorePluginData, Information, SyncData +from cppython.plugins.cmake.builder import Builder +from cppython.plugins.cmake.resolution import resolve_cmake_data +from cppython.plugins.cmake.schema import CMakeSyncData + + +class CMakeGenerator(Generator): + """CMake generator""" + + def __init__(self, group_data: GeneratorPluginGroupData, core_data: CorePluginData, data: dict[str, Any]) -> None: + """Initializes the generator""" + self.group_data = group_data + self.core_data = core_data + self.data = resolve_cmake_data(data, core_data) + self.builder = Builder() + + @staticmethod + def features(_: Path) -> SupportedGeneratorFeatures: + """Queries if CMake is supported + + Returns: + Supported? + """ + return SupportedGeneratorFeatures() + + @staticmethod + def information() -> Information: + """Queries plugin info + + Returns: + Plugin information + """ + return Information() + + @staticmethod + def sync_types() -> list[type[SyncData]]: + """Returns types in order of preference + + Returns: + The available types + """ + return [CMakeSyncData] + + def sync(self, sync_data: SyncData) -> None: + """Disk sync point + + 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) + + self.builder.write_provider_preset(provider_directory, sync_data) + + cppython_preset_file = self.builder.write_cppython_preset( + cppython_preset_directory, provider_directory, sync_data + ) + + self.builder.write_root_presets(self.data.preset_file, cppython_preset_file) diff --git a/cppython/plugins/cmake/resolution.py b/cppython/plugins/cmake/resolution.py new file mode 100644 index 0000000..ddae351 --- /dev/null +++ b/cppython/plugins/cmake/resolution.py @@ -0,0 +1,27 @@ +"""Builder to help resolve cmake state""" + +from typing import Any + +from cppython.core.schema import CorePluginData +from cppython.plugins.cmake.schema import CMakeConfiguration, CMakeData + + +def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMakeData: + """Resolves the input data table from defaults to requirements + + Args: + data: The input table + core_data: The core data to help with the resolve + + Returns: + The resolved data + """ + parsed_data = CMakeConfiguration(**data) + + root_directory = core_data.project_data.pyproject_file.parent.absolute() + + modified_preset = parsed_data.preset_file + if not modified_preset.is_absolute(): + modified_preset = root_directory / modified_preset + + return CMakeData(preset_file=modified_preset, configuration_name=parsed_data.configuration_name) diff --git a/cppython/plugins/cmake/schema.py b/cppython/plugins/cmake/schema.py new file mode 100644 index 0000000..2e7cabb --- /dev/null +++ b/cppython/plugins/cmake/schema.py @@ -0,0 +1,74 @@ +"""CMake data definitions""" + +from enum import Enum, auto +from pathlib import Path +from typing import Annotated + +from pydantic import Field +from pydantic.types import FilePath + +from cppython.core.schema import CPPythonModel, SyncData + + +class VariableType(Enum): + """_summary_ + + Args: + Enum: _description_ + """ + + BOOL = (auto(),) # Boolean ON/OFF value. + PATH = (auto(),) # Path to a directory. + FILEPATH = (auto(),) # Path to a file. + STRING = (auto(),) # Generic string value. + INTERNAL = (auto(),) # Do not present in GUI at all. + STATIC = (auto(),) # Value managed by CMake, do not change. + UNINITIALIZED = auto() # Type not yet specified. + + +class CacheVariable(CPPythonModel, extra='forbid'): + """_summary_""" + + type: None | VariableType + value: bool | str + + +class ConfigurePreset(CPPythonModel, extra='allow'): + """Partial Configure Preset specification to allow cache variable injection""" + + name: str + cacheVariables: dict[str, None | bool | str | CacheVariable] | None + + +class CMakePresets(CPPythonModel, extra='allow'): + """The schema for the CMakePresets and CMakeUserPresets files. + + The only information needed is the configure preset list for cache variable injection + """ + + configurePresets: Annotated[list[ConfigurePreset], Field(description='The list of configure presets')] = [] + + +class CMakeSyncData(SyncData): + """The CMake sync data""" + + top_level_includes: FilePath + + +class CMakeData(CPPythonModel): + """Resolved CMake data""" + + preset_file: FilePath + configuration_name: str + + +class CMakeConfiguration(CPPythonModel): + """Configuration""" + + preset_file: Annotated[ + FilePath, + Field( + description="The CMakePreset.json file that 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')] diff --git a/cppython/plugins/git/__init__.py b/cppython/plugins/git/__init__.py new file mode 100644 index 0000000..557ca36 --- /dev/null +++ b/cppython/plugins/git/__init__.py @@ -0,0 +1,6 @@ +"""The Git SCM plugin for CPPython. + +This module implements the Git SCM plugin, which provides version control +functionality using Git. It includes features for extracting repository +information, handling version metadata, and managing project descriptions. +""" diff --git a/cppython/plugins/git/plugin.py b/cppython/plugins/git/plugin.py new file mode 100644 index 0000000..5bb6d45 --- /dev/null +++ b/cppython/plugins/git/plugin.py @@ -0,0 +1,62 @@ +"""Git SCM plugin""" + +from pathlib import Path + +from dulwich.errors import NotGitRepository +from dulwich.repo import Repo + +from cppython.core.plugin_schema.scm import ( + SCM, + SCMPluginGroupData, + SupportedSCMFeatures, +) +from cppython.core.schema import Information + + +class GitSCM(SCM): + """Git implementation hooks""" + + def __init__(self, group_data: SCMPluginGroupData) -> None: + """Initializes the plugin""" + self.group_data = group_data + + @staticmethod + def features(directory: Path) -> SupportedSCMFeatures: + """Broadcasts the shared features of the SCM plugin to CPPython + + Args: + directory: The root directory where features are evaluated + + Returns: + The supported features + """ + is_repository = True + try: + Repo(str(directory)) + except NotGitRepository: + is_repository = False + + return SupportedSCMFeatures(repository=is_repository) + + @staticmethod + def information() -> Information: + """Extracts the system's version metadata + + Returns: + A version + """ + return Information() + + @staticmethod + def version(_: Path) -> str: + """Extracts the system's version metadata + + Returns: + The git version + """ + return '' + + @staticmethod + def description() -> str | None: + """Requests extraction of the project description""" + return None diff --git a/cppython/plugins/pdm/__init__.py b/cppython/plugins/pdm/__init__.py new file mode 100644 index 0000000..2db3d2b --- /dev/null +++ b/cppython/plugins/pdm/__init__.py @@ -0,0 +1,6 @@ +"""The PDM interface plugin for CPPython. + +This module implements the PDM interface plugin, which integrates CPPython with +the PDM tool. It includes functionality for handling post-install actions, +writing configuration data, and managing project-specific settings. +""" diff --git a/cppython/plugins/pdm/plugin.py b/cppython/plugins/pdm/plugin.py new file mode 100644 index 0000000..eca64fe --- /dev/null +++ b/cppython/plugins/pdm/plugin.py @@ -0,0 +1,55 @@ +"""Implementation of the PDM Interface Plugin""" + +from logging import getLogger +from typing import Any + +from pdm.core import Core +from pdm.project.core import Project +from pdm.signals import post_install + +from cppython.core.schema import Interface, ProjectConfiguration +from cppython.project import Project as CPPythonProject + + +class CPPythonPlugin(Interface): + """Implementation of the PDM Interface Plugin""" + + def __init__(self, _: Core) -> None: + """Initializes the plugin""" + post_install.connect(self.on_post_install, weak=False) + self.logger = getLogger('cppython.interface.pdm') + + def write_pyproject(self) -> None: + """Write to file""" + + def write_configuration(self) -> None: + """Write to configuration""" + + def on_post_install(self, project: Project, dry_run: bool, **_kwargs: Any) -> None: + """Called after a pdm install command is called + + Args: + project: The input PDM project + dry_run: If true, won't perform any actions + _kwargs: Sink for unknown arguments + """ + pyproject_file = project.root.absolute() / project.PYPROJECT_FILENAME + + # 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 + ) + + self.logger.info("CPPython: Entered 'on_post_install'") + + if (pdm_pyproject := project.pyproject.read()) is None: + self.logger.info('CPPython: Project data was not available') + return + + cppython_project = CPPythonProject(project_configuration, self, pdm_pyproject) + + if not dry_run: + cppython_project.install() diff --git a/cppython/plugins/vcpkg/__init__.py b/cppython/plugins/vcpkg/__init__.py new file mode 100644 index 0000000..4758507 --- /dev/null +++ b/cppython/plugins/vcpkg/__init__.py @@ -0,0 +1,7 @@ +"""The vcpkg provider plugin for CPPython. + +This module implements the vcpkg provider plugin, which manages C++ dependencies +using the vcpkg package manager. It includes functionality for resolving +configuration data, generating manifests, and handling installation and updates +of dependencies. +""" diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py new file mode 100644 index 0000000..f5464ab --- /dev/null +++ b/cppython/plugins/vcpkg/plugin.py @@ -0,0 +1,233 @@ +"""The vcpkg provider implementation""" + +import json +from logging import getLogger +from os import name as system_name +from pathlib import Path, PosixPath, WindowsPath +from typing import Any + +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.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.utility import TypeName + + +class VcpkgProvider(Provider): + """vcpkg Provider""" + + 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: VcpkgData = resolve_vcpkg_data(configuration_data, core_data) + + @staticmethod + def features(directory: Path) -> SupportedProviderFeatures: + """Queries vcpkg support + + Args: + directory: The directory to query + + Returns: + Supported features + """ + return SupportedProviderFeatures() + + @staticmethod + def supported_sync_type(sync_type: type[SyncData]) -> bool: + """_summary_ + + Args: + sync_type: _description_ + + Returns: + _description_ + """ + return sync_type in CMakeGenerator.sync_types() + + @staticmethod + def information() -> Information: + """Returns plugin information + + Returns: + Plugin information + """ + return Information() + + @classmethod + def _update_provider(cls, path: Path) -> None: + """Calls the vcpkg tool install script + + Args: + path: The path where the script is located + """ + logger = getLogger('cppython.vcpkg') + + try: + if system_name == 'nt': + subprocess_call([str(WindowsPath('bootstrap-vcpkg.bat'))], logger=logger, cwd=path, shell=True) + elif system_name == 'posix': + subprocess_call(['./' + str(PosixPath('bootstrap-vcpkg.sh'))], 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: + """Gathers a data object for the given generator + + Args: + consumer: The input consumer + + Raises: + NotSupportedError: If not supported + + Returns: + The synch data object + """ + 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')) + + raise NotSupportedError('OOF') + + @classmethod + def tooling_downloaded(cls, path: Path) -> bool: + """Returns whether the provider tooling needs to be downloaded + + Args: + path: The directory to check for downloaded tooling + + Raises: + ProcessError: Failed vcpkg calls + + Returns: + Whether the tooling has been downloaded or not + """ + logger = getLogger('cppython.vcpkg') + + try: + # Hide output, given an error output is a logic conditional + subprocess_call( + ['git', 'rev-parse', '--is-inside-work-tree'], + logger=logger, + suppress=True, + cwd=path, + ) + + except ProcessError: + return False + + return True + + @classmethod + async def download_tooling(cls, directory: Path) -> None: + """Installs the external tooling required by the provider + + Args: + directory: The directory to download any extra tooling to + + Raises: + ProcessError: Failed vcpkg calls + """ + logger = getLogger('cppython.vcpkg') + + if cls.tooling_downloaded(directory): + try: + 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) + except ProcessError: + logger.exception('Unable to update the vcpkg repository') + raise + else: + try: + logger.debug("Cloning the vcpkg repository to '%s'", directory.absolute()) + + # The entire history is need for vcpkg 'baseline' information + subprocess_call( + ['git', 'clone', 'https://github.com/microsoft/vcpkg', '.'], + logger=logger, + cwd=directory, + ) + + except ProcessError: + logger.exception('Unable to clone the vcpkg repository') + raise + + cls._update_provider(directory) + + def install(self) -> None: + """Called when dependencies need to be installed from a lock file. + + Raises: + ProcessError: Failed vcpkg calls + """ + manifest_directory = self.core_data.project_data.pyproject_file.parent + manifest = generate_manifest(self.core_data, self.data) + + # Write out the manifest + serialized = json.loads(manifest.model_dump_json(exclude_none=True, by_alias=True)) + with open(manifest_directory / 'vcpkg.json', 'w', encoding='utf8') as file: + json.dump(serialized, file, ensure_ascii=False, indent=4) + + executable = self.core_data.cppython_data.install_path / 'vcpkg' + logger = getLogger('cppython.vcpkg') + try: + subprocess_call( + [ + executable, + 'install', + f'--x-install-root={self.data.install_directory}', + ], + logger=logger, + cwd=self.core_data.cppython_data.build_path, + ) + except ProcessError: + logger.exception('Unable to install project dependencies') + raise + + def update(self) -> None: + """Called when dependencies need to be updated and written to the lock file. + + Raises: + ProcessError: Failed vcpkg calls + """ + manifest_directory = self.core_data.project_data.pyproject_file.parent + manifest = generate_manifest(self.core_data, self.data) + + # Write out the manifest + serialized = json.loads(manifest.model_dump_json(exclude_none=True, by_alias=True)) + with open(manifest_directory / 'vcpkg.json', 'w', encoding='utf8') as file: + json.dump(serialized, file, ensure_ascii=False, indent=4) + + executable = self.core_data.cppython_data.install_path / 'vcpkg' + logger = getLogger('cppython.vcpkg') + try: + subprocess_call( + [ + executable, + 'install', + f'--x-install-root={self.data.install_directory}', + ], + logger=logger, + cwd=self.core_data.cppython_data.build_path, + ) + except ProcessError: + logger.exception('Unable to install project dependencies') + raise diff --git a/cppython/plugins/vcpkg/resolution.py b/cppython/plugins/vcpkg/resolution.py new file mode 100644 index 0000000..61229c8 --- /dev/null +++ b/cppython/plugins/vcpkg/resolution.py @@ -0,0 +1,64 @@ +"""Builder to help build vcpkg state""" + +from typing import Any + +from cppython.core.schema import CorePluginData +from cppython.plugins.vcpkg.schema import ( + Manifest, + VcpkgConfiguration, + VcpkgData, + VcpkgDependency, +) + + +def generate_manifest(core_data: CorePluginData, data: VcpkgData) -> Manifest: + """From the input configuration data, construct a Vcpkg specific Manifest type + + Args: + core_data: The core data to help with the resolve + data: Converted vcpkg data + + Returns: + The manifest + """ + manifest = { + 'name': core_data.pep621_data.name, + 'version_string': core_data.pep621_data.version, + 'dependencies': data.dependencies, + } + + return Manifest(**manifest) + + +def resolve_vcpkg_data(data: dict[str, Any], core_data: CorePluginData) -> VcpkgData: + """Resolves the input data table from defaults to requirements + + Args: + data: The input table + core_data: The core data to help with the resolve + + Returns: + The resolved data + """ + parsed_data = VcpkgConfiguration(**data) + + root_directory = core_data.project_data.pyproject_file.parent.absolute() + + modified_install_directory = parsed_data.install_directory + + # Add the project location to all relative paths + if not modified_install_directory.is_absolute(): + modified_install_directory = root_directory / modified_install_directory + + # Create directories + 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) + + return VcpkgData( + install_directory=modified_install_directory, + dependencies=vcpkg_dependencies, + ) diff --git a/cppython/plugins/vcpkg/schema.py b/cppython/plugins/vcpkg/schema.py new file mode 100644 index 0000000..ef34113 --- /dev/null +++ b/cppython/plugins/vcpkg/schema.py @@ -0,0 +1,49 @@ +"""Definitions for the plugin""" + +from pathlib import Path +from typing import Annotated + +from pydantic import Field, HttpUrl +from pydantic.types import DirectoryPath + +from cppython.core.schema import CPPythonModel + + +class VcpkgDependency(CPPythonModel): + """Vcpkg dependency type""" + + name: str + + +class VcpkgData(CPPythonModel): + """Resolved vcpkg data""" + + install_directory: DirectoryPath + dependencies: list[VcpkgDependency] + + +class VcpkgConfiguration(CPPythonModel): + """vcpkg provider data""" + + install_directory: Annotated[ + Path, + Field( + alias='install-directory', + description='The referenced dependencies defined by the local vcpkg.json manifest file', + ), + ] = Path('build') + + dependencies: Annotated[ + list[VcpkgDependency], Field(description='The directory to store the manifest file, vcpkg.json') + ] = [] + + +class Manifest(CPPythonModel): + """The manifest schema""" + + name: Annotated[str, Field(description='The project name')] + + version_string: Annotated[str, Field(alias='version-string', description='The arbitrary version string')] = '' + + homepage: Annotated[HttpUrl | None, Field(description='Homepage URL')] = None + dependencies: Annotated[list[VcpkgDependency], Field(description='List of dependencies')] = [] diff --git a/cppython/project.py b/cppython/project.py index f4e7d3a..40c871f 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -4,11 +4,10 @@ import logging from typing import Any -from cppython_core.exceptions import ConfigException -from cppython_core.resolution import resolve_model -from cppython_core.schema import Interface, ProjectConfiguration, PyProject - from cppython.builder import Builder +from cppython.core.exception import ConfigException +from cppython.core.resolution import resolve_model +from cppython.core.schema import Interface, ProjectConfiguration, PyProject from cppython.schema import API @@ -18,13 +17,14 @@ class Project(API): def __init__( self, project_configuration: ProjectConfiguration, interface: Interface, pyproject_data: dict[str, Any] ) -> None: + """Initializes the project""" self._enabled = False self._interface = interface - self.logger = logging.getLogger("cppython") + self.logger = logging.getLogger('cppython') builder = Builder(project_configuration, self.logger) - self.logger.info("Initializing project") + self.logger.info('Initializing project') try: pyproject = resolve_model(PyProject, pyproject_data) @@ -40,7 +40,7 @@ def __init__( self._enabled = True - self.logger.info("Initialized project successfully") + self.logger.info('Initialized project successfully') @property def enabled(self) -> bool: @@ -58,19 +58,19 @@ def install(self) -> None: Exception: Raised if failed """ if not self._enabled: - self.logger.info("Skipping install because the project is not enabled") + self.logger.info('Skipping install because the project is not enabled') return - self.logger.info("Installing tools") + self.logger.info('Installing tools') asyncio.run(self._data.download_provider_tools()) - self.logger.info("Installing project") - self.logger.info("Installing %s provider", self._data.plugins.provider.name()) + self.logger.info('Installing project') + self.logger.info('Installing %s provider', self._data.plugins.provider.name()) try: self._data.plugins.provider.install() except Exception as exception: - self.logger.error("Provider %s failed to install", self._data.plugins.provider.name()) + self.logger.error('Provider %s failed to install', self._data.plugins.provider.name()) raise exception self._data.sync() @@ -82,19 +82,19 @@ def update(self) -> None: Exception: Raised if failed """ if not self._enabled: - self.logger.info("Skipping update because the project is not enabled") + self.logger.info('Skipping update because the project is not enabled') return - self.logger.info("Updating tools") + self.logger.info('Updating tools') asyncio.run(self._data.download_provider_tools()) - self.logger.info("Updating project") - self.logger.info("Updating %s provider", self._data.plugins.provider.name()) + self.logger.info('Updating project') + self.logger.info('Updating %s provider', self._data.plugins.provider.name()) try: self._data.plugins.provider.update() except Exception as exception: - self.logger.error("Provider %s failed to update", self._data.plugins.provider.name()) + self.logger.error('Provider %s failed to update', self._data.plugins.provider.name()) raise exception self._data.sync() diff --git a/cppython/schema.py b/cppython/schema.py index 2f43a0c..83380a9 100644 --- a/cppython/schema.py +++ b/cppython/schema.py @@ -15,5 +15,4 @@ def install(self) -> None: @abstractmethod def update(self) -> None: """Updates project dependencies""" - raise NotImplementedError() diff --git a/cppython/test/__init__.py b/cppython/test/__init__.py new file mode 100644 index 0000000..bbf00b9 --- /dev/null +++ b/cppython/test/__init__.py @@ -0,0 +1,7 @@ +"""Testing utilities for the CPPython project. + +This module provides various utilities and mock implementations to facilitate +the testing of CPPython plugins and core functionalities. It includes shared +test types, fixtures, and mock classes that simulate real-world scenarios and +edge cases. +""" diff --git a/cppython/test/mock/__init__.py b/cppython/test/mock/__init__.py new file mode 100644 index 0000000..b7a7809 --- /dev/null +++ b/cppython/test/mock/__init__.py @@ -0,0 +1,7 @@ +"""Mock implementations for testing CPPython plugins. + +This module provides mock implementations of various CPPython plugin interfaces, +enabling comprehensive testing of plugin behavior. The mocks include providers, +generators, and SCMs, each designed to simulate real-world scenarios and edge +cases. +""" diff --git a/cppython/test/mock/generator.py b/cppython/test/mock/generator.py new file mode 100644 index 0000000..e392ac3 --- /dev/null +++ b/cppython/test/mock/generator.py @@ -0,0 +1,62 @@ +"""Shared definitions for testing.""" + +from typing import Any + +from pydantic import DirectoryPath + +from cppython.core.plugin_schema.generator import ( + Generator, + GeneratorPluginGroupData, + SupportedGeneratorFeatures, +) +from cppython.core.schema import CorePluginData, CPPythonModel, Information, SyncData + + +class MockSyncData(SyncData): + """A Mock data type""" + + +class MockGeneratorData(CPPythonModel): + """Dummy data""" + + +class MockGenerator(Generator): + """A mock generator class for behavior testing""" + + def __init__( + self, group_data: GeneratorPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any] + ) -> None: + """Initializes the mock generator""" + self.group_data = group_data + self.core_data = core_data + self.configuration_data = MockGeneratorData(**configuration_data) + + @staticmethod + def features(_: DirectoryPath) -> SupportedGeneratorFeatures: + """Broadcasts the shared features of the generator plugin to CPPython + + Returns: + The supported features + """ + return SupportedGeneratorFeatures() + + @staticmethod + def information() -> Information: + """Returns plugin information + + Returns: + The plugin information + """ + return Information() + + @staticmethod + def sync_types() -> list[type[SyncData]]: + """_summary_ + + Returns: + _description_ + """ + return [MockSyncData] + + def sync(self, _: SyncData) -> None: + """Synchronizes generator files and state with the providers input""" diff --git a/cppython/test/mock/interface.py b/cppython/test/mock/interface.py new file mode 100644 index 0000000..87379b4 --- /dev/null +++ b/cppython/test/mock/interface.py @@ -0,0 +1,13 @@ +"""Mock interface definitions""" + +from cppython.core.schema import Interface + + +class MockInterface(Interface): + """A mock interface class for behavior testing""" + + def write_pyproject(self) -> None: + """Implementation of Interface function""" + + def write_configuration(self) -> None: + """Implementation of Interface function""" diff --git a/cppython/test/mock/provider.py b/cppython/test/mock/provider.py new file mode 100644 index 0000000..bfc0ef7 --- /dev/null +++ b/cppython/test/mock/provider.py @@ -0,0 +1,92 @@ +"""Mock provider definitions""" + +from typing import Any + +from pydantic import DirectoryPath + +from cppython.core.plugin_schema.generator import SyncConsumer +from cppython.core.plugin_schema.provider import ( + Provider, + ProviderPluginGroupData, + SupportedProviderFeatures, +) +from cppython.core.schema import CorePluginData, CPPythonModel, Information, SyncData +from cppython.test.mock.generator import MockSyncData + + +class MockProviderData(CPPythonModel): + """Dummy data""" + + +class MockProvider(Provider): + """A mock provider class for behavior testing""" + + downloaded: DirectoryPath | None = None + + def __init__( + self, group_data: ProviderPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any] + ) -> None: + """Initializes the mock provider""" + self.group_data = group_data + self.core_data = core_data + self.configuration_data = MockProviderData(**configuration_data) + + @staticmethod + def features(_: DirectoryPath) -> SupportedProviderFeatures: + """Broadcasts the shared features of the Provider plugin to CPPython + + Returns: + The supported features + """ + return SupportedProviderFeatures() + + @staticmethod + def information() -> Information: + """Returns plugin information + + Returns: + The plugin information + """ + return Information() + + @staticmethod + def supported_sync_type(sync_type: type[SyncData]) -> bool: + """Broadcasts supported types + + Args: + sync_type: The input type + + Returns: + Support + """ + return sync_type == MockSyncData + + def sync_data(self, consumer: SyncConsumer) -> SyncData | None: + """Gathers synchronization data + + Args: + consumer: The input consumer + + Returns: + The sync data object + """ + # This is a mock class, so any generator sync type is OK + for sync_type in consumer.sync_types(): + match sync_type: + case underlying_type if underlying_type is MockSyncData: + return MockSyncData(provider_name=self.name()) + + return None + + @classmethod + async def download_tooling(cls, directory: DirectoryPath) -> None: + """Downloads the provider tooling""" + cls.downloaded = directory + + def install(self) -> None: + """Installs the provider""" + pass + + def update(self) -> None: + """Updates the provider""" + pass diff --git a/cppython/test/mock/scm.py b/cppython/test/mock/scm.py new file mode 100644 index 0000000..9058406 --- /dev/null +++ b/cppython/test/mock/scm.py @@ -0,0 +1,45 @@ +"""Mock SCM definitions""" + +from pydantic import DirectoryPath + +from cppython.core.plugin_schema.scm import ( + SCM, + SCMPluginGroupData, + SupportedSCMFeatures, +) +from cppython.core.schema import Information + + +class MockSCM(SCM): + """A mock generator class for behavior testing""" + + def __init__(self, group_data: SCMPluginGroupData) -> None: + """Initializes the mock generator""" + self.group_data = group_data + + @staticmethod + def features(_: DirectoryPath) -> SupportedSCMFeatures: + """Broadcasts the shared features of the SCM plugin to CPPython + + Returns: + The supported features + """ + return SupportedSCMFeatures(repository=True) + + @staticmethod + def information() -> Information: + """Returns plugin information + + Returns: + The plugin information + """ + return Information() + + @staticmethod + def version(_: DirectoryPath) -> str: + """Extracts the system's version metadata + + Returns: + A version + """ + return '1.0.0' diff --git a/cppython/test/pytest/__init__.py b/cppython/test/pytest/__init__.py new file mode 100644 index 0000000..a312d6a --- /dev/null +++ b/cppython/test/pytest/__init__.py @@ -0,0 +1,7 @@ +"""Test harness for CPPython plugins using pytest. + +This module provides a test harness for CPPython plugins, enabling them to be +tested using pytest. It includes shared test types, fixtures, and utilities +that facilitate the testing of plugin interfaces, project configurations, and +plugin-specific features. +""" diff --git a/cppython/test/pytest/shared.py b/cppython/test/pytest/shared.py new file mode 100644 index 0000000..3363357 --- /dev/null +++ b/cppython/test/pytest/shared.py @@ -0,0 +1,480 @@ +"""Composable test types""" + +from abc import ABCMeta, abstractmethod +from importlib.metadata import entry_points +from typing import Any, LiteralString, cast + +import pytest + +from cppython.core.plugin_schema.generator import Generator, GeneratorPluginGroupData +from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData +from cppython.core.plugin_schema.scm import SCM, SCMPluginGroupData +from cppython.core.resolution import ( + resolve_cppython_plugin, + resolve_generator, + resolve_provider, + resolve_scm, +) +from cppython.core.schema import ( + CorePluginData, + CPPythonData, + CPPythonPluginData, + DataPlugin, + DataPluginGroupData, + PEP621Data, + Plugin, + PluginGroupData, + ProjectConfiguration, + ProjectData, +) +from cppython.test.pytest.variants import ( + generator_variants, + provider_variants, + scm_variants, +) + + +class BaseTests[T: Plugin](metaclass=ABCMeta): + """Shared testing information for all plugin test classes.""" + + @abstractmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type(self) -> type[T]: + """A required testing hook that allows type generation""" + raise NotImplementedError('Override this fixture') + + @staticmethod + @pytest.fixture( + name='cppython_plugin_data', + scope='session', + ) + def fixture_cppython_plugin_data(cppython_data: CPPythonData, plugin_type: type[T]) -> CPPythonPluginData: + """Fixture for created the plugin CPPython table + + Args: + cppython_data: The CPPython table to help the resolve + plugin_type: The data plugin type + + Returns: + The plugin specific CPPython table information + """ + return resolve_cppython_plugin(cppython_data, plugin_type) + + @staticmethod + @pytest.fixture( + name='core_plugin_data', + scope='session', + ) + def fixture_core_plugin_data( + cppython_plugin_data: CPPythonPluginData, project_data: ProjectData, pep621_data: PEP621Data + ) -> CorePluginData: + """Fixture for creating the wrapper CoreData type + + Args: + cppython_plugin_data: CPPython data + project_data: The project data + pep621_data: Project table data + + Returns: + Wrapper Core Type + """ + return CorePluginData(cppython_data=cppython_plugin_data, project_data=project_data, pep621_data=pep621_data) + + @staticmethod + @pytest.fixture(name='plugin_group_name', scope='session') + def fixture_plugin_group_name() -> LiteralString: + """A required testing hook that allows plugin group name generation + + Returns: + The plugin group name + """ + return 'cppython' + + +class BaseIntegrationTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): + """Integration testing information for all plugin test classes""" + + @staticmethod + def test_entry_point(plugin_type: type[T], plugin_group_name: LiteralString) -> None: + """Verify that the plugin was registered + + Args: + plugin_type: The type to register + plugin_group_name: The group name for the plugin type + """ + # We only require the entry point to be registered if the plugin is not a Mocked type + if plugin_type.name() == 'mock': + pytest.skip('Mocked plugin type') + + types = [] + for entry in list(entry_points(group=f'{plugin_group_name}.{plugin_type.group()}')): + types.append(entry.load()) + + assert plugin_type in types + + @staticmethod + def test_name(plugin_type: type[Plugin]) -> None: + """Verifies the the class name allows name extraction + + Args: + plugin_type: The type to register + """ + assert plugin_type.group() + assert len(plugin_type.group()) + + assert plugin_type.name() + assert len(plugin_type.name()) + + +class BaseUnitTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): + """Unit testing information for all plugin test classes""" + + @staticmethod + def test_feature_extraction(plugin_type: type[T], project_configuration: ProjectConfiguration) -> None: + """Test the feature extraction of a plugin. + + This method tests the feature extraction functionality of a plugin by asserting that the features + returned by the plugin are correct for the given project configuration. + + Args: + plugin_type: The type of plugin to test. + project_configuration: The project configuration to use for testing. + """ + assert plugin_type.features(project_configuration.pyproject_file.parent) + + @staticmethod + def test_information(plugin_type: type[T]) -> None: + """Test the information method of a plugin. + + This method asserts that the `information` method of the given plugin type returns a value. + + Args: + plugin_type: The type of the plugin to test. + """ + assert plugin_type.information() + + +class PluginTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): + """Testing information for basic plugin test classes.""" + + @staticmethod + @pytest.fixture( + name='plugin', + scope='session', + ) + def fixture_plugin( + plugin_type: type[T], + plugin_group_data: PluginGroupData, + ) -> T: + """Overridden plugin generator for creating a populated data plugin type + + Args: + plugin_type: Plugin type + plugin_group_data: The data group configuration + + Returns: + A newly constructed provider + """ + plugin = plugin_type(plugin_group_data) + + return plugin + + +class PluginIntegrationTests[T: Plugin](BaseIntegrationTests[T], metaclass=ABCMeta): + """Integration testing information for basic plugin test classes""" + + +class PluginUnitTests[T: Plugin](BaseUnitTests[T], metaclass=ABCMeta): + """Unit testing information for basic plugin test classes""" + + +class DataPluginTests[T: DataPlugin](BaseTests[T], metaclass=ABCMeta): + """Shared testing information for all data plugin test classes.""" + + @staticmethod + @pytest.fixture( + name='plugin', + scope='session', + ) + def fixture_plugin( + plugin_type: type[T], + plugin_group_data: DataPluginGroupData, + core_plugin_data: CorePluginData, + plugin_data: dict[str, Any], + ) -> T: + """Overridden plugin generator for creating a populated data plugin type + + Args: + plugin_type: Plugin type + plugin_group_data: The data group configuration + core_plugin_data: The core metadata + plugin_data: The data table + + Returns: + A newly constructed provider + """ + plugin = plugin_type(plugin_group_data, core_plugin_data, plugin_data) + + return plugin + + +class DataPluginIntegrationTests[T: DataPlugin](BaseIntegrationTests[T], metaclass=ABCMeta): + """Integration testing information for all data plugin test classes""" + + +class DataPluginUnitTests[T: DataPlugin](BaseUnitTests[T], metaclass=ABCMeta): + """Unit testing information for all data plugin test classes""" + + +class ProviderTests[T: Provider](DataPluginTests[T], metaclass=ABCMeta): + """Shared functionality between the different Provider testing categories""" + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[ProviderPluginGroupData]: + """A required testing hook that allows plugin configuration data generation + + Returns: + The configuration type + """ + return ProviderPluginGroupData + + @staticmethod + @pytest.fixture(name='plugin_group_data', scope='session') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> ProviderPluginGroupData: + """Generates plugin configuration data generation from environment configuration + + Args: + project_data: The project data fixture + cppython_plugin_data:The plugin configuration fixture + + Returns: + The plugin configuration + """ + return resolve_provider(project_data=project_data, cppython_data=cppython_plugin_data) + + @staticmethod + @pytest.fixture( + name='provider_type', + scope='session', + params=provider_variants, + ) + def fixture_provider_type(plugin_type: type[T]) -> type[T]: + """Fixture defining all testable variations mock Providers + + Args: + plugin_type: Plugin type + + Returns: + Variation of a Provider + """ + return plugin_type + + @staticmethod + @pytest.fixture( + name='generator_type', + scope='session', + params=generator_variants, + ) + def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: + """Fixture defining all testable variations mock Generator + + Args: + request: Parameterization list + + Returns: + Variation of a Generator + """ + generator_type = cast(type[Generator], request.param) + + return generator_type + + @staticmethod + @pytest.fixture( + name='scm_type', + scope='session', + params=scm_variants, + ) + def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: + """Fixture defining all testable variations mock Generator + + Args: + request: Parameterization list + + Returns: + Variation of a Generator + """ + scm_type = cast(type[SCM], request.param) + + return scm_type + + +class GeneratorTests[T: Generator](DataPluginTests[T], metaclass=ABCMeta): + """Shared functionality between the different Generator testing categories""" + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[GeneratorPluginGroupData]: + """A required testing hook that allows plugin configuration data generation + + Returns: + The configuration type + """ + return GeneratorPluginGroupData + + @staticmethod + @pytest.fixture(name='plugin_group_data', scope='session') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> GeneratorPluginGroupData: + """Generates plugin configuration data generation from environment configuration + + Args: + project_data: The project data fixture + cppython_plugin_data:The plugin configuration fixture + + Returns: + The plugin configuration + """ + return resolve_generator(project_data=project_data, cppython_data=cppython_plugin_data) + + @staticmethod + @pytest.fixture( + name='provider_type', + scope='session', + params=provider_variants, + ) + def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: + """Fixture defining all testable variations mock Providers + + Args: + request: Parameterization list + + Returns: + Variation of a Provider + """ + provider_type = cast(type[Provider], request.param) + + return provider_type + + @staticmethod + @pytest.fixture( + name='generator_type', + scope='session', + ) + def fixture_generator_type(plugin_type: type[T]) -> type[T]: + """Override + + Args: + plugin_type: Plugin type + + Returns: + Plugin type + """ + return plugin_type + + @staticmethod + @pytest.fixture( + name='scm_type', + scope='session', + params=scm_variants, + ) + def fixture_scm_type(request: pytest.FixtureRequest) -> type[SCM]: + """Fixture defining all testable variations mock Generator + + Args: + request: Parameterization list + + Returns: + Variation of a Generator + """ + scm_type = cast(type[SCM], request.param) + + return scm_type + + +class SCMTests[T: SCM](PluginTests[T], metaclass=ABCMeta): + """Shared functionality between the different SCM testing categories""" + + @staticmethod + @pytest.fixture(name='plugin_configuration_type', scope='session') + def fixture_plugin_configuration_type() -> type[SCMPluginGroupData]: + """A required testing hook that allows plugin configuration data generation + + Returns: + The configuration type + """ + return SCMPluginGroupData + + @staticmethod + @pytest.fixture(name='plugin_group_data', scope='session') + def fixture_plugin_group_data( + project_data: ProjectData, cppython_plugin_data: CPPythonPluginData + ) -> SCMPluginGroupData: + """Generates plugin configuration data generation from environment configuration + + Args: + project_data: The project data fixture + cppython_plugin_data:The plugin configuration fixture + + Returns: + The plugin configuration + """ + return resolve_scm(project_data=project_data, cppython_data=cppython_plugin_data) + + @staticmethod + @pytest.fixture( + name='provider_type', + scope='session', + params=provider_variants, + ) + def fixture_provider_type(request: pytest.FixtureRequest) -> type[Provider]: + """Fixture defining all testable variations mock Providers + + Args: + request: Parameterization list + + Returns: + Variation of a Provider + """ + provider_type = cast(type[Provider], request.param) + + return provider_type + + @staticmethod + @pytest.fixture( + name='generator_type', + scope='session', + params=generator_variants, + ) + def fixture_generator_type(request: pytest.FixtureRequest) -> type[Generator]: + """Fixture defining all testable variations mock Generator + + Args: + request: Parameterization list + + Returns: + Variation of a Generator + """ + generator_type = cast(type[Generator], request.param) + + return generator_type + + @staticmethod + @pytest.fixture( + name='scm_type', + scope='session', + params=scm_variants, + ) + def fixture_scm_type(plugin_type: type[T]) -> type[SCM]: + """Fixture defining all testable variations mock Generator + + Args: + plugin_type: Parameterization list + + Returns: + Variation of a Generator + """ + return plugin_type diff --git a/cppython/test/pytest/tests.py b/cppython/test/pytest/tests.py new file mode 100644 index 0000000..dd70a62 --- /dev/null +++ b/cppython/test/pytest/tests.py @@ -0,0 +1,108 @@ +"""Types to inherit from""" + +import asyncio +from abc import ABCMeta +from pathlib import Path + +import pytest + +from cppython.core.plugin_schema.generator import Generator +from cppython.core.plugin_schema.provider import Provider +from cppython.core.plugin_schema.scm import SCM +from cppython.test.pytest.shared import ( + DataPluginIntegrationTests, + DataPluginUnitTests, + GeneratorTests, + PluginIntegrationTests, + PluginUnitTests, + ProviderTests, + SCMTests, +) +from cppython.utility.utility import canonicalize_type + + +class ProviderIntegrationTests[T: Provider](DataPluginIntegrationTests[T], ProviderTests[T], metaclass=ABCMeta): + """Base class for all provider integration tests that test plugin agnostic behavior""" + + @staticmethod + @pytest.fixture(autouse=True, scope='session') + def _fixture_install_dependency(plugin: T, install_path: Path) -> None: + """Forces the download to only happen once per test session""" + path = install_path / canonicalize_type(type(plugin)).name + path.mkdir(parents=True, exist_ok=True) + + asyncio.run(plugin.download_tooling(path)) + + @staticmethod + def test_install(plugin: T) -> None: + """Ensure that the vanilla install command functions + + Args: + plugin: A newly constructed provider + """ + plugin.install() + + @staticmethod + def test_update(plugin: T) -> None: + """Ensure that the vanilla update command functions + + Args: + plugin: A newly constructed provider + """ + plugin.update() + + @staticmethod + def test_group_name(plugin_type: type[T]) -> None: + """Verifies that the group name is the same as the plugin type + + Args: + plugin_type: The type to register + """ + assert canonicalize_type(plugin_type).group == 'provider' + + +class ProviderUnitTests[T: Provider](DataPluginUnitTests[T], ProviderTests[T], metaclass=ABCMeta): + """Base class for all provider unit tests that test plugin agnostic behavior. + + Custom implementations of the Provider class should inherit from this class for its tests. + """ + + +class GeneratorIntegrationTests[T: Generator](DataPluginIntegrationTests[T], GeneratorTests[T], metaclass=ABCMeta): + """Base class for all scm integration tests that test plugin agnostic behavior""" + + @staticmethod + def test_group_name(plugin_type: type[T]) -> None: + """Verifies that the group name is the same as the plugin type + + Args: + plugin_type: The type to register + """ + assert canonicalize_type(plugin_type).group == 'generator' + + +class GeneratorUnitTests[T: Generator](DataPluginUnitTests[T], GeneratorTests[T], metaclass=ABCMeta): + """Base class for all Generator unit tests that test plugin agnostic behavior. + + Custom implementations of the Generator class should inherit from this class for its tests. + """ + + +class SCMIntegrationTests[T: SCM](PluginIntegrationTests[T], SCMTests[T], metaclass=ABCMeta): + """Base class for all generator integration tests that test plugin agnostic behavior""" + + @staticmethod + def test_group_name(plugin_type: type[T]) -> None: + """Verifies that the group name is the same as the plugin type + + Args: + plugin_type: The type to register + """ + assert canonicalize_type(plugin_type).group == 'scm' + + +class SCMUnitTests[T: SCM](PluginUnitTests[T], SCMTests[T], metaclass=ABCMeta): + """Base class for all Generator unit tests that test plugin agnostic behavior. + + Custom implementations of the Generator class should inherit from this class for its tests. + """ diff --git a/cppython/test/pytest/variants.py b/cppython/test/pytest/variants.py new file mode 100644 index 0000000..3c16992 --- /dev/null +++ b/cppython/test/pytest/variants.py @@ -0,0 +1,131 @@ +"""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/utility/__init__.py b/cppython/utility/__init__.py new file mode 100644 index 0000000..9aead6a --- /dev/null +++ b/cppython/utility/__init__.py @@ -0,0 +1,6 @@ +"""Utility functions for the CPPython project. + +This module contains various utility functions that assist with different +aspects of the CPPython project. The utilities include subprocess management, +exception handling, and type canonicalization. +""" diff --git a/cppython/utility/exception.py b/cppython/utility/exception.py new file mode 100644 index 0000000..2c43681 --- /dev/null +++ b/cppython/utility/exception.py @@ -0,0 +1,70 @@ +"""Exception definitions""" + + +class ProcessError(Exception): + """Raised when there is a configuration error""" + + def __init__(self, error: str) -> None: + """Initializes the error + + Args: + error: The error message + """ + self._error = error + + super().__init__(error) + + @property + def error(self) -> str: + """Returns the underlying error + + Returns: + str -- The underlying error + """ + return self._error + + +class PluginError(Exception): + """Raised when there is a plugin error""" + + def __init__(self, error: str) -> None: + """Initializes the error + + Args: + error: The error message + """ + self._error = error + + super().__init__(error) + + @property + def error(self) -> str: + """Returns the underlying error + + Returns: + str -- The underlying error + """ + return self._error + + +class NotSupportedError(Exception): + """Raised when something is not supported""" + + def __init__(self, error: str) -> None: + """Initializes the error + + Args: + error: The error message + """ + self._error = error + + super().__init__(error) + + @property + def error(self) -> str: + """Returns the underlying error + + Returns: + str -- The underlying error + """ + return self._error diff --git a/cppython/utility/plugin.py b/cppython/utility/plugin.py new file mode 100644 index 0000000..1c404c6 --- /dev/null +++ b/cppython/utility/plugin.py @@ -0,0 +1,36 @@ +"""Defines the base plugin type and related types.""" + +from typing import Protocol + +from cppython.utility.utility import TypeGroup, TypeID, TypeName, canonicalize_name + + +class Plugin(Protocol): + """A protocol for defining a plugin type""" + + @classmethod + def id(cls) -> TypeID: + """The type identifier for the plugin + + Returns: + The type identifier + """ + return canonicalize_name(cls.__name__) + + @classmethod + def name(cls) -> TypeName: + """The name of the plugin + + Returns: + The name + """ + return cls.id().name + + @classmethod + def group(cls) -> TypeGroup: + """The group of the plugin + + Returns: + The group + """ + return cls.id().group diff --git a/cppython/utility/py.typed b/cppython/utility/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/cppython/utility/subprocess.py b/cppython/utility/subprocess.py new file mode 100644 index 0000000..6cc0c72 --- /dev/null +++ b/cppython/utility/subprocess.py @@ -0,0 +1,40 @@ +"""Subprocess definitions""" + +import logging +import subprocess +from pathlib import Path +from typing import Any + +from cppython.utility.exception import ProcessError + + +def call( + arguments: list[str | Path], + logger: logging.Logger, + log_level: int = logging.WARNING, + suppress: bool = False, + **kwargs: Any, +) -> None: + """Executes a subprocess call with logger and utility attachments. Captures STDOUT and STDERR + + Args: + 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. + suppress: Mutes logging output. Defaults to False. + kwargs: Keyword arguments to pass to subprocess.Popen + + Raises: + ProcessError: If the underlying process fails + """ + with subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, **kwargs) as process: + if process.stdout is None: + return + + with process.stdout as pipe: + for line in iter(pipe.readline, ''): + if not suppress: + logger.log(log_level, line.rstrip()) + + if process.returncode != 0: + raise ProcessError('Subprocess task failed') diff --git a/cppython/utility/utility.py b/cppython/utility/utility.py new file mode 100644 index 0000000..dd76f57 --- /dev/null +++ b/cppython/utility/utility.py @@ -0,0 +1,44 @@ +"""Utility definitions""" + +import re +from typing import Any, NamedTuple, NewType + +TypeName = NewType('TypeName', str) +TypeGroup = NewType('TypeGroup', str) + + +class TypeID(NamedTuple): + """Represents a type ID with a name and group.""" + + name: TypeName + group: TypeGroup + + +_canonicalize_regex = re.compile(r'((?<=[a-z])[A-Z]|(? TypeID: + """Extracts the type identifier from an input string + + Args: + name: The string to parse + + Returns: + The type identifier + """ + sub = re.sub(_canonicalize_regex, r' \1', name) + values = sub.split(' ') + result = ''.join(values[:-1]) + return TypeID(TypeName(result.lower()), TypeGroup(values[-1].lower())) + + +def canonicalize_type(input_type: type[Any]) -> TypeID: + """Extracts the plugin identifier from a type + + Args: + input_type: The input type to resolve + + Returns: + The type identifier + """ + return canonicalize_name(input_type.__name__) diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 0000000..2212372 --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,5 @@ +name: cppython +version: 0.1.0 +title: CPPython +nav: + - modules/ROOT/nav.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc new file mode 100644 index 0000000..99cea0f --- /dev/null +++ b/docs/modules/ROOT/nav.adoc @@ -0,0 +1 @@ +* 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 new file mode 100644 index 0000000..e69de29 diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 0000000..2120a3c --- /dev/null +++ b/docs/modules/ROOT/pages/index.adoc @@ -0,0 +1 @@ += CPPython Website \ No newline at end of file diff --git a/docs/modules/tests/nav.adoc b/docs/modules/tests/nav.adoc new file mode 100644 index 0000000..b08b38f --- /dev/null +++ b/docs/modules/tests/nav.adoc @@ -0,0 +1 @@ +* 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 new file mode 100644 index 0000000..2f5f65d --- /dev/null +++ b/docs/modules/tests/pages/fixtures.adoc @@ -0,0 +1,14 @@ += Fixtures + +Fixtures can be accessed by installing the optional `pytest` module, `cppython.pytest`. + +== 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_` Fixture + +* Discovered from `tests/build/` +* The contents of the directory are copied to a temporary directory + diff --git a/pdm.lock b/pdm.lock index ef06942..6da657b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -1,432 +1,811 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "lint", "test"] -strategy = ["cross_platform"] -lock_version = "4.4.1" -content_hash = "sha256:017552fe0fbb7c28de2016f00448dcb31947be6084adb54e69b470ee2eed83ba" - -[[package]] -name = "annotated-types" -version = "0.6.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "astroid" -version = "3.1.0" -requires_python = ">=3.8.0" -summary = "An abstract syntax tree for Python with inference support." -files = [ - {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, - {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, -] - -[[package]] -name = "black" -version = "24.2.0" -requires_python = ">=3.8" -summary = "The uncompromising code formatter." -dependencies = [ - "click>=8.0.0", - "mypy-extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=0.9.0", - "platformdirs>=2", -] -files = [ - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, -] - -[[package]] -name = "click" -version = "8.1.7" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -dependencies = [ - "colorama; platform_system == \"Windows\"", -] -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.4.3" -requires_python = ">=3.8" -summary = "Code coverage measurement for Python" -files = [ - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, -] - -[[package]] -name = "coverage" -version = "7.4.3" -extras = ["toml"] -requires_python = ">=3.8" -summary = "Code coverage measurement for Python" -dependencies = [ - "coverage==7.4.3", -] -files = [ - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, -] - -[[package]] -name = "cppython-core" -version = "0.7.1.dev14" -requires_python = ">=3.12" -summary = "Data definitions for CPPython" -dependencies = [ - "pydantic>=2.6.3", - "synodic-utilities>=0.1.1.dev3", -] -files = [ - {file = "cppython_core-0.7.1.dev14-py3-none-any.whl", hash = "sha256:dc3f8a27b9f02ef3c11a113ed5f1d836c744170863cdaf62721b99ed65366307"}, - {file = "cppython_core-0.7.1.dev14.tar.gz", hash = "sha256:87673de19b7998480aceee5ef7a4bc112dfaf5c349a4d76b0ac547b8b9a925af"}, -] - -[[package]] -name = "dill" -version = "0.3.8" -requires_python = ">=3.8" -summary = "serialize all of Python" -files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" -summary = "brain-dead simple config-ini parsing" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "isort" -version = "5.13.2" -requires_python = ">=3.8.0" -summary = "A Python utility / library to sort Python imports." -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 = "mccabe" -version = "0.7.0" -requires_python = ">=3.6" -summary = "McCabe checker, plugin for flake8" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mypy" -version = "1.9.0" -requires_python = ">=3.8" -summary = "Optional static typing for Python" -dependencies = [ - "mypy-extensions>=1.0.0", - "typing-extensions>=4.1.0", -] -files = [ - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" -summary = "Type system extensions for programs checked with the mypy type checker." -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "packaging" -version = "24.0" -requires_python = ">=3.7" -summary = "Core utilities for Python packages" -files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -requires_python = ">=3.8" -summary = "Utility library for gitignore style pattern matching of file paths." -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.0" -requires_python = ">=3.8" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[[package]] -name = "pluggy" -version = "1.4.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" -files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, -] - -[[package]] -name = "pydantic" -version = "2.6.4" -requires_python = ">=3.8" -summary = "Data validation using Python type hints" -dependencies = [ - "annotated-types>=0.4.0", - "pydantic-core==2.16.3", - "typing-extensions>=4.6.1", -] -files = [ - {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, - {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, -] - -[[package]] -name = "pydantic-core" -version = "2.16.3" -requires_python = ">=3.8" -summary = "" -dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, -] - -[[package]] -name = "pylint" -version = "3.1.0" -requires_python = ">=3.8.0" -summary = "python code static checker" -dependencies = [ - "astroid<=3.2.0-dev0,>=3.1.0", - "colorama>=0.4.5; sys_platform == \"win32\"", - "dill>=0.3.6; python_version >= \"3.11\"", - "dill>=0.3.7; python_version >= \"3.12\"", - "isort!=5.13.0,<6,>=4.2.5", - "mccabe<0.8,>=0.6", - "platformdirs>=2.2.0", - "tomlkit>=0.10.1", -] -files = [ - {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, - {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, -] - -[[package]] -name = "pytest" -version = "8.1.1" -requires_python = ">=3.8" -summary = "pytest: simple powerful testing with Python" -dependencies = [ - "colorama; sys_platform == \"win32\"", - "iniconfig", - "packaging", - "pluggy<2.0,>=1.4", -] -files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, -] - -[[package]] -name = "pytest-click" -version = "1.1.0" -summary = "Pytest plugin for Click" -dependencies = [ - "click>=6.0", - "pytest>=5.0", -] -files = [ - {file = "pytest_click-1.1.0-py3-none-any.whl", hash = "sha256:eade4742c2f02c345e78a32534a43e8db04acf98d415090539dacc880b7cd0e9"}, - {file = "pytest_click-1.1.0.tar.gz", hash = "sha256:fdd9f6721f877dda021e7c5dc73e70aecd37e5ed23ec6820f8a7b3fd7b4f8d30"}, -] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -requires_python = ">=3.7" -summary = "Pytest plugin for measuring coverage." -dependencies = [ - "coverage[toml]>=5.2.1", - "pytest>=4.6", -] -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[[package]] -name = "pytest-cppython" -version = "0.3.1.dev50" -requires_python = ">=3.12" -summary = "A pytest plugin that imports CPPython testing types" -dependencies = [ - "cppython-core>=0.4.1.dev13", - "pydantic>=2.6.3", - "pytest-mock>=3.12.0", - "pytest-synodic>=0.0.0", - "pytest>=8.0.0", -] -files = [ - {file = "pytest_cppython-0.3.1.dev50-py3-none-any.whl", hash = "sha256:1b22cce6af81dbd6e6c1e6f8e15654ad6b0149c3d38e1312e8be36324b17f88a"}, - {file = "pytest_cppython-0.3.1.dev50.tar.gz", hash = "sha256:c39ff9ee013af4c7e8b500d95eeb4e8dabc33099a9a8a9ab15139e328d8782c2"}, -] - -[[package]] -name = "pytest-mock" -version = "3.12.0" -requires_python = ">=3.8" -summary = "Thin-wrapper around the mock package for easier use with pytest" -dependencies = [ - "pytest>=5.0", -] -files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, -] - -[[package]] -name = "pytest-synodic" -version = "0.1.1.dev6" -requires_python = ">=3.12" -summary = "Synodic Pytest utilities" -dependencies = [ - "pytest>=8.0.2", - "synodic-utilities>=0.1.1.dev5", -] -files = [ - {file = "pytest_synodic-0.1.1.dev6-py3-none-any.whl", hash = "sha256:9c4e0a450379c54bcb4d2f3875fe8db1ffe3c58ea90ce5a41df5e79fe3a01d46"}, - {file = "pytest_synodic-0.1.1.dev6.tar.gz", hash = "sha256:16739c0021a778dc69ea5d57b264118e68ef7cc5429312a1a389bd5c1a784239"}, -] - -[[package]] -name = "synodic-utilities" -version = "0.1.1.dev8" -requires_python = ">=3.12" -summary = "Synodic python utility library" -files = [ - {file = "synodic_utilities-0.1.1.dev8-py3-none-any.whl", hash = "sha256:3034f10f780629ef5eea2078fcf82875f7aedee3b0e3fe2c04759bac4a55662a"}, - {file = "synodic_utilities-0.1.1.dev8.tar.gz", hash = "sha256:e25a9dbd0cdd77bd555705ef3d13489591d43cd8bd93ac211cf9bead4e476c68"}, -] - -[[package]] -name = "tomlkit" -version = "0.12.4" -requires_python = ">=3.7" -summary = "Style preserving TOML library" -files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, -] - -[[package]] -name = "typing-extensions" -version = "4.10.0" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, -] +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "git", "lint", "pdm", "pytest", "test"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:56348598f9d12ee9b21d2682e7ec128491f8508f8444494c469381e44908ab15" + +[[metadata.targets]] +requires_python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +requires_python = ">=3.9" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +groups = ["pdm"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.1; python_version < \"3.11\"", +] +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"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +requires_python = ">=3.9" +summary = "Fast, simple object-to-object and broadcast signaling" +groups = ["pdm"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["pdm"] +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "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"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default", "pytest", "test"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.7" +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"}, +] + +[[package]] +name = "coverage" +version = "7.6.7" +extras = ["toml"] +requires_python = ">=3.9" +summary = "Code coverage measurement for Python" +groups = ["test"] +dependencies = [ + "coverage==7.6.7", + "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"}, +] + +[[package]] +name = "dep-logic" +version = "0.4.9" +requires_python = ">=3.8" +summary = "Python dependency specifications supporting logical operations" +groups = ["pdm"] +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"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +summary = "Distribution utilities" +groups = ["pdm"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "dulwich" +version = "0.22.6" +requires_python = ">=3.9" +summary = "Python Git Library" +groups = ["git"] +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"}, +] + +[[package]] +name = "filelock" +version = "3.16.1" +requires_python = ">=3.8" +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"}, +] + +[[package]] +name = "findpython" +version = "0.6.2" +requires_python = ">=3.8" +summary = "A utility to find python versions on your system" +groups = ["pdm"] +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"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["pdm"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "hishel" +version = "0.0.33" +requires_python = ">=3.8" +summary = "Persistent cache implementation for httpx and httpcore" +groups = ["pdm"] +dependencies = [ + "httpx>=0.22.0", + "typing-extensions>=4.8.0", +] +files = [ + {file = "hishel-0.0.33-py3-none-any.whl", hash = "sha256:6e6c6cdaf432ff4c4981e7792ef7d1fa4c8ede58b9dbbcefb9ab3fc9770f2a07"}, + {file = "hishel-0.0.33.tar.gz", hash = "sha256:ab5b2661d5e2252f305fd0fb20e8c76bfab3ea73458f20f2591c53c37b270089"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["pdm"] +dependencies = [ + "certifi", + "h11<0.15,>=0.13", +] +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[[package]] +name = "httpx" +version = "0.27.2" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["pdm"] +dependencies = [ + "anyio", + "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"}, +] + +[[package]] +name = "httpx" +version = "0.27.2" +extras = ["socks"] +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["pdm"] +dependencies = [ + "httpx==0.27.2", + "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"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["pdm"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +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"}, +] + +[[package]] +name = "installer" +version = "0.7.0" +requires_python = ">=3.7" +summary = "A library for installing Python wheels." +groups = ["pdm"] +files = [ + {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, + {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" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default", "pdm"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default", "pdm"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +requires_python = ">=3.8" +summary = "MessagePack serializer" +groups = ["pdm"] +files = [ + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +requires_python = ">=3.8" +summary = "Optional static typing for Python" +groups = ["lint"] +dependencies = [ + "mypy-extensions>=1.0.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "mypy-1.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"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["lint"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.2" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default", "pdm", "pytest", "test"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pbs-installer" +version = "2024.10.16" +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"}, +] + +[[package]] +name = "pdm" +version = "2.20.1" +requires_python = ">=3.8" +summary = "A modern Python package and dependency manager supporting the latest PEP standards" +groups = ["pdm"] +dependencies = [ + "blinker", + "certifi>=2024.8.30", + "dep-logic>=0.4.4", + "filelock>=3.13", + "findpython<1.0.0a0,>=0.6.0", + "hishel<0.1.0,>=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", + "pbs-installer>=2024.4.18", + "platformdirs", + "pyproject-hooks", + "python-dotenv>=0.15", + "resolvelib>=1.1", + "rich>=12.3.0", + "shellingham>=1.3.2", + "tomli>=1.1.0; python_version < \"3.11\"", + "tomlkit<1,>=0.11.1", + "truststore>=0.9; python_version >= \"3.10\"", + "unearth>=0.17.0", + "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"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +requires_python = ">=3.8" +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"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["pytest", "test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +requires_python = ">=3.8" +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\"", +] +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +requires_python = ">=3.8" +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"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +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"}, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +requires_python = ">=3.7" +summary = "Wrappers to call pyproject.toml-based build backend hooks." +groups = ["pdm"] +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[package]] +name = "pytest" +version = "8.3.3" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["pytest", "test"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +requires_python = ">=3.9" +summary = "Pytest plugin for measuring coverage." +groups = ["test"] +dependencies = [ + "coverage[toml]>=7.5", + "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"}, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +requires_python = ">=3.8" +summary = "Thin-wrapper around the mock package for easier use with pytest" +groups = ["pytest", "test"] +dependencies = [ + "pytest>=6.2.5", +] +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +requires_python = ">=3.8" +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"}, +] + +[[package]] +name = "resolvelib" +version = "1.1.0" +requires_python = ">=3.7" +summary = "Resolve abstract dependencies into concrete ones" +groups = ["pdm"] +files = [ + {file = "resolvelib-1.1.0-py2.py3-none-any.whl", hash = "sha256:f80de38ae744bcf4e918e27a681a5c6cb63a08d9a926c0989c0730bcdd089049"}, + {file = "resolvelib-1.1.0.tar.gz", hash = "sha256:b68591ef748f58c1e2a2ac28d0961b3586ae8b25f60b0ba9a5e4f3d87c1d6a79"}, +] + +[[package]] +name = "rich" +version = "13.9.4" +requires_python = ">=3.8.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default", "pdm"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", +] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[[package]] +name = "ruff" +version = "0.7.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"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +requires_python = ">=3.7" +summary = "Tool to Detect Surrounding Shell" +groups = ["default", "pdm"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["pdm"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "socksio" +version = "1.0.0" +requires_python = ">=3.6" +summary = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." +groups = ["pdm"] +files = [ + {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, + {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +requires_python = ">=3.8" +summary = "Style preserving TOML library" +groups = ["pdm"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "truststore" +version = "0.10.0" +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"}, +] + +[[package]] +name = "typer" +version = "0.13.1" +requires_python = ">=3.7" +summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." +groups = ["default"] +dependencies = [ + "click>=8.0.0", + "rich>=10.11.0", + "shellingham>=1.3.0", + "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"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default", "lint", "pdm"] +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"}, +] + +[[package]] +name = "unearth" +version = "0.17.2" +requires_python = ">=3.8" +summary = "A utility to fetch and download python packages" +groups = ["pdm"] +dependencies = [ + "httpx<1,>=0.27.0", + "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"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["git"] +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[[package]] +name = "virtualenv" +version = "20.27.1" +requires_python = ">=3.8" +summary = "Virtual Python Environment builder" +groups = ["pdm"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "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"}, +] diff --git a/pyproject.toml b/pyproject.toml index c1855d7..f69a6ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,107 +1,133 @@ -[project] -description = "A Python management solution for C++ dependencies" -name = "cppython" - -license = {text = "MIT"} - -authors = [ - {name = "Synodic Software", email = "contact@synodic.software"}, -] -readme = "README.md" - -dynamic = ["version"] - -requires-python = ">=3.12" - -dependencies = [ - "click>=8.1.3", - "tomlkit>=0.12.4", - "cppython-core>=0.4.1.dev19", - "pydantic>=2.6.3", - "packaging>=21.3", -] - -[project.license-files] -paths = ["LICENSE.md"] - -[project.urls] -homepage = "https://github.com/Synodic-Software/CPPython" -repository = "https://github.com/Synodic-Software/CPPython" - -[tool.pdm.options] -update = ["--update-all"] - -[tool.pdm.version] -source = "scm" - -[tool.pdm.dev-dependencies] -lint = [ - "black>=24.2.0", - "pylint>=3.0.0", - "isort>=5.10.1", - "mypy>=1.9", -] -test = [ - "pytest>=8.0.2", - "pytest-cov>=3.0.0", - "pytest-click>=1.1", - "pytest-mock>=3.8.2", - "pytest-cppython>=0.2.0.dev0", -] - -[project.scripts] -cppython = "cppython.console.interface:cli" - -[tool.pdm.scripts] -analyze = {shell = "pylint --verbose cppython tests"} -format = {shell = "black --check --verbose ."} -lint = {composite = ["analyze", "format", "sort-imports", "type-check"]} -sort-imports = {shell = "isort --check-only --diff --verbose ."} -test = {shell = "pytest --cov=cppython --verbose tests"} -type-check = {shell = "mypy ."} - -[tool.pytest.ini_options] -log_cli = true -testpaths = [ - "tests", -] - -[tool.black] -line-length = 120 -preview = true - -[tool.isort] -profile = "black" -skip_gitignore = true - -[tool.mypy] -exclude = "__pypackages__" -plugins = ["pydantic.mypy"] -strict = true - -[tool.pylint.MAIN] -load-plugins = [ - "pylint.extensions.code_style", - "pylint.extensions.typing", - "pylint.extensions.docstyle", - "pylint.extensions.docparams", - "pylint.extensions.private_import", - "pylint.extensions.bad_builtin", -] - -[tool.pylint.format] -max-line-length = "120" - -[tool.pylint.parameter_documentation] -accept-no-param-doc = false -accept-no-raise-doc = false -accept-no-return-doc = false -accept-no-yields-doc = false -default-docstring-type = "google" - -[tool.coverage.report] -skip_empty = true - -[build-system] -build-backend = "pdm.backend" -requires = ["pdm.backend"] +[project] +description = "A Python management solution for C++ dependencies" +name = "cppython" + +license = {text = "MIT"} + +authors = [ + {name = "Synodic Software", email = "contact@synodic.software"}, +] +readme = "README.md" + +dynamic = ["version"] + +requires-python = ">=3.13" + +dependencies = [ + "typer>=0.13.1", + "pydantic>=2.8.2", + "packaging>=24.1", +] + +[project.optional-dependencies] +pytest = [ + "pytest>=8.3.3", + "pytest-mock>=3.14.0", +] + +git = [ + "dulwich>=0.22.5", +] + +pdm = [ + "pdm>=2.20.1", +] + +[project.urls] +homepage = "https://github.com/Synodic-Software/CPPython" +repository = "https://github.com/Synodic-Software/CPPython" + +[project.entry-points."cppython.scm"] +git = "cppython.plugins.git.plugin:GitSCM" + +[project.entry-points."cppython.generator"] +cmake = "cppython.plugins.cmake.plugin:CMakeGenerator" + +[project.entry-points.pdm] +cppython = "cppython.plugins.pdm.plugin:CPPythonPlugin" + +[project.entry-points."cppython.provider"] +vcpkg = "cppython.plugins.vcpkg.plugin:VcpkgProvider" + +[dependency-groups] +lint = [ + "ruff>=0.7.4", + "mypy>=1.13", + "isort>=5.13.2", +] +test = [ + "pytest>=8.3.3", + "pytest-cov>=6.0.0", + "pytest-mock>=3.14.0", +] + +[project.scripts] +cppython = "cppython.console.entry:app" + +[tool.pytest.ini_options] +log_cli = true +testpaths = [ + "tests", +] + +[tool.mypy] +exclude = "__pypackages__" +plugins = ["pydantic.mypy"] +strict = true + +[tool.isort] +profile = "black" + +[tool.ruff] +line-length = 120 +preview = true + +[tool.ruff.lint] +ignore = [ + "D206", + "D300", + "D415", + "E111", + "E114", + "E117", +] +select = [ + "D", # pydocstyle + "F", # Pyflakes + "I", # isort + "PL", # pylint + "UP", # pyupgrade + "E", # pycodestyle + "B", # flake8-bugbear + "SIM", # flake8-simplify + "PT", # flake8-pytest-style +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +docstring-code-format = true +indent-style = "space" +quote-style = "single" + +[tool.coverage.report] +skip_empty = true + +[tool.pdm.options] +update = ["--update-all"] + +[tool.pdm.version] +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 ."} +test = {shell = "pytest --cov=cppython --verbose tests"} +type-check = {shell = "mypy ."} + +[build-system] +build-backend = "pdm.backend" +requires = ["pdm.backend"] diff --git a/tests/__init__.py b/tests/__init__.py index 5f28270..73173d5 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,7 @@ - \ No newline at end of file +"""Unit tests for the CPPython project. + +This module contains various unit tests to ensure the correct functionality of +different components within the CPPython project. The tests cover a wide range +of features, including plugin interfaces, project configurations, and utility +functions. +""" diff --git a/tests/build/test_build/build.txt b/tests/build/test_build/build.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2e1f173 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,343 @@ +"""Data variations for testing""" + +# from pathlib import Path +from pathlib import Path +from typing import cast + +import pytest + +from cppython.core.plugin_schema.generator import Generator +from cppython.core.plugin_schema.provider import Provider +from cppython.core.plugin_schema.scm import SCM +from cppython.core.resolution import ( + PluginBuildData, + PluginCPPythonData, + resolve_cppython, + resolve_pep621, + resolve_project_configuration, +) +from cppython.core.schema import ( + CoreData, + CPPythonData, + CPPythonGlobalConfiguration, + CPPythonLocalConfiguration, + PEP621Configuration, + PEP621Data, + ProjectConfiguration, + ProjectData, + PyProject, + ToolData, +) +from cppython.plugins.cmake.schema import CMakeConfiguration +from cppython.test.pytest.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 + + +@pytest.fixture( + name='install_path', + scope='session', +) +def fixture_install_path(tmp_path_factory: pytest.TempPathFactory) -> Path: + """Creates temporary install location + + Args: + tmp_path_factory: Factory for centralized temporary directories + + Returns: + A temporary directory + """ + path = tmp_path_factory.getbasetemp() + path.mkdir(parents=True, exist_ok=True) + return path + + +@pytest.fixture( + name='pep621_configuration', + scope='session', + params=pep621_variants, +) +def fixture_pep621_configuration(request: pytest.FixtureRequest) -> PEP621Configuration: + """Fixture defining all testable variations of PEP621 + + Args: + request: Parameterization list + + Returns: + PEP621 variant + """ + return cast(PEP621Configuration, request.param) + + +@pytest.fixture( + name='pep621_data', + scope='session', +) +def fixture_pep621_data( + pep621_configuration: PEP621Configuration, project_configuration: ProjectConfiguration +) -> PEP621Data: + """Resolved project table fixture + + Args: + pep621_configuration: The input configuration to resolve + project_configuration: The project configuration to help with the resolve + + Returns: + The resolved project table + """ + return resolve_pep621(pep621_configuration, project_configuration, None) + + +@pytest.fixture( + name='cppython_local_configuration', + scope='session', + params=cppython_local_variants, +) +def fixture_cppython_local_configuration( + request: pytest.FixtureRequest, install_path: Path +) -> CPPythonLocalConfiguration: + """Fixture defining all testable variations of CPPythonData + + Args: + request: Parameterization list + install_path: The temporary install directory + + Returns: + Variation of CPPython data + """ + cppython_local_configuration = cast(CPPythonLocalConfiguration, request.param) + + data = cppython_local_configuration.model_dump(by_alias=True) + + # Pin the install location to the base temporary directory + data['install-path'] = install_path + + # Fill the plugin names with mocked values + data['provider-name'] = 'mock' + data['generator-name'] = 'mock' + + return CPPythonLocalConfiguration(**data) + + +@pytest.fixture( + name='cppython_global_configuration', + scope='session', + params=cppython_global_variants, +) +def fixture_cppython_global_configuration(request: pytest.FixtureRequest) -> CPPythonGlobalConfiguration: + """Fixture defining all testable variations of CPPythonData + + Args: + request: Parameterization list + + Returns: + Variation of CPPython data + """ + cppython_global_configuration = cast(CPPythonGlobalConfiguration, request.param) + + return cppython_global_configuration + + +@pytest.fixture( + name='plugin_build_data', + scope='session', +) +def fixture_plugin_build_data( + provider_type: type[Provider], + generator_type: type[Generator], + scm_type: type[SCM], +) -> PluginBuildData: + """Fixture for constructing resolved CPPython table data + + Args: + provider_type: The provider type + generator_type: The generator type + scm_type: The scm type + + Returns: + The plugin build data + """ + return PluginBuildData(generator_type=generator_type, provider_type=provider_type, scm_type=scm_type) + + +@pytest.fixture( + name='plugin_cppython_data', + scope='session', +) +def fixture_plugin_cppython_data( + provider_type: type[Provider], + generator_type: type[Generator], + scm_type: type[SCM], +) -> PluginCPPythonData: + """Fixture for constructing resolved CPPython table data + + Args: + provider_type: The provider type + generator_type: The generator type + scm_type: The scm type + + Returns: + The plugin data for CPPython resolution + """ + return PluginCPPythonData( + generator_name=generator_type.name(), provider_name=provider_type.name(), scm_name=scm_type.name() + ) + + +@pytest.fixture( + name='cppython_data', + scope='session', +) +def fixture_cppython_data( + cppython_local_configuration: CPPythonLocalConfiguration, + cppython_global_configuration: CPPythonGlobalConfiguration, + project_data: ProjectData, + plugin_cppython_data: PluginCPPythonData, +) -> CPPythonData: + """Fixture for constructing resolved CPPython table data + + Args: + cppython_local_configuration: The local configuration to resolve + cppython_global_configuration: The global configuration to resolve + project_data: The project data to help with the resolve + plugin_cppython_data: Plugin data for CPPython resolution + + Returns: + The resolved CPPython table + """ + return resolve_cppython( + cppython_local_configuration, cppython_global_configuration, project_data, plugin_cppython_data + ) + + +@pytest.fixture( + name='core_data', +) +def fixture_core_data(cppython_data: CPPythonData, project_data: ProjectData) -> CoreData: + """Fixture for creating the wrapper CoreData type + + Args: + cppython_data: CPPython data + project_data: The project data + + Returns: + Wrapper Core Type + """ + return CoreData(cppython_data=cppython_data, project_data=project_data) + + +@pytest.fixture( + name='project_configuration', + scope='session', + params=project_variants, +) +def fixture_project_configuration( + request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory +) -> ProjectConfiguration: + """Project configuration fixture. + + Here we provide overrides on the input variants so that we can use a temporary directory for testing purposes. + + Args: + request: Parameterized configuration data + tmp_path_factory: Factory for centralized temporary directories + + 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 + + return configuration + + +@pytest.fixture( + name='project_data', + scope='session', +) +def fixture_project_data(project_configuration: ProjectConfiguration) -> ProjectData: + """Fixture that creates a project space at 'workspace/test_project/pyproject.toml' + + Args: + project_configuration: Project data + + Returns: + A project data object that has populated a function level temporary directory + """ + return resolve_project_configuration(project_configuration) + + +@pytest.fixture(name='project') +def fixture_project( + cppython_local_configuration: CPPythonLocalConfiguration, pep621_configuration: PEP621Configuration +) -> PyProject: + """Parameterized construction of PyProject data + + Args: + cppython_local_configuration: The parameterized cppython table + pep621_configuration: The project table + + 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') diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 5f28270..73e5ba0 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1 +1,7 @@ - \ No newline at end of file +"""Integration tests for the CPPython project. + +This module contains integration tests to ensure the correct functionality of +different components within the CPPython project. The tests cover a wide range +of features, including plugin interfaces, project configurations, and utility +functions. +""" diff --git a/tests/integration/plugins/__init__.py b/tests/integration/plugins/__init__.py new file mode 100644 index 0000000..e88b7fb --- /dev/null +++ b/tests/integration/plugins/__init__.py @@ -0,0 +1,7 @@ +"""Integration tests for the CPPython plugins. + +This module contains integration tests for various CPPython plugins, ensuring that +each plugin behaves as expected under different conditions. The tests cover +different aspects of the plugins' functionality, including data generation, +installation, update processes, and feature extraction. +""" diff --git a/tests/integration/plugins/cmake/__init__.py b/tests/integration/plugins/cmake/__init__.py new file mode 100644 index 0000000..73a604d --- /dev/null +++ b/tests/integration/plugins/cmake/__init__.py @@ -0,0 +1,7 @@ +"""Integration tests for the CMake generator plugin. + +This module contains integration tests for the CMake generator plugin, ensuring that +the plugin behaves as expected under various conditions. The tests cover +different aspects of the plugin's functionality, including preset writing, +data synchronization, and feature extraction. +""" diff --git a/tests/integration/plugins/cmake/test_generator.py b/tests/integration/plugins/cmake/test_generator.py new file mode 100644 index 0000000..d3d172d --- /dev/null +++ b/tests/integration/plugins/cmake/test_generator.py @@ -0,0 +1,36 @@ +"""Integration tests for the provider""" + +from typing import Any + +import pytest + +from cppython.plugins.cmake.plugin import CMakeGenerator +from cppython.plugins.cmake.schema import CMakeConfiguration +from cppython.test.pytest.tests import GeneratorIntegrationTests + + +class TestCPPythonGenerator(GeneratorIntegrationTests[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]: + """A required testing hook that allows data generation + + Args: + cmake_data: The input data + + Returns: + The constructed plugin data + """ + return cmake_data.model_dump() + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[CMakeGenerator]: + """A required testing hook that allows type generation + + Returns: + The type of the Generator + """ + return CMakeGenerator diff --git a/tests/integration/plugins/pdm/__init__.py b/tests/integration/plugins/pdm/__init__.py new file mode 100644 index 0000000..a38eeca --- /dev/null +++ b/tests/integration/plugins/pdm/__init__.py @@ -0,0 +1,7 @@ +"""Integration tests for the PDM interface plugin. + +This module contains integration tests for the PDM interface plugin, ensuring that +the plugin behaves as expected under various conditions. The tests cover +different aspects of the plugin's functionality, including project configuration +and integration with the PDM tool. +""" diff --git a/tests/integration/plugins/pdm/test_interface.py b/tests/integration/plugins/pdm/test_interface.py new file mode 100644 index 0000000..7a78e3b --- /dev/null +++ b/tests/integration/plugins/pdm/test_interface.py @@ -0,0 +1,38 @@ +"""Integration tests for the interface""" + +import pytest +from pdm.core import Core +from pytest_mock import MockerFixture + +from cppython.plugins.pdm.plugin import CPPythonPlugin + + +class TestCPPythonInterface: + """The tests for the PDM interface""" + + @staticmethod + @pytest.fixture(name='interface') + def fixture_interface(plugin_type: type[CPPythonPlugin]) -> CPPythonPlugin: + """A hook allowing implementations to override the fixture + + Args: + plugin_type: An input interface type + + Returns: + A newly constructed interface + """ + return plugin_type(Core()) + + @staticmethod + def test_entrypoint(mocker: MockerFixture) -> None: + """Verify that this project's plugin hook is setup correctly + + Args: + mocker: Mocker fixture for plugin patch + """ + patch = mocker.patch('cppython.plugins.pdm.plugin.CPPythonPlugin') + + core = Core() + core.load_plugins() + + assert patch.called diff --git a/tests/integration/plugins/vcpkg/__init__.py b/tests/integration/plugins/vcpkg/__init__.py new file mode 100644 index 0000000..1a0c18e --- /dev/null +++ b/tests/integration/plugins/vcpkg/__init__.py @@ -0,0 +1,7 @@ +"""Integration tests for the vcpkg provider plugin. + +This module contains integration tests for the vcpkg provider plugin, ensuring that +the plugin behaves as expected under various conditions. The tests cover +different aspects of the plugin's functionality, including data generation, +installation, and update processes. +""" diff --git a/tests/integration/plugins/vcpkg/test_provider.py b/tests/integration/plugins/vcpkg/test_provider.py new file mode 100644 index 0000000..1f08637 --- /dev/null +++ b/tests/integration/plugins/vcpkg/test_provider.py @@ -0,0 +1,32 @@ +"""Integration tests for the provider""" + +from typing import Any + +import pytest + +from cppython.plugins.vcpkg.plugin import VcpkgProvider +from cppython.test.pytest.tests import ProviderIntegrationTests + + +class TestCPPythonProvider(ProviderIntegrationTests[VcpkgProvider]): + """The tests for the vcpkg 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[VcpkgProvider]: + """A required testing hook that allows type generation + + Returns: + The type of the Provider + """ + return VcpkgProvider diff --git a/tests/integration/test/__init__.py b/tests/integration/test/__init__.py new file mode 100644 index 0000000..4e267f6 --- /dev/null +++ b/tests/integration/test/__init__.py @@ -0,0 +1,7 @@ +"""Integration tests for the public test harness used by CPPython plugins. + +This module contains integration tests for the public test harness that plugins +can use to ensure their functionality. The tests cover various aspects of the +plugin integration, including entry points, group names, and plugin-specific +features. +""" diff --git a/tests/integration/test/test_generator.py b/tests/integration/test/test_generator.py new file mode 100644 index 0000000..f34820d --- /dev/null +++ b/tests/integration/test/test_generator.py @@ -0,0 +1,32 @@ +"""Tests the integration test plugin""" + +from typing import Any + +import pytest + +from cppython.test.mock.generator import MockGenerator +from cppython.test.pytest.tests import GeneratorIntegrationTests + + +class TestCPPythonGenerator(GeneratorIntegrationTests[MockGenerator]): + """The tests for the Mock generator""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """Returns mock data + + Returns: + An overridden data instance + """ + return {} + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[MockGenerator]: + """A required testing hook that allows type generation + + Returns: + An overridden generator type + """ + return MockGenerator diff --git a/tests/integration/test/test_provider.py b/tests/integration/test/test_provider.py new file mode 100644 index 0000000..71752ab --- /dev/null +++ b/tests/integration/test/test_provider.py @@ -0,0 +1,32 @@ +"""Test the integrations related to the internal provider implementation and the 'Provider' interface itself""" + +from typing import Any + +import pytest + +from cppython.test.mock.provider import MockProvider +from cppython.test.pytest.tests import ProviderIntegrationTests + + +class TestMockProvider(ProviderIntegrationTests[MockProvider]): + """The tests for our Mock provider""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """Returns mock data + + Returns: + An overridden data instance + """ + return {} + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[MockProvider]: + """A required testing hook that allows type generation + + Returns: + The overridden provider type + """ + return MockProvider diff --git a/tests/integration/test/test_scm.py b/tests/integration/test/test_scm.py new file mode 100644 index 0000000..76ab30e --- /dev/null +++ b/tests/integration/test/test_scm.py @@ -0,0 +1,32 @@ +"""Tests the integration test plugin""" + +from typing import Any + +import pytest + +from cppython.test.mock.scm import MockSCM +from cppython.test.pytest.tests import SCMIntegrationTests + + +class TestCPPythonSCM(SCMIntegrationTests[MockSCM]): + """The tests for the Mock version control""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """Returns mock data + + Returns: + An overridden data instance + """ + return {} + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[MockSCM]: + """A required testing hook that allows type generation + + Returns: + An overridden version control type + """ + return MockSCM diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 5f28270..73173d5 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1 +1,7 @@ - \ No newline at end of file +"""Unit tests for the CPPython project. + +This module contains various unit tests to ensure the correct functionality of +different components within the CPPython project. The tests cover a wide range +of features, including plugin interfaces, project configurations, and utility +functions. +""" diff --git a/tests/unit/core/__init__.py b/tests/unit/core/__init__.py new file mode 100644 index 0000000..0314681 --- /dev/null +++ b/tests/unit/core/__init__.py @@ -0,0 +1,7 @@ +"""Unit tests for the core functionality of the CPPython project. + +This module contains unit tests for the core components of the CPPython project, +ensuring that the core functionality behaves as expected under various conditions. +The tests cover different aspects of the core functionality, including schema +validation, resolution processes, and plugin schema handling. +""" diff --git a/tests/unit/core/test_plugin_schema.py b/tests/unit/core/test_plugin_schema.py new file mode 100644 index 0000000..b367050 --- /dev/null +++ b/tests/unit/core/test_plugin_schema.py @@ -0,0 +1,111 @@ +"""Test plugin schemas""" + +import pytest + +from cppython.core.plugin_schema.generator import SyncConsumer +from cppython.core.plugin_schema.provider import SyncProducer +from cppython.core.schema import SyncData +from cppython.utility.utility import TypeName + + +class TestSchema: + """Test validation""" + + class GeneratorSyncDataSuccess(SyncData): + """Dummy generator data""" + + success: bool + + class GeneratorSyncDataFail(SyncData): + """Dummy generator data""" + + failure: bool + + class Consumer(SyncConsumer): + """Dummy consumer""" + + @staticmethod + def sync_types() -> list[type[SyncData]]: + """Fulfils protocol + + Returns: + Fulfils protocol + """ + return [TestSchema.GeneratorSyncDataSuccess, TestSchema.GeneratorSyncDataFail] + + @staticmethod + def sync(sync_data: SyncData) -> None: + """Fulfils protocol + + Args: + sync_data: Fulfils protocol + """ + if isinstance(sync_data, TestSchema.GeneratorSyncDataSuccess): + assert sync_data.success + else: + pytest.fail('Invalid sync data') + + class Producer(SyncProducer): + """Dummy producer""" + + @staticmethod + def supported_sync_type(sync_type: type[SyncData]) -> bool: + """Fulfils protocol + + Args: + sync_type: Fulfils protocol + + Returns: + Fulfils protocol + """ + return sync_type == TestSchema.GeneratorSyncDataSuccess + + @staticmethod + def sync_data(consumer: SyncConsumer) -> SyncData | None: + """Fulfils protocol + + Args: + consumer: Fulfils protocol + + Returns: + Fulfils protocol + """ + for sync_type in consumer.sync_types(): + if sync_type == TestSchema.GeneratorSyncDataSuccess: + return TestSchema.GeneratorSyncDataSuccess(provider_name=TypeName('Dummy'), success=True) + + return None + + def test_sync_broadcast(self) -> None: + """Verifies broadcast support""" + consumer = self.Consumer() + producer = self.Producer() + + types = consumer.sync_types() + + assert producer.supported_sync_type(types[0]) + assert not producer.supported_sync_type(types[1]) + + def test_sync_production(self) -> None: + """Verifies the variant behavior of SyncData""" + producer = self.Producer() + consumer = self.Consumer() + assert producer.sync_data(consumer) + + def test_sync_consumption(self) -> None: + """Verifies the variant behavior of SyncData""" + consumer = self.Consumer() + + data = self.GeneratorSyncDataSuccess(provider_name=TypeName('Dummy'), success=True) + consumer.sync(data) + + def test_sync_flow(self) -> None: + """Verifies the variant behavior of SyncData""" + consumer = self.Consumer() + producer = self.Producer() + + types = consumer.sync_types() + + for test in types: + if producer.supported_sync_type(test) and (data := producer.sync_data(consumer)): + consumer.sync(data) diff --git a/tests/unit/core/test_resolution.py b/tests/unit/core/test_resolution.py new file mode 100644 index 0000000..d4bf9d0 --- /dev/null +++ b/tests/unit/core/test_resolution.py @@ -0,0 +1,165 @@ +"""Test data resolution""" + +from pathlib import Path +from typing import Annotated + +import pytest +from pydantic import Field + +from cppython.core.exception import ConfigException +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.resolution import ( + PluginCPPythonData, + resolve_cppython, + resolve_cppython_plugin, + resolve_generator, + resolve_model, + resolve_pep621, + resolve_project_configuration, + resolve_provider, + resolve_scm, +) +from cppython.core.schema import ( + CPPythonGlobalConfiguration, + CPPythonLocalConfiguration, + CPPythonModel, + PEP621Configuration, + ProjectConfiguration, + ProjectData, +) +from cppython.utility.utility import TypeName + + +class TestResolve: + """Test resolution of data""" + + @staticmethod + def test_pep621_resolve() -> 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) + + class_variables = vars(resolved) + + assert len(class_variables) + assert None not in class_variables.values() + + @staticmethod + def test_project_resolve() -> None: + """Tests project configuration resolution""" + config = ProjectConfiguration(pyproject_file=Path('pyproject.toml'), version='0.1.0') + assert resolve_project_configuration(config) + + @staticmethod + def test_cppython_resolve() -> 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) + + plugin_build_data = PluginCPPythonData( + generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') + ) + + cppython_data = resolve_cppython( + cppython_local_configuration, cppython_global_configuration, project_data, plugin_build_data + ) + + assert cppython_data + + @staticmethod + def test_model_resolve() -> None: + """Test model resolution""" + + class MockModel(CPPythonModel): + """Mock model for testing""" + + field: Annotated[str, Field()] + + bad_data = {'field': 4} + + with pytest.raises(ConfigException) as error: + 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: + """Test generator resolution""" + project_data = ProjectData(pyproject_file=Path('pyproject.toml')) + 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) + + plugin_build_data = PluginCPPythonData( + generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') + ) + + cppython_data = resolve_cppython( + cppython_local_configuration, cppython_global_configuration, project_data, plugin_build_data + ) + + MockGenerator = type('MockGenerator', (Generator,), {}) + + cppython_plugin_data = resolve_cppython_plugin(cppython_data, MockGenerator) + + assert resolve_generator(project_data, cppython_plugin_data) + + @staticmethod + def test_provider_resolve() -> None: + """Test provider resolution""" + project_data = ProjectData(pyproject_file=Path('pyproject.toml')) + 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) + + plugin_build_data = PluginCPPythonData( + generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') + ) + + cppython_data = resolve_cppython( + cppython_local_configuration, cppython_global_configuration, project_data, plugin_build_data + ) + + MockProvider = type('MockProvider', (Provider,), {}) + + cppython_plugin_data = resolve_cppython_plugin(cppython_data, MockProvider) + + assert resolve_provider(project_data, cppython_plugin_data) + + @staticmethod + def test_scm_resolve() -> None: + """Test scm resolution""" + project_data = ProjectData(pyproject_file=Path('pyproject.toml')) + 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) + + plugin_build_data = PluginCPPythonData( + generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') + ) + + cppython_data = resolve_cppython( + cppython_local_configuration, cppython_global_configuration, project_data, plugin_build_data + ) + + MockSCM = type('MockSCM', (SCM,), {}) + + cppython_plugin_data = resolve_cppython_plugin(cppython_data, MockSCM) + + assert resolve_scm(project_data, cppython_plugin_data) diff --git a/tests/unit/core/test_schema.py b/tests/unit/core/test_schema.py new file mode 100644 index 0000000..454812d --- /dev/null +++ b/tests/unit/core/test_schema.py @@ -0,0 +1,61 @@ +"""Test custom schema validation that cannot be verified by the Pydantic validation""" + +from tomllib import loads +from typing import Annotated + +import pytest +from pydantic import Field + +from cppython.core.schema import ( + CPPythonGlobalConfiguration, + CPPythonLocalConfiguration, + CPPythonModel, + PEP621Configuration, +) + + +class TestSchema: + """Test validation""" + + class Model(CPPythonModel): + """Testing Model""" + + aliased_variable: Annotated[bool, Field(alias='aliased-variable', description='Alias test')] = False + + 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 + + model = self.Model(**{'aliased-variable': True}) + assert model.aliased_variable is True + + def test_model_construction_from_data(self) -> None: + """Verifies that the base model type has the expected construction behaviors""" + toml_str = """ + aliased_variable = false\n + aliased-variable = true + """ + + data = loads(toml_str) + result = self.Model.model_validate(data) + assert result.aliased_variable is True + + @staticmethod + def test_cppython_local() -> None: + """Ensures that the CPPython local config data can be defaulted""" + CPPythonLocalConfiguration() + + @staticmethod + def test_cppython_global() -> None: + """Ensures that the CPPython global config data can be defaulted""" + CPPythonGlobalConfiguration() + + @staticmethod + def test_pep621_version() -> None: + """Tests the dynamic version validation""" + with pytest.raises(ValueError, match="'version' is not a dynamic field. It must be defined"): + PEP621Configuration(name='empty-test') + + with pytest.raises(ValueError, match="'version' is a dynamic field. It must not be defined"): + PEP621Configuration(name='both-test', version='1.0.0', dynamic=['version']) diff --git a/tests/unit/plugins/__init__.py b/tests/unit/plugins/__init__.py new file mode 100644 index 0000000..2a857ac --- /dev/null +++ b/tests/unit/plugins/__init__.py @@ -0,0 +1,7 @@ +"""Unit tests for the CPPython plugins. + +This module contains unit tests for various CPPython plugins, ensuring that +each plugin behaves as expected under different conditions. The tests cover +different aspects of the plugins' functionality, including data generation, +installation, update processes, and feature extraction. +""" diff --git a/tests/unit/plugins/cmake/__init__.py b/tests/unit/plugins/cmake/__init__.py new file mode 100644 index 0000000..94d6fa1 --- /dev/null +++ b/tests/unit/plugins/cmake/__init__.py @@ -0,0 +1,7 @@ +"""Unit tests for the CMake generator plugin. + +This module contains unit tests for the CMake generator plugin, ensuring that +the plugin behaves as expected under various conditions. The tests cover +different aspects of the plugin's functionality, including preset writing, +data synchronization, and feature extraction. +""" diff --git a/tests/unit/plugins/cmake/test_generator.py b/tests/unit/plugins/cmake/test_generator.py new file mode 100644 index 0000000..9d5c0ce --- /dev/null +++ b/tests/unit/plugins/cmake/test_generator.py @@ -0,0 +1,143 @@ +"""Unit test the provider plugin""" + +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 ( + CMakeConfiguration, + CMakePresets, + CMakeSyncData, +) +from cppython.test.pytest.tests import GeneratorUnitTests +from cppython.utility.utility import TypeName + + +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]: + """A required testing hook that allows data generation + + Args: + cmake_data: The input data + + Returns: + The constructed plugin data + """ + return cmake_data.model_dump() + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[CMakeGenerator]: + """A required testing hook that allows type generation + + Returns: + The type of the Generator + """ + return CMakeGenerator + + @staticmethod + def test_provider_write(tmp_path: Path) -> None: + """Verifies that the provider preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + includes_file = tmp_path / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(tmp_path, data) + + @staticmethod + def test_cppython_write(tmp_path: Path) -> None: + """Verifies that the cppython preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + provider_directory = tmp_path / 'providers' + provider_directory.mkdir(parents=True, exist_ok=True) + + includes_file = provider_directory / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(provider_directory, data) + + builder.write_cppython_preset(tmp_path, provider_directory, data) + + @staticmethod + def test_root_write(tmp_path: Path) -> None: + """Verifies that the root preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + cppython_preset_directory = tmp_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) + + includes_file = provider_directory / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + root_file = tmp_path / 'CMakePresets.json' + presets = CMakePresets() + write_model_json(root_file, presets) + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(provider_directory, data) + + cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + + builder.write_root_presets(root_file, cppython_preset_file) + + @staticmethod + def test_relative_root_write(tmp_path: Path) -> None: + """Verifies that the root preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + cppython_preset_directory = tmp_path / 'tool' / 'cppython' + cppython_preset_directory.mkdir(parents=True, exist_ok=True) + + provider_directory = cppython_preset_directory / 'providers' + provider_directory.mkdir(parents=True, exist_ok=True) + + includes_file = provider_directory / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + relative_indirection = tmp_path / 'nested' + relative_indirection.mkdir(parents=True, exist_ok=True) + + root_file = relative_indirection / 'CMakePresets.json' + presets = CMakePresets() + write_model_json(root_file, presets) + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(provider_directory, data) + + cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + builder.write_root_presets(root_file, cppython_preset_file) diff --git a/tests/unit/plugins/git/__init__.py b/tests/unit/plugins/git/__init__.py new file mode 100644 index 0000000..ba3d660 --- /dev/null +++ b/tests/unit/plugins/git/__init__.py @@ -0,0 +1,7 @@ +"""Unit tests for the Git SCM plugin. + +This module contains unit tests for the Git SCM plugin, ensuring that +the plugin behaves as expected under various conditions. The tests cover +different aspects of the plugin's functionality, including feature extraction, +version control operations, and project description handling. +""" diff --git a/tests/unit/plugins/git/test_version_control.py b/tests/unit/plugins/git/test_version_control.py new file mode 100644 index 0000000..9319e4f --- /dev/null +++ b/tests/unit/plugins/git/test_version_control.py @@ -0,0 +1,20 @@ +"""Unit tests for the cppython SCM plugin""" + +import pytest + +from cppython.plugins.git.plugin import GitSCM +from cppython.test.pytest.tests import SCMUnitTests + + +class TestGitInterface(SCMUnitTests[GitSCM]): + """Unit 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/plugins/pdm/__init__.py b/tests/unit/plugins/pdm/__init__.py new file mode 100644 index 0000000..f081a05 --- /dev/null +++ b/tests/unit/plugins/pdm/__init__.py @@ -0,0 +1,7 @@ +"""Unit tests for the PDM interface plugin. + +This module contains unit tests for the PDM interface plugin, ensuring that +the plugin behaves as expected under various conditions. The tests cover +different aspects of the plugin's functionality, including project configuration +and integration with the PDM tool. +""" diff --git a/tests/unit/plugins/pdm/test_interface.py b/tests/unit/plugins/pdm/test_interface.py new file mode 100644 index 0000000..fd1dccb --- /dev/null +++ b/tests/unit/plugins/pdm/test_interface.py @@ -0,0 +1,33 @@ +"""Unit tests for the interface""" + +import pytest +from pdm.core import Core +from pdm.project.core import Project + +from cppython.plugins.pdm.plugin import CPPythonPlugin + + +class TestCPPythonInterface: + """The tests for the PDM interface""" + + @staticmethod + @pytest.fixture(name='interface') + def fixture_interface(plugin_type: type[CPPythonPlugin]) -> CPPythonPlugin: + """A hook allowing implementations to override the fixture + + Args: + plugin_type: An input interface type + + Returns: + A newly constructed interface + """ + return plugin_type(Core()) + + @staticmethod + def test_pdm_project() -> None: + """Verify that this PDM won't return empty data""" + core = Core() + core.load_plugins() + pdm_project = Project(core, root_path=None) + + assert pdm_project diff --git a/tests/unit/plugins/vcpkg/__init__.py b/tests/unit/plugins/vcpkg/__init__.py new file mode 100644 index 0000000..8b02cf6 --- /dev/null +++ b/tests/unit/plugins/vcpkg/__init__.py @@ -0,0 +1,7 @@ +"""Unit tests for the vcpkg provider plugin. + +This module contains unit tests for the vcpkg provider plugin, ensuring that +the plugin behaves as expected under various conditions. The tests cover +different aspects of the plugin's functionality, including data generation, +installation, and update processes. +""" diff --git a/tests/unit/plugins/vcpkg/test_provider.py b/tests/unit/plugins/vcpkg/test_provider.py new file mode 100644 index 0000000..e73853a --- /dev/null +++ b/tests/unit/plugins/vcpkg/test_provider.py @@ -0,0 +1,32 @@ +"""Unit test the provider plugin""" + +from typing import Any + +import pytest + +from cppython.plugins.vcpkg.plugin import VcpkgProvider +from cppython.test.pytest.tests import ProviderUnitTests + + +class TestCPPythonProvider(ProviderUnitTests[VcpkgProvider]): + """The tests for the vcpkg 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[VcpkgProvider]: + """A required testing hook that allows type generation + + Returns: + The type of the Provider + """ + return VcpkgProvider diff --git a/tests/unit/test/__init__.py b/tests/unit/test/__init__.py new file mode 100644 index 0000000..28f02b8 --- /dev/null +++ b/tests/unit/test/__init__.py @@ -0,0 +1,6 @@ +"""Unit tests for the public test harness used by CPPython plugins. + +This module contains tests for various utility functions, including subprocess +calls, logging, and name canonicalization. The tests ensure that the utility +functions behave as expected under different conditions. +""" diff --git a/tests/unit/test/test_fixtures.py b/tests/unit/test/test_fixtures.py new file mode 100644 index 0000000..2aac2ed --- /dev/null +++ b/tests/unit/test/test_fixtures.py @@ -0,0 +1,18 @@ +"""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/test_generator.py b/tests/unit/test/test_generator.py new file mode 100644 index 0000000..4ca34d9 --- /dev/null +++ b/tests/unit/test/test_generator.py @@ -0,0 +1,32 @@ +"""Tests the integration test plugin""" + +from typing import Any + +import pytest + +from cppython.test.mock.generator import MockGenerator +from cppython.test.pytest.tests import GeneratorUnitTests + + +class TestCPPythonGenerator(GeneratorUnitTests[MockGenerator]): + """The tests for the Mock generator""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """Returns mock data + + Returns: + An overridden data instance + """ + return {} + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[MockGenerator]: + """A required testing hook that allows type generation + + Returns: + An overridden generator type + """ + return MockGenerator diff --git a/tests/unit/test/test_provider.py b/tests/unit/test/test_provider.py new file mode 100644 index 0000000..695e688 --- /dev/null +++ b/tests/unit/test/test_provider.py @@ -0,0 +1,47 @@ +"""Test the functions related to the internal provider implementation and the 'Provider' interface itself""" + +from typing import Any + +import pytest +from pytest_mock import MockerFixture + +from cppython.test.mock.generator import MockGenerator +from cppython.test.mock.provider import MockProvider +from cppython.test.pytest.tests import ProviderUnitTests + + +class TestMockProvider(ProviderUnitTests[MockProvider]): + """The tests for our Mock provider""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_provider_data() -> dict[str, Any]: + """Returns mock data + + Returns: + An overridden data instance + """ + return {} + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[MockProvider]: + """A required testing hook that allows type generation + + Returns: + An overridden provider type + """ + return MockProvider + + @staticmethod + def test_sync_types(plugin: MockProvider, mocker: MockerFixture) -> None: + """Verify that the mock provider can handle the mock generator's sync data + + Args: + plugin: The plugin instance + mocker: The pytest-mock fixture + """ + mock_generator = mocker.Mock(spec=MockGenerator) + mock_generator.sync_types.return_value = MockGenerator.sync_types() + + assert plugin.sync_data(mock_generator) diff --git a/tests/unit/test/test_scm.py b/tests/unit/test/test_scm.py new file mode 100644 index 0000000..38aa4f0 --- /dev/null +++ b/tests/unit/test/test_scm.py @@ -0,0 +1,32 @@ +"""Tests the unit test plugin""" + +from typing import Any + +import pytest + +from cppython.test.mock.scm import MockSCM +from cppython.test.pytest.tests import SCMUnitTests + + +class TestCPPythonSCM(SCMUnitTests[MockSCM]): + """The tests for the Mock version control""" + + @staticmethod + @pytest.fixture(name='plugin_data', scope='session') + def fixture_plugin_data() -> dict[str, Any]: + """Returns mock data + + Returns: + An overridden data instance + """ + return {} + + @staticmethod + @pytest.fixture(name='plugin_type', scope='session') + def fixture_plugin_type() -> type[MockSCM]: + """A required testing hook that allows type generation + + Returns: + An overridden version control type + """ + return MockSCM diff --git a/tests/unit/test_builder.py b/tests/unit/test_builder.py index 2b4e4b8..befb64f 100644 --- a/tests/unit/test_builder.py +++ b/tests/unit/test_builder.py @@ -1,26 +1,31 @@ """Tests the Builder and Resolver types""" import logging +from importlib import metadata -import pytest_cppython -from cppython_core.schema import ( +from pytest_mock import MockerFixture + +from cppython.builder import Builder, Resolver +from cppython.core.schema import ( CPPythonLocalConfiguration, PEP621Configuration, ProjectConfiguration, ProjectData, ) - -from cppython.builder import Builder, Resolver +from cppython.test.mock.generator import MockGenerator +from cppython.test.mock.provider import MockProvider +from cppython.test.mock.scm import MockSCM class TestBuilder: """Various tests for the Builder type""" + @staticmethod def test_build( - self, project_configuration: ProjectConfiguration, pep621_configuration: PEP621Configuration, cppython_local_configuration: CPPythonLocalConfiguration, + mocker: MockerFixture, ) -> None: """Verifies that the builder can build a project with all test variants @@ -28,18 +33,26 @@ def test_build( project_configuration: Variant fixture for the project configuration pep621_configuration: Variant fixture for PEP 621 configuration cppython_local_configuration: Variant fixture for cppython configuration + mocker: Pytest mocker fixture """ logger = logging.getLogger() builder = Builder(project_configuration, logger) + mocker.patch.object( + metadata, + '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) class TestResolver: """Various tests for the Resolver type""" + @staticmethod def test_generate_plugins( - self, project_configuration: ProjectConfiguration, cppython_local_configuration: CPPythonLocalConfiguration, project_data: ProjectData, diff --git a/tests/unit/test_console.py b/tests/unit/test_console.py new file mode 100644 index 0000000..0ea412f --- /dev/null +++ b/tests/unit/test_console.py @@ -0,0 +1,35 @@ +"""Tests the typer interface type""" + +from typer.testing import CliRunner + +from cppython.console.entry import app + +runner = CliRunner() + + +class TestConsole: + """Various tests for the typer interface""" + + @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 diff --git a/tests/unit/test_data.py b/tests/unit/test_data.py index 8539e9d..0357d6d 100644 --- a/tests/unit/test_data.py +++ b/tests/unit/test_data.py @@ -3,35 +3,37 @@ import logging import pytest -import pytest_cppython -from cppython_core.resolution import PluginBuildData -from cppython_core.schema import ( + +from cppython.builder import Builder +from cppython.core.resolution import PluginBuildData +from cppython.core.schema import ( CPPythonLocalConfiguration, PEP621Configuration, ProjectConfiguration, ) -from pytest_cppython.mock.generator import MockGenerator -from pytest_cppython.mock.provider import MockProvider -from pytest_cppython.mock.scm import MockSCM - -from cppython.builder import Builder from cppython.data import Data +from cppython.test.mock.generator import MockGenerator +from cppython.test.mock.provider import MockProvider +from cppython.test.mock.scm import MockSCM class TestData: """Various tests for the Data type""" + @staticmethod @pytest.fixture( - name="data", - scope="session", + name='data', + scope='session', ) def fixture_data( - self, project_configuration: ProjectConfiguration, pep621_configuration: PEP621Configuration, cppython_local_configuration: CPPythonLocalConfiguration, ) -> Data: - """Creates a mock plugins fixture. We want all the plugins to use the same data variants at the same time, so we have to resolve data inside the fixture instead of using other data fixtures + """Creates a mock plugins fixture. + + We want all the plugins to use the same data variants at the same time, so we + have to resolve data inside the fixture instead of using other data fixtures Args: project_configuration: Variant fixture for the project configuration @@ -40,8 +42,8 @@ def fixture_data( Returns: The mock plugins fixture - """ + """ logger = logging.getLogger() builder = Builder(project_configuration, logger) @@ -49,7 +51,8 @@ def fixture_data( return builder.build(pep621_configuration, cppython_local_configuration, plugin_build_data) - def test_sync(self, data: Data) -> None: + @staticmethod + def test_sync(data: Data) -> None: """Verifies that the sync method executes without error Args: diff --git a/tests/unit/test_interface.py b/tests/unit/test_interface.py deleted file mode 100644 index 396ed28..0000000 --- a/tests/unit/test_interface.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Tests the click interface type""" - -from click.testing import CliRunner - -from cppython.console.interface import cli - - -class TestInterface: - """Various tests for the click interface""" - - def test_info(self, cli_runner: CliRunner) -> None: - """Verifies that the info command functions with CPPython hooks - - Args: - cli_runner: The click runner - """ - - result = cli_runner.invoke(cli, ["info"], catch_exceptions=False) - assert result.exit_code == 0 - - def test_list(self, cli_runner: CliRunner) -> None: - """Verifies that the list command functions with CPPython hooks - - Args: - cli_runner: The click runner - """ - - result = cli_runner.invoke(cli, ["list"], catch_exceptions=False) - assert result.exit_code == 0 - - def test_update(self, cli_runner: CliRunner) -> None: - """Verifies that the update command functions with CPPython hooks - - Args: - cli_runner: The click runner - """ - - result = cli_runner.invoke(cli, ["update"], catch_exceptions=False) - assert result.exit_code == 0 - - def test_install(self, cli_runner: CliRunner) -> None: - """Verifies that the install command functions with CPPython hooks - - Args: - cli_runner: The click runner - """ - - result = cli_runner.invoke(cli, ["install"], catch_exceptions=False) - assert result.exit_code == 0 diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 73c4e56..3bc8315 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -1,55 +1,60 @@ """Tests the Project type""" +import tomllib +from importlib import metadata from pathlib import Path -import tomlkit -from cppython_core.schema import ( +import pytest +from pytest_mock import MockerFixture + +from cppython.core.schema import ( CPPythonLocalConfiguration, PEP621Configuration, ProjectConfiguration, PyProject, ToolData, ) -from pytest import FixtureRequest -from pytest_cppython.mock.interface import MockInterface - from cppython.project import Project +from cppython.test.mock.generator import MockGenerator +from cppython.test.mock.interface import MockInterface +from cppython.test.mock.provider import MockProvider +from cppython.test.mock.scm import MockSCM -pep621 = PEP621Configuration(name="test-project", version="0.1.0") +pep621 = PEP621Configuration(name='test-project', version='0.1.0') class TestProject: """Various tests for the project object""" - def test_self_construction(self, request: FixtureRequest) -> None: + @staticmethod + def test_self_construction(request: pytest.FixtureRequest) -> None: """The project type should be constructable with this projects configuration Args: request: The pytest request fixture """ - # Use the CPPython directory as the test data - file = request.config.rootpath / "pyproject.toml" + file = request.config.rootpath / 'pyproject.toml' project_configuration = ProjectConfiguration(pyproject_file=file, version=None) interface = MockInterface() - pyproject_data = tomlkit.loads(file.read_text(encoding="utf-8")) + pyproject_data = tomllib.loads(file.read_text(encoding='utf-8')) project = Project(project_configuration, interface, pyproject_data) # Doesn't have the cppython table assert not project.enabled - def test_missing_tool_table(self, tmp_path: Path) -> None: + @staticmethod + def test_missing_tool_table(tmp_path: Path) -> None: """The project type should be constructable without the tool table Args: tmp_path: Temporary directory for dummy data """ + file_path = tmp_path / 'pyproject.toml' - file_path = tmp_path / "pyproject.toml" - - with open(file_path, "a", encoding="utf8") as file: - file.write("") + with open(file_path, 'a', encoding='utf8'): + pass project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) interface = MockInterface() @@ -59,17 +64,17 @@ def test_missing_tool_table(self, tmp_path: Path) -> None: assert not project.enabled - def test_missing_cppython_table(self, tmp_path: Path) -> None: + @staticmethod + def test_missing_cppython_table(tmp_path: Path) -> None: """The project type should be constructable without the cppython table Args: tmp_path: Temporary directory for dummy data """ + file_path = tmp_path / 'pyproject.toml' - file_path = tmp_path / "pyproject.toml" - - with open(file_path, "a", encoding="utf8") as file: - file.write("") + with open(file_path, 'a', encoding='utf8'): + pass project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) interface = MockInterface() @@ -80,17 +85,25 @@ def test_missing_cppython_table(self, tmp_path: Path) -> None: assert not project.enabled - def test_default_cppython_table(self, tmp_path: Path) -> None: + @staticmethod + def test_default_cppython_table(tmp_path: Path, mocker: MockerFixture) -> None: """The project type should be constructable with the default cppython table Args: tmp_path: Temporary directory for dummy data + mocker: Pytest mocker fixture """ + mocker.patch.object( + metadata, + 'entry_points', + return_value=[metadata.EntryPoint(name='mock', value='mock', group='mock')], + ) + mocker.patch.object(metadata.EntryPoint, 'load', side_effect=[MockGenerator, MockProvider, MockSCM]) - file_path = tmp_path / "pyproject.toml" + file_path = tmp_path / 'pyproject.toml' - with open(file_path, "a", encoding="utf8") as file: - file.write("") + with open(file_path, 'a', encoding='utf8'): + pass project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) interface = MockInterface() diff --git a/tests/unit/utility/__init__.py b/tests/unit/utility/__init__.py new file mode 100644 index 0000000..001920a --- /dev/null +++ b/tests/unit/utility/__init__.py @@ -0,0 +1,6 @@ +"""Unit tests for the utility functions in the CPPython project. + +This module contains tests for various utility functions, including subprocess +calls, logging, and name canonicalization. The tests ensure that the utility +functions behave as expected under different conditions. +""" diff --git a/tests/unit/utility/test_plugin.py b/tests/unit/utility/test_plugin.py new file mode 100644 index 0000000..5b2b258 --- /dev/null +++ b/tests/unit/utility/test_plugin.py @@ -0,0 +1,17 @@ +"""This module tests the plugin functionality""" + +from cppython.utility.plugin import Plugin + + +class MockPlugin(Plugin): + """A mock plugin""" + + +class TestPlugin: + """Tests the plugin functionality""" + + @staticmethod + def test_plugin() -> None: + """Test that the plugin functionality works""" + assert MockPlugin.name() == 'mock' + assert MockPlugin.group() == 'plugin' diff --git a/tests/unit/utility/test_utility.py b/tests/unit/utility/test_utility.py new file mode 100644 index 0000000..93692c5 --- /dev/null +++ b/tests/unit/utility/test_utility.py @@ -0,0 +1,218 @@ +"""Tests the scope of utilities""" + +import logging +from logging import StreamHandler +from pathlib import Path +from sys import executable +from typing import NamedTuple + +import pytest + +from cppython.utility.exception import ProcessError +from cppython.utility.subprocess import call +from cppython.utility.utility import canonicalize_name + +cppython_logger = logging.getLogger('cppython') +cppython_logger.addHandler(StreamHandler()) + + +class TestUtility: + """Tests the utility functionality""" + + class ModelTest(NamedTuple): + """Model definition to help test IO utilities""" + + test_path: Path + test_int: int + + @staticmethod + def test_none() -> None: + """Verifies that no exception is thrown with an empty string""" + test = canonicalize_name('') + + assert not test.group + assert not test.name + + @staticmethod + def test_only_group() -> None: + """Verifies that no exception is thrown when only a group is specified""" + test = canonicalize_name('Group') + + assert test.group == 'group' + assert not test.name + + @staticmethod + def test_name_group() -> None: + """Test that canonicalization works""" + test = canonicalize_name('NameGroup') + + assert test.group == 'group' + assert test.name == 'name' + + @staticmethod + def test_group_only_caps() -> None: + """Test that canonicalization works""" + test = canonicalize_name('NameGROUP') + + assert test.group == 'group' + assert test.name == 'name' + + @staticmethod + def test_name_only_caps() -> None: + """Test that canonicalization works""" + test = canonicalize_name('NAMEGroup') + assert test.group == 'group' + assert test.name == 'name' + + @staticmethod + def test_name_multi_caps() -> None: + """Test that caps works""" + test = canonicalize_name('NAmeGroup') + assert test.group == 'group' + assert test.name == 'name' + + +class TestSubprocess: + """Subprocess testing""" + + @staticmethod + def test_subprocess_stdout(caplog: pytest.LogCaptureFixture) -> None: + """Test subprocess_call + + Args: + caplog: Fixture for capturing logging input + """ + python = Path(executable) + + with caplog.at_level(logging.INFO): + call( + [python, '-c', "import sys; print('Test Out', file = sys.stdout)"], + cppython_logger, + ) + + assert len(caplog.records) == 1 + assert caplog.records[0].message == 'Test Out' + + @staticmethod + def test_subprocess_stderr(caplog: pytest.LogCaptureFixture) -> None: + """Test subprocess_call + + Args: + caplog: Fixture for capturing logging input + """ + python = Path(executable) + + with caplog.at_level(logging.INFO): + call( + [python, '-c', "import sys; print('Test Error', file = sys.stderr)"], + cppython_logger, + ) + + assert len(caplog.records) == 1 + assert caplog.records[0].message == 'Test Error' + + @staticmethod + def test_subprocess_suppression(caplog: pytest.LogCaptureFixture) -> None: + """Test subprocess_call suppression flag + + Args: + caplog: Fixture for capturing logging input + """ + python = Path(executable) + + with caplog.at_level(logging.INFO): + call( + [python, '-c', "import sys; print('Test Out', file = sys.stdout)"], + cppython_logger, + suppress=True, + ) + assert len(caplog.records) == 0 + + @staticmethod + def test_subprocess_exit(caplog: pytest.LogCaptureFixture) -> None: + """Test subprocess_call exception output + + Args: + caplog: Fixture for capturing logging input + """ + 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')"], + cppython_logger, + ) + + assert len(caplog.records) == 1 + assert caplog.records[0].message == 'Test Exit Output' + + assert 'Subprocess task failed' in str(exec_info.value) + + @staticmethod + def test_subprocess_exception(caplog: pytest.LogCaptureFixture) -> None: + """Test subprocess_call exception output + + Args: + caplog: Fixture for capturing logging input + """ + 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')"], + cppython_logger, + ) + assert len(caplog.records) == 1 + assert caplog.records[0].message == 'Test Exception Output' + + assert 'Subprocess task failed' in str(exec_info.value) + + @staticmethod + def test_stderr_exception(caplog: pytest.LogCaptureFixture) -> None: + """Verify print and exit + + Args: + caplog: Fixture for capturing logging input + """ + python = Path(executable) + with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): + call( + [ + python, + '-c', + "import sys; print('Test Out', file = sys.stdout); sys.exit('Test Exit Out')", + ], + cppython_logger, + ) + + LOG_COUNT = 2 + assert len(caplog.records) == LOG_COUNT + assert caplog.records[0].message == 'Test Out' + assert caplog.records[1].message == 'Test Exit Out' + + assert 'Subprocess task failed' in str(exec_info.value) + + @staticmethod + def test_stdout_exception(caplog: pytest.LogCaptureFixture) -> None: + """Verify print and exit + + Args: + caplog: Fixture for capturing logging input + """ + python = Path(executable) + with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): + call( + [ + python, + '-c', + "import sys; print('Test Error', file = sys.stderr); sys.exit('Test Exit Error')", + ], + cppython_logger, + ) + + LOG_COUNT = 2 + assert len(caplog.records) == LOG_COUNT + assert caplog.records[0].message == 'Test Error' + assert caplog.records[1].message == 'Test Exit Error' + + assert 'Subprocess task failed' in str(exec_info.value)