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.dev8
to
0.7.1.dev9
+1
cppython/plugins/__init__.py

"""Git VCS plugin
"""
from pathlib import Path
from cppython_core.schema import VersionControl
from dulwich.errors import NotGitRepository
from dulwich.repo import Repo
class Git(VersionControl):
"""Git implementation hooks"""
@staticmethod
def name() -> str:
"""The VCS name
Returns:
The name
"""
return "git"
def is_repository(self, path: Path) -> bool:
"""Queries repository status of a path
Args:
path: The input path to query
Returns:
Whether the given path is a repository root
"""
try:
Repo(str(path))
return True
except NotGitRepository:
return False
def extract_version(self, path: Path) -> str:
"""Extracts the system's version metadata
Args:
path: The repository path
Returns:
A version
"""
return "0.1.0"
"""Project test fixtures for all cppython tests
"""
from typing import Any
import pytest
from cppython_core.schema import PEP621, CPPythonData, PyProject, ToolData
from pytest_cppython.fixtures import CPPythonFixtures
from pytest_cppython.mock import MockProviderData
class CPPythonProjectFixtures(CPPythonFixtures):
"""Additional fixtures to help test projects"""
@pytest.fixture(name="tool", scope="session")
def fixture_tool(self, cppython: CPPythonData) -> ToolData:
"""The tool data
Args:
cppython: The parameterized cppython table
Returns:
Wrapped CPPython data
"""
return ToolData(cppython=cppython)
@pytest.fixture(name="project", scope="session")
def fixture_project(self, tool: ToolData, pep621: PEP621) -> PyProject:
"""Parameterized construction of PyProject data
Args:
tool: The tool table with internal cppython data
pep621: The project table
Returns:
All the data as one object
"""
return PyProject(project=pep621, tool=tool)
@pytest.fixture(name="mock_project", scope="session")
def fixture_mock_project(self, project: PyProject) -> dict[str, Any]:
"""Extension of the 'project' fixture with mock data attached
Args:
project: The input project
Returns:
All the data as a dictionary
"""
mocked_pyproject = project.dict(by_alias=True)
mocked_pyproject["tool"]["cppython"]["mock"] = MockProviderData()
return mocked_pyproject
"""Tests the cppython built-in VCS plugin
"""
from pytest_cppython.plugin import VersionControlIntegrationTests
from cppython.plugins.git import Git
class TestGitInterface(VersionControlIntegrationTests[Git]):
"""Integration tests for the Git VCS plugin"""
"""Unit tests for the cppython VCS plugin
"""
import pytest
from pytest_cppython.plugin import VersionControlUnitTests
from cppython.plugins.git import Git
class TestGitInterface(VersionControlUnitTests[Git]):
"""Unit tests for the Git VCS plugin"""
@pytest.fixture(name="version_control_type")
def fixture_version_control_type(self) -> type[Git]:
"""A required testing hook that allows type generation
Returns:
The VCS type
"""
return Git
+114
-105

@@ -0,8 +1,8 @@

"""Everything needed to build a CPPython project
"""
TODO
"""
from collections.abc import Sequence
from importlib import metadata
from pathlib import Path
from typing import Type
from logging import Logger
from typing import Any

@@ -12,7 +12,9 @@ from cppython_core.schema import (

CPPythonDataResolved,
Generator,
GeneratorConfiguration,
PEP621Resolved,
PluginT,
Plugin,
ProjectConfiguration,
Provider,
ProviderConfiguration,
ProviderDataResolvedT,
ProviderDataT,
PyProject,

@@ -23,33 +25,88 @@ ToolData,

from cppython.schema import CMakePresets, ConfigurePreset
from cppython.utility import read_json, write_json, write_model_json
class PluginBuilder:
"""Collection of utilities to collect and build plugins"""
class Builder:
"""
TODO
"""
def __init__(self, group: str, logger: Logger) -> None:
self._group = group
self._logger = logger
def __init__(self, configuration: ProjectConfiguration) -> None:
self.configuration = configuration
def gather_entries(self) -> list[metadata.EntryPoint]:
"""Gather all the available entry points for the grouping
def gather_plugins(self, plugin_type: Type[PluginT]) -> list[Type[PluginT]]:
Returns:
List of entries
"""
TODO
return list(metadata.entry_points(group=f"cppython.{self._group}"))
def load(self, entry_points: list[metadata.EntryPoint]) -> list[type[Plugin]]:
"""Loads a set of entry points
Args:
entry_points: The entry points to load
Raises:
TypeError: If an entry point is not a subclass of the 'Plugin' type
Returns:
List of plugin types
"""
plugins = []
entry_points = metadata.entry_points(group=f"cppython.{plugin_type.group()}")
for entry_point in entry_points:
loaded_plugin_type = entry_point.load()
if issubclass(loaded_plugin_type, plugin_type) & (loaded_plugin_type is not plugin_type):
plugins.append(loaded_plugin_type)
plugin = entry_point.load()
if not issubclass(plugin, Plugin):
raise TypeError("The CPPython plugin must be an instance of Plugin")
plugins.append(plugin)
return plugins
def generate_model(self, plugins: list[Type[Generator]]) -> Type[PyProject]:
class Builder:
"""Helper class for building CPPython projects"""
def __init__(self, configuration: ProjectConfiguration, logger: Logger) -> None:
self.configuration = configuration
self.logger = logger
def discover_providers(self) -> list[type[Provider[Any, Any]]]:
"""Discovers Provider plugin types
Raises:
TypeError: Raised if the Plugin type is not subclass of 'Provider'
Returns:
List of Provider types
"""
TODO: Proper return type hint
provider_builder = PluginBuilder(Provider.group(), self.logger)
# Gather provider entry points without any filtering
provider_entry_points = provider_builder.gather_entries()
provider_types = provider_builder.load(provider_entry_points)
plugins = []
for provider_type in provider_types:
if not issubclass(provider_type, Provider):
raise TypeError("The CPPython plugin must be an instance of Plugin")
plugins.append(provider_type)
return plugins
def generate_model(
self, plugins: Sequence[type[Provider[ProviderDataT, ProviderDataResolvedT]]]
) -> type[PyProject]:
"""Constructs a dynamic type that contains plugin specific data requirements
Args:
plugins: List of Provider types
Returns:
An extended PyProject type containing dynamic plugin data requirements
"""
plugin_fields = {}
plugin_fields: dict[str, Any] = {}
for plugin_type in plugins:

@@ -76,8 +133,15 @@ plugin_fields[plugin_type.name()] = (plugin_type.data_type(), ...)

def generate_resolved_cppython_model(self, plugins: list[Type[Generator]]) -> Type[CPPythonDataResolved]:
def generate_resolved_cppython_model(
self, plugins: Sequence[type[Provider[ProviderDataT, ProviderDataResolvedT]]]
) -> type[CPPythonDataResolved]:
"""Constructs a dynamic resolved type that contains plugin specific data requirements
Args:
plugins: List of Provider types
Returns:
An extended CPPython resolved type containing dynamic plugin data requirements
"""
TODO
"""
plugin_fields = {}
plugin_fields: dict[str, Any] = {}
for plugin_type in plugins:

@@ -94,87 +158,32 @@ # The unresolved type is still appended to the CPPythonDataResolved type

def create_generators(
def create_providers(
self,
plugins: list[Type[Generator]],
plugins: Sequence[type[Provider[ProviderDataT, ProviderDataResolvedT]]],
project_configuration: ProjectConfiguration,
configuration: GeneratorConfiguration,
project: PEP621Resolved,
cppython: CPPythonDataResolved,
) -> list[Generator]:
"""
TODO
"""
_generators = []
for plugin_type in plugins:
name = plugin_type.name()
generator_data = getattr(cppython, name)
resolved_generator_data = generator_data.resolve(project_configuration)
_generators.append(plugin_type(configuration, project, cppython, resolved_generator_data))
configuration: ProviderConfiguration,
static_resolved_project_data: tuple[PEP621Resolved, CPPythonDataResolved],
) -> list[Provider[ProviderDataT, ProviderDataResolvedT]]:
"""Creates Providers from input data
return _generators
Args:
plugins: List of Provider plugins to construct
project_configuration: Project configuration data
configuration: Provider configuration data
static_resolved_project_data: Resolved project data
def write_presets(self, path: Path, generator_output: list[tuple[str, ConfigurePreset]]) -> Path:
Returns:
List of constructed providers
"""
Write the cppython presets.
Returns the
"""
path.mkdir(parents=True, exist_ok=True)
project, cppython = static_resolved_project_data
def write_generator_presets(path: Path, generator_name: str, configure_preset: ConfigurePreset) -> Path:
"""
Write a generator preset.
@returns - The written json file
"""
generator_tool_path = path / generator_name
generator_tool_path.mkdir(parents=True, exist_ok=True)
_providers = []
for plugin_type in plugins:
name = plugin_type.name()
provider_data = getattr(cppython, name)
resolved_provider_data = provider_data.resolve(project_configuration)
resolved_cppython_data = cppython.provider_resolve(plugin_type)
configure_preset.hidden = True
presets = CMakePresets(configurePresets=[configure_preset])
_providers.append(plugin_type(configuration, project, resolved_cppython_data, resolved_provider_data))
json_path = generator_tool_path / f"{generator_name}.json"
write_model_json(json_path, presets)
return json_path
names = []
includes = []
path = path / "cppython"
for generator_name, configure_preset in generator_output:
preset_file = write_generator_presets(path, generator_name, configure_preset)
relative_file = preset_file.relative_to(path)
names.append(generator_name)
includes.append(str(relative_file))
configure_preset = ConfigurePreset(name="cppython", hidden=True, inherits=names)
presets = CMakePresets(configurePresets=[configure_preset], include=includes)
json_path = path / "cppython.json"
write_model_json(json_path, presets)
return json_path
def write_root_presets(self, path: Path):
"""
Read the top level json file and replace the include reference.
Receives a relative path to the tool cmake json file
"""
root_preset_path = self.configuration.pyproject_file.parent / "CMakePresets.json"
root_preset = read_json(root_preset_path)
root_model = CMakePresets.parse_obj(root_preset)
if root_model.include is not None:
for index, include_path in enumerate(root_model.include):
if Path(include_path).name == "cppython.json":
root_model.include[index] = "build/" + path.as_posix()
# 'dict.update' wont apply to nested types, manual replacement
root_preset["include"] = root_model.include
write_json(root_preset_path, root_preset)
return _providers

@@ -0,20 +1,27 @@

"""A click CLI for CPPython interfacing
"""
A click CLI for CPPython interfacing
"""
from logging import getLogger
from pathlib import Path
from typing import Any, Type
import click
import tomlkit
from cppython_core.schema import GeneratorDataT, Interface, InterfaceConfiguration
from cppython_core.schema import (
Interface,
InterfaceConfiguration,
ProjectConfiguration,
ProviderDataT,
VersionControl,
)
from cppython.console.vcs.git import Git
from cppython.project import Project, ProjectConfiguration
from cppython.builder import PluginBuilder
from cppython.project import Project
def _find_pyproject_file() -> Path:
"""Searches upward for a pyproject.toml file
Returns:
The found directory
"""
TODO
"""

@@ -35,23 +42,9 @@ # Search for a path upward

def _create_pyproject(path: Path) -> dict[str, Any]:
"""
TODO
"""
class Configuration:
"""Click configuration object"""
# Load file
data = tomlkit.loads(path.read_text(encoding="utf-8"))
# Interpret and validate data
return data
class Config:
"""
The data object that will be expanded alongside 'pass_obj'
"""
def __init__(self):
def __init__(self) -> None:
path = _find_pyproject_file()
file_path = path / "pyproject.toml"
self.pyproject_data = _create_pyproject(file_path)
self.pyproject_data = tomlkit.loads(file_path.read_text(encoding="utf-8"))

@@ -61,24 +54,62 @@ configuration = InterfaceConfiguration()

# TODO: Don't assume git SCM. Implement importing and scm selection
plugin_builder = PluginBuilder("version_control", getLogger())
version = Git().extract_version(path)
self.configuration = ProjectConfiguration(pyproject_file=file_path, version=version.base_version)
# Don't filter entries
entries = plugin_builder.gather_entries()
vcs_types = plugin_builder.load(entries)
plugins: list[type[VersionControl]] = []
# Verify the plugin type
for vcs_type in vcs_types:
if not issubclass(vcs_type, VersionControl):
raise TypeError("The VCS plugin must be an instance of VersionControl")
plugins.append(vcs_type)
# Extract the first plugin that identifies the repository
plugin = None
for plugin_type in plugins:
plugin = plugin_type()
plugin.is_repository(path)
break
if plugin is None:
raise TypeError("No VCS plugin found")
version = plugin.extract_version(path)
self.configuration = ProjectConfiguration(pyproject_file=file_path, version=version)
def create_project(self) -> Project:
"""Creates the project type from input data
Returns:
The project
"""
TODO
"""
return Project(self.configuration, self.interface, self.pyproject_data)
def query_vcs(self) -> str:
"""Queries the VCS system for its version
pass_config = click.make_pass_decorator(Config, ensure=True)
Returns:
The version
"""
return "TODO"
# 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")
@pass_config
def cli(config, verbose: int):
def cli(config: Configuration, verbose: int) -> None:
"""entry_point group for the CLI commands
Args:
config: The CLI configuration object
verbose: The verbosity level
"""
entry_point group for the CLI commands
"""
config.configuration.verbosity = verbose

@@ -89,6 +120,8 @@

@pass_config
def info(config):
def info(config: Configuration) -> None:
"""Prints project information
Args:
config: The CLI configuration object
"""
TODO
"""
config.create_project()

@@ -99,6 +132,8 @@

@pass_config
def install(config):
def install(config: Configuration) -> None:
"""Install API call
Args:
config: The CLI configuration object
"""
TODO
"""
project = config.create_project()

@@ -110,6 +145,8 @@ project.install()

@pass_config
def update(config):
def update(config: Configuration) -> None:
"""Update API call
Args:
config: The CLI configuration object
"""
TODO
"""
project = config.create_project()

@@ -119,33 +156,26 @@ project.update()

@cli.command()
@pass_config
def build(config):
"""
TODO
"""
project = config.create_project()
project.build()
class ConsoleInterface(Interface):
"""
Interface implementation to pass to the project
"""
"""Interface implementation to pass to the project"""
def __init__(self, configuration: InterfaceConfiguration) -> None:
super().__init__(configuration)
@staticmethod
def name() -> str:
"""Returns the name of the interface
Returns:
The name
"""
return "console"
def read_generator_data(self, generator_data_type: Type[GeneratorDataT]) -> GeneratorDataT:
def read_provider_data(self, provider_data_type: type[ProviderDataT]) -> ProviderDataT:
"""Requests provider information
Args:
provider_data_type: The type to construct
Returns:
The constructed provider data type
"""
Requests generator information
"""
return generator_data_type()
return provider_data_type()
def write_pyproject(self) -> None:
"""
Write output
"""
"""Write output"""

@@ -0,5 +1,5 @@

"""Manages data flow to and from plugins
"""
The central delegation of the CPPython project
"""
import asyncio
import logging

@@ -10,7 +10,6 @@ from typing import Any

CPPythonDataResolved,
Generator,
GeneratorConfiguration,
Interface,
PEP621Resolved,
ProjectConfiguration,
ProviderConfiguration,
)

@@ -23,5 +22,3 @@

class Project(API):
"""
The object constructed at each entry_point
"""
"""The object constructed at each entry_point"""

@@ -32,10 +29,9 @@ def __init__(

self._enabled = False
self._configuration = configuration
# Default logging levels
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
# Add default output stream
console_handler = logging.StreamHandler()
self.logger = logging.getLogger("cppython")
self.logger.addHandler(console_handler)
self.logger.addHandler(logging.StreamHandler())
self.logger.setLevel(levels[configuration.verbosity])

@@ -45,16 +41,14 @@

self._builder = Builder(self.configuration)
plugins = self._builder.gather_plugins(Generator)
builder = Builder(configuration, self.logger)
if not plugins:
self.logger.error("No generator plugin was found")
if not (plugins := builder.discover_providers()):
self.logger.error("No provider plugin was found")
return
for plugin in plugins:
self.logger.warning(f"Generator plugin found: {plugin.name()}")
self.logger.warning("Provider plugin found: %s", plugin.name())
extended_pyproject_type = self._builder.generate_model(plugins)
pyproject = extended_pyproject_type(**pyproject_data)
extended_pyproject_type = builder.generate_model(plugins)
if pyproject is None:
if (pyproject := extended_pyproject_type(**pyproject_data)) is None:
self.logger.error("Data is not defined")

@@ -75,11 +69,11 @@ return

resolved_cppython_model = self._builder.generate_resolved_cppython_model(plugins)
self._resolved_project_data = pyproject.project.resolve(self.configuration)
self._resolved_cppython_data = pyproject.tool.cppython.resolve(resolved_cppython_model, self.configuration)
resolved_cppython_model = builder.generate_resolved_cppython_model(plugins)
self._resolved_project_data = pyproject.project.resolve(configuration)
self._resolved_cppython_data = pyproject.tool.cppython.resolve(resolved_cppython_model, configuration)
self._interface = interface
generator_configuration = GeneratorConfiguration(root_directory=self.configuration.pyproject_file.parent)
self._generators = self._builder.create_generators(
plugins, self.configuration, generator_configuration, self.project, self.cppython
provider_configuration = ProviderConfiguration(root_directory=configuration.pyproject_file.parent)
self._providers = builder.create_providers(
plugins, configuration, provider_configuration, (self.project, self.cppython)
)

@@ -91,19 +85,16 @@

def enabled(self) -> bool:
"""Queries if the project was is initialized for full functionality
Returns:
The query result
"""
TODO
"""
return self._enabled
@property
def configuration(self) -> ProjectConfiguration:
"""
TODO
"""
return self._configuration
def project(self) -> PEP621Resolved:
"""Resolved project data
@property
def project(self) -> PEP621Resolved:
Returns:
The resolved 'project' table
"""
The resolved pyproject project table
"""
return self._resolved_project_data

@@ -113,13 +104,13 @@

def cppython(self) -> CPPythonDataResolved:
"""The resolved CPPython data
Returns:
Resolved 'cppython' table
"""
The resolved CPPython data
"""
return self._resolved_cppython_data
def download_generator_tools(self) -> None:
"""
Download the generator tooling if required
"""
async def download_provider_tools(self) -> None:
"""Download the provider tooling if required"""
if not self._enabled:
self.logger.info("Skipping 'download_generator_tools' because the project is not enabled")
self.logger.info("Skipping 'download_provider_tools' because the project is not enabled")
return

@@ -129,38 +120,22 @@

for generator in self._generators:
path = base_path / generator.name()
for provider in self._providers:
path = base_path / provider.name()
path.mkdir(parents=True, exist_ok=True)
if not generator.generator_downloaded(path):
self.logger.warning(f"Downloading the {generator.name()} requirements to {path}")
if not provider.tooling_downloaded(path):
self.logger.warning("Downloading the %s requirements to %s", provider.name(), path)
# TODO: Make async with progress bar
generator.download_generator(path)
await provider.download_tooling(path)
self.logger.warning("Download complete")
else:
self.logger.info(f"The {generator.name()} generator is already downloaded")
self.logger.info("The %s provider is already downloaded", provider.name())
def update_generator_tools(self) -> None:
"""
Update the generator tooling if available
"""
if not self._enabled:
self.logger.info("Skipping 'update_generator_tools' because the project is not enabled")
return
self.download_generator_tools()
base_path = self.cppython.install_path
for generator in self._generators:
path = base_path / generator.name()
generator.update_generator(path)
# API Contract
def install(self) -> None:
"""Installs project dependencies
Raises:
Exception: Raised if failed
"""
TODO
"""
if not self._enabled:

@@ -171,28 +146,21 @@ self.logger.info("Skipping install because the project is not enabled")

self.logger.info("Installing tools")
self.download_generator_tools()
asyncio.run(self.download_provider_tools())
self.logger.info("Installing project")
preset_path = self.cppython.build_path
generator_output = []
for provider in self._providers:
self.logger.info("Installing %s provider", provider.name())
# TODO: Async
for generator in self._generators:
self.logger.info(f"Installing {generator.name()} generator")
try:
generator.install()
config_preset = generator.generate_cmake_config()
generator_output.append((generator.name(), config_preset))
provider.install()
except Exception as exception:
self.logger.error(f"Generator {generator.name()} failed to install")
self.logger.error("Provider %s failed to install", provider.name())
raise exception
project_presets = self._builder.write_presets(preset_path, generator_output)
self._builder.write_root_presets(project_presets.relative_to(preset_path))
def update(self) -> None:
"""Updates project dependencies
def update(self) -> None:
Raises:
Exception: Raised if failed
"""
TODO
"""
if not self._enabled:

@@ -203,23 +171,13 @@ self.logger.info("Skipping update because the project is not enabled")

self.logger.info("Updating tools")
self.update_generator_tools()
asyncio.run(self.download_provider_tools())
self.logger.info("Updating project")
preset_path = self.cppython.build_path
for provider in self._providers:
self.logger.info("Updating %s provider", provider.name())
generator_output = []
# TODO: Async
for generator in self._generators:
self.logger.info(f"Updating {generator.name()} generator")
try:
generator.update()
config_preset = generator.generate_cmake_config()
generator_output.append((generator.name(), config_preset))
provider.update()
except Exception as exception:
self.logger.error(f"Generator {generator.name()} failed to update")
self.logger.error("Provider %s failed to update", provider.name())
raise exception
project_presets = self._builder.write_presets(preset_path, generator_output)
self._builder.write_root_presets(project_presets.relative_to(preset_path))

@@ -0,80 +1,13 @@

"""Project schema specifications
"""
TODO
"""
from __future__ import annotations # Required for self-referenced pydantic types
from abc import abstractmethod
from pathlib import Path
from typing import Any, Optional
from cppython_core.schema import ConfigurePreset, CPPythonModel, Preset
from pydantic import Extra, Field, validator
class BuildPreset(Preset):
"""
Partial Build Preset specification
"""
configurePreset: Optional[str] = Field(default=None)
inheritConfigureEnvironment: Optional[bool] = Field(default=None)
class TestPreset(Preset):
"""
Partial Test Preset specification
"""
configurePreset: Optional[str] = Field(default=None)
inheritConfigureEnvironment: Optional[bool] = Field(default=None)
class CMakeVersion(CPPythonModel, extra=Extra.forbid):
"""
The version specification for CMake
"""
major: int = Field(default=3)
minor: int = Field(default=23)
patch: int = Field(default=1)
class CMakePresets(CPPythonModel, extra=Extra.forbid):
"""
The schema for the CMakePresets and CMakeUserPresets files
"""
version: int = Field(default=4, const=True)
cmakeMinimumRequired: CMakeVersion = Field(default=CMakeVersion()) # TODO: 'version' compatibility validation
include: Optional[list[str]] = Field(default=None)
vendor: Optional[Any] = Field(default=None)
configurePresets: Optional[list[ConfigurePreset]] = Field(default=None)
buildPresets: Optional[list[BuildPreset]] = Field(default=None)
testPresets: Optional[list[TestPreset]] = Field(default=None)
@validator("include")
def validate_path(cls, values): # pylint: disable=E0213
"""
TODO
"""
if values is not None:
output = []
for value in values:
output.append(Path(value).as_posix())
return output
return None
class API:
"""
Project API
"""
"""Project API specification"""
@abstractmethod
def install(self) -> None:
"""
TODO
"""
"""Installs project dependencies"""
raise NotImplementedError()

@@ -84,6 +17,4 @@

def update(self) -> None:
"""
TODO
"""
"""Updates project dependencies"""
raise NotImplementedError()
Metadata-Version: 2.1
Name: cppython
Version: 0.7.1.dev8
Summary: A Python package manager agnostic plugin integrating a transparent Conan and CMake workflow.
Version: 0.7.1.dev9
Summary: A Python management solution for C++ dependencies
License: MIT

@@ -13,3 +13,3 @@ Author-email: Synodic Software <contact@synodic.software>

# CPPython
A library for managing dependencies with CMake for C++ projects.
A Python management solution for C++ dependencies
[project]
description = " A Python package manager agnostic plugin integrating a transparent Conan and CMake workflow."
description = "A Python management solution for C++ dependencies"
name = "cppython"

@@ -19,3 +19,3 @@ license-expression = "MIT"

]
version = "0.7.1.dev8"
version = "0.7.1.dev9"

@@ -31,2 +31,5 @@ [project.license-files]

[project.entry-points."cppython.version_control"]
cmake = "cppython.plugins.git:Git"
[project.scripts]

@@ -43,2 +46,3 @@ cppython = "cppython.console.interface:cli"

"isort>=5.10.1",
"mypy>=0.971",
]

@@ -52,3 +56,27 @@ test = [

[tool.pdm.scripts.analyze]
shell = "pylint --verbose cppython tests"
[tool.pdm.scripts.format]
shell = "black --check --verbose ."
[tool.pdm.scripts.lint]
composite = [
"analyze",
"format",
"sort-imports",
"type-check",
]
[tool.pdm.scripts.sort-imports]
shell = "isort --check-only --diff --verbose ."
[tool.pdm.scripts.test]
shell = "pytest --cov=cppython --verbose tests"
[tool.pdm.scripts.type-check]
shell = "mypy ."
[tool.pytest.ini_options]
log_cli = true
testpaths = [

@@ -64,5 +92,22 @@ "tests",

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.messages_control]
disable = "C0330, C0326, logging-fstring-interpolation"
extension-pkg-whitelist = "pydantic"

@@ -73,2 +118,12 @@

[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]

@@ -75,0 +130,0 @@ build-backend = "pdm.pep517.api"

# CPPython
A library for managing dependencies with CMake for C++ projects.
A Python management solution for C++ dependencies

@@ -0,4 +1,3 @@

"""Test the integrations related to the internal interface implementation and the 'Interface' interface itself
"""
Test the integrations related to the internal interface implementation and the 'Interface' interface itself
"""

@@ -12,12 +11,15 @@ import pytest

class TestCLIInterface(InterfaceIntegrationTests):
"""
The tests for our CLI interface
"""
class TestCLIInterface(InterfaceIntegrationTests[ConsoleInterface]):
"""The tests for our CLI interface"""
@pytest.fixture(name="interface")
def fixture_interface(self):
"""
Override of the plugin provided interface fixture.
def fixture_interface(
self, interface_type: type[ConsoleInterface], interface_configuration: InterfaceConfiguration
) -> ConsoleInterface:
"""Override of the plugin provided interface fixture.
Args:
interface_type: The input interface type
interface_configuration: Interface configuration for construction
Returns:

@@ -24,0 +26,0 @@ ConsoleInterface -- The Interface object to use for the CPPython defined tests

@@ -0,78 +1,35 @@

"""Test the functions related to the internal interface implementation and the 'Interface' interface itself
"""
Test the functions related to the internal interface implementation and the 'Interface' interface itself
"""
from typing import Type
import pytest
from click.testing import CliRunner
from cppython_core.schema import PEP621, CPPythonData, PyProject, TargetEnum, ToolData
from pytest_cppython.plugin import InterfaceUnitTests
from pytest_mock.plugin import MockerFixture
from cppython.console.interface import Config, ConsoleInterface, cli
from cppython.schema import API
from cppython.console.interface import Configuration, ConsoleInterface, cli
from tests.data.fixtures import CPPythonProjectFixtures
default_pep621 = PEP621(name="test_name", version="1.0")
default_cppython_data = CPPythonData(target=TargetEnum.EXE)
default_tool_data = ToolData(cppython=default_cppython_data)
default_pyproject = PyProject(project=default_pep621, tool=default_tool_data)
class TestCLIInterface(CPPythonProjectFixtures, InterfaceUnitTests[ConsoleInterface]):
"""The tests for our CLI interface"""
class TestCLIInterface(InterfaceUnitTests):
"""
The tests for our CLI interface
"""
@pytest.fixture(name="interface_type")
def fixture_interface_type(self) -> type[ConsoleInterface]:
"""A required testing hook that allows type generation
@pytest.fixture(name="interface_type")
def fixture_interface_type(self) -> Type[ConsoleInterface]:
Returns:
The interface type
"""
A required testing hook that allows type generation
"""
return ConsoleInterface
# Grab the API methods and parameterize them for automatic testing of the entry_points
method_list = [func for func in dir(API) if callable(getattr(API, func)) and not func.startswith("__")]
def test_config(self) -> None:
"""Verify that the configuration object can be constructed"""
@pytest.mark.parametrize("command", method_list)
def test_command(self, command: str, mocker: MockerFixture):
"""
_summary_
Configuration()
Arguments:
command {str} -- The CLI command with the same name as the CPPython API call
mocker {MockerFixture} -- pytest-mock fixture
"""
# Patch the project initialization
mocker.patch("cppython.project.Project.__init__", return_value=None)
def test_verbosity(self) -> None:
"""Test that verbosity is passed through to the CLI"""
# Patch the reading of data
mocker.patch("cppython.console.interface._create_pyproject", return_value=default_pyproject)
config = Configuration()
config = Config()
# Patch out the implementation
mocked_command = mocker.patch(f"cppython.project.Project.{command}")
runner = CliRunner()
result = runner.invoke(cli, [command], obj=config, catch_exceptions=False)
assert result.exit_code == 0
mocked_command.assert_called_once()
def test_config(self):
"""
TODO
"""
Config()
def test_verbosity(self):
"""
TODO
"""
config = Config()
runner = CliRunner()
result = runner.invoke(cli, "-v info", obj=config, catch_exceptions=False)

@@ -79,0 +36,0 @@

@@ -0,94 +1,92 @@

"""Test the functions related to the internal interface implementation and the 'Interface' interface itself
"""
Test the functions related to the internal interface implementation and the 'Interface' interface itself
"""
from __future__ import annotations
from pathlib import Path
from logging import getLogger
from typing import Any
from cppython_core.schema import (
PEP621,
ConfigurePreset,
CPPythonData,
CPPythonDataResolved,
Generator,
GeneratorConfiguration,
PEP621Resolved,
ProjectConfiguration,
ProviderConfiguration,
PyProject,
ToolData,
)
from pytest_cppython.mock import MockGenerator, MockGeneratorData
from pytest_cppython.mock import MockProvider, MockProviderData
from pytest_mock import MockerFixture
from cppython.builder import Builder
from cppython.project import Project, ProjectConfiguration
from cppython.utility import read_json, write_json
from cppython.project import Project
from tests.data.fixtures import CPPythonProjectFixtures
default_pep621 = PEP621(name="test_name", version="1.0")
default_cppython_data = CPPythonData()
default_tool_data = ToolData(**{"cppython": default_cppython_data})
default_pyproject = PyProject(**{"project": default_pep621, "tool": default_tool_data})
mocked_pyproject = default_pyproject.dict(by_alias=True)
mocked_pyproject["tool"]["cppython"]["mock"] = MockGeneratorData()
class MockExtendedCPPython(CPPythonData):
"""Mocked extended data for comparison verification"""
mock: MockProviderData
class ExtendedCPPython(CPPythonData):
"""
TODO
"""
mock: MockGeneratorData
class TestProject(CPPythonProjectFixtures):
"""Grouping for Project class testing"""
def test_construction_without_plugins(
self, mocker: MockerFixture, project: PyProject, workspace: ProjectConfiguration
) -> None:
"""Verification that no error is thrown and output is gracefully handled if no provider plugins are found
class TestProject:
"""
TODO
"""
def test_construction_without_plugins(self, mocker: MockerFixture):
Args:
mocker: Mocking fixture for interface mocking
project: PyProject data to construct with
workspace: Temporary workspace for path resolution
"""
TODO
"""
interface_mock = mocker.MagicMock()
configuration = ProjectConfiguration(pyproject_file=Path("pyproject.toml"), version="1.0.0")
Project(configuration, interface_mock, default_pyproject.dict(by_alias=True))
Project(workspace, interface_mock, project.dict(by_alias=True))
def test_construction_with_plugins(self, mocker: MockerFixture):
def test_construction_with_plugins(
self, mocker: MockerFixture, workspace: ProjectConfiguration, mock_project: dict[str, Any]
) -> None:
"""Verification of full construction with mock provider plugin
Args:
mocker: Mocking fixture for interface mocking
workspace: Temporary workspace for path resolution
mock_project: PyProject data to construct with
"""
TODO
"""
mocked_plugin_list = [MockGenerator]
mocker.patch("cppython.builder.Builder.gather_plugins", return_value=mocked_plugin_list)
mocked_plugin_list = [MockProvider]
mocker.patch("cppython.builder.Builder.discover_providers", return_value=mocked_plugin_list)
interface_mock = mocker.MagicMock()
configuration = ProjectConfiguration(pyproject_file=Path("pyproject.toml"), version="1.0.0")
Project(configuration, interface_mock, mocked_pyproject)
Project(workspace, interface_mock, mock_project)
class TestBuilder:
"""
TODO
"""
class TestBuilder(CPPythonProjectFixtures):
"""Tests of builder steps"""
def test_plugin_gather(self):
def test_plugin_gather(self, workspace: ProjectConfiguration) -> None:
"""Verifies that provider discovery works with no results
Args:
workspace: Temporary workspace for path resolution
"""
TODO
"""
configuration = ProjectConfiguration(pyproject_file=Path("pyproject.toml"), version="1.0.0")
builder = Builder(configuration)
plugins = builder.gather_plugins(Generator)
builder = Builder(workspace, getLogger())
plugins = builder.discover_providers()
assert len(plugins) == 0
def test_generator_data_construction(self, mocker: MockerFixture):
def test_provider_data_construction(
self, mocker: MockerFixture, workspace: ProjectConfiguration, project: PyProject
) -> None:
"""Tests that the input data for providers can be constructed
Args:
mocker: Mocking fixture for interface mocking
workspace: Temporary workspace for path resolution
project: PyProject data to construct with
"""
TODO
"""
configuration = ProjectConfiguration(pyproject_file=Path("pyproject.toml"), version="1.0.0")
builder = Builder(configuration)
builder = Builder(workspace, getLogger())
model_type = builder.generate_model([])

@@ -98,11 +96,11 @@

generator_type = mocker.Mock()
generator_type.name.return_value = "mock"
generator_type.data_type.return_value = MockGeneratorData
provider_type = mocker.Mock()
provider_type.name.return_value = "mock"
provider_type.data_type.return_value = MockProviderData
model_type = builder.generate_model([generator_type])
model_type = builder.generate_model([provider_type])
project_data = default_pyproject.dict(by_alias=True)
project_data = project.dict(by_alias=True)
mock_data = MockGeneratorData()
mock_data = MockProviderData()
project_data["tool"]["cppython"]["mock"] = mock_data.dict(by_alias=True)

@@ -113,115 +111,39 @@ result = model_type(**project_data)

assert result.tool.cppython is not None
assert result.tool.cppython.mock is not None
def test_generator_creation(self, mocker: MockerFixture):
def test_provider_creation(
self, mocker: MockerFixture, workspace: ProjectConfiguration, pep621: PEP621, cppython: CPPythonData
) -> None:
"""Test that providers can be created with the mock data available
Args:
mocker: Mocking fixture for interface mocking
workspace: Temporary workspace for path resolution
pep621: One of many parameterized Project data tables
cppython: One of many parameterized CPPython data tables
"""
TODO
"""
configuration = ProjectConfiguration(pyproject_file=Path("pyproject.toml"), version="1.0.0")
builder = Builder(configuration)
builder = Builder(workspace, getLogger())
generator_configuration = GeneratorConfiguration(root_directory=configuration.pyproject_file.parent)
provider_configuration = ProviderConfiguration(root_directory=workspace.pyproject_file.parent)
resolved = builder.generate_resolved_cppython_model([])
generators = builder.create_generators(
[],
configuration,
generator_configuration,
default_pep621.resolve(configuration),
default_cppython_data.resolve(resolved, configuration),
)
assert not generators
provider_type = mocker.Mock()
provider_type.name.return_value = "mock"
provider_type.data_type.return_value = MockProviderData
generator_type = mocker.Mock()
generator_type.name.return_value = "mock"
generator_type.data_type.return_value = MockGeneratorData
mock_data = MockGeneratorData()
extended_cppython_dict = default_cppython_data.dict(exclude_defaults=True)
mock_data = MockProviderData()
extended_cppython_dict = cppython.dict(by_alias=True)
extended_cppython_dict["mock"] = mock_data
extended_cppython = ExtendedCPPython(**extended_cppython_dict)
extended_cppython = MockExtendedCPPython(**extended_cppython_dict)
resolved = builder.generate_resolved_cppython_model([generator_type])
resolved = builder.generate_resolved_cppython_model([provider_type])
generators = builder.create_generators(
[generator_type],
configuration,
generator_configuration,
default_pep621.resolve(configuration),
extended_cppython.resolve(resolved, configuration),
providers = builder.create_providers(
[provider_type],
workspace,
provider_configuration,
(pep621.resolve(workspace), extended_cppython.resolve(resolved, workspace)),
)
assert len(generators) == 1
def test_presets(self, tmp_path: Path):
"""
TODO
"""
# Write a dummy file for the config
test_file = tmp_path / "pyproject.toml"
test_file.write_text("Test File", encoding="utf-8")
configuration = ProjectConfiguration(pyproject_file=test_file, version="1.0.0")
builder = Builder(configuration)
input_toolchain = tmp_path / "input.cmake"
with open(input_toolchain, "w", encoding="utf8") as file:
file.write("")
configure_preset = ConfigurePreset(name="test_preset", toolchainFile=str(input_toolchain))
generator_output = [("test", configure_preset)]
builder.write_presets(tmp_path, generator_output)
cppython_tool = tmp_path / "cppython"
assert cppython_tool.exists()
cppython_file = cppython_tool / "cppython.json"
assert cppython_file.exists()
test_tool = cppython_tool / "test"
assert test_tool.exists()
test_file = test_tool / "test.json"
assert test_file.exists()
def test_root_unmodified(self, tmp_path: Path):
"""
TODO
"""
# Write a dummy file for the config
test_file = tmp_path / "pyproject.toml"
test_file.write_text("Test File", encoding="utf-8")
configuration = ProjectConfiguration(pyproject_file=test_file, version="1.0.0")
builder = Builder(configuration)
# TODO: Translate into reuseable testing data
output = {
"version": 4,
"cmakeMinimumRequired": {"major": 3, "minor": 23, "patch": 1},
"include": ["should/be/replaced/cppython.json"],
"configurePresets": [
{
"name": "default",
"inherits": ["cppython"],
"hidden": True,
"description": "Tests that generator isn't removed",
"generator": "Should exist",
},
],
}
input_preset = tmp_path / "CMakePresets.json"
write_json(input_preset, output)
builder.write_root_presets(tmp_path / "test_location")
data = read_json(input_preset)
# TODO: Assert the differences affect nothing but what is written by the builder
assert len(providers) == 1
"""
TODO
"""
from abc import ABC, abstractmethod
from pathlib import Path
from packaging.version import Version
class VCS(ABC):
"""
Base class for version control systems
"""
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
@abstractmethod
def is_repository(self, path: Path) -> bool:
"""
TODO
"""
raise NotImplementedError()
@abstractmethod
def extract_version(self, path: Path) -> Version:
"""
TODO
"""
raise NotImplementedError()
"""
TODO
"""
from pathlib import Path
from dulwich.porcelain import tag_list
from dulwich.repo import Repo
from packaging.version import Version
from cppython.console.vcs.base import VCS
class Git(VCS):
"""
Git implementation hooks
"""
def is_repository(self, path: Path) -> bool:
"""
TODO
"""
try:
Repo(str(path))
return True
except Exception:
return False
def extract_version(self, path: Path) -> Version:
"""
TODO
"""
repo = Repo(str(path))
tags = tag_list(repo)
try:
tag = tags[-1].decode("utf-8")
except Exception:
tag = "v0.1.0"
return Version(tag)
"""
TODO
"""
import json
from pathlib import Path
from typing import Any, Type
from cppython_core.schema import ModelT
from pydantic import BaseModel
def read_model_json(path: Path, model: Type[ModelT]) -> ModelT:
"""
Reading routine. Only keeps Model data
"""
return model.parse_file(path=path)
def read_json(path: Path) -> Any:
"""
Reading routine
"""
with open(path, "r", encoding="utf-8") as file:
return json.load(file)
def write_model_json(path: Path, model: BaseModel) -> None:
"""
Writing routine. Only writes model data
"""
serialized = json.loads(model.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
"""
with open(path, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
"""
TODO
"""
from pathlib import Path
from cppython_core.schema import CPPythonModel
from cppython.utility import read_model_json, write_model_json
class TestBuilder:
"""
TODO
"""
class ModelTest(CPPythonModel):
"""
TODO
"""
test_path: Path
test_int: int
def test_model_read_write(self, tmpdir):
"""
TODO
"""
test_model = TestBuilder.ModelTest(test_path=Path(), test_int=3)
temporary_directory = Path(tmpdir)
json_path = temporary_directory / "test.json"
write_model_json(json_path, test_model)
output = read_model_json(json_path, TestBuilder.ModelTest)
assert test_model == output
"""
TODO
"""
from pathlib import Path
from cppython.console.vcs.git import Git
class TestGit:
"""
TODO
"""
def test_version(self):
"""
TODO
"""
directory = Path()
git = Git()
result = git.extract_version(directory)
assert result != ""