From f741e87adab9e5c79718d5d76350040db11b49bb Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 23 Feb 2026 12:02:46 -0800 Subject: [PATCH] Info Command --- cppython/console/entry.py | 39 ++++++++++++++++++++++-- cppython/core/schema.py | 32 +++++++++++++++++++ cppython/plugins/cmake/plugin.py | 24 ++++++++++++++- cppython/plugins/conan/builder.py | 25 ++++++++++++--- cppython/plugins/conan/plugin.py | 26 +++++++++++++++- cppython/project.py | 23 ++++++++++++++ cppython/schema.py | 11 ++++++- tests/unit/plugins/conan/test_builder.py | 16 ++++++++++ 8 files changed, 186 insertions(+), 10 deletions(-) diff --git a/cppython/console/entry.py b/cppython/console/entry.py index f2ee589..6839e2e 100644 --- a/cppython/console/entry.py +++ b/cppython/console/entry.py @@ -5,10 +5,11 @@ import typer from rich import print +from rich.syntax import Syntax from cppython.configuration import ConfigurationLoader from cppython.console.schema import ConsoleConfiguration, ConsoleInterface -from cppython.core.schema import ProjectConfiguration +from cppython.core.schema import PluginReport, ProjectConfiguration from cppython.project import Project app = typer.Typer(no_args_is_help=True) @@ -124,9 +125,41 @@ def main( @app.command() def info( - _: typer.Context, + context: typer.Context, ) -> None: - """Prints project information""" + """Prints project information including plugin configuration, managed files, and templates.""" + project = get_enabled_project(context) + project_info = project.info() + + if not project_info: + return + + for role in ('provider', 'generator'): + entry = project_info.get(role) + if entry is None: + continue + + name: str = entry['name'] + report: PluginReport = entry['report'] + + print(f'\n[bold]{role.title()}:[/bold] {name}') + + if report.configuration: + print(' [bold]Configuration:[/bold]') + for key, value in report.configuration.items(): + print(f' {key}: {value}') + + if report.managed_files: + print(' [bold]Managed files:[/bold]') + for path in report.managed_files: + print(f' {path}') + + if report.template_files: + print(' [bold]Templates:[/bold]') + for filename, content in report.template_files.items(): + print(f' [cyan]{filename}[/cyan]') + print() + print(Syntax(content, 'python', theme='monokai', line_numbers=True)) @app.command() diff --git a/cppython/core/schema.py b/cppython/core/schema.py index fdb7d77..6d9f529 100644 --- a/cppython/core/schema.py +++ b/cppython/core/schema.py @@ -145,6 +145,27 @@ def validate_absolute_path(cls, value: Path) -> Path: CPPythonPluginData = NewType('CPPythonPluginData', CPPythonData) +class PluginReport(CPPythonModel): + """Report returned by a data plugin's ``plugin_info()`` method. + + Contains the plugin's current configuration, any managed files it writes, + and the content of user-facing template files it can generate. + """ + + configuration: Annotated[ + dict[str, Any], + Field(description='Key-value pairs of the resolved plugin configuration'), + ] = {} + managed_files: Annotated[ + list[Path], + Field(description='Paths to files that are fully managed (auto-generated) by the plugin'), + ] = [] + template_files: Annotated[ + dict[str, str], + Field(description='Mapping of template file names to their current content'), + ] = {} + + class SyncData(CPPythonModel): """Data that passes in a plugin sync""" @@ -249,6 +270,17 @@ def features(directory: DirectoryPath) -> SupportedFeatures: """ raise NotImplementedError + def plugin_info(self) -> PluginReport: + """Return a report describing this plugin's configuration, managed files, and templates. + + Plugins should override this method to provide meaningful information. + The default implementation returns an empty report. + + Returns: + A :class:`PluginReport` with plugin-specific details. + """ + return PluginReport() + @classmethod async def download_tooling(cls, directory: DirectoryPath) -> None: """Installs the external tooling required by the plugin. Should be overridden if required diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index 6c64af2..f082225 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -9,7 +9,7 @@ GeneratorPluginGroupData, SupportedGeneratorFeatures, ) -from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData +from cppython.core.schema import CorePluginData, Information, PluginReport, SupportedFeatures, SyncData from cppython.plugins.cmake.builder import Builder from cppython.plugins.cmake.resolution import resolve_cmake_data from cppython.plugins.cmake.schema import CMakeSyncData @@ -181,3 +181,25 @@ def run(self, target: str, configuration: str | None = None) -> None: executable = executables[0] subprocess.run([str(executable)], check=True, cwd=self.data.preset_file.parent) + + def plugin_info(self) -> PluginReport: + """Return a report describing the CMake generator's configuration and managed files. + + Returns: + A :class:`PluginReport` with CMake-specific details. + """ + managed = [self._cppython_preset_directory / 'CPPython.json'] + + config: dict[str, object] = { + 'preset_file': str(self.data.preset_file), + 'configuration_name': self.data.configuration_name, + } + if self.data.cmake_binary is not None: + config['cmake_binary'] = str(self.data.cmake_binary) + if self.data.default_configuration is not None: + config['default_configuration'] = self.data.default_configuration + + return PluginReport( + configuration=config, + managed_files=managed, + ) diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 59654b5..1ea2001 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -76,12 +76,19 @@ def build_requirements(self): base_file.write_text(content, encoding='utf-8') @staticmethod - def _create_conanfile( - conan_file: Path, + def _conanfile_content( name: str, version: str, - ) -> None: - """Creates a conanfile.py file that inherits from CPPython base.""" + ) -> str: + """Return the conanfile.py template content as a string without writing to disk. + + Args: + name: The project name + version: The project version + + Returns: + The full conanfile.py template string + """ class_name = name.replace('-', '_').title().replace('_', '') content = f'''from conan.tools.cmake import CMake, CMakeConfigDeps, CMakeToolchain from conan.tools.files import copy @@ -154,6 +161,16 @@ def export_sources(self): copy(self, "src/*", src=self.recipe_folder, dst=self.export_sources_folder) copy(self, "cmake/*", src=self.recipe_folder, dst=self.export_sources_folder) ''' + return content + + @staticmethod + def _create_conanfile( + conan_file: Path, + name: str, + version: str, + ) -> None: + """Creates a conanfile.py file that inherits from CPPython base.""" + content = Builder._conanfile_content(name, version) conan_file.write_text(content, encoding='utf-8') def generate_conanfile( diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 60c3bae..24a6919 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -15,7 +15,7 @@ from cppython.core.plugin_schema.generator import SyncConsumer from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures -from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData +from cppython.core.schema import CorePluginData, Information, PluginReport, SupportedFeatures, SyncData from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import CMakeSyncData from cppython.plugins.conan.builder import Builder @@ -467,3 +467,27 @@ def _upload_package(self, logger) -> None: error_msg = str(e) logger.error('Conan upload failed: %s', error_msg, exc_info=True) raise ProviderInstallationError('conan', error_msg, e) from e + + def plugin_info(self) -> PluginReport: + """Return a report describing the Conan provider's configuration, managed files, and templates. + + Returns: + A :class:`PluginReport` with Conan-specific details. + """ + project_root = self.core_data.project_data.project_root + + template_content = Builder._conanfile_content( + self.core_data.pep621_data.name, + self.core_data.pep621_data.version, + ) + + return PluginReport( + configuration={ + 'build_types': self.data.build_types, + 'remotes': self.data.remotes, + 'profile_dir': str(self.data.profile_dir), + 'skip_upload': self.data.skip_upload, + }, + managed_files=[project_root / 'conanfile_base.py'], + template_files={'conanfile.py': template_content}, + ) diff --git a/cppython/project.py b/cppython/project.py index 5c8c741..6a5f56a 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -63,6 +63,29 @@ def enabled(self) -> bool: """ return self._enabled + def info(self) -> dict[str, Any]: + """Return project and plugin information. + + Returns: + A dictionary containing: + - ``provider``: name and :class:`PluginReport` for the active provider plugin + - ``generator``: name and :class:`PluginReport` for the active generator plugin + """ + if not self._enabled: + self.logger.info('Skipping info because the project is not enabled') + return {} + + return { + 'provider': { + 'name': self._data.plugins.provider.name(), + 'report': self._data.plugins.provider.plugin_info(), + }, + 'generator': { + 'name': self._data.plugins.generator.name(), + 'report': self._data.plugins.generator.plugin_info(), + }, + } + def install(self, groups: list[str] | None = None) -> None: """Installs project dependencies diff --git a/cppython/schema.py b/cppython/schema.py index 8b7038e..522d9e3 100644 --- a/cppython/schema.py +++ b/cppython/schema.py @@ -1,12 +1,21 @@ """Project schema specifications""" from abc import abstractmethod -from typing import Protocol +from typing import Any, Protocol class API(Protocol): """Project API specification""" + @abstractmethod + def info(self) -> dict[str, Any]: + """Return project and template information. + + Returns: + A dictionary with project metadata and template status. + """ + raise NotImplementedError() + @abstractmethod def install(self, groups: list[str] | None = None) -> None: """Installs project dependencies diff --git a/tests/unit/plugins/conan/test_builder.py b/tests/unit/plugins/conan/test_builder.py index f04d859..a9b8332 100644 --- a/tests/unit/plugins/conan/test_builder.py +++ b/tests/unit/plugins/conan/test_builder.py @@ -171,3 +171,19 @@ def test_inheritance_chain(self, builder: Builder, tmp_path: Path) -> None: assert 'class TestProjectPackage(CPPythonBase):' in user_content assert 'super().requirements()' in user_content assert 'super().build_requirements()' in user_content + + +class TestConanfileContent: + """Tests for conanfile.py template content generation.""" + + @pytest.fixture + def builder(self) -> Builder: + """Create a Builder instance for testing.""" + return Builder() + + def test_conanfile_content_is_valid_python(self, builder: Builder, tmp_path: Path) -> None: + """_conanfile_content returns valid Python without version markers.""" + content = Builder._conanfile_content('my-project', '0.1.0') + assert 'cppython-template-version' not in content + assert 'from conanfile_base import CPPythonBase' in content + assert 'class MyProjectPackage(CPPythonBase):' in content