Skip to content

Commit 11a432b

Browse files
committed
Console Overhaul
1 parent 21ecfa4 commit 11a432b

File tree

10 files changed

+535
-151
lines changed

10 files changed

+535
-151
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: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
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

@@ -25,11 +29,21 @@
2529
app.add_typer(list_app, name='list')
2630

2731

28-
def get_enabled_project(context: typer.Context) -> Project:
29-
"""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+
"""
3038
configuration = context.find_object(ConsoleConfiguration)
3139
if configuration is None:
3240
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)
3347

3448
# Use ConfigurationLoader to load and merge all configuration sources
3549
loader = ConfigurationLoader(configuration.project_configuration.project_root)
@@ -47,6 +61,23 @@ def get_enabled_project(context: typer.Context) -> Project:
4761
return project
4862

4963

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+
5081
def _parse_groups_argument(groups: str | None) -> list[str] | None:
5182
"""Parse pip-style dependency groups from command argument.
5283
@@ -211,12 +242,10 @@ def install(
211242
Raises:
212243
ValueError: If the configuration object is missing
213244
"""
214-
project = get_enabled_project(context)
215-
216-
# Parse groups from pip-style syntax
217245
group_list = _parse_groups_argument(groups)
218246

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

221250

222251
@app.command()
@@ -239,12 +268,10 @@ def update(
239268
Raises:
240269
ValueError: If the configuration object is missing
241270
"""
242-
project = get_enabled_project(context)
243-
244-
# Parse groups from pip-style syntax
245271
group_list = _parse_groups_argument(groups)
246272

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

249276

250277
@list_app.command()
@@ -295,8 +322,8 @@ def publish(
295322
Raises:
296323
ValueError: If the configuration object is missing
297324
"""
298-
project = get_enabled_project(context)
299-
project.publish()
325+
with _session_project(context) as project:
326+
project.publish()
300327

301328

302329
@app.command()
@@ -315,8 +342,8 @@ def build(
315342
context: The CLI configuration object
316343
configuration: Optional named configuration
317344
"""
318-
project = get_enabled_project(context)
319-
project.build(configuration=configuration)
345+
with _session_project(context) as project:
346+
project.build(configuration=configuration)
320347

321348

322349
@app.command()
@@ -335,8 +362,8 @@ def test(
335362
context: The CLI configuration object
336363
configuration: Optional named configuration
337364
"""
338-
project = get_enabled_project(context)
339-
project.test(configuration=configuration)
365+
with _session_project(context) as project:
366+
project.test(configuration=configuration)
340367

341368

342369
@app.command()
@@ -355,8 +382,8 @@ def bench(
355382
context: The CLI configuration object
356383
configuration: Optional named configuration
357384
"""
358-
project = get_enabled_project(context)
359-
project.bench(configuration=configuration)
385+
with _session_project(context) as project:
386+
project.bench(configuration=configuration)
360387

361388

362389
@app.command()
@@ -380,5 +407,5 @@ def run(
380407
target: The name of the build target to run
381408
configuration: Optional named configuration
382409
"""
383-
project = get_enabled_project(context)
384-
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: 8 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,7 @@ 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)
184187

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

cppython/plugins/conan/plugin.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
installation, and synchronization with other tools.
66
"""
77

8+
import contextlib
9+
import io
810
import os
911
from logging import Logger, getLogger
1012
from pathlib import Path
@@ -48,6 +50,43 @@ def __init__(
4850
self._ensure_default_profiles()
4951

5052
self._cmake_binary: str | None = None
53+
self._logger = getLogger('cppython.conan')
54+
55+
def _capture_conan_call(self, args: list[str]) -> None:
56+
"""Run a Conan CLI command while capturing stdout/stderr to the logger.
57+
58+
Args:
59+
args: Command arguments to pass to ``conan_api.command.run``.
60+
61+
Raises:
62+
Exception: Re-raises any exception from the Conan API after logging.
63+
"""
64+
stdout_capture = io.StringIO()
65+
stderr_capture = io.StringIO()
66+
67+
try:
68+
with contextlib.redirect_stdout(stdout_capture), contextlib.redirect_stderr(stderr_capture):
69+
self._conan_api.command.run(args)
70+
except Exception:
71+
# Log captured output before re-raising
72+
captured_out = stdout_capture.getvalue()
73+
captured_err = stderr_capture.getvalue()
74+
if captured_out:
75+
for line in captured_out.splitlines():
76+
self._logger.error('%s', line)
77+
if captured_err:
78+
for line in captured_err.splitlines():
79+
self._logger.error('%s', line)
80+
raise
81+
else:
82+
captured_out = stdout_capture.getvalue()
83+
captured_err = stderr_capture.getvalue()
84+
if captured_out:
85+
for line in captured_out.splitlines():
86+
self._logger.debug('%s', line)
87+
if captured_err:
88+
for line in captured_err.splitlines():
89+
self._logger.debug('%s', line)
5190

5291
@staticmethod
5392
def features(directory: Path) -> SupportedFeatures:
@@ -203,7 +242,7 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str
203242
original_cwd = os.getcwd()
204243
try:
205244
os.chdir(str(self.core_data.project_data.project_root))
206-
self._conan_api.command.run(command_args)
245+
self._capture_conan_call(command_args)
207246
finally:
208247
os.chdir(original_cwd)
209248
except Exception as e:
@@ -432,7 +471,7 @@ def _run_conan_create(self, conanfile_path: Path, build_type: str, logger: Logge
432471
original_cwd = os.getcwd()
433472
try:
434473
os.chdir(str(self.core_data.project_data.project_root))
435-
self._conan_api.command.run(command_args)
474+
self._capture_conan_call(command_args)
436475
finally:
437476
os.chdir(original_cwd)
438477

@@ -451,7 +490,7 @@ def _upload_package(self, logger) -> None:
451490
logger.info('Executing conan upload command: conan %s', ' '.join(command_args))
452491

453492
try:
454-
self._conan_api.command.run(command_args)
493+
self._capture_conan_call(command_args)
455494
except Exception as e:
456495
error_msg = str(e)
457496
logger.error('Conan upload failed for remote %s: %s', remote, error_msg, exc_info=True)
@@ -462,7 +501,7 @@ def _upload_package(self, logger) -> None:
462501
logger.info('Executing conan upload command: conan %s', ' '.join(command_args))
463502

464503
try:
465-
self._conan_api.command.run(command_args)
504+
self._capture_conan_call(command_args)
466505
except Exception as e:
467506
error_msg = str(e)
468507
logger.error('Conan upload failed: %s', error_msg, exc_info=True)

0 commit comments

Comments
 (0)