Skip to content

Commit 6a23ffe

Browse files
committed
Group CLI
1 parent d0b81cb commit 6a23ffe

File tree

7 files changed

+269
-30
lines changed

7 files changed

+269
-30
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/data.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,61 @@ def __init__(self, core_data: CoreData, plugins: Plugins, logger: Logger) -> Non
2727
self._core_data = core_data
2828
self._plugins = plugins
2929
self.logger = logger
30+
self._active_groups: list[str] | None = None
3031

3132
@property
3233
def plugins(self) -> Plugins:
3334
"""The plugin data for CPPython"""
3435
return self._plugins
3536

37+
def set_active_groups(self, groups: list[str] | None) -> None:
38+
"""Set the active dependency groups for the current operation.
39+
40+
Args:
41+
groups: List of group names to activate, or None for no additional groups
42+
"""
43+
self._active_groups = groups
44+
if groups:
45+
self.logger.info('Active dependency groups: %s', ', '.join(groups))
46+
47+
# Validate that requested groups exist
48+
available_groups = set(self._core_data.cppython_data.dependency_groups.keys())
49+
requested_groups = set(groups)
50+
missing_groups = requested_groups - available_groups
51+
52+
if missing_groups:
53+
self.logger.warning(
54+
'Requested dependency groups not found: %s. Available groups: %s',
55+
', '.join(sorted(missing_groups)),
56+
', '.join(sorted(available_groups)) if available_groups else 'none',
57+
)
58+
59+
def apply_dependency_groups(self, groups: list[str] | None) -> None:
60+
"""Validate and log the dependency groups to be used.
61+
62+
Args:
63+
groups: List of group names to apply, or None for base dependencies only
64+
"""
65+
if groups:
66+
self.set_active_groups(groups)
67+
68+
def get_active_dependencies(self) -> list:
69+
"""Get the combined list of base dependencies and active group dependencies.
70+
71+
Returns:
72+
Combined list of Requirement objects from base and active groups
73+
"""
74+
from packaging.requirements import Requirement
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/project.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ def enabled(self) -> bool:
5252
"""
5353
return self._enabled
5454

55-
def install(self) -> None:
55+
def install(self, groups: list[str] | None = None) -> None:
5656
"""Installs project dependencies
5757
58+
Args:
59+
groups: Optional list of dependency groups to install in addition to base dependencies
60+
5861
Raises:
5962
Exception: Provider-specific exceptions are propagated with full context
6063
"""
@@ -66,17 +69,28 @@ def install(self) -> None:
6669
asyncio.run(self._data.download_provider_tools())
6770

6871
self.logger.info('Installing project')
72+
73+
# Log active groups
74+
if groups:
75+
self.logger.info('Installing with dependency groups: %s', ', '.join(groups))
76+
6977
self.logger.info('Installing %s provider', self._data.plugins.provider.name())
7078

79+
# Validate and log active groups
80+
self._data.apply_dependency_groups(groups)
81+
7182
# Sync before install to allow provider to access generator's resolved configuration
7283
self._data.sync()
7384

7485
# Let provider handle its own exceptions for better error context
75-
self._data.plugins.provider.install()
86+
self._data.plugins.provider.install(groups=groups)
7687

77-
def update(self) -> None:
88+
def update(self, groups: list[str] | None = None) -> None:
7889
"""Updates project dependencies
7990
91+
Args:
92+
groups: Optional list of dependency groups to update in addition to base dependencies
93+
8094
Raises:
8195
Exception: Provider-specific exception
8296
"""
@@ -88,13 +102,21 @@ def update(self) -> None:
88102
asyncio.run(self._data.download_provider_tools())
89103

90104
self.logger.info('Updating project')
105+
106+
# Log active groups
107+
if groups:
108+
self.logger.info('Updating with dependency groups: %s', ', '.join(groups))
109+
91110
self.logger.info('Updating %s provider', self._data.plugins.provider.name())
92111

112+
# Validate and log active groups
113+
self._data.apply_dependency_groups(groups)
114+
93115
# Sync before update to allow provider to access generator's resolved configuration
94116
self._data.sync()
95117

96118
# Let provider handle its own exceptions for better error context
97-
self._data.plugins.provider.update()
119+
self._data.plugins.provider.update(groups=groups)
98120

99121
def publish(self) -> None:
100122
"""Publishes the project

cppython/schema.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ class API(Protocol):
88
"""Project API specification"""
99

1010
@abstractmethod
11-
def install(self) -> None:
12-
"""Installs project dependencies"""
11+
def install(self, groups: list[str] | None = None) -> None:
12+
"""Installs project dependencies
13+
14+
Args:
15+
groups: Optional list of dependency groups to install
16+
"""
1317
raise NotImplementedError()
1418

1519
@abstractmethod
16-
def update(self) -> None:
17-
"""Updates project dependencies"""
20+
def update(self, groups: list[str] | None = None) -> None:
21+
"""Updates project dependencies
22+
23+
Args:
24+
groups: Optional list of dependency groups to update
25+
"""
1826
raise NotImplementedError()

0 commit comments

Comments
 (0)