Skip to content

Commit 5afecea

Browse files
authored
Info Command (synodic#136)
1 parent f380f9c commit 5afecea

File tree

8 files changed

+186
-10
lines changed

8 files changed

+186
-10
lines changed

cppython/console/entry.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
import typer
77
from rich import print
8+
from rich.syntax import Syntax
89

910
from cppython.configuration import ConfigurationLoader
1011
from cppython.console.schema import ConsoleConfiguration, ConsoleInterface
11-
from cppython.core.schema import ProjectConfiguration
12+
from cppython.core.schema import PluginReport, ProjectConfiguration
1213
from cppython.project import Project
1314

1415
app = typer.Typer(no_args_is_help=True)
@@ -124,9 +125,41 @@ def main(
124125

125126
@app.command()
126127
def info(
127-
_: typer.Context,
128+
context: typer.Context,
128129
) -> None:
129-
"""Prints project information"""
130+
"""Prints project information including plugin configuration, managed files, and templates."""
131+
project = get_enabled_project(context)
132+
project_info = project.info()
133+
134+
if not project_info:
135+
return
136+
137+
for role in ('provider', 'generator'):
138+
entry = project_info.get(role)
139+
if entry is None:
140+
continue
141+
142+
name: str = entry['name']
143+
report: PluginReport = entry['report']
144+
145+
print(f'\n[bold]{role.title()}:[/bold] {name}')
146+
147+
if report.configuration:
148+
print(' [bold]Configuration:[/bold]')
149+
for key, value in report.configuration.items():
150+
print(f' {key}: {value}')
151+
152+
if report.managed_files:
153+
print(' [bold]Managed files:[/bold]')
154+
for path in report.managed_files:
155+
print(f' {path}')
156+
157+
if report.template_files:
158+
print(' [bold]Templates:[/bold]')
159+
for filename, content in report.template_files.items():
160+
print(f' [cyan]{filename}[/cyan]')
161+
print()
162+
print(Syntax(content, 'python', theme='monokai', line_numbers=True))
130163

131164

132165
@app.command()

cppython/core/schema.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,27 @@ def validate_absolute_path(cls, value: Path) -> Path:
145145
CPPythonPluginData = NewType('CPPythonPluginData', CPPythonData)
146146

147147

148+
class PluginReport(CPPythonModel):
149+
"""Report returned by a data plugin's ``plugin_info()`` method.
150+
151+
Contains the plugin's current configuration, any managed files it writes,
152+
and the content of user-facing template files it can generate.
153+
"""
154+
155+
configuration: Annotated[
156+
dict[str, Any],
157+
Field(description='Key-value pairs of the resolved plugin configuration'),
158+
] = {}
159+
managed_files: Annotated[
160+
list[Path],
161+
Field(description='Paths to files that are fully managed (auto-generated) by the plugin'),
162+
] = []
163+
template_files: Annotated[
164+
dict[str, str],
165+
Field(description='Mapping of template file names to their current content'),
166+
] = {}
167+
168+
148169
class SyncData(CPPythonModel):
149170
"""Data that passes in a plugin sync"""
150171

@@ -249,6 +270,17 @@ def features(directory: DirectoryPath) -> SupportedFeatures:
249270
"""
250271
raise NotImplementedError
251272

273+
def plugin_info(self) -> PluginReport:
274+
"""Return a report describing this plugin's configuration, managed files, and templates.
275+
276+
Plugins should override this method to provide meaningful information.
277+
The default implementation returns an empty report.
278+
279+
Returns:
280+
A :class:`PluginReport` with plugin-specific details.
281+
"""
282+
return PluginReport()
283+
252284
@classmethod
253285
async def download_tooling(cls, directory: DirectoryPath) -> None:
254286
"""Installs the external tooling required by the plugin. Should be overridden if required

cppython/plugins/cmake/plugin.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
GeneratorPluginGroupData,
1010
SupportedGeneratorFeatures,
1111
)
12-
from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData
12+
from cppython.core.schema import CorePluginData, Information, PluginReport, SupportedFeatures, SyncData
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
@@ -181,3 +181,25 @@ def run(self, target: str, configuration: str | None = None) -> None:
181181

182182
executable = executables[0]
183183
subprocess.run([str(executable)], check=True, cwd=self.data.preset_file.parent)
184+
185+
def plugin_info(self) -> PluginReport:
186+
"""Return a report describing the CMake generator's configuration and managed files.
187+
188+
Returns:
189+
A :class:`PluginReport` with CMake-specific details.
190+
"""
191+
managed = [self._cppython_preset_directory / 'CPPython.json']
192+
193+
config: dict[str, object] = {
194+
'preset_file': str(self.data.preset_file),
195+
'configuration_name': self.data.configuration_name,
196+
}
197+
if self.data.cmake_binary is not None:
198+
config['cmake_binary'] = str(self.data.cmake_binary)
199+
if self.data.default_configuration is not None:
200+
config['default_configuration'] = self.data.default_configuration
201+
202+
return PluginReport(
203+
configuration=config,
204+
managed_files=managed,
205+
)

cppython/plugins/conan/builder.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,19 @@ def build_requirements(self):
7676
base_file.write_text(content, encoding='utf-8')
7777

7878
@staticmethod
79-
def _create_conanfile(
80-
conan_file: Path,
79+
def _conanfile_content(
8180
name: str,
8281
version: str,
83-
) -> None:
84-
"""Creates a conanfile.py file that inherits from CPPython base."""
82+
) -> str:
83+
"""Return the conanfile.py template content as a string without writing to disk.
84+
85+
Args:
86+
name: The project name
87+
version: The project version
88+
89+
Returns:
90+
The full conanfile.py template string
91+
"""
8592
class_name = name.replace('-', '_').title().replace('_', '')
8693
content = f'''from conan.tools.cmake import CMake, CMakeConfigDeps, CMakeToolchain
8794
from conan.tools.files import copy
@@ -154,6 +161,16 @@ def export_sources(self):
154161
copy(self, "src/*", src=self.recipe_folder, dst=self.export_sources_folder)
155162
copy(self, "cmake/*", src=self.recipe_folder, dst=self.export_sources_folder)
156163
'''
164+
return content
165+
166+
@staticmethod
167+
def _create_conanfile(
168+
conan_file: Path,
169+
name: str,
170+
version: str,
171+
) -> None:
172+
"""Creates a conanfile.py file that inherits from CPPython base."""
173+
content = Builder._conanfile_content(name, version)
157174
conan_file.write_text(content, encoding='utf-8')
158175

159176
def generate_conanfile(

cppython/plugins/conan/plugin.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from cppython.core.plugin_schema.generator import SyncConsumer
1717
from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures
18-
from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData
18+
from cppython.core.schema import CorePluginData, Information, PluginReport, SupportedFeatures, SyncData
1919
from cppython.plugins.cmake.plugin import CMakeGenerator
2020
from cppython.plugins.cmake.schema import CMakeSyncData
2121
from cppython.plugins.conan.builder import Builder
@@ -467,3 +467,27 @@ def _upload_package(self, logger) -> None:
467467
error_msg = str(e)
468468
logger.error('Conan upload failed: %s', error_msg, exc_info=True)
469469
raise ProviderInstallationError('conan', error_msg, e) from e
470+
471+
def plugin_info(self) -> PluginReport:
472+
"""Return a report describing the Conan provider's configuration, managed files, and templates.
473+
474+
Returns:
475+
A :class:`PluginReport` with Conan-specific details.
476+
"""
477+
project_root = self.core_data.project_data.project_root
478+
479+
template_content = Builder._conanfile_content(
480+
self.core_data.pep621_data.name,
481+
self.core_data.pep621_data.version,
482+
)
483+
484+
return PluginReport(
485+
configuration={
486+
'build_types': self.data.build_types,
487+
'remotes': self.data.remotes,
488+
'profile_dir': str(self.data.profile_dir),
489+
'skip_upload': self.data.skip_upload,
490+
},
491+
managed_files=[project_root / 'conanfile_base.py'],
492+
template_files={'conanfile.py': template_content},
493+
)

cppython/project.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,29 @@ def enabled(self) -> bool:
6363
"""
6464
return self._enabled
6565

66+
def info(self) -> dict[str, Any]:
67+
"""Return project and plugin information.
68+
69+
Returns:
70+
A dictionary containing:
71+
- ``provider``: name and :class:`PluginReport` for the active provider plugin
72+
- ``generator``: name and :class:`PluginReport` for the active generator plugin
73+
"""
74+
if not self._enabled:
75+
self.logger.info('Skipping info because the project is not enabled')
76+
return {}
77+
78+
return {
79+
'provider': {
80+
'name': self._data.plugins.provider.name(),
81+
'report': self._data.plugins.provider.plugin_info(),
82+
},
83+
'generator': {
84+
'name': self._data.plugins.generator.name(),
85+
'report': self._data.plugins.generator.plugin_info(),
86+
},
87+
}
88+
6689
def install(self, groups: list[str] | None = None) -> None:
6790
"""Installs project dependencies
6891

cppython/schema.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
"""Project schema specifications"""
22

33
from abc import abstractmethod
4-
from typing import Protocol
4+
from typing import Any, Protocol
55

66

77
class API(Protocol):
88
"""Project API specification"""
99

10+
@abstractmethod
11+
def info(self) -> dict[str, Any]:
12+
"""Return project and template information.
13+
14+
Returns:
15+
A dictionary with project metadata and template status.
16+
"""
17+
raise NotImplementedError()
18+
1019
@abstractmethod
1120
def install(self, groups: list[str] | None = None) -> None:
1221
"""Installs project dependencies

tests/unit/plugins/conan/test_builder.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,19 @@ def test_inheritance_chain(self, builder: Builder, tmp_path: Path) -> None:
171171
assert 'class TestProjectPackage(CPPythonBase):' in user_content
172172
assert 'super().requirements()' in user_content
173173
assert 'super().build_requirements()' in user_content
174+
175+
176+
class TestConanfileContent:
177+
"""Tests for conanfile.py template content generation."""
178+
179+
@pytest.fixture
180+
def builder(self) -> Builder:
181+
"""Create a Builder instance for testing."""
182+
return Builder()
183+
184+
def test_conanfile_content_is_valid_python(self, builder: Builder, tmp_path: Path) -> None:
185+
"""_conanfile_content returns valid Python without version markers."""
186+
content = Builder._conanfile_content('my-project', '0.1.0')
187+
assert 'cppython-template-version' not in content
188+
assert 'from conanfile_base import CPPythonBase' in content
189+
assert 'class MyProjectPackage(CPPythonBase):' in content

0 commit comments

Comments
 (0)