cppython
Advanced tools
| """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""" |
| """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 |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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 |
| """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 |
| """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 | ||
| """ |
| """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 |
| """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 |
| """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) |
| """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' |
| """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. | ||
| """ |
| """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. | ||
| """ |
| """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) |
| """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) |
| """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) |
| """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')] |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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() |
| """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. | ||
| """ |
| """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 |
| """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, | ||
| ) |
| """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')] = [] |
| """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. | ||
| """ |
| """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. | ||
| """ |
| """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""" |
| """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""" |
| """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 |
| """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' |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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() |
| """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. | ||
| """ |
| """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 |
| """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 |
| """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') |
| """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]|(?<!\A)[A-Z](?=[a-z]))') | ||
| def canonicalize_name(name: str) -> 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__) |
| """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/<directory> where <directory> is the fixture suffix | ||
| build_data_path = metafunc.config.rootpath / 'tests' / 'build' / directory | ||
| metafunc.parametrize(fixture, [build_data_path], scope='session') |
| """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. | ||
| """ |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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 |
| """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 |
| """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 |
| """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. | ||
| """ |
| """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) |
| """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) |
| """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']) |
| """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. | ||
| """ |
| """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. | ||
| """ |
| """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) |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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 |
| """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. | ||
| """ |
| """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 |
| """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 |
| """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. | ||
| """ |
| """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() |
| """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 |
| """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) |
| """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 |
| """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. | ||
| """ |
| """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' |
| """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) |
@@ -1,1 +0,7 @@ | ||
| | ||
| """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. | ||
| """ |
+46
-54
| """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 | ||
@@ -9,7 +9,6 @@ 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.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, | ||
@@ -25,3 +24,3 @@ PluginCPPythonData, | ||
| ) | ||
| from cppython_core.schema import ( | ||
| from cppython.core.schema import ( | ||
| CoreData, | ||
@@ -37,4 +36,5 @@ CorePluginData, | ||
| ) | ||
| from cppython.data import Data, Plugins | ||
| from cppython.defaults import DefaultSCM | ||
| from cppython.utility.exception import PluginError | ||
@@ -46,3 +46,3 @@ | ||
| def __init__(self, project_configuration: ProjectConfiguration, logger: Logger) -> None: | ||
| """Initializes the resolver""" | ||
| self._project_configuration = project_configuration | ||
@@ -63,3 +63,2 @@ self._logger = logger | ||
| """ | ||
| raw_generator_plugins = self.find_generators() | ||
@@ -69,3 +68,3 @@ generator_plugins = self.filter_plugins( | ||
| cppython_local_configuration.generator_name, | ||
| "Generator", | ||
| 'Generator', | ||
| ) | ||
@@ -77,3 +76,3 @@ | ||
| cppython_local_configuration.provider_name, | ||
| "Provider", | ||
| 'Provider', | ||
| ) | ||
@@ -90,3 +89,4 @@ | ||
| 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 | ||
@@ -100,3 +100,2 @@ | ||
| """ | ||
| return PluginCPPythonData( | ||
@@ -108,4 +107,5 @@ generator_name=plugin_build_data.generator_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: | ||
@@ -124,3 +124,4 @@ """Generates the PEP621 data from configuration sources | ||
| def resolve_global_config(self) -> CPPythonGlobalConfiguration: | ||
| @staticmethod | ||
| def resolve_global_config() -> CPPythonGlobalConfiguration: | ||
| """Generates the global configuration object | ||
@@ -131,3 +132,2 @@ | ||
| """ | ||
| return CPPythonGlobalConfiguration() | ||
@@ -144,8 +144,7 @@ | ||
| """ | ||
| 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() | ||
@@ -158,7 +157,7 @@ if not issubclass(loaded_type, Generator): | ||
| 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') | ||
@@ -176,8 +175,7 @@ return 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() | ||
@@ -190,7 +188,7 @@ if not issubclass(loaded_type, Provider): | ||
| 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') | ||
@@ -208,8 +206,7 @@ return 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() | ||
@@ -222,13 +219,13 @@ if not issubclass(loaded_type, SCM): | ||
| 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 | ||
@@ -247,3 +244,2 @@ | ||
| """ | ||
| # Lookup the requested plugin if given | ||
@@ -254,3 +250,3 @@ if pinned_name is not None: | ||
| 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)}' | ||
| ) | ||
@@ -266,3 +262,3 @@ return [loaded_type] | ||
| 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)}' | ||
| ) | ||
@@ -273,3 +269,3 @@ supported_types.append(loaded_type) | ||
| 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.') | ||
@@ -285,9 +281,5 @@ return supported_types | ||
| 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: | ||
@@ -297,6 +289,9 @@ if scm_type.features(project_data.pyproject_file.parent).repository: | ||
| 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]]: | ||
@@ -315,3 +310,2 @@ """Selects the first generator and provider that can work together | ||
| """ | ||
| combos: list[tuple[type[Generator], type[Provider]]] = [] | ||
@@ -328,8 +322,8 @@ | ||
| 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, | ||
@@ -347,3 +341,2 @@ scm_type: type[SCM], | ||
| """ | ||
| cppython_plugin_data = resolve_cppython_plugin(core_data.cppython_data, scm_type) | ||
@@ -374,3 +367,2 @@ scm_data = resolve_scm(core_data.project_data, cppython_plugin_data) | ||
| """ | ||
| cppython_plugin_data = resolve_cppython_plugin(core_data.cppython_data, generator_type) | ||
@@ -411,3 +403,2 @@ | ||
| """ | ||
| cppython_plugin_data = resolve_cppython_plugin(core_data.cppython_data, provider_type) | ||
@@ -435,2 +426,3 @@ | ||
| def __init__(self, project_configuration: ProjectConfiguration, logger: Logger) -> None: | ||
| """Initializes the builder""" | ||
| self._project_configuration = project_configuration | ||
@@ -446,3 +438,3 @@ self._logger = logger | ||
| self._logger.info("Logging setup complete") | ||
| self._logger.info('Logging setup complete') | ||
@@ -462,3 +454,4 @@ self._resolver = Resolver(self._project_configuration, self._logger) | ||
| 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 | ||
@@ -468,3 +461,2 @@ Returns: | ||
| """ | ||
| project_data = resolve_project_configuration(self._project_configuration) | ||
@@ -471,0 +463,0 @@ |
@@ -1,1 +0,6 @@ | ||
| | ||
| """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. | ||
| """ |
+7
-7
@@ -6,7 +6,7 @@ """Defines the post-construction data management for CPPython""" | ||
| 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 | ||
@@ -27,2 +27,3 @@ | ||
| def __init__(self, core_data: CoreData, plugins: Plugins, logger: Logger) -> None: | ||
| """Initializes the data""" | ||
| self._core_data = core_data | ||
@@ -43,3 +44,2 @@ self._plugins = plugins | ||
| """ | ||
| if (sync_data := self.plugins.provider.sync_data(self.plugins.generator)) is None: | ||
@@ -58,3 +58,3 @@ raise PluginError("The provider doesn't support the generator") | ||
| 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) |
+17
-17
@@ -7,7 +7,6 @@ """Manages data flow to and from plugins""" | ||
| 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 | ||
@@ -22,9 +21,10 @@ | ||
| ) -> 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') | ||
@@ -45,3 +45,3 @@ try: | ||
| self.logger.info("Initialized project successfully") | ||
| self.logger.info('Initialized project successfully') | ||
@@ -64,10 +64,10 @@ @property | ||
| 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()) | ||
@@ -77,3 +77,3 @@ try: | ||
| 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 | ||
@@ -90,10 +90,10 @@ | ||
| 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()) | ||
@@ -103,5 +103,5 @@ try: | ||
| 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() |
@@ -18,3 +18,2 @@ """Project schema specifications""" | ||
| """Updates project dependencies""" | ||
| raise NotImplementedError() |
+14
-10
| Metadata-Version: 2.1 | ||
| Name: cppython | ||
| Version: 0.7.1.dev35 | ||
| Version: 0.7.1.dev36 | ||
| Summary: A Python management solution for C++ dependencies | ||
| Home-page: https://github.com/Synodic-Software/CPPython | ||
| Author-Email: Synodic Software <contact@synodic.software> | ||
| License: MIT | ||
| Project-URL: Homepage, https://github.com/Synodic-Software/CPPython | ||
| Project-URL: Repository, https://github.com/Synodic-Software/CPPython | ||
| Requires-Python: >=3.12 | ||
| Requires-Dist: click>=8.1.3 | ||
| Requires-Dist: tomlkit>=0.12.4 | ||
| Requires-Dist: cppython-core>=0.4.1.dev19 | ||
| Requires-Dist: pydantic>=2.6.3 | ||
| Requires-Dist: packaging>=21.3 | ||
| Project-URL: homepage, https://github.com/Synodic-Software/CPPython | ||
| Project-URL: repository, https://github.com/Synodic-Software/CPPython | ||
| Requires-Python: >=3.13 | ||
| Requires-Dist: typer>=0.13.1 | ||
| Requires-Dist: pydantic>=2.8.2 | ||
| Requires-Dist: packaging>=24.1 | ||
| Provides-Extra: pytest | ||
| Requires-Dist: pytest>=8.3.3; extra == "pytest" | ||
| Requires-Dist: pytest-mock>=3.14.0; extra == "pytest" | ||
| Provides-Extra: git | ||
| Requires-Dist: dulwich>=0.22.5; extra == "git" | ||
| Provides-Extra: pdm | ||
| Requires-Dist: pdm>=2.20.1; extra == "pdm" | ||
| Description-Content-Type: text/markdown | ||
@@ -17,0 +21,0 @@ |
+95
-73
@@ -9,11 +9,9 @@ [project] | ||
| dynamic = [] | ||
| requires-python = ">=3.12" | ||
| requires-python = ">=3.13" | ||
| dependencies = [ | ||
| "click>=8.1.3", | ||
| "tomlkit>=0.12.4", | ||
| "cppython-core>=0.4.1.dev19", | ||
| "pydantic>=2.6.3", | ||
| "packaging>=21.3", | ||
| "typer>=0.13.1", | ||
| "pydantic>=2.8.2", | ||
| "packaging>=24.1", | ||
| ] | ||
| version = "0.7.1.dev35" | ||
| version = "0.7.1.dev36" | ||
@@ -23,6 +21,13 @@ [project.license] | ||
| [project.license-files] | ||
| paths = [ | ||
| "LICENSE.md", | ||
| [project.optional-dependencies] | ||
| pytest = [ | ||
| "pytest>=8.3.3", | ||
| "pytest-mock>=3.14.0", | ||
| ] | ||
| git = [ | ||
| "dulwich>=0.22.5", | ||
| ] | ||
| pdm = [ | ||
| "pdm>=2.20.1", | ||
| ] | ||
@@ -33,5 +38,81 @@ [project.urls] | ||
| [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" | ||
| [project.scripts] | ||
| cppython = "cppython.console.interface:cli" | ||
| cppython = "cppython.console.entry:app" | ||
| [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", | ||
| ] | ||
| [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", | ||
| "F", | ||
| "I", | ||
| "PL", | ||
| "UP", | ||
| "E", | ||
| "B", | ||
| "SIM", | ||
| "PT", | ||
| ] | ||
| [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] | ||
@@ -45,22 +126,7 @@ update = [ | ||
| [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", | ||
| ] | ||
| [tool.pdm.scripts.analyze] | ||
| shell = "pylint --verbose cppython tests" | ||
| shell = "ruff check cppython tests" | ||
| [tool.pdm.scripts.format] | ||
| shell = "black --check --verbose ." | ||
| shell = "ruff format" | ||
@@ -76,3 +142,3 @@ [tool.pdm.scripts.lint] | ||
| [tool.pdm.scripts.sort-imports] | ||
| shell = "isort --check-only --diff --verbose ." | ||
| shell = "isort --check-only --diff ." | ||
@@ -85,46 +151,2 @@ [tool.pdm.scripts.test] | ||
| [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] | ||
@@ -131,0 +153,0 @@ build-backend = "pdm.backend" |
@@ -1,1 +0,7 @@ | ||
| | ||
| """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. | ||
| """ |
@@ -1,1 +0,7 @@ | ||
| | ||
| """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. | ||
| """ |
@@ -1,1 +0,7 @@ | ||
| | ||
| """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. | ||
| """ |
| """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, | ||
@@ -12,14 +15,16 @@ PEP621Configuration, | ||
| ) | ||
| from cppython.test.mock.generator import MockGenerator | ||
| from cppython.test.mock.provider import MockProvider | ||
| from cppython.test.mock.scm import MockSCM | ||
| from cppython.builder import Builder, Resolver | ||
| 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: | ||
@@ -32,2 +37,3 @@ """Verifies that the builder can build a project with all test variants | ||
| cppython_local_configuration: Variant fixture for cppython configuration | ||
| mocker: Pytest mocker fixture | ||
| """ | ||
@@ -37,2 +43,9 @@ logger = logging.getLogger() | ||
| 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) | ||
@@ -44,4 +57,4 @@ | ||
| @staticmethod | ||
| def test_generate_plugins( | ||
| self, | ||
| project_configuration: ProjectConfiguration, | ||
@@ -48,0 +61,0 @@ cppython_local_configuration: CPPythonLocalConfiguration, |
+17
-14
@@ -6,5 +6,6 @@ """Tests the Data type""" | ||
| 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, | ||
@@ -14,8 +15,6 @@ PEP621Configuration, | ||
| ) | ||
| 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 | ||
@@ -26,8 +25,8 @@ | ||
| @staticmethod | ||
| @pytest.fixture( | ||
| name="data", | ||
| scope="session", | ||
| name='data', | ||
| scope='session', | ||
| ) | ||
| def fixture_data( | ||
| self, | ||
| project_configuration: ProjectConfiguration, | ||
@@ -37,4 +36,7 @@ pep621_configuration: PEP621Configuration, | ||
| ) -> 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: | ||
@@ -47,4 +49,4 @@ project_configuration: Variant fixture for the project configuration | ||
| The mock plugins fixture | ||
| """ | ||
| logger = logging.getLogger() | ||
@@ -57,3 +59,4 @@ builder = Builder(project_configuration, logger) | ||
| def test_sync(self, data: Data) -> None: | ||
| @staticmethod | ||
| def test_sync(data: Data) -> None: | ||
| """Verifies that the sync method executes without error | ||
@@ -60,0 +63,0 @@ |
| """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, | ||
@@ -13,8 +17,9 @@ PEP621Configuration, | ||
| ) | ||
| 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') | ||
@@ -25,3 +30,4 @@ | ||
| 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 | ||
@@ -32,9 +38,8 @@ | ||
| """ | ||
| # 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) | ||
@@ -45,3 +50,4 @@ | ||
| 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 | ||
@@ -52,8 +58,7 @@ | ||
| """ | ||
| file_path = tmp_path / 'pyproject.toml' | ||
| file_path = tmp_path / "pyproject.toml" | ||
| with open(file_path, 'a', encoding='utf8'): | ||
| pass | ||
| with open(file_path, "a", encoding="utf8") as file: | ||
| file.write("") | ||
| project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) | ||
@@ -67,3 +72,4 @@ interface = MockInterface() | ||
| 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 | ||
@@ -74,8 +80,7 @@ | ||
| """ | ||
| file_path = tmp_path / 'pyproject.toml' | ||
| file_path = tmp_path / "pyproject.toml" | ||
| with open(file_path, 'a', encoding='utf8'): | ||
| pass | ||
| with open(file_path, "a", encoding="utf8") as file: | ||
| file.write("") | ||
| project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) | ||
@@ -90,3 +95,4 @@ interface = MockInterface() | ||
| 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 | ||
@@ -96,8 +102,15 @@ | ||
| 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 | ||
@@ -104,0 +117,0 @@ project_configuration = ProjectConfiguration(pyproject_file=file_path, version=None) |
| """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""" |
| """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 |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
177594
372.56%94
394.74%4127
435.97%