diff --git a/cppython/builder.py b/cppython/builder.py index 501b981..f456966 100644 --- a/cppython/builder.py +++ b/cppython/builder.py @@ -1,13 +1,13 @@ """Everything needed to build a CPPython project """ +import logging from importlib import metadata from inspect import getmodule from logging import Logger -from pathlib import Path from typing import Any -from cppython_core.exceptions import ConfigError, PluginError +from cppython_core.exceptions import PluginError from cppython_core.plugin_schema.generator import Generator from cppython_core.plugin_schema.provider import Provider from cppython_core.plugin_schema.scm import SCM @@ -20,15 +20,17 @@ resolve_pep621, resolve_project_configuration, resolve_provider, + resolve_scm, ) from cppython_core.schema import ( CoreData, CorePluginData, CPPythonGlobalConfiguration, - CPPythonLocalConfiguration, DataPluginT, - PEP621Configuration, + PEP621Data, ProjectConfiguration, + ProjectData, + PyProject, ) @@ -38,90 +40,104 @@ class Builder: def __init__(self, logger: Logger) -> None: self.logger = logger - def generate_core_data( - self, - configuration: ProjectConfiguration, - pep621_configuration: PEP621Configuration, - cppython_configuration: CPPythonLocalConfiguration, - plugin_build_date: PluginBuildData, - ) -> CoreData: - """Parses and returns resolved data from all configuration sources + def setup_logger(self, project_configuration: ProjectConfiguration) -> None: + """_summary_ Args: - configuration: Input configuration - pep621_configuration: Project table configuration - cppython_configuration: Tool configuration - plugin_build_date: Build data + project_configuration: _description_ + """ + # Default logging levels + levels = [logging.WARNING, logging.INFO, logging.DEBUG] - Raises: - ConfigError: Raised if data cannot be parsed + # Add default output stream + self.logger.addHandler(logging.StreamHandler()) + self.logger.setLevel(levels[project_configuration.verbosity]) + + self.logger.info("Logging setup complete") + + def generate_project_data(self, project_configuration: ProjectConfiguration) -> ProjectData: + """_summary_ + + Args: + project_configuration: _description_ Returns: - The resolved core object + _description_ """ - global_configuration = CPPythonGlobalConfiguration() + return resolve_project_configuration(project_configuration) - project_data = resolve_project_configuration(configuration) + def generate_data_plugins(self, pyproject: PyProject) -> PluginBuildData: + """_summary_ - try: - pep621_data = resolve_pep621(pep621_configuration, configuration) + Args: + pyproject: _description_ - except ConfigError: - configuration.version = self.extract_scm_version(configuration.pyproject_file.parent) - pep621_data = resolve_pep621(pep621_configuration, configuration) + Returns: + _description_ + """ - cppython_data = resolve_cppython(cppython_configuration, global_configuration, project_data, plugin_build_date) + raw_generator_plugins = self.find_generators() + generator_plugins = self.filter_plugins( + raw_generator_plugins, + pyproject.tool.cppython.generator_name, + "Generator", + ) - return CoreData(project_data=project_data, pep621_data=pep621_data, cppython_data=cppython_data) + raw_provider_plugins = self.find_providers() + provider_plugins = self.filter_plugins( + raw_provider_plugins, + pyproject.tool.cppython.provider_name, + "Provider", + ) - def extract_scm_version(self, path: Path) -> str: - """Locates an available SCM plugin that can report version information about the given path + # Solve the messy interactions between plugins + generator_type, provider_type = self.solve(generator_plugins, provider_plugins) - Args: - path: The directory to query + return PluginBuildData(generator_type=generator_type, provider_type=provider_type) - Raises: - PluginError: If no SCM plugin can be found + def generate_pep621_data( + self, pyproject: PyProject, project_configuration: ProjectConfiguration, scm: SCM | None + ) -> PEP621Data: + """_summary_ + + Args: + pyproject: _description_ + project_configuration: _description_ + scm: _description_ Returns: - A version token + _description_ """ + return resolve_pep621(pyproject.project, project_configuration, scm) - group = "SCM" - group_lower = group.lower() - - scm_types: list[type[SCM]] = [] + def generate_core_data( + self, + project_data: ProjectData, + pyproject: PyProject, + pep621_data: PEP621Data, + plugin_build_date: PluginBuildData, + ) -> CoreData: + """Parses and returns resolved data from all configuration sources - if not (entries := list(metadata.entry_points(group=f"cppython.{group_lower}"))): - raise PluginError("No SCM plugin found") + Args: + project_data: Project data + pyproject: TODO + pep621_data: TODO + plugin_build_date: TODO - # Filter entries - for entry_point in entries: - plugin_type = entry_point.load() - if not issubclass(plugin_type, SCM): - self.logger.warning( - f"Found incompatible plugin. The '{resolve_name(plugin_type)}' plugin must be an instance of" - f" '{group_lower}'" - ) - else: - scm_types.append(plugin_type) + Raises: + ConfigError: Raised if data cannot be parsed - # Deduce the SCM repository - plugin = None - for scm_type in scm_types: - scm = scm_type() - if scm.supported(path): - plugin = scm - break + Returns: + The resolved core object + """ - if not plugin: - raise PluginError("No applicable SCM plugin found for the given path") + global_configuration = CPPythonGlobalConfiguration() - if (version := plugin.version(path)) is None: - raise PluginError("Project has no version information") + cppython_data = resolve_cppython(pyproject.tool.cppython, global_configuration, project_data, plugin_build_date) - return version + return CoreData(project_data=project_data, pep621_data=pep621_data, cppython_data=cppython_data) def find_generators(self) -> list[type[Generator]]: """_summary_ @@ -188,13 +204,12 @@ def find_providers(self) -> list[type[Provider]]: return plugin_types def filter_plugins( - self, plugin_types: list[type[DataPluginT]], directory: Path, pinned_name: str | None, group_name: str + self, plugin_types: list[type[DataPluginT]], pinned_name: str | None, group_name: str ) -> list[type[DataPluginT]]: """Finds and filters data plugins Args: plugin_types: The plugin type to lookup - directory: The data to query support for the filtered plugins pinned_name: The configuration name group_name: The group name @@ -220,11 +235,10 @@ def filter_plugins( # Deduce types for loaded_type in plugin_types: - if loaded_type.supported(directory): - self.logger.warning( - f"A {group_name} plugin is supported: {resolve_name(loaded_type)} from {getmodule(loaded_type)}" - ) - supported_types.append(loaded_type) + self.logger.warning( + f"A {group_name} plugin is supported: {resolve_name(loaded_type)} from {getmodule(loaded_type)}" + ) + supported_types.append(loaded_type) # Fail if supported_types is None: @@ -263,6 +277,53 @@ def solve( return combos[0] + def create_scm( + self, + project_data: ProjectData, + ) -> SCM | None: + """_summary_ + + Args: + project_data: _description_ + + Raises: + PluginError: Ya + + Returns: + _description_ + """ + group = "scm" + path = project_data.pyproject_file.parent + + scm_types: list[type[SCM]] = [] + + if not (entries := list(metadata.entry_points(group=f"cppython.{group}"))): + raise PluginError("No SCM plugin found") + + # Filter entries + for entry_point in entries: + plugin_type = entry_point.load() + if not issubclass(plugin_type, SCM): + self.logger.warning( + f"Found incompatible plugin. The '{resolve_name(plugin_type)}' plugin must be an instance of" + f" '{group}'" + ) + else: + scm_types.append(plugin_type) + + # Deduce the SCM repository + plugin = None + for scm_type in scm_types: + if scm_type.features(path).repository: + scm_data = resolve_scm(project_data) + plugin = scm_type(scm_data) + break + + if not plugin: + self.logger.error("No applicable SCM plugin found for the given path") + + return plugin + def create_generator( self, core_data: CoreData, generator_configuration: dict[str, Any], generator_type: type[Generator] ) -> Generator: diff --git a/cppython/plugins/git.py b/cppython/plugins/git.py index 846bd20..a89e600 100644 --- a/cppython/plugins/git.py +++ b/cppython/plugins/git.py @@ -2,7 +2,11 @@ from pathlib import Path -from cppython_core.plugin_schema.scm import SCM +from cppython_core.plugin_schema.scm import ( + SCM, + SCMPluginGroupData, + SupportedSCMFeatures, +) from cppython_core.schema import Information from dulwich.errors import NotGitRepository from dulwich.repo import Repo @@ -11,23 +15,27 @@ class GitSCM(SCM): """Git implementation hooks""" + def __init__(self, group_data: SCMPluginGroupData) -> None: + self.group_data = group_data + @staticmethod - def supported(directory: Path) -> bool: - """Queries repository status of a path + def features(directory: Path) -> SupportedSCMFeatures: + """Broadcasts the shared features of the SCM plugin to CPPython Args: - directory: The input path to query + directory: The root directory where features are evaluated Returns: - Whether the given path is a repository root + The supported features """ + is_repository = True try: Repo(str(directory)) - return True - except NotGitRepository: - return False + is_repository = False + + return SupportedSCMFeatures(repository=is_repository) @staticmethod def information() -> Information: @@ -38,11 +46,11 @@ def information() -> Information: """ return Information() - def version(self, path: Path) -> str: + def version(self, directory: Path) -> str: """Extracts the system's version metadata Args: - path: The repository path + directory: The repository path Returns: The git version diff --git a/cppython/project.py b/cppython/project.py index f5f3684..b018abf 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -6,7 +6,8 @@ from typing import Any from cppython_core.exceptions import ConfigError, PluginError -from cppython_core.resolution import PluginBuildData, resolve_name +from cppython_core.plugin_schema.scm import SCM +from cppython_core.resolution import resolve_name from cppython_core.schema import CoreData, Interface, ProjectConfiguration, PyProject from pydantic import ValidationError @@ -18,57 +19,42 @@ class Project(API): """The object constructed at each entry_point""" def __init__( - self, configuration: ProjectConfiguration, interface: Interface, pyproject_data: dict[str, Any] + self, project_configuration: ProjectConfiguration, interface: Interface, pyproject_data: dict[str, Any] ) -> None: self._enabled = False self._interface = interface - - # Default logging levels - levels = [logging.WARNING, logging.INFO, logging.DEBUG] - - # Add default output stream self.logger = logging.getLogger("cppython") - self.logger.addHandler(logging.StreamHandler()) - self.logger.setLevel(levels[configuration.verbosity]) - - self.logger.info("Initializing project") try: - pyproject = PyProject(**pyproject_data) - builder = Builder(self.logger) + builder.setup_logger(project_configuration) - raw_generator_plugins = builder.find_generators() - generator_plugins = builder.filter_plugins( - raw_generator_plugins, - configuration.pyproject_file.parent, - pyproject.tool.cppython.generator_name, - "Generator", - ) + self.logger.info("Initializing project") - raw_provider_plugins = builder.find_providers() - provider_plugins = builder.filter_plugins( - raw_provider_plugins, - configuration.pyproject_file.parent, - pyproject.tool.cppython.provider_name, - "Provider", - ) + project_data = builder.generate_project_data(project_configuration) + self._scm = builder.create_scm(project_data) - # Solve the messy interactions between plugins - generator_type, provider_type = builder.solve(generator_plugins, provider_plugins) + pyproject = PyProject(**pyproject_data) - pyproject_build_data = PluginBuildData(generator_type=generator_type, provider_type=provider_type) + plugin_build_data = builder.generate_data_plugins(pyproject) # Once the plugins are resolved, the core data is complete and can be generated + + pep621_data = builder.generate_pep621_data(pyproject, project_configuration, self._scm) self._core_data = builder.generate_core_data( - configuration, pyproject.project, pyproject.tool.cppython, pyproject_build_data + project_data, + pyproject, + pep621_data, + plugin_build_data, ) # Create the chosen plugins self._generator = builder.create_generator( - self.core_data, pyproject.tool.cppython.generator, generator_type + self._core_data, pyproject.tool.cppython.generator, plugin_build_data.generator_type + ) + self._provider = builder.create_provider( + self._core_data, pyproject.tool.cppython.provider, plugin_build_data.provider_type ) - self._provider = builder.create_provider(self.core_data, pyproject.tool.cppython.provider, provider_type) except ConfigError: logging.exception("Unhandled configuration. CPPython will process no further") @@ -91,13 +77,22 @@ def enabled(self) -> bool: return self._enabled @property - def core_data(self) -> CoreData: + def core_data(self) -> CoreData | None: """Core data Returns: - Core data + Core data, if enabled + """ + return self._core_data if self._enabled else None + + @property + def scm(self) -> SCM | None: + """SCM + + Returns: + SCM, if enabled """ - return self._core_data + return self._scm if self._enabled else None async def download_provider_tools(self) -> None: """Download the provider tooling if required""" @@ -106,7 +101,7 @@ async def download_provider_tools(self) -> None: return name = resolve_name(type(self._provider)) - base_path = self.core_data.cppython_data.install_path + base_path = self._core_data.cppython_data.install_path path = base_path / name diff --git a/pdm.lock b/pdm.lock index b8af2a4..a777cb8 100644 --- a/pdm.lock +++ b/pdm.lock @@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "cppython-core" -version = "0.6.1.dev57" +version = "0.6.1.dev63" requires_python = ">=3.11" summary = "Data definitions for CPPython" dependencies = [ @@ -207,7 +207,7 @@ dependencies = [ [[package]] name = "pytest-cppython" -version = "0.3.1.dev37" +version = "0.3.1.dev39" requires_python = ">=3.11" summary = "A pytest plugin that imports CPPython testing types" dependencies = [ @@ -349,9 +349,9 @@ content_hash = "sha256:5a4fbce743e784ef715a342da7048d20117766aa05d1e4e8761f6954d {url = "https://files.pythonhosted.org/packages/e5/d4/d8ee18c995b806b3d0d38dc17ab5888de148244da5be8d3dfbd7cea6cd7e/coverage-7.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:299bc75cb2a41e6741b5e470b8c9fb78d931edbd0cd009c58e5c84de57c06731"}, {url = "https://files.pythonhosted.org/packages/f5/6f/7811ed60d4088b6a54bb48d57b48f647e55c876ee9088e3fa123eb879673/coverage-7.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:92ebc1619650409da324d001b3a36f14f63644c7f0a588e331f3b0f67491f512"}, ] -"cppython-core 0.6.1.dev57" = [ - {url = "https://files.pythonhosted.org/packages/04/0f/2936cf8c297bff7755afb491b9b8fea62267d3f86457f68eeca73adac68e/cppython_core-0.6.1.dev57-py3-none-any.whl", hash = "sha256:00367649451652f0e07eec86c61751c870c40e527dccc80c2b0fbee541962509"}, - {url = "https://files.pythonhosted.org/packages/bb/36/779b0168af1831fae3ec6d489dec05d6e9500ea3db933cd101834f8fe2c0/cppython-core-0.6.1.dev57.tar.gz", hash = "sha256:8748fc1905b1017b8624a11b509b62f6cf5d2b6a0c16341ac20c399455b5b4ef"}, +"cppython-core 0.6.1.dev63" = [ + {url = "https://files.pythonhosted.org/packages/92/be/0672bd329224471399772f69cbd2c0507888bcbf9d9b8f6e3caf96e3c8de/cppython-core-0.6.1.dev63.tar.gz", hash = "sha256:291dfd1ca7b2af407a5121c24f1fac0460db1c5e27e706d6d33cc203f4a7f3dd"}, + {url = "https://files.pythonhosted.org/packages/a4/ca/4973a411d8b9f6ff3b2c518197d1d279135de9ed2965e1f9554e88782cbf/cppython_core-0.6.1.dev63-py3-none-any.whl", hash = "sha256:7808263ded9e350c0650b0c7fc4ddf3ea4cb528433de2789fc72df5931a9f96f"}, ] "dill 0.3.6" = [ {url = "https://files.pythonhosted.org/packages/7c/e7/364a09134e1062d4d5ff69b853a56cf61c223e0afcc6906b6832bcd51ea8/dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, @@ -567,9 +567,9 @@ content_hash = "sha256:5a4fbce743e784ef715a342da7048d20117766aa05d1e4e8761f6954d {url = "https://files.pythonhosted.org/packages/ea/70/da97fd5f6270c7d2ce07559a19e5bf36a76f0af21500256f005a69d9beba/pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {url = "https://files.pythonhosted.org/packages/fe/1f/9ec0ddd33bd2b37d6ec50bb39155bca4fe7085fa78b3b434c05459a860e3/pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] -"pytest-cppython 0.3.1.dev37" = [ - {url = "https://files.pythonhosted.org/packages/7e/ba/770d3c25b7ce62b13a69644ffa7e10b7d5e9bca244ddec5293d2b9508318/pytest-cppython-0.3.1.dev37.tar.gz", hash = "sha256:859bcf12722475ac4d86e99cb60b024445323d9ac21ab403add3680596c43bbb"}, - {url = "https://files.pythonhosted.org/packages/d3/f8/5670b71704c5f126b29d8852db3fb0ede8002abdb77b9834abd1bb5bf8f2/pytest_cppython-0.3.1.dev37-py3-none-any.whl", hash = "sha256:c069c56aae8e6eb52204ed2f0802e79e5fc4b84f003f3500e0990244518c8d32"}, +"pytest-cppython 0.3.1.dev39" = [ + {url = "https://files.pythonhosted.org/packages/7c/02/a3dccb9d15dba5e3ab072b3d18aadf49aaedbed33530abc48a22ef43e8a3/pytest_cppython-0.3.1.dev39-py3-none-any.whl", hash = "sha256:26afa56b7ef665b0c64436cc53c13b6e567099d3120ceebaa766ee0528e0ed00"}, + {url = "https://files.pythonhosted.org/packages/c2/98/923eb45689bedbc524bc0c26e6ee2b058aede1736211bd206d36af2cf6a0/pytest-cppython-0.3.1.dev39.tar.gz", hash = "sha256:22899e172efd2e326cb132cb674496a036296b678c9c566c8bc40a4dfda653d2"}, ] "pytest-mock 3.10.0" = [ {url = "https://files.pythonhosted.org/packages/91/84/c951790e199cd54ddbf1021965b62a5415b81193ebdb4f4af2659fd06a73/pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"},