You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

cppython

Package Overview
Dependencies
Maintainers
1
Versions
121
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cppython - pypi Package Compare versions

Comparing version
0.7.1.dev35
to
0.7.1.dev36
+115
cppython/console/entry.py
"""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)
+7
-1

@@ -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.
"""

@@ -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)

@@ -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 @@

@@ -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,

@@ -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