diff --git a/cppython/builder.py b/cppython/builder.py index 650f4f50..6a4fb209 100644 --- a/cppython/builder.py +++ b/cppython/builder.py @@ -5,12 +5,14 @@ from importlib.metadata import entry_points from inspect import getmodule from logging import Logger +from pathlib import Path from pprint import pformat from typing import Any, cast from rich.console import Console from rich.logging import RichHandler +from cppython.configuration import ConfigurationLoader from cppython.core.plugin_schema.generator import Generator from cppython.core.plugin_schema.provider import Provider from cppython.core.plugin_schema.scm import SCM, SupportedSCMFeatures @@ -20,6 +22,7 @@ resolve_cppython, resolve_cppython_plugin, resolve_generator, + resolve_model, resolve_pep621, resolve_project_configuration, resolve_provider, @@ -187,11 +190,21 @@ def generate_pep621_data( @staticmethod def resolve_global_config() -> CPPythonGlobalConfiguration: - """Generates the global configuration object + """Generates the global configuration object by loading from ~/.cppython/config.toml Returns: - The global configuration object + The global configuration object with loaded or default values """ + loader = ConfigurationLoader(Path.cwd()) + + try: + global_config_data = loader.load_global_config() + if global_config_data: + return resolve_model(CPPythonGlobalConfiguration, global_config_data) + except (FileNotFoundError, ValueError): + # If global config doesn't exist or is invalid, use defaults + pass + return CPPythonGlobalConfiguration() def find_generators(self) -> list[type[Generator]]: diff --git a/cppython/configuration.py b/cppython/configuration.py new file mode 100644 index 00000000..08a73f0e --- /dev/null +++ b/cppython/configuration.py @@ -0,0 +1,189 @@ +"""Configuration loading and merging for CPPython + +This module handles loading configuration from multiple sources: +1. Global configuration (~/.cppython/config.toml) - User-wide settings for all projects +2. Project configuration (pyproject.toml or cppython.toml) - Project-specific settings +3. Local overrides (.cppython.toml) - Overrides for global configuration +""" + +from pathlib import Path +from tomllib import loads +from typing import Any + + +class ConfigurationLoader: + """Loads and merges CPPython configuration from multiple sources""" + + def __init__(self, project_root: Path) -> None: + """Initialize the configuration loader + + Args: + project_root: The root directory of the project + """ + self.project_root = project_root + self.pyproject_path = project_root / 'pyproject.toml' + self.cppython_path = project_root / 'cppython.toml' + self.local_override_path = project_root / '.cppython.toml' + self.global_config_path = Path.home() / '.cppython' / 'config.toml' + + def load_pyproject_data(self) -> dict[str, Any]: + """Load complete pyproject.toml data + + Returns: + Dictionary containing the full pyproject.toml data + + Raises: + FileNotFoundError: If pyproject.toml does not exist + """ + if not self.pyproject_path.exists(): + raise FileNotFoundError(f'pyproject.toml not found at {self.pyproject_path}') + + return loads(self.pyproject_path.read_text(encoding='utf-8')) + + def load_cppython_config(self) -> dict[str, Any] | None: + """Load CPPython configuration from cppython.toml if it exists + + Returns: + Dictionary containing the cppython table data, or None if file doesn't exist + """ + if not self.cppython_path.exists(): + return None + + data = loads(self.cppython_path.read_text(encoding='utf-8')) + + # Validate that it contains a cppython table + if 'cppython' not in data: + raise ValueError(f'{self.cppython_path} must contain a [cppython] table') + + return data['cppython'] + + def load_global_config(self) -> dict[str, Any] | None: + """Load global configuration from ~/.cppython/config.toml if it exists + + Returns: + Dictionary containing the global configuration, or None if file doesn't exist + """ + if not self.global_config_path.exists(): + return None + + data = loads(self.global_config_path.read_text(encoding='utf-8')) + + # Validate that it contains a cppython table + if 'cppython' not in data: + raise ValueError(f'{self.global_config_path} must contain a [cppython] table') + + return data['cppython'] + + def load_local_overrides(self) -> dict[str, Any] | None: + """Load local overrides from .cppython.toml if it exists + + These overrides only affect the global configuration, not project configuration. + + Returns: + Dictionary containing local override data, or None if file doesn't exist + """ + if not self.local_override_path.exists(): + return None + + return loads(self.local_override_path.read_text(encoding='utf-8')) + + def merge_configurations(self, base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: + """Deep merge two configuration dictionaries + + Args: + base: Base configuration dictionary + override: Override configuration dictionary + + Returns: + Merged configuration with overrides taking precedence + """ + result = base.copy() + + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + # Recursively merge nested dictionaries + result[key] = self.merge_configurations(result[key], value) + else: + # Override value + result[key] = value + + return result + + def load_cppython_table(self) -> dict[str, Any] | None: + """Load and merge the CPPython configuration table from all sources + + Priority (highest to lowest): + 1. Project configuration (pyproject.toml or cppython.toml) + 2. Local overrides (.cppython.toml) merged with global config + 3. Global configuration (~/.cppython/config.toml) + + Returns: + Merged CPPython configuration dictionary, or None if no config found + """ + # Start with global configuration + global_config = self.load_global_config() + + # Apply local overrides to global config + local_overrides = self.load_local_overrides() + if local_overrides is not None and global_config is not None: + global_config = self.merge_configurations(global_config, local_overrides) + elif local_overrides is not None and global_config is None: + # Local overrides exist but no global config - use overrides as base + global_config = local_overrides + + # Load project configuration (pyproject.toml or cppython.toml) + pyproject_data = self.load_pyproject_data() + project_config = pyproject_data.get('tool', {}).get('cppython') + + # Try cppython.toml as alternative + cppython_toml_config = self.load_cppython_config() + if cppython_toml_config is not None: + if project_config is not None: + raise ValueError( + 'CPPython configuration found in both pyproject.toml and cppython.toml. ' + 'Please use only one configuration source.' + ) + project_config = cppython_toml_config + + # Merge: global config (with local overrides) + project config + # Project config has highest priority + if project_config is not None and global_config is not None: + return self.merge_configurations(global_config, project_config) + elif project_config is not None: + return project_config + elif global_config is not None: + return global_config + + return None + + def get_project_data(self) -> dict[str, Any]: + """Get the complete pyproject data with merged CPPython configuration + + Returns: + Dictionary containing pyproject data with merged tool.cppython table + """ + pyproject_data = self.load_pyproject_data() + + # Load merged CPPython config + cppython_config = self.load_cppython_table() + + # Update the pyproject data with merged config + if cppython_config is not None: + if 'tool' not in pyproject_data: + pyproject_data['tool'] = {} + pyproject_data['tool']['cppython'] = cppython_config + + return pyproject_data + + def config_source_info(self) -> dict[str, bool]: + """Get information about which configuration files exist + + Returns: + Dictionary with boolean flags for each config file's existence + """ + return { + 'global_config': self.global_config_path.exists(), + 'pyproject': self.pyproject_path.exists(), + 'cppython': self.cppython_path.exists(), + 'local_overrides': self.local_override_path.exists(), + } diff --git a/cppython/console/entry.py b/cppython/console/entry.py index 13d11f17..123aa4ec 100644 --- a/cppython/console/entry.py +++ b/cppython/console/entry.py @@ -1,12 +1,12 @@ """A click CLI for CPPython interfacing""" from pathlib import Path -from tomllib import loads from typing import Annotated import typer from rich import print +from cppython.configuration import ConfigurationLoader from cppython.console.schema import ConsoleConfiguration, ConsoleInterface from cppython.core.schema import ProjectConfiguration from cppython.project import Project @@ -20,16 +20,63 @@ def get_enabled_project(context: typer.Context) -> Project: if configuration is None: raise ValueError('The configuration object is missing') - path = configuration.project_configuration.project_root / 'pyproject.toml' - pyproject_data = loads(path.read_text(encoding='utf-8')) + # Use ConfigurationLoader to load and merge all configuration sources + loader = ConfigurationLoader(configuration.project_configuration.project_root) + pyproject_data = loader.get_project_data() project = Project(configuration.project_configuration, configuration.interface, pyproject_data) if not project.enabled: - print('[bold red]Error[/bold red]: Project is not enabled. Please check your pyproject.toml configuration.') + print('[bold red]Error[/bold red]: Project is not enabled. Please check your configuration files.') + print('Configuration files checked:') + config_info = loader.config_source_info() + for config_file, exists in config_info.items(): + status = '✓' if exists else '✗' + print(f' {status} {config_file}') raise typer.Exit(code=1) return project +def _parse_groups_argument(groups: str | None) -> list[str] | None: + """Parse pip-style dependency groups from command argument. + + Args: + groups: Groups string like '[test]' or '[dev,test]' or None + + Returns: + List of group names or None if no groups specified + + Raises: + typer.BadParameter: If the groups format is invalid + """ + if groups is None: + return None + + # Strip whitespace + groups = groups.strip() + + if not groups: + return None + + # Check for square brackets + if not (groups.startswith('[') and groups.endswith(']')): + raise typer.BadParameter(f"Invalid groups format: '{groups}'. Use square brackets like: [test] or [dev,test]") + + # Extract content between brackets and split by comma + content = groups[1:-1].strip() + + if not content: + raise typer.BadParameter('Empty groups specification. Provide at least one group name.') + + # Split by comma and strip whitespace from each group + group_list = [g.strip() for g in content.split(',')] + + # Validate group names are not empty + if any(not g for g in group_list): + raise typer.BadParameter('Group names cannot be empty.') + + return group_list + + def _find_pyproject_file() -> Path: """Searches upward for a pyproject.toml file @@ -83,33 +130,57 @@ def info( @app.command() def install( context: typer.Context, + groups: Annotated[ + str | None, + typer.Argument( + help='Dependency groups to install in addition to base dependencies. ' + 'Use square brackets like: [test] or [dev,test]' + ), + ] = None, ) -> None: """Install API call Args: context: The CLI configuration object + groups: Optional dependency groups to install (e.g., [test] or [dev,test]) Raises: ValueError: If the configuration object is missing """ project = get_enabled_project(context) - project.install() + + # Parse groups from pip-style syntax + group_list = _parse_groups_argument(groups) + + project.install(groups=group_list) @app.command() def update( context: typer.Context, + groups: Annotated[ + str | None, + typer.Argument( + help='Dependency groups to update in addition to base dependencies. ' + 'Use square brackets like: [test] or [dev,test]' + ), + ] = None, ) -> None: """Update API call Args: context: The CLI configuration object + groups: Optional dependency groups to update (e.g., [test] or [dev,test]) Raises: ValueError: If the configuration object is missing """ project = get_enabled_project(context) - project.update() + + # Parse groups from pip-style syntax + group_list = _parse_groups_argument(groups) + + project.update(groups=group_list) @app.command(name='list') diff --git a/cppython/console/schema.py b/cppython/console/schema.py index 263bcd68..99aaf595 100644 --- a/cppython/console/schema.py +++ b/cppython/console/schema.py @@ -9,10 +9,13 @@ class ConsoleInterface(Interface): """Interface implementation to pass to the project""" def write_pyproject(self) -> None: - """Write output""" + """Write output to pyproject.toml""" def write_configuration(self) -> None: - """Write output""" + """Write output to primary configuration (pyproject.toml or cppython.toml)""" + + def write_user_configuration(self) -> None: + """Write output to global user configuration (~/.cppython/config.toml)""" class ConsoleConfiguration(CPPythonModel): diff --git a/cppython/core/plugin_schema/provider.py b/cppython/core/plugin_schema/provider.py index 518fe405..b4028660 100644 --- a/cppython/core/plugin_schema/provider.py +++ b/cppython/core/plugin_schema/provider.py @@ -80,13 +80,21 @@ def features(directory: DirectoryPath) -> SupportedFeatures: raise NotImplementedError @abstractmethod - def install(self) -> None: - """Called when dependencies need to be installed from a lock file.""" + def install(self, groups: list[str] | None = None) -> None: + """Called when dependencies need to be installed from a lock file. + + Args: + groups: Optional list of dependency group names to install in addition to base dependencies + """ raise NotImplementedError @abstractmethod - def update(self) -> None: - """Called when dependencies need to be updated and written to the lock file.""" + def update(self, groups: list[str] | None = None) -> None: + """Called when dependencies need to be updated and written to the lock file. + + Args: + groups: Optional list of dependency group names to update in addition to base dependencies + """ raise NotImplementedError @abstractmethod diff --git a/cppython/core/resolution.py b/cppython/core/resolution.py index 8cb8f6a5..76431449 100644 --- a/cppython/core/resolution.py +++ b/cppython/core/resolution.py @@ -76,6 +76,21 @@ def resolve_pep621( return pep621_data +def _resolve_absolute_path(path: Path, root_directory: Path) -> Path: + """Convert a path to absolute, using root_directory as base for relative paths. + + Args: + path: The path to resolve + root_directory: The base directory for relative paths + + Returns: + The absolute path + """ + if path.is_absolute(): + return path + return root_directory / path + + class PluginBuildData(CPPythonModel): """Data needed to construct CoreData""" @@ -114,34 +129,20 @@ def resolve_cppython( """ root_directory = project_data.project_root.absolute() - # Add the base path to all relative paths + # Resolve configuration path modified_configuration_path = local_configuration.configuration_path - - # TODO: Grab configuration from the project, user, or system if modified_configuration_path is None: modified_configuration_path = root_directory / 'cppython.json' + else: + modified_configuration_path = _resolve_absolute_path(modified_configuration_path, root_directory) - if not modified_configuration_path.is_absolute(): - modified_configuration_path = root_directory / modified_configuration_path - - modified_install_path = local_configuration.install_path - - if not modified_install_path.is_absolute(): - 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 + # Resolve other paths + modified_install_path = _resolve_absolute_path(local_configuration.install_path, root_directory) + modified_tool_path = _resolve_absolute_path(local_configuration.tool_path, root_directory) + modified_build_path = _resolve_absolute_path(local_configuration.build_path, root_directory) modified_provider_name = plugin_build_data.provider_name modified_generator_name = plugin_build_data.generator_name - modified_scm_name = plugin_build_data.scm_name # Extract provider and generator configuration data @@ -166,6 +167,18 @@ def resolve_cppython( except InvalidRequirement as error: invalid_requirements.append(f"Invalid requirement '{dependency}': {error}") + # Construct dependency groups from the local configuration + dependency_groups: dict[str, list[Requirement]] = {} + if local_configuration.dependency_groups: + for group_name, group_dependencies in local_configuration.dependency_groups.items(): + resolved_group: list[Requirement] = [] + for dependency in group_dependencies: + try: + resolved_group.append(Requirement(dependency)) + except InvalidRequirement as error: + invalid_requirements.append(f"Invalid requirement '{dependency}' in group '{group_name}': {error}") + dependency_groups[group_name] = resolved_group + if invalid_requirements: raise ConfigException('\n'.join(invalid_requirements), []) @@ -179,6 +192,7 @@ def resolve_cppython( generator_name=modified_generator_name, scm_name=modified_scm_name, dependencies=dependencies, + dependency_groups=dependency_groups, provider_data=provider_data, generator_data=generator_data, ) @@ -208,6 +222,7 @@ def resolve_cppython_plugin(cppython_data: CPPythonData, plugin_type: type[Plugi generator_name=cppython_data.generator_name, scm_name=cppython_data.scm_name, dependencies=cppython_data.dependencies, + dependency_groups=cppython_data.dependency_groups, provider_data=cppython_data.provider_data, generator_data=cppython_data.generator_data, ) diff --git a/cppython/core/schema.py b/cppython/core/schema.py index dfaac846..31a70d5c 100644 --- a/cppython/core/schema.py +++ b/cppython/core/schema.py @@ -117,6 +117,7 @@ class CPPythonData(CPPythonModel, extra='forbid'): generator_name: TypeName scm_name: TypeName dependencies: list[Requirement] + dependency_groups: dict[str, list[Requirement]] provider_data: Annotated[dict[str, Any], Field(description='Resolved provider configuration data')] generator_data: Annotated[dict[str, Any], Field(description='Resolved generator configuration data')] @@ -329,6 +330,15 @@ class CPPythonLocalConfiguration(CPPythonModel, extra='forbid'): ), ] = None + dependency_groups: Annotated[ + dict[str, list[str]] | None, + Field( + alias='dependency-groups', + description='Named groups of dependencies. Key is the group name, value is a list of pip compatible' + ' requirements strings. Similar to PEP 735 dependency groups.', + ), + ] = None + class ToolData(CPPythonModel): """Tool entry of pyproject.toml""" @@ -361,5 +371,16 @@ def write_pyproject(self) -> None: @abstractmethod def write_configuration(self) -> None: - """Called when CPPython requires the interface to write out configuration changes""" + """Called when CPPython requires the interface to write out configuration changes + + This writes to the primary configuration source (pyproject.toml or cppython.toml) + """ + raise NotImplementedError + + @abstractmethod + def write_user_configuration(self) -> None: + """Called when CPPython requires the interface to write out global configuration changes + + This writes to ~/.cppython/config.toml for global user configuration + """ raise NotImplementedError diff --git a/cppython/data.py b/cppython/data.py index c2c76be4..94ffe370 100644 --- a/cppython/data.py +++ b/cppython/data.py @@ -3,6 +3,8 @@ from dataclasses import dataclass from logging import Logger +from packaging.requirements import Requirement + from cppython.core.plugin_schema.generator import Generator from cppython.core.plugin_schema.provider import Provider from cppython.core.plugin_schema.scm import SCM @@ -27,12 +29,59 @@ def __init__(self, core_data: CoreData, plugins: Plugins, logger: Logger) -> Non self._core_data = core_data self._plugins = plugins self.logger = logger + self._active_groups: list[str] | None = None @property def plugins(self) -> Plugins: """The plugin data for CPPython""" return self._plugins + def set_active_groups(self, groups: list[str] | None) -> None: + """Set the active dependency groups for the current operation. + + Args: + groups: List of group names to activate, or None for no additional groups + """ + self._active_groups = groups + if groups: + self.logger.info('Active dependency groups: %s', ', '.join(groups)) + + # Validate that requested groups exist + available_groups = set(self._core_data.cppython_data.dependency_groups.keys()) + requested_groups = set(groups) + missing_groups = requested_groups - available_groups + + if missing_groups: + self.logger.warning( + 'Requested dependency groups not found: %s. Available groups: %s', + ', '.join(sorted(missing_groups)), + ', '.join(sorted(available_groups)) if available_groups else 'none', + ) + + def apply_dependency_groups(self, groups: list[str] | None) -> None: + """Validate and log the dependency groups to be used. + + Args: + groups: List of group names to apply, or None for base dependencies only + """ + if groups: + self.set_active_groups(groups) + + def get_active_dependencies(self) -> list: + """Get the combined list of base dependencies and active group dependencies. + + Returns: + Combined list of Requirement objects from base and active groups + """ + dependencies: list[Requirement] = list(self._core_data.cppython_data.dependencies) + + if self._active_groups: + for group_name in self._active_groups: + if group_name in self._core_data.cppython_data.dependency_groups: + dependencies.extend(self._core_data.cppython_data.dependency_groups[group_name]) + + return dependencies + def sync(self) -> None: """Gathers sync information from providers and passes it to the generator diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index b264cd70..3346eef9 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -19,7 +19,10 @@ def __init__(self) -> None: @staticmethod def generate_cppython_preset( - cppython_preset_directory: Path, provider_preset_file: Path, provider_data: CMakeSyncData + cppython_preset_directory: Path, + provider_preset_file: Path, + provider_data: CMakeSyncData, + project_root: Path, ) -> CMakePresets: """Generates the cppython preset which inherits from the provider presets @@ -27,6 +30,7 @@ def generate_cppython_preset( cppython_preset_directory: The tool directory provider_preset_file: Path to the provider's preset file provider_data: The provider's synchronization data + project_root: The project root directory (where CMakeLists.txt is located) Returns: A CMakePresets object @@ -43,7 +47,8 @@ def generate_cppython_preset( ) if provider_data.toolchain_file: - default_configure.toolchainFile = provider_data.toolchain_file.as_posix() + relative_toolchain = provider_data.toolchain_file.relative_to(project_root, walk_up=True) + default_configure.toolchainFile = relative_toolchain.as_posix() configure_presets.append(default_configure) @@ -55,7 +60,10 @@ def generate_cppython_preset( @staticmethod def write_cppython_preset( - cppython_preset_directory: Path, provider_preset_file: Path, provider_data: CMakeSyncData + cppython_preset_directory: Path, + provider_preset_file: Path, + provider_data: CMakeSyncData, + project_root: Path, ) -> Path: """Write the cppython presets which inherit from the provider presets @@ -63,12 +71,13 @@ def write_cppython_preset( cppython_preset_directory: The tool directory provider_preset_file: Path to the provider's preset file provider_data: The provider's synchronization data + project_root: The project root directory (where CMakeLists.txt is located) Returns: A file path to the written data """ generated_preset = Builder.generate_cppython_preset( - cppython_preset_directory, provider_preset_file, provider_data + cppython_preset_directory, provider_preset_file, provider_data, project_root ) cppython_preset_file = cppython_preset_directory / 'cppython.json' diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index e008a70a..c86ca4e7 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -66,8 +66,10 @@ def sync(self, sync_data: SyncData) -> None: cppython_preset_file = self._cppython_preset_directory / 'CPPython.json' + project_root = self.core_data.project_data.project_root + cppython_preset_file = self.builder.write_cppython_preset( - self._cppython_preset_directory, cppython_preset_file, sync_data + self._cppython_preset_directory, cppython_preset_file, sync_data, project_root ) self.builder.write_root_presets( diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 0b68b815..70ac9058 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -121,9 +121,16 @@ def __init__(self) -> None: self._filename = 'conanfile.py' @staticmethod - def _create_conanfile(conan_file: Path, dependencies: list[ConanDependency], name: str, version: str) -> None: + def _create_conanfile( + conan_file: Path, + dependencies: list[ConanDependency], + dependency_groups: dict[str, list[ConanDependency]], + name: str, + version: str, + ) -> None: """Creates a conanfile.py file with the necessary content.""" template_string = """ + import os from conan import ConanFile from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout from conan.tools.files import copy @@ -133,6 +140,7 @@ class AutoPackage(ConanFile): version = "${version}" settings = "os", "compiler", "build_type", "arch" requires = ${dependencies} + test_requires = ${test_requires} def layout(self): cmake_layout(self) @@ -154,7 +162,10 @@ def package(self): cmake.install() def package_info(self): - self.cpp_info.libs = ["${name}"] + # Use native CMake config files to preserve FILE_SET information for C++ modules + # This tells CMakeDeps to skip generating files and use the package's native config + self.cpp_info.set_property("cmake_find_mode", "none") + self.cpp_info.builddirs = ["."] def export_sources(self): copy(self, "CMakeLists.txt", src=self.recipe_folder, dst=self.export_sources_folder) @@ -164,10 +175,13 @@ def export_sources(self): template = Template(dedent(template_string)) + test_dependencies = dependency_groups.get('test', []) + values = { 'name': name, 'version': version, 'dependencies': [dependency.requires() for dependency in dependencies], + 'test_requires': [dependency.requires() for dependency in test_dependencies], } result = template.substitute(values) @@ -179,6 +193,7 @@ def generate_conanfile( self, directory: DirectoryPath, dependencies: list[ConanDependency], + dependency_groups: dict[str, list[ConanDependency]], name: str, version: str, ) -> None: @@ -195,4 +210,4 @@ def generate_conanfile( conan_file.write_text(modified.code, encoding='utf-8') else: directory.mkdir(parents=True, exist_ok=True) - self._create_conanfile(conan_file, dependencies, name, version) + self._create_conanfile(conan_file, dependencies, dependency_groups, name, version) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index c7413c01..a4cad9ed 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -69,19 +69,20 @@ def information() -> Information: """ return Information() - def _install_dependencies(self, *, update: bool = False) -> None: + def _install_dependencies(self, *, update: bool = False, groups: list[str] | None = None) -> None: """Install/update dependencies using Conan CLI. Args: update: If True, check remotes for newer versions/revisions and install those. If False, use cached versions when available. + groups: Optional list of dependency group names to include """ operation = 'update' if update else 'install' logger = getLogger('cppython.conan') try: # Setup environment and generate conanfile - conanfile_path = self._prepare_installation() + conanfile_path = self._prepare_installation(groups=groups) except Exception as e: raise ProviderInstallationError('conan', f'Failed to prepare {operation} environment: {e}', e) from e @@ -93,17 +94,32 @@ def _install_dependencies(self, *, update: bool = False) -> None: except Exception as e: raise ProviderInstallationError('conan', f'Failed to install dependencies: {e}', e) from e - def _prepare_installation(self) -> Path: + def _prepare_installation(self, groups: list[str] | None = None) -> Path: """Prepare the installation environment and generate conanfile. + Args: + groups: Optional list of dependency group names to include + Returns: Path to conanfile.py """ - # Resolve dependencies and generate conanfile.py + # Resolve base dependencies resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] + + # Resolve only the requested dependency groups + resolved_dependency_groups = {} + if groups: + for group_name in groups: + if group_name in self.core_data.cppython_data.dependency_groups: + resolved_dependency_groups[group_name] = [ + resolve_conan_dependency(req) + for req in self.core_data.cppython_data.dependency_groups[group_name] + ] + self.builder.generate_conanfile( self.core_data.project_data.project_root, resolved_dependencies, + resolved_dependency_groups, self.core_data.pep621_data.name, self.core_data.pep621_data.version, ) @@ -172,13 +188,21 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str logger.error('Conan install failed: %s', error_msg, exc_info=True) raise ProviderInstallationError('conan', error_msg, e) from e - def install(self) -> None: - """Installs the provider""" - self._install_dependencies(update=False) + def install(self, groups: list[str] | None = None) -> None: + """Installs the provider + + Args: + groups: Optional list of dependency group names to install + """ + self._install_dependencies(update=False, groups=groups) + + def update(self, groups: list[str] | None = None) -> None: + """Updates the provider - def update(self) -> None: - """Updates the provider""" - self._install_dependencies(update=True) + Args: + groups: Optional list of dependency group names to update + """ + self._install_dependencies(update=True, groups=groups) @staticmethod def supported_sync_type(sync_type: type[SyncData]) -> bool: diff --git a/cppython/plugins/pdm/plugin.py b/cppython/plugins/pdm/plugin.py index b1e28037..64482f9d 100644 --- a/cppython/plugins/pdm/plugin.py +++ b/cppython/plugins/pdm/plugin.py @@ -21,15 +21,25 @@ def __init__(self, core: Core) -> None: """Initializes the plugin""" post_install.connect(self.on_post_install, weak=False) self.logger = getLogger('cppython.interface.pdm') + self._core = core # Register the cpp command register_commands(core) def write_pyproject(self) -> None: - """Write to file""" + """Called when CPPython requires the interface to write out pyproject.toml changes""" + self._core.ui.echo('Writing out pyproject.toml') + # TODO: Implement writing to pyproject.toml through PDM def write_configuration(self) -> None: - """Write to configuration""" + """Called when CPPython requires the interface to write out configuration changes""" + self._core.ui.echo('Writing out configuration') + # TODO: Implement writing to cppython.toml + + def write_user_configuration(self) -> None: + """Called when CPPython requires the interface to write out user-specific configuration changes""" + self._core.ui.echo('Writing out user configuration') + # TODO: Implement writing to .cppython.toml def on_post_install(self, project: Project, dry_run: bool, **_kwargs: Any) -> None: """Called after a pdm install command is called diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index 2fb02368..0f64e616 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -231,8 +231,12 @@ async def download_tooling(cls, directory: Path) -> None: cls._update_provider(directory) - def install(self) -> None: - """Called when dependencies need to be installed from a lock file.""" + def install(self, groups: list[str] | None = None) -> None: + """Called when dependencies need to be installed from a lock file. + + Args: + groups: Optional list of dependency group names to install (currently not used by vcpkg) + """ manifest_directory = self.core_data.project_data.project_root manifest = generate_manifest(self.core_data, self.data) @@ -257,8 +261,12 @@ def install(self) -> None: except subprocess.CalledProcessError as e: self._handle_subprocess_error(logger, 'install project dependencies', e, ProviderInstallationError) - def update(self) -> None: - """Called when dependencies need to be updated and written to the lock file.""" + def update(self, groups: list[str] | None = None) -> None: + """Called when dependencies need to be updated and written to the lock file. + + Args: + groups: Optional list of dependency group names to update (currently not used by vcpkg) + """ manifest_directory = self.core_data.project_data.project_root manifest = generate_manifest(self.core_data, self.data) diff --git a/cppython/project.py b/cppython/project.py index f4049b64..9087e8f2 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -17,7 +17,13 @@ class Project(API): def __init__( self, project_configuration: ProjectConfiguration, interface: Interface, pyproject_data: dict[str, Any] ) -> None: - """Initializes the project""" + """Initializes the project + + Args: + project_configuration: Project-wide configuration + interface: Interface for callbacks to write configuration changes + pyproject_data: Merged configuration data from all sources + """ self._enabled = False self._interface = interface self.logger = logging.getLogger('cppython') @@ -52,9 +58,12 @@ def enabled(self) -> bool: """ return self._enabled - def install(self) -> None: + def install(self, groups: list[str] | None = None) -> None: """Installs project dependencies + Args: + groups: Optional list of dependency groups to install in addition to base dependencies + Raises: Exception: Provider-specific exceptions are propagated with full context """ @@ -66,17 +75,28 @@ def install(self) -> None: asyncio.run(self._data.download_provider_tools()) self.logger.info('Installing project') + + # Log active groups + if groups: + self.logger.info('Installing with dependency groups: %s', ', '.join(groups)) + self.logger.info('Installing %s provider', self._data.plugins.provider.name()) + # Validate and log active groups + self._data.apply_dependency_groups(groups) + # Sync before install to allow provider to access generator's resolved configuration self._data.sync() # Let provider handle its own exceptions for better error context - self._data.plugins.provider.install() + self._data.plugins.provider.install(groups=groups) - def update(self) -> None: + def update(self, groups: list[str] | None = None) -> None: """Updates project dependencies + Args: + groups: Optional list of dependency groups to update in addition to base dependencies + Raises: Exception: Provider-specific exception """ @@ -88,13 +108,21 @@ def update(self) -> None: asyncio.run(self._data.download_provider_tools()) self.logger.info('Updating project') + + # Log active groups + if groups: + self.logger.info('Updating with dependency groups: %s', ', '.join(groups)) + self.logger.info('Updating %s provider', self._data.plugins.provider.name()) + # Validate and log active groups + self._data.apply_dependency_groups(groups) + # Sync before update to allow provider to access generator's resolved configuration self._data.sync() # Let provider handle its own exceptions for better error context - self._data.plugins.provider.update() + self._data.plugins.provider.update(groups=groups) def publish(self) -> None: """Publishes the project diff --git a/cppython/schema.py b/cppython/schema.py index 83380a9a..ab2d7964 100644 --- a/cppython/schema.py +++ b/cppython/schema.py @@ -8,11 +8,19 @@ class API(Protocol): """Project API specification""" @abstractmethod - def install(self) -> None: - """Installs project dependencies""" + def install(self, groups: list[str] | None = None) -> None: + """Installs project dependencies + + Args: + groups: Optional list of dependency groups to install + """ raise NotImplementedError() @abstractmethod - def update(self) -> None: - """Updates project dependencies""" + def update(self, groups: list[str] | None = None) -> None: + """Updates project dependencies + + Args: + groups: Optional list of dependency groups to update + """ raise NotImplementedError() diff --git a/cppython/test/mock/interface.py b/cppython/test/mock/interface.py index 87379b4b..983b73c4 100644 --- a/cppython/test/mock/interface.py +++ b/cppython/test/mock/interface.py @@ -11,3 +11,6 @@ def write_pyproject(self) -> None: def write_configuration(self) -> None: """Implementation of Interface function""" + + def write_user_configuration(self) -> None: + """Implementation of Interface function""" diff --git a/cppython/test/mock/provider.py b/cppython/test/mock/provider.py index 50c58b1c..65c2090d 100644 --- a/cppython/test/mock/provider.py +++ b/cppython/test/mock/provider.py @@ -83,12 +83,20 @@ async def download_tooling(cls, directory: DirectoryPath) -> None: """Downloads the provider tooling""" cls.downloaded = directory - def install(self) -> None: - """Installs the provider""" + def install(self, groups: list[str] | None = None) -> None: + """Installs the provider + + Args: + groups: Optional list of dependency group names to install + """ pass - def update(self) -> None: - """Updates the provider""" + def update(self, groups: list[str] | None = None) -> None: + """Updates the provider + + Args: + groups: Optional list of dependency group names to update + """ pass def publish(self) -> None: diff --git a/examples/conan_cmake/library/test_package/CMakeLists.txt b/examples/conan_cmake/library/test_package/CMakeLists.txt index 45c3b3dd..780bd9a9 100644 --- a/examples/conan_cmake/library/test_package/CMakeLists.txt +++ b/examples/conan_cmake/library/test_package/CMakeLists.txt @@ -1,9 +1,13 @@ cmake_minimum_required(VERSION 4.0) +# Enable std module support for MSVC - MUST be before project() +set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") + project(MathUtilsConsumer LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_MODULE_STD ON) find_package(mathutils REQUIRED) diff --git a/pdm.lock b/pdm.lock index d98a9650..b1ff5e74 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "cmake", "conan", "git", "lint", "pdm", "pytest", "release", "test"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:c835d6b653b2162c04becd475f7e4fbc401f55ac1631168c5b9ce2af35d69782" +content_hash = "sha256:003592f9dc90a509e4177a7123612b87ce3fc731c82415d955af48c1897d542e" [[metadata.targets]] requires_python = ">=3.14" @@ -122,7 +122,7 @@ files = [ [[package]] name = "conan" -version = "2.22.1" +version = "2.22.2" requires_python = ">=3.7" summary = "Conan C/C++ package manager" dependencies = [ @@ -137,7 +137,7 @@ dependencies = [ "urllib3<3.0,>=1.26.6", ] files = [ - {file = "conan-2.22.1.tar.gz", hash = "sha256:c33f4d538f0827e7664813a5e2b5db5ce6a7aa58fb3d0eb6145b2261b56b005c"}, + {file = "conan-2.22.2.tar.gz", hash = "sha256:8df71de27eca903a1a92b292ce32e4b79eee2407ac001e28cae1d1b49869f883"}, ] [[package]] @@ -301,7 +301,7 @@ files = [ [[package]] name = "dulwich" -version = "0.24.8" +version = "0.24.10" requires_python = ">=3.9" summary = "Python Git Library" dependencies = [ @@ -309,10 +309,10 @@ dependencies = [ "urllib3>=2.2.2", ] files = [ - {file = "dulwich-0.24.8-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:19855e8a0ce299cdcdafdc8bc4f6653bea9e02124a5022e13cda8103fb36912d"}, - {file = "dulwich-0.24.8-cp314-cp314-android_24_x86_64.whl", hash = "sha256:da03c7a6629b7ed37e7139739a175f2c9678080a45444418c54ab28d2ec6524b"}, - {file = "dulwich-0.24.8-py3-none-any.whl", hash = "sha256:6ffdd616135bcb31eb2edcccf82d4408720f1db69f596f687ffa2d26c2f5e6f4"}, - {file = "dulwich-0.24.8.tar.gz", hash = "sha256:c9f4748bbcca56fb57458c71c0d30e2351ac15e0583d428c739c09228be68f05"}, + {file = "dulwich-0.24.10-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:44f62e0244531a8c43ca7771e201ec9e7f6a2fb27f8c3c623939bc03c1f50423"}, + {file = "dulwich-0.24.10-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e2eda4a634d6f1ac4c0d4786f8772495c8840dfc2b3e595507376bf5e5b0f9c5"}, + {file = "dulwich-0.24.10-py3-none-any.whl", hash = "sha256:15b32f8c3116a1c0a042dde8da96f65a607e263e860ee42b3d4a98ce2c2f4a06"}, + {file = "dulwich-0.24.10.tar.gz", hash = "sha256:30e028979b6fa7220c913da9c786026611c10746c06496149742602b36a11f6b"}, ] [[package]] @@ -462,32 +462,33 @@ files = [ [[package]] name = "libcst" -version = "1.8.5" +version = "1.8.6" requires_python = ">=3.9" -summary = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." +summary = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.14 programs." dependencies = [ - "pyyaml-ft>=8.0.0; python_version >= \"3.13\"", + "pyyaml-ft>=8.0.0; python_version == \"3.13\"", "pyyaml>=5.2; python_version < \"3.13\"", + "pyyaml>=6.0.3; python_version >= \"3.14\"", "typing-extensions; python_version < \"3.10\"", ] files = [ - {file = "libcst-1.8.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f350ff2867b3075ba97a022de694f2747c469c25099216cef47b58caaee96314"}, - {file = "libcst-1.8.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b95db09d04d125619a63f191c9534853656c4c76c303b8b4c5f950c8e610fba"}, - {file = "libcst-1.8.5-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:60e62e966b45b7dee6f0ec0fd7687704d29be18ae670c5bc6c9c61a12ccf589f"}, - {file = "libcst-1.8.5-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:7cbb330a352dde570059c73af7b7bbfaa84ae121f54d2ce46c5530351f57419d"}, - {file = "libcst-1.8.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71b2b1ef2305cba051252342a1a4f8e94e6b8e95d7693a7c15a00ce8849ef722"}, - {file = "libcst-1.8.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0f504d06dfba909d1ba6a4acf60bfe3f22275444d6e0d07e472a5da4a209b0be"}, - {file = "libcst-1.8.5-cp314-cp314-win_amd64.whl", hash = "sha256:c69d2b39e360dea5490ccb5dcf5957dcbb1067d27dc1f3f0787d4e287f7744e2"}, - {file = "libcst-1.8.5-cp314-cp314-win_arm64.whl", hash = "sha256:63405cb548b2d7b78531535a7819231e633b13d3dee3eb672d58f0f3322892ca"}, - {file = "libcst-1.8.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8a5921105610f35921cc4db6fa5e68e941c6da20ce7f9f93b41b6c66b5481353"}, - {file = "libcst-1.8.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:abded10e8d92462fa982d19b064c6f24ed7ead81cf3c3b71011e9764cb12923d"}, - {file = "libcst-1.8.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dd7bdb14545c4b77a6c0eb39c86a76441fe833da800f6ca63e917e1273621029"}, - {file = "libcst-1.8.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6dc28d33ab8750a84c28b5625f7916846ecbecefd89bf75a5292a35644b6efbd"}, - {file = "libcst-1.8.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:970b7164a71c65e13c961965f9677bbbbeb21ce2e7e6655294f7f774156391c4"}, - {file = "libcst-1.8.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd74c543770e6a61dcb8846c9689dfcce2ad686658896f77f3e21b6ce94bcb2e"}, - {file = "libcst-1.8.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3d8e80cd1ed6577166f0bab77357f819f12564c2ed82307612e2bcc93e684d72"}, - {file = "libcst-1.8.5-cp314-cp314t-win_arm64.whl", hash = "sha256:a026aaa19cb2acd8a4d9e2a215598b0a7e2c194bf4482eb9dec4d781ec6e10b2"}, - {file = "libcst-1.8.5.tar.gz", hash = "sha256:e72e1816eed63f530668e93a4c22ff1cf8b91ddce0ec53e597d3f6c53e103ec7"}, + {file = "libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c"}, + {file = "libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661"}, + {file = "libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474"}, + {file = "libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8"}, + {file = "libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a"}, + {file = "libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47"}, + {file = "libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4"}, + {file = "libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9"}, + {file = "libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1"}, + {file = "libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4"}, + {file = "libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28"}, + {file = "libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa"}, + {file = "libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1"}, + {file = "libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996"}, + {file = "libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82"}, + {file = "libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f"}, + {file = "libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b"}, ] [[package]] @@ -623,49 +624,58 @@ files = [ [[package]] name = "pydantic" -version = "2.12.3" +version = "2.12.4" requires_python = ">=3.9" summary = "Data validation using Python type hints" dependencies = [ "annotated-types>=0.6.0", - "pydantic-core==2.41.4", + "pydantic-core==2.41.5", "typing-extensions>=4.14.1", "typing-inspection>=0.4.2", ] files = [ - {file = "pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf"}, - {file = "pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74"}, + {file = "pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e"}, + {file = "pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac"}, ] [[package]] name = "pydantic-core" -version = "2.41.4" +version = "2.41.5" requires_python = ">=3.9" summary = "Core functionality for Pydantic validation and serialization" dependencies = [ "typing-extensions>=4.14.1", ] files = [ - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}, - {file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, ] [[package]] @@ -688,38 +698,38 @@ files = [ [[package]] name = "pyrefly" -version = "0.39.4" +version = "0.41.3" requires_python = ">=3.8" summary = "A fast type checker and language server for Python with powerful IDE features" files = [ - {file = "pyrefly-0.39.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac4b5f98743f9f9525521ac7744cbdad3fee9e1ff16e564b371412aa3667ea35"}, - {file = "pyrefly-0.39.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c9be0811081c0792d3da3ecc44549e4ee5a5e4216847d4e6fe50ae1d999e4a34"}, - {file = "pyrefly-0.39.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d462501988a3fa048ff9844ef1491d26bf44365b7b6b14e60d7d33686216cc30"}, - {file = "pyrefly-0.39.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83384004d0c76012b57be54d00104e1a0d41b92c677bc1df3c850c22fdceccf6"}, - {file = "pyrefly-0.39.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f437e97f7003a444918f41a28538a8a392018750393bbc9a483be170a385f12"}, - {file = "pyrefly-0.39.4-py3-none-win32.whl", hash = "sha256:82c20315f287ecf29c9a41b4b388a64bbfb4b6a0d5c25e3be5b7a5126754863d"}, - {file = "pyrefly-0.39.4-py3-none-win_amd64.whl", hash = "sha256:90dd45fec11a0b02ad9eb2150f0ad0d500df9f1a301a9e97a5959b8cc6f68153"}, - {file = "pyrefly-0.39.4-py3-none-win_arm64.whl", hash = "sha256:b17aad782b37894784f0320d36a9f2b6476fdbf1b10d9838397e83564e58c442"}, - {file = "pyrefly-0.39.4.tar.gz", hash = "sha256:6a9d8cd6d2ac77d9f37b32dd6fa8d75718d34d5f3c360c6b9aaa61e0ef71757b"}, + {file = "pyrefly-0.41.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:75a8efa9737bc862edcaea9f41b56e97686d173b87a85e58ca73f93dcdd1a5f6"}, + {file = "pyrefly-0.41.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e920195930fb498266395c433ab5123db31a7488772664c66aa52d1d5893b6f7"}, + {file = "pyrefly-0.41.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f60887a7a2a819355665a949d8a51414e9a75c41706b90a1e63f4913202af18"}, + {file = "pyrefly-0.41.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f234b22396edcad35204e81b1925fc803e106ce4029fcaebd0a7d6889ca838c4"}, + {file = "pyrefly-0.41.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6beb41f6025036c098fd0a98bb9d3df602b973532fe4d1a413b5dbf0a6ff0e83"}, + {file = "pyrefly-0.41.3-py3-none-win32.whl", hash = "sha256:ca5903b7a9305b1e4b443ad8f55046cda49355de07bc6e7332456632d66c993a"}, + {file = "pyrefly-0.41.3-py3-none-win_amd64.whl", hash = "sha256:4a78d3f1819fb7a483c7f0d0ba37ed744ecc284bc84521ff051f433f5fef352f"}, + {file = "pyrefly-0.41.3-py3-none-win_arm64.whl", hash = "sha256:37d0c8e0aca9c693574304daa076267a732560521ee470743bdd2fe14f272175"}, + {file = "pyrefly-0.41.3.tar.gz", hash = "sha256:a2e47040a1f1e5d236775fed41f63270eb7ffd7ca2fed38d227ea133ccbbac68"}, ] [[package]] name = "pytest" -version = "8.4.2" -requires_python = ">=3.9" +version = "9.0.1" +requires_python = ">=3.10" summary = "pytest: simple powerful testing with Python" dependencies = [ "colorama>=0.4; sys_platform == \"win32\"", "exceptiongroup>=1; python_version < \"3.11\"", - "iniconfig>=1", - "packaging>=20", + "iniconfig>=1.0.1", + "packaging>=22", "pluggy<2,>=1.5", "pygments>=2.7.2", "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, + {file = "pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad"}, + {file = "pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8"}, ] [[package]] @@ -773,43 +783,29 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.2" -summary = "" -files = [ - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyyaml-ft" -version = "8.0.0" -summary = "" -files = [ - {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, - {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +version = "6.0.3" +requires_python = ">=3.8" +summary = "YAML parser and emitter for Python" +files = [ + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] [[package]] @@ -852,29 +848,29 @@ files = [ [[package]] name = "ruff" -version = "0.14.2" +version = "0.14.5" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1"}, - {file = "ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11"}, - {file = "ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770"}, - {file = "ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9"}, - {file = "ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af"}, - {file = "ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a"}, - {file = "ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96"}, + {file = "ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594"}, + {file = "ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72"}, + {file = "ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367"}, + {file = "ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b"}, + {file = "ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621"}, + {file = "ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4"}, + {file = "ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 1df5ba15..0003bf48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,18 +13,18 @@ requires-python = ">=3.14" dependencies = [ "typer>=0.20.0", - "pydantic>=2.12.3", + "pydantic>=2.12.4", "packaging>=25.0", "requests>=2.32.5", "types-requests>=2.32.4.20250913", ] [project.optional-dependencies] -pytest = ["pytest>=8.4.2", "pytest-mock>=3.15.1"] -git = ["dulwich>=0.24.8"] +pytest = ["pytest>=9.0.1", "pytest-mock>=3.15.1"] +git = ["dulwich>=0.24.10"] pdm = ["pdm>=2.26.1"] cmake = ["cmake>=4.1.2"] -conan = ["conan>=2.22.1", "libcst>=1.8.5"] +conan = ["conan>=2.22.2", "libcst>=1.8.6"] [project.urls] homepage = "https://github.com/Synodic-Software/CPPython" @@ -47,8 +47,8 @@ cppython = "cppython.plugins.pdm.plugin:CPPythonPlugin" cppython = "cppython.test.pytest.fixtures" [dependency-groups] -lint = ["ruff>=0.14.2", "pyrefly>=0.39.4"] -test = ["pytest>=8.4.2", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1"] +lint = ["ruff>=0.14.5", "pyrefly>=0.41.3"] +test = ["pytest>=9.0.1", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1"] [project.scripts] cppython = "cppython.console.entry:app" diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py index d01019ce..933b7dcc 100644 --- a/tests/fixtures/conan.py +++ b/tests/fixtures/conan.py @@ -51,7 +51,7 @@ def clean_conan_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): if user_profiles.exists(): test_profiles = conan_home / 'profiles' test_profiles.mkdir(parents=True, exist_ok=True) - + for profile_file in ('default', 'default_build'): if (src := user_profiles / profile_file).exists(): src.copy(test_profiles / profile_file) diff --git a/tests/unit/plugins/cmake/test_presets.py b/tests/unit/plugins/cmake/test_presets.py index cb0cdb27..2d21350f 100644 --- a/tests/unit/plugins/cmake/test_presets.py +++ b/tests/unit/plugins/cmake/test_presets.py @@ -87,7 +87,9 @@ def test_root_write(project_data: ProjectData) -> None: data = CMakeSyncData(provider_name=TypeName('test-provider')) - cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_preset_file, data) + cppython_preset_file = builder.write_cppython_preset( + cppython_preset_directory, provider_preset_file, data, project_data.project_root + ) build_directory = project_data.project_root / 'build' builder.write_root_presets( @@ -133,7 +135,11 @@ def test_relative_root_write(project_data: ProjectData) -> None: data = CMakeSyncData(provider_name=TypeName('test-provider')) - cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_preset_file, data) + # For this test, the root file is in a relative indirection subdirectory + project_root = root_file.parent + cppython_preset_file = builder.write_cppython_preset( + cppython_preset_directory, provider_preset_file, data, project_root + ) build_directory = project_data.project_root / 'build' builder.write_root_presets( diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py new file mode 100644 index 00000000..0ff701e0 --- /dev/null +++ b/tests/unit/test_configuration.py @@ -0,0 +1,505 @@ +"""Tests for the configuration loading and merging system""" + +from pathlib import Path + +import pytest + +from cppython.configuration import ConfigurationLoader + + +class TestConfigurationLoader: + """Tests for ConfigurationLoader class""" + + def test_load_pyproject_only(self, tmp_path: Path) -> None: + """Test loading configuration from pyproject.toml only""" + pyproject_path = tmp_path / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" + +[tool.cppython] +install-path = ".cppython" +dependencies = ["fmt>=10.0.0"] +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(tmp_path) + config = loader.load_cppython_table() + + assert config is not None + assert config['install-path'] == '.cppython' + assert config['dependencies'] == ['fmt>=10.0.0'] + + def test_load_cppython_toml(self, tmp_path: Path) -> None: + """Test loading configuration from cppython.toml""" + pyproject_path = tmp_path / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" +""", + encoding='utf-8', + ) + + cppython_path = tmp_path / 'cppython.toml' + cppython_path.write_text( + """ +[cppython] +install-path = ".cppython" +dependencies = ["fmt>=10.0.0"] +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(tmp_path) + config = loader.load_cppython_table() + + assert config is not None + assert config['install-path'] == '.cppython' + assert config['dependencies'] == ['fmt>=10.0.0'] + + def test_load_with_global_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test loading with global configuration""" + # Create a fake home directory with global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + global_config_dir = fake_home / '.cppython' + global_config_dir.mkdir() + global_config_path = global_config_dir / 'config.toml' + global_config_path.write_text( + """ +[cppython] +install-path = "/global/install" +tool-path = "global-tools" + +[cppython.providers.conan] +remotes = ["global-remote"] +""", + encoding='utf-8', + ) + + # Mock Path.home() to return fake home + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Create project with minimal config + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" + +[tool.cppython] +dependencies = ["fmt>=10.0.0"] +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(project_root) + config = loader.load_cppython_table() + + assert config is not None + # Project config overrides global + assert config['dependencies'] == ['fmt>=10.0.0'] + # Global config provides defaults + assert config['install-path'] == '/global/install' + assert config['tool-path'] == 'global-tools' + assert config['providers']['conan']['remotes'] == ['global-remote'] + + def test_local_overrides_affect_global_only(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that .cppython.toml only overrides global config, not project config""" + # Create fake home with global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + global_config_dir = fake_home / '.cppython' + global_config_dir.mkdir() + global_config_path = global_config_dir / 'config.toml' + global_config_path.write_text( + """ +[cppython] +install-path = "/global/install" +build-path = "global-build" + +[cppython.providers.conan] +remotes = ["global-remote"] +profile_dir = "global-profiles" +""", + encoding='utf-8', + ) + + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Create project + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" + +[tool.cppython] +dependencies = ["fmt>=10.0.0"] +build-path = "project-build" +""", + encoding='utf-8', + ) + + # Create local overrides + local_override_path = project_root / '.cppython.toml' + local_override_path.write_text( + """ +install-path = "/local/install" +build-path = "local-build" + +[providers.conan] +profile_dir = "/local/profiles" +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(project_root) + config = loader.load_cppython_table() + + assert config is not None + # Project config has highest priority (not overridden by local) + assert config['build-path'] == 'project-build' + assert config['dependencies'] == ['fmt>=10.0.0'] + + # Local override affects global config + assert config['install-path'] == '/local/install' + + # Provider settings: project doesn't override, so local override applies + assert config['providers']['conan']['profile_dir'] == '/local/profiles' + assert config['providers']['conan']['remotes'] == ['global-remote'] + + def test_conflicting_configs_error(self, tmp_path: Path) -> None: + """Test that using both pyproject.toml and cppython.toml raises error""" + pyproject_path = tmp_path / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" + +[tool.cppython] +install-path = ".cppython" +""", + encoding='utf-8', + ) + + cppython_path = tmp_path / 'cppython.toml' + cppython_path.write_text( + """ +[cppython] +install-path = "/other/path" +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(tmp_path) + + with pytest.raises(ValueError, match='both pyproject.toml and cppython.toml'): + loader.load_cppython_table() + + def test_deep_merge(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test deep merging of nested dictionaries across all config layers""" + # Global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + global_config_dir = fake_home / '.cppython' + global_config_dir.mkdir() + global_config_path = global_config_dir / 'config.toml' + global_config_path.write_text( + """ +[cppython.providers.conan] +remotes = ["global-remote"] +profile_dir = "global-profiles" +skip_upload = false + +[cppython.providers.vcpkg] +some_setting = "global-value" +""", + encoding='utf-8', + ) + + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Project config + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" + +[tool.cppython.providers.conan] +remotes = ["project-remote"] + +[tool.cppython.providers.vcpkg] +another_setting = "project-value" +""", + encoding='utf-8', + ) + + # Local overrides + local_override_path = project_root / '.cppython.toml' + local_override_path.write_text( + """ +[providers.conan] +profile_dir = "/custom/profiles" + +[providers.vcpkg] +some_setting = "override-value" +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(project_root) + config = loader.load_cppython_table() + + assert config is not None + # Project config overrides everything for conan remotes + assert config['providers']['conan']['remotes'] == ['project-remote'] + # Local override affects global, then project merges + assert config['providers']['conan']['profile_dir'] == '/custom/profiles' + assert config['providers']['conan']['skip_upload'] is False + + # vcpkg: deep merge across all layers + assert config['providers']['vcpkg']['some_setting'] == 'override-value' + assert config['providers']['vcpkg']['another_setting'] == 'project-value' + + def test_list_override_in_project_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that project config list values override global completely""" + # Global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + global_config_dir = fake_home / '.cppython' + global_config_dir.mkdir() + global_config_path = global_config_dir / 'config.toml' + global_config_path.write_text( + """ +[cppython] +dependencies = ["fmt>=10.0.0", "spdlog>=1.12.0"] +""", + encoding='utf-8', + ) + + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Project config + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" + +[tool.cppython] +dependencies = ["catch2>=3.0.0"] +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(project_root) + config = loader.load_cppython_table() + + assert config is not None + # Project list replaces global entirely + assert config['dependencies'] == ['catch2>=3.0.0'] + + def test_config_source_info(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test config_source_info returns correct existence flags""" + # Create fake home with global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + global_config_dir = fake_home / '.cppython' + global_config_dir.mkdir() + global_config_path = global_config_dir / 'config.toml' + global_config_path.write_text('[cppython]\ninstall-path = "/path"', encoding='utf-8') + + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Create project + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text('[project]\nname = "test"', encoding='utf-8') + + local_override_path = project_root / '.cppython.toml' + local_override_path.write_text('install-path = "/local"', encoding='utf-8') + + loader = ConfigurationLoader(project_root) + info = loader.config_source_info() + + assert info['global_config'] is True + assert info['pyproject'] is True + assert info['cppython'] is False + assert info['local_overrides'] is True + + def test_no_cppython_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test when no CPPython configuration exists anywhere""" + # No global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Project with no cppython config + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(project_root) + config = loader.load_cppython_table() + + assert config is None + + def test_only_local_overrides(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test local overrides when no global or project config exists""" + # No global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Project with only local overrides + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" +""", + encoding='utf-8', + ) + + local_override_path = project_root / '.cppython.toml' + local_override_path.write_text( + """ +install-path = "/custom/path" + +[providers.conan] +remotes = ["my-remote"] +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(project_root) + config = loader.load_cppython_table() + + assert config is not None + assert config['install-path'] == '/custom/path' + assert config['providers']['conan']['remotes'] == ['my-remote'] + + def test_global_config_missing_cppython_table(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that global config.toml without [cppython] table raises error""" + fake_home = tmp_path / 'home' + fake_home.mkdir() + global_config_dir = fake_home / '.cppython' + global_config_dir.mkdir() + global_config_path = global_config_dir / 'config.toml' + global_config_path.write_text( + """ +[other] +some_value = "value" +""", + encoding='utf-8', + ) + + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text('[project]\nname = "test"', encoding='utf-8') + + loader = ConfigurationLoader(project_root) + + with pytest.raises(ValueError, match='must contain a \\[cppython\\] table'): + loader.load_global_config() + + def test_cppython_toml_missing_cppython_table(self, tmp_path: Path) -> None: + """Test that cppython.toml without [cppython] table raises error""" + pyproject_path = tmp_path / 'pyproject.toml' + pyproject_path.write_text('[project]\nname = "test"', encoding='utf-8') + + cppython_path = tmp_path / 'cppython.toml' + cppython_path.write_text( + """ +[other] +some_value = "value" +""", + encoding='utf-8', + ) + + loader = ConfigurationLoader(tmp_path) + + with pytest.raises(ValueError, match='must contain a \\[cppython\\] table'): + loader.load_cppython_config() + + def test_get_project_data(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_project_data returns merged data in pyproject format""" + # Global config + fake_home = tmp_path / 'home' + fake_home.mkdir() + global_config_dir = fake_home / '.cppython' + global_config_dir.mkdir() + global_config_path = global_config_dir / 'config.toml' + global_config_path.write_text( + """ +[cppython] +install-path = "/global/install" +""", + encoding='utf-8', + ) + + monkeypatch.setattr(Path, 'home', lambda: fake_home) + + # Project config + project_root = tmp_path / 'project' + project_root.mkdir() + pyproject_path = project_root / 'pyproject.toml' + pyproject_path.write_text( + """ +[project] +name = "test-project" +version = "1.0.0" + +[tool.cppython] +dependencies = ["fmt>=10.0.0"] +""", + encoding='utf-8', + ) + + # Local overrides + local_override_path = project_root / '.cppython.toml' + local_override_path.write_text('install-path = "/custom/path"', encoding='utf-8') + + loader = ConfigurationLoader(project_root) + project_data = loader.get_project_data() + + assert project_data['project']['name'] == 'test-project' + # Project config has priority + assert project_data['tool']['cppython']['dependencies'] == ['fmt>=10.0.0'] + # Local override affects global, then merged with project + assert project_data['tool']['cppython']['install-path'] == '/custom/path' diff --git a/tests/unit/test_console.py b/tests/unit/test_console.py index 10311044..a2076c4c 100644 --- a/tests/unit/test_console.py +++ b/tests/unit/test_console.py @@ -1,8 +1,10 @@ """Tests the typer interface type""" +import pytest +import typer from typer.testing import CliRunner -from cppython.console.entry import app +from cppython.console.entry import _parse_groups_argument, app runner = CliRunner() @@ -15,3 +17,72 @@ def test_entrypoint() -> None: """Verifies that the entry functions with CPPython hooks""" with runner.isolated_filesystem(): runner.invoke(app, []) + + +class TestParseGroupsArgument: + """Tests for the _parse_groups_argument helper function""" + + @staticmethod + def test_none_input() -> None: + """Test that None input returns None""" + assert _parse_groups_argument(None) is None + + @staticmethod + def test_empty_string() -> None: + """Test that empty string returns None""" + assert _parse_groups_argument('') is None + assert _parse_groups_argument(' ') is None + + @staticmethod + def test_single_group() -> None: + """Test parsing a single group""" + result = _parse_groups_argument('[test]') + assert result == ['test'] + + @staticmethod + def test_multiple_groups() -> None: + """Test parsing multiple groups""" + result = _parse_groups_argument('[dev,test]') + assert result == ['dev', 'test'] + + @staticmethod + def test_groups_with_spaces() -> None: + """Test parsing groups with whitespace""" + result = _parse_groups_argument('[dev, test, docs]') + assert result == ['dev', 'test', 'docs'] + + @staticmethod + def test_missing_brackets() -> None: + """Test that missing brackets raises an error""" + with pytest.raises(typer.BadParameter, match='Invalid groups format'): + _parse_groups_argument('test') + + @staticmethod + def test_missing_opening_bracket() -> None: + """Test that missing opening bracket raises an error""" + with pytest.raises(typer.BadParameter, match='Invalid groups format'): + _parse_groups_argument('test]') + + @staticmethod + def test_missing_closing_bracket() -> None: + """Test that missing closing bracket raises an error""" + with pytest.raises(typer.BadParameter, match='Invalid groups format'): + _parse_groups_argument('[test') + + @staticmethod + def test_empty_brackets() -> None: + """Test that empty brackets raises an error""" + with pytest.raises(typer.BadParameter, match='Empty groups specification'): + _parse_groups_argument('[]') + + @staticmethod + def test_empty_group_name() -> None: + """Test that empty group names raise an error""" + with pytest.raises(typer.BadParameter, match='Group names cannot be empty'): + _parse_groups_argument('[test,,dev]') + + @staticmethod + def test_whitespace_only_group() -> None: + """Test that whitespace-only group names raise an error""" + with pytest.raises(typer.BadParameter, match='Group names cannot be empty'): + _parse_groups_argument('[test, ,dev]')