Skip to content

Commit ba0a1f2

Browse files
committed
Group CLI
1 parent d0b81cb commit ba0a1f2

File tree

11 files changed

+316
-60
lines changed

11 files changed

+316
-60
lines changed

cppython/console/entry.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,47 @@ def get_enabled_project(context: typer.Context) -> Project:
3030
return project
3131

3232

33+
def _parse_groups_argument(groups: str | None) -> list[str] | None:
34+
"""Parse pip-style dependency groups from command argument.
35+
36+
Args:
37+
groups: Groups string like '[test]' or '[dev,test]' or None
38+
39+
Returns:
40+
List of group names or None if no groups specified
41+
42+
Raises:
43+
typer.BadParameter: If the groups format is invalid
44+
"""
45+
if groups is None:
46+
return None
47+
48+
# Strip whitespace
49+
groups = groups.strip()
50+
51+
if not groups:
52+
return None
53+
54+
# Check for square brackets
55+
if not (groups.startswith('[') and groups.endswith(']')):
56+
raise typer.BadParameter(f"Invalid groups format: '{groups}'. Use square brackets like: [test] or [dev,test]")
57+
58+
# Extract content between brackets and split by comma
59+
content = groups[1:-1].strip()
60+
61+
if not content:
62+
raise typer.BadParameter('Empty groups specification. Provide at least one group name.')
63+
64+
# Split by comma and strip whitespace from each group
65+
group_list = [g.strip() for g in content.split(',')]
66+
67+
# Validate group names are not empty
68+
if any(not g for g in group_list):
69+
raise typer.BadParameter('Group names cannot be empty.')
70+
71+
return group_list
72+
73+
3374
def _find_pyproject_file() -> Path:
3475
"""Searches upward for a pyproject.toml file
3576
@@ -83,33 +124,57 @@ def info(
83124
@app.command()
84125
def install(
85126
context: typer.Context,
127+
groups: Annotated[
128+
str | None,
129+
typer.Argument(
130+
help='Dependency groups to install in addition to base dependencies. '
131+
'Use square brackets like: [test] or [dev,test]'
132+
),
133+
] = None,
86134
) -> None:
87135
"""Install API call
88136
89137
Args:
90138
context: The CLI configuration object
139+
groups: Optional dependency groups to install (e.g., [test] or [dev,test])
91140
92141
Raises:
93142
ValueError: If the configuration object is missing
94143
"""
95144
project = get_enabled_project(context)
96-
project.install()
145+
146+
# Parse groups from pip-style syntax
147+
group_list = _parse_groups_argument(groups)
148+
149+
project.install(groups=group_list)
97150

98151

99152
@app.command()
100153
def update(
101154
context: typer.Context,
155+
groups: Annotated[
156+
str | None,
157+
typer.Argument(
158+
help='Dependency groups to update in addition to base dependencies. '
159+
'Use square brackets like: [test] or [dev,test]'
160+
),
161+
] = None,
102162
) -> None:
103163
"""Update API call
104164
105165
Args:
106166
context: The CLI configuration object
167+
groups: Optional dependency groups to update (e.g., [test] or [dev,test])
107168
108169
Raises:
109170
ValueError: If the configuration object is missing
110171
"""
111172
project = get_enabled_project(context)
112-
project.update()
173+
174+
# Parse groups from pip-style syntax
175+
group_list = _parse_groups_argument(groups)
176+
177+
project.update(groups=group_list)
113178

114179

115180
@app.command(name='list')

cppython/core/plugin_schema/provider.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,21 @@ def features(directory: DirectoryPath) -> SupportedFeatures:
8080
raise NotImplementedError
8181

8282
@abstractmethod
83-
def install(self) -> None:
84-
"""Called when dependencies need to be installed from a lock file."""
83+
def install(self, groups: list[str] | None = None) -> None:
84+
"""Called when dependencies need to be installed from a lock file.
85+
86+
Args:
87+
groups: Optional list of dependency group names to install in addition to base dependencies
88+
"""
8589
raise NotImplementedError
8690

8791
@abstractmethod
88-
def update(self) -> None:
89-
"""Called when dependencies need to be updated and written to the lock file."""
92+
def update(self, groups: list[str] | None = None) -> None:
93+
"""Called when dependencies need to be updated and written to the lock file.
94+
95+
Args:
96+
groups: Optional list of dependency group names to update in addition to base dependencies
97+
"""
9098
raise NotImplementedError
9199

92100
@abstractmethod

cppython/core/resolution.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ def resolve_pep621(
7676
return pep621_data
7777

7878

79+
def _resolve_absolute_path(path: Path, root_directory: Path) -> Path:
80+
"""Convert a path to absolute, using root_directory as base for relative paths.
81+
82+
Args:
83+
path: The path to resolve
84+
root_directory: The base directory for relative paths
85+
86+
Returns:
87+
The absolute path
88+
"""
89+
if path.is_absolute():
90+
return path
91+
return root_directory / path
92+
93+
7994
class PluginBuildData(CPPythonModel):
8095
"""Data needed to construct CoreData"""
8196

@@ -114,34 +129,20 @@ def resolve_cppython(
114129
"""
115130
root_directory = project_data.project_root.absolute()
116131

117-
# Add the base path to all relative paths
132+
# Resolve configuration path
118133
modified_configuration_path = local_configuration.configuration_path
119-
120-
# TODO: Grab configuration from the project, user, or system
121134
if modified_configuration_path is None:
122135
modified_configuration_path = root_directory / 'cppython.json'
136+
else:
137+
modified_configuration_path = _resolve_absolute_path(modified_configuration_path, root_directory)
123138

124-
if not modified_configuration_path.is_absolute():
125-
modified_configuration_path = root_directory / modified_configuration_path
126-
127-
modified_install_path = local_configuration.install_path
128-
129-
if not modified_install_path.is_absolute():
130-
modified_install_path = root_directory / modified_install_path
131-
132-
modified_tool_path = local_configuration.tool_path
133-
134-
if not modified_tool_path.is_absolute():
135-
modified_tool_path = root_directory / modified_tool_path
136-
137-
modified_build_path = local_configuration.build_path
138-
139-
if not modified_build_path.is_absolute():
140-
modified_build_path = root_directory / modified_build_path
139+
# Resolve other paths
140+
modified_install_path = _resolve_absolute_path(local_configuration.install_path, root_directory)
141+
modified_tool_path = _resolve_absolute_path(local_configuration.tool_path, root_directory)
142+
modified_build_path = _resolve_absolute_path(local_configuration.build_path, root_directory)
141143

142144
modified_provider_name = plugin_build_data.provider_name
143145
modified_generator_name = plugin_build_data.generator_name
144-
145146
modified_scm_name = plugin_build_data.scm_name
146147

147148
# Extract provider and generator configuration data

cppython/data.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from dataclasses import dataclass
44
from logging import Logger
55

6+
from packaging.requirements import Requirement
7+
68
from cppython.core.plugin_schema.generator import Generator
79
from cppython.core.plugin_schema.provider import Provider
810
from cppython.core.plugin_schema.scm import SCM
@@ -27,12 +29,59 @@ def __init__(self, core_data: CoreData, plugins: Plugins, logger: Logger) -> Non
2729
self._core_data = core_data
2830
self._plugins = plugins
2931
self.logger = logger
32+
self._active_groups: list[str] | None = None
3033

3134
@property
3235
def plugins(self) -> Plugins:
3336
"""The plugin data for CPPython"""
3437
return self._plugins
3538

39+
def set_active_groups(self, groups: list[str] | None) -> None:
40+
"""Set the active dependency groups for the current operation.
41+
42+
Args:
43+
groups: List of group names to activate, or None for no additional groups
44+
"""
45+
self._active_groups = groups
46+
if groups:
47+
self.logger.info('Active dependency groups: %s', ', '.join(groups))
48+
49+
# Validate that requested groups exist
50+
available_groups = set(self._core_data.cppython_data.dependency_groups.keys())
51+
requested_groups = set(groups)
52+
missing_groups = requested_groups - available_groups
53+
54+
if missing_groups:
55+
self.logger.warning(
56+
'Requested dependency groups not found: %s. Available groups: %s',
57+
', '.join(sorted(missing_groups)),
58+
', '.join(sorted(available_groups)) if available_groups else 'none',
59+
)
60+
61+
def apply_dependency_groups(self, groups: list[str] | None) -> None:
62+
"""Validate and log the dependency groups to be used.
63+
64+
Args:
65+
groups: List of group names to apply, or None for base dependencies only
66+
"""
67+
if groups:
68+
self.set_active_groups(groups)
69+
70+
def get_active_dependencies(self) -> list:
71+
"""Get the combined list of base dependencies and active group dependencies.
72+
73+
Returns:
74+
Combined list of Requirement objects from base and active groups
75+
"""
76+
dependencies: list[Requirement] = list(self._core_data.cppython_data.dependencies)
77+
78+
if self._active_groups:
79+
for group_name in self._active_groups:
80+
if group_name in self._core_data.cppython_data.dependency_groups:
81+
dependencies.extend(self._core_data.cppython_data.dependency_groups[group_name])
82+
83+
return dependencies
84+
3685
def sync(self) -> None:
3786
"""Gathers sync information from providers and passes it to the generator
3887

cppython/plugins/conan/plugin.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,20 @@ def information() -> Information:
6969
"""
7070
return Information()
7171

72-
def _install_dependencies(self, *, update: bool = False) -> None:
72+
def _install_dependencies(self, *, update: bool = False, groups: list[str] | None = None) -> None:
7373
"""Install/update dependencies using Conan CLI.
7474
7575
Args:
7676
update: If True, check remotes for newer versions/revisions and install those.
7777
If False, use cached versions when available.
78+
groups: Optional list of dependency group names to include
7879
"""
7980
operation = 'update' if update else 'install'
8081
logger = getLogger('cppython.conan')
8182

8283
try:
8384
# Setup environment and generate conanfile
84-
conanfile_path = self._prepare_installation()
85+
conanfile_path = self._prepare_installation(groups=groups)
8586
except Exception as e:
8687
raise ProviderInstallationError('conan', f'Failed to prepare {operation} environment: {e}', e) from e
8788

@@ -93,20 +94,27 @@ def _install_dependencies(self, *, update: bool = False) -> None:
9394
except Exception as e:
9495
raise ProviderInstallationError('conan', f'Failed to install dependencies: {e}', e) from e
9596

96-
def _prepare_installation(self) -> Path:
97+
def _prepare_installation(self, groups: list[str] | None = None) -> Path:
9798
"""Prepare the installation environment and generate conanfile.
9899
100+
Args:
101+
groups: Optional list of dependency group names to include
102+
99103
Returns:
100104
Path to conanfile.py
101105
"""
102-
# Resolve dependencies and generate conanfile.py
106+
# Resolve base dependencies
103107
resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies]
104108

105-
# Resolve dependency groups
106-
resolved_dependency_groups = {
107-
group_name: [resolve_conan_dependency(req) for req in group_requirements]
108-
for group_name, group_requirements in self.core_data.cppython_data.dependency_groups.items()
109-
}
109+
# Resolve only the requested dependency groups
110+
resolved_dependency_groups = {}
111+
if groups:
112+
for group_name in groups:
113+
if group_name in self.core_data.cppython_data.dependency_groups:
114+
resolved_dependency_groups[group_name] = [
115+
resolve_conan_dependency(req)
116+
for req in self.core_data.cppython_data.dependency_groups[group_name]
117+
]
110118

111119
self.builder.generate_conanfile(
112120
self.core_data.project_data.project_root,
@@ -180,13 +188,21 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str
180188
logger.error('Conan install failed: %s', error_msg, exc_info=True)
181189
raise ProviderInstallationError('conan', error_msg, e) from e
182190

183-
def install(self) -> None:
184-
"""Installs the provider"""
185-
self._install_dependencies(update=False)
191+
def install(self, groups: list[str] | None = None) -> None:
192+
"""Installs the provider
193+
194+
Args:
195+
groups: Optional list of dependency group names to install
196+
"""
197+
self._install_dependencies(update=False, groups=groups)
198+
199+
def update(self, groups: list[str] | None = None) -> None:
200+
"""Updates the provider
186201
187-
def update(self) -> None:
188-
"""Updates the provider"""
189-
self._install_dependencies(update=True)
202+
Args:
203+
groups: Optional list of dependency group names to update
204+
"""
205+
self._install_dependencies(update=True, groups=groups)
190206

191207
@staticmethod
192208
def supported_sync_type(sync_type: type[SyncData]) -> bool:

cppython/plugins/vcpkg/plugin.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,12 @@ async def download_tooling(cls, directory: Path) -> None:
231231

232232
cls._update_provider(directory)
233233

234-
def install(self) -> None:
235-
"""Called when dependencies need to be installed from a lock file."""
234+
def install(self, groups: list[str] | None = None) -> None:
235+
"""Called when dependencies need to be installed from a lock file.
236+
237+
Args:
238+
groups: Optional list of dependency group names to install (currently not used by vcpkg)
239+
"""
236240
manifest_directory = self.core_data.project_data.project_root
237241
manifest = generate_manifest(self.core_data, self.data)
238242

@@ -257,8 +261,12 @@ def install(self) -> None:
257261
except subprocess.CalledProcessError as e:
258262
self._handle_subprocess_error(logger, 'install project dependencies', e, ProviderInstallationError)
259263

260-
def update(self) -> None:
261-
"""Called when dependencies need to be updated and written to the lock file."""
264+
def update(self, groups: list[str] | None = None) -> None:
265+
"""Called when dependencies need to be updated and written to the lock file.
266+
267+
Args:
268+
groups: Optional list of dependency group names to update (currently not used by vcpkg)
269+
"""
262270
manifest_directory = self.core_data.project_data.project_root
263271

264272
manifest = generate_manifest(self.core_data, self.data)

0 commit comments

Comments
 (0)