Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions cppython/console/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
32 changes: 32 additions & 0 deletions cppython/core/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion cppython/plugins/cmake/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
25 changes: 21 additions & 4 deletions cppython/plugins/conan/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
26 changes: 25 additions & 1 deletion cppython/plugins/conan/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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},
)
23 changes: 23 additions & 0 deletions cppython/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 10 additions & 1 deletion cppython/schema.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/plugins/conan/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading