Skip to content

Commit b4a9fbd

Browse files
authored
Console Overhaul (#138)
1 parent 04636b0 commit b4a9fbd

File tree

10 files changed

+562
-152
lines changed

10 files changed

+562
-152
lines changed

cppython/build/prepare.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
from pathlib import Path
1212
from typing import Any
1313

14+
from rich.console import Console
15+
1416
from cppython.core.interface import NoOpInterface
1517
from cppython.core.schema import ProjectConfiguration, SyncData
1618
from cppython.project import Project
1719
from cppython.utility.exception import InstallationVerificationError
20+
from cppython.utility.output import OutputSession
1821

1922

2023
@dataclass
@@ -89,31 +92,35 @@ def prepare(self) -> BuildPreparationResult:
8992
verbosity=1,
9093
)
9194

92-
# Create the CPPython project
93-
interface = BuildInterface()
94-
project = Project(project_config, interface, pyproject_data)
95+
# Use a headless console on stderr — no spinner in build backend context
96+
console = Console(stderr=True, width=120)
97+
98+
with OutputSession(console, verbose=False) as session:
99+
# Create the CPPython project
100+
interface = BuildInterface()
101+
project = Project(project_config, interface, pyproject_data, session=session)
95102

96-
if not project.enabled:
97-
self.logger.info('CPPython: Project not enabled, skipping preparation')
98-
return BuildPreparationResult()
103+
if not project.enabled:
104+
self.logger.info('CPPython: Project not enabled, skipping preparation')
105+
return BuildPreparationResult()
99106

100-
# Sync and verify — does NOT install dependencies
101-
self.logger.info('CPPython: Verifying C++ dependencies are installed')
107+
# Sync and verify — does NOT install dependencies
108+
self.logger.info('CPPython: Verifying C++ dependencies are installed')
102109

103-
try:
104-
sync_data = project.prepare_build()
105-
except InstallationVerificationError:
106-
self.logger.error(
107-
"CPPython: C++ dependencies not installed. Run 'cppython install' or 'pdm install' before building."
108-
)
109-
raise
110+
try:
111+
sync_data = project.prepare_build()
112+
except InstallationVerificationError:
113+
self.logger.error(
114+
"CPPython: C++ dependencies not installed. Run 'cppython install' or 'pdm install' before building."
115+
)
116+
raise
110117

111-
if sync_data:
112-
self.logger.info('CPPython: Sync data obtained from provider: %s', type(sync_data).__name__)
113-
else:
114-
self.logger.warning('CPPython: No sync data generated')
118+
if sync_data:
119+
self.logger.info('CPPython: Sync data obtained from provider: %s', type(sync_data).__name__)
120+
else:
121+
self.logger.warning('CPPython: No sync data generated')
115122

116-
return BuildPreparationResult(sync_data=sync_data)
123+
return BuildPreparationResult(sync_data=sync_data)
117124

118125

119126
def prepare_build(source_dir: Path) -> BuildPreparationResult:

cppython/builder.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def __init__(self, project_configuration: ProjectConfiguration, logger: Logger)
395395
# Informal standard to check for color
396396
force_color = os.getenv('FORCE_COLOR', '1') != '0'
397397

398-
console = Console(
398+
self._console = Console(
399399
force_terminal=force_color,
400400
color_system='auto',
401401
width=120,
@@ -404,7 +404,7 @@ def __init__(self, project_configuration: ProjectConfiguration, logger: Logger)
404404
)
405405

406406
rich_handler = RichHandler(
407-
console=console,
407+
console=self._console,
408408
rich_tracebacks=True,
409409
show_time=False,
410410
show_path=False,
@@ -420,6 +420,11 @@ def __init__(self, project_configuration: ProjectConfiguration, logger: Logger)
420420

421421
self._resolver = Resolver(self._project_configuration, self._logger)
422422

423+
@property
424+
def console(self) -> Console:
425+
"""The Rich console instance used for terminal output."""
426+
return self._console
427+
423428
def build(
424429
self,
425430
pep621_configuration: PEP621Configuration,

cppython/console/entry.py

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
11
"""A Typer CLI for CPPython interfacing"""
22

3+
import contextlib
4+
from collections.abc import Generator
35
from importlib.metadata import entry_points
46
from pathlib import Path
57
from typing import Annotated
68

79
import typer
810
from rich import print
11+
from rich.console import Console
912
from rich.syntax import Syntax
1013

1114
from cppython.configuration import ConfigurationLoader
1215
from cppython.console.schema import ConsoleConfiguration, ConsoleInterface
1316
from cppython.core.schema import PluginReport, ProjectConfiguration
1417
from cppython.project import Project
18+
from cppython.utility.output import OutputSession
1519

1620
app = typer.Typer(no_args_is_help=True)
1721

18-
info_app = typer.Typer(no_args_is_help=True, help='Prints project information including plugin configuration, managed files, and templates.')
22+
info_app = typer.Typer(
23+
no_args_is_help=True,
24+
help='Prints project information including plugin configuration, managed files, and templates.',
25+
)
1926
app.add_typer(info_app, name='info')
2027

2128
list_app = typer.Typer(no_args_is_help=True, help='List project entities.')
2229
app.add_typer(list_app, name='list')
2330

2431

25-
def get_enabled_project(context: typer.Context) -> Project:
26-
"""Helper to load and validate an enabled Project from CLI context."""
32+
def _get_configuration(context: typer.Context) -> ConsoleConfiguration:
33+
"""Extract the ConsoleConfiguration object from the CLI context.
34+
35+
Raises:
36+
ValueError: If the configuration object is missing
37+
"""
2738
configuration = context.find_object(ConsoleConfiguration)
2839
if configuration is None:
2940
raise ValueError('The configuration object is missing')
41+
return configuration
42+
43+
44+
def get_enabled_project(context: typer.Context) -> Project:
45+
"""Helper to load and validate an enabled Project from CLI context."""
46+
configuration = _get_configuration(context)
3047

3148
# Use ConfigurationLoader to load and merge all configuration sources
3249
loader = ConfigurationLoader(configuration.project_configuration.project_root)
@@ -44,6 +61,23 @@ def get_enabled_project(context: typer.Context) -> Project:
4461
return project
4562

4663

64+
@contextlib.contextmanager
65+
def _session_project(context: typer.Context) -> Generator[Project]:
66+
"""Create an enabled Project wrapped in an OutputSession.
67+
68+
Yields the project with its session already attached. The session
69+
(spinner + log file) is torn down when the ``with`` block exits.
70+
"""
71+
project = get_enabled_project(context)
72+
configuration = _get_configuration(context)
73+
verbose = configuration.project_configuration.verbosity > 0
74+
console = Console(width=120)
75+
76+
with OutputSession(console, verbose=verbose) as session:
77+
project.session = session
78+
yield project
79+
80+
4781
def _parse_groups_argument(groups: str | None) -> list[str] | None:
4882
"""Parse pip-style dependency groups from command argument.
4983
@@ -208,12 +242,10 @@ def install(
208242
Raises:
209243
ValueError: If the configuration object is missing
210244
"""
211-
project = get_enabled_project(context)
212-
213-
# Parse groups from pip-style syntax
214245
group_list = _parse_groups_argument(groups)
215246

216-
project.install(groups=group_list)
247+
with _session_project(context) as project:
248+
project.install(groups=group_list)
217249

218250

219251
@app.command()
@@ -236,12 +268,10 @@ def update(
236268
Raises:
237269
ValueError: If the configuration object is missing
238270
"""
239-
project = get_enabled_project(context)
240-
241-
# Parse groups from pip-style syntax
242271
group_list = _parse_groups_argument(groups)
243272

244-
project.update(groups=group_list)
273+
with _session_project(context) as project:
274+
project.update(groups=group_list)
245275

246276

247277
@list_app.command()
@@ -292,8 +322,8 @@ def publish(
292322
Raises:
293323
ValueError: If the configuration object is missing
294324
"""
295-
project = get_enabled_project(context)
296-
project.publish()
325+
with _session_project(context) as project:
326+
project.publish()
297327

298328

299329
@app.command()
@@ -312,8 +342,8 @@ def build(
312342
context: The CLI configuration object
313343
configuration: Optional named configuration
314344
"""
315-
project = get_enabled_project(context)
316-
project.build(configuration=configuration)
345+
with _session_project(context) as project:
346+
project.build(configuration=configuration)
317347

318348

319349
@app.command()
@@ -332,8 +362,8 @@ def test(
332362
context: The CLI configuration object
333363
configuration: Optional named configuration
334364
"""
335-
project = get_enabled_project(context)
336-
project.test(configuration=configuration)
365+
with _session_project(context) as project:
366+
project.test(configuration=configuration)
337367

338368

339369
@app.command()
@@ -352,8 +382,8 @@ def bench(
352382
context: The CLI configuration object
353383
configuration: Optional named configuration
354384
"""
355-
project = get_enabled_project(context)
356-
project.bench(configuration=configuration)
385+
with _session_project(context) as project:
386+
project.bench(configuration=configuration)
357387

358388

359389
@app.command()
@@ -377,5 +407,5 @@ def run(
377407
target: The name of the build target to run
378408
configuration: Optional named configuration
379409
"""
380-
project = get_enabled_project(context)
381-
project.run(target, configuration=configuration)
410+
with _session_project(context) as project:
411+
project.run(target, configuration=configuration)

cppython/plugins/cmake/plugin.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""The CMake generator implementation"""
22

3-
import subprocess
3+
from logging import getLogger
44
from pathlib import Path
55
from typing import Any
66

@@ -13,6 +13,9 @@
1313
from cppython.plugins.cmake.builder import Builder
1414
from cppython.plugins.cmake.resolution import resolve_cmake_data
1515
from cppython.plugins.cmake.schema import CMakeSyncData
16+
from cppython.utility.subprocess import run_subprocess
17+
18+
logger = getLogger('cppython.cmake')
1619

1720

1821
class CMakeGenerator(Generator):
@@ -136,7 +139,7 @@ def build(self, configuration: str | None = None) -> None:
136139
"""
137140
preset = self._resolve_configuration(configuration)
138141
cmd = [self._cmake_command(), '--build', '--preset', preset]
139-
subprocess.run(cmd, check=True, cwd=self.data.preset_file.parent)
142+
run_subprocess(cmd, cwd=self.data.preset_file.parent, logger=logger)
140143

141144
def test(self, configuration: str | None = None) -> None:
142145
"""Runs tests using ctest with the resolved preset.
@@ -146,7 +149,7 @@ def test(self, configuration: str | None = None) -> None:
146149
"""
147150
preset = self._resolve_configuration(configuration)
148151
cmd = [self._ctest_command(), '--preset', preset]
149-
subprocess.run(cmd, check=True, cwd=self.data.preset_file.parent)
152+
run_subprocess(cmd, cwd=self.data.preset_file.parent, logger=logger)
150153

151154
def bench(self, configuration: str | None = None) -> None:
152155
"""Runs benchmarks using ctest with the resolved preset.
@@ -156,7 +159,7 @@ def bench(self, configuration: str | None = None) -> None:
156159
"""
157160
preset = self._resolve_configuration(configuration)
158161
cmd = [self._ctest_command(), '--preset', preset]
159-
subprocess.run(cmd, check=True, cwd=self.data.preset_file.parent)
162+
run_subprocess(cmd, cwd=self.data.preset_file.parent, logger=logger)
160163

161164
def run(self, target: str, configuration: str | None = None) -> None:
162165
"""Runs a built executable by target name.
@@ -180,7 +183,30 @@ def run(self, target: str, configuration: str | None = None) -> None:
180183
raise FileNotFoundError(f"Could not find executable '{target}' in build directory: {build_path}")
181184

182185
executable = executables[0]
183-
subprocess.run([str(executable)], check=True, cwd=self.data.preset_file.parent)
186+
run_subprocess([str(executable)], cwd=self.data.preset_file.parent, logger=logger)
187+
188+
def list_targets(self) -> list[str]:
189+
"""Lists discovered build targets/executables in the CMake build directory.
190+
191+
Searches the build directory for executable files, excluding common
192+
non-target files.
193+
194+
Returns:
195+
A sorted list of unique target names found.
196+
"""
197+
build_path = self.core_data.cppython_data.build_path
198+
199+
if not build_path.exists():
200+
return []
201+
202+
# Collect executable files from the build directory
203+
targets: set[str] = set()
204+
for candidate in build_path.rglob('*'):
205+
if candidate.is_file() and (candidate.stat().st_mode & 0o111 or candidate.suffix == '.exe'):
206+
# Use the stem (name without extension) as the target name
207+
targets.add(candidate.stem)
208+
209+
return sorted(targets)
184210

185211
def list_targets(self) -> list[str]:
186212
"""Lists discovered build targets/executables in the CMake build directory.

0 commit comments

Comments
 (0)