Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions cppython/console/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,47 @@ def get_enabled_project(context: typer.Context) -> Project:
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

Expand Down Expand Up @@ -83,33 +124,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')
Expand Down
16 changes: 12 additions & 4 deletions cppython/core/plugin_schema/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 36 additions & 21 deletions cppython/core/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -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
Expand All @@ -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), [])

Expand All @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down
10 changes: 10 additions & 0 deletions cppython/core/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Expand Down Expand Up @@ -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"""
Expand Down
49 changes: 49 additions & 0 deletions cppython/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
17 changes: 13 additions & 4 deletions cppython/plugins/cmake/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ 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

Args:
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
Expand All @@ -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)

Expand All @@ -55,20 +60,24 @@ 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

Args:
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'

Expand Down
4 changes: 3 additions & 1 deletion cppython/plugins/cmake/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading