diff --git a/cppython/project.py b/cppython/project.py index 30582d4..bd36f21 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -3,13 +3,12 @@ """ import logging -from dataclasses import dataclass from importlib import metadata +from pathlib import Path from typing import Any, Type, TypeVar from cppython_core.core import cppython_logger from cppython_core.schema import ( - API, CPPythonData, Generator, GeneratorConfiguration, @@ -20,28 +19,8 @@ ) from pydantic import create_model - -@dataclass -class ProjectConfiguration: - """ - TODO - """ - - _verbosity: int = 0 - - @property - def verbosity(self) -> int: - """ - TODO - """ - return self._verbosity - - @verbosity.setter - def verbosity(self, value: int) -> None: - """ - TODO - """ - self._verbosity = min(max(value, 0), 2) +from cppython.schema import API, CMakePresets, ConfigurePreset, ProjectConfiguration +from cppython.utility import write_preset class ProjectBuilder: @@ -188,45 +167,106 @@ def download(self): """ Download the generator tooling if required """ - if self._enabled: - base_path = self.pyproject.tool.cppython.install_path + if not self._enabled: + cppython_logger.info("Skipping download because the project is not enabled") + return - for generator in self._generators: + base_path = self.pyproject.tool.cppython.install_path - path = base_path / generator.name() + for generator in self._generators: - path.mkdir(parents=True, exist_ok=True) + path = base_path / generator.name() - if not generator.generator_downloaded(path): - cppython_logger.warning(f"Downloading the {generator.name()} requirements to {path}") + path.mkdir(parents=True, exist_ok=True) - # TODO: Make async with progress bar - generator.download_generator(path) - cppython_logger.warning("Download complete") + if not generator.generator_downloaded(path): + cppython_logger.warning(f"Downloading the {generator.name()} requirements to {path}") - # API Contract + # TODO: Make async with progress bar + generator.download_generator(path) + cppython_logger.warning("Download complete") + else: + cppython_logger.info(f"The {generator.name()} generator is already downloaded") + + def _write_generator_presets(self, tool_path: Path, generator: Generator, toolchain_path: Path) -> Path: + """ + Write a generator preset. + @returns - The written directory + """ + generator_tool_path = tool_path / generator.name() + generator_tool_path.mkdir(parents=True, exist_ok=True) + + configure_preset = ConfigurePreset(name=generator.name(), hidden=True, toolchainFile=toolchain_path) + presets = CMakePresets(configurePresets=[configure_preset]) + + write_preset(generator_tool_path, presets) + + return generator_tool_path + + def _write_presets(self, tool_path: Path, names: list[str], includes: list[Path]) -> None: + """ + Write the cppython main preset + """ + + configure_preset = ConfigurePreset(name="cppython", hidden=True, inherits=names) + presets = CMakePresets(configurePresets=[configure_preset], include=includes) + write_preset(tool_path, presets) + + # API Contract def install(self) -> None: - if self._enabled: - cppython_logger.info("Installing project") - self.download() + """ + TODO + """ + if not self._enabled: + cppython_logger.info("Skipping install because the project is not enabled") + return + + cppython_logger.info("Installing project") + self.download() + + tool_path = self.pyproject.tool.cppython.tool_path + tool_path.mkdir(parents=True, exist_ok=True) + + names = [] + includes = [] - for generator in self._generators: - cppython_logger.info(f"Installing {generator.name()} generator") - generator.install() + # TODO: Async + for generator in self._generators: + cppython_logger.info(f"Installing {generator.name()} generator") + + toolchain_path = generator.install() + + directory = self._write_generator_presets(tool_path, generator, toolchain_path) + includes.append(directory) + names.append(generator.name()) + + self._write_presets(tool_path, names, includes) def update(self) -> None: - if self._enabled: - cppython_logger.info("Updating project") + """ + TODO + """ + if not self._enabled: + cppython_logger.info("Skipping update because the project is not enabled") + return + + cppython_logger.info("Updating project") + + tool_path = self.pyproject.tool.cppython.tool_path + tool_path.mkdir(parents=True, exist_ok=True) + + names = [] + includes = [] + + # TODO: Async + for generator in self._generators: + cppython_logger.info(f"Updating {generator.name()} generator") - for generator in self._generators: - cppython_logger.info(f"Updating {generator.name()} generator") - generator.update() + toolchain_path = generator.update() - def build(self) -> None: - if self._enabled: - cppython_logger.info("Building project") + directory = self._write_generator_presets(tool_path, generator, toolchain_path) + includes.append(directory) + names.append(generator.name()) - for generator in self._generators: - cppython_logger.info(f"Building {generator.name()} generator") - generator.build() + self._write_presets(tool_path, names, includes) diff --git a/cppython/schema.py b/cppython/schema.py new file mode 100644 index 0000000..56b5989 --- /dev/null +++ b/cppython/schema.py @@ -0,0 +1,118 @@ +""" +TODO +""" + +from __future__ import annotations # Required for self-referenced pydantic types + +from abc import abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional + +from pydantic import BaseModel, Extra, Field + + +class Preset(BaseModel): + """ + Partial Preset specification + """ + + name: str + hidden: Optional[bool] = None + inherits: list[str] = [] + displayName: Optional[str] = None + description: Optional[str] = None + + +class ConfigurePreset(Preset): + """ + Partial Configure Preset specification + """ + + toolchainFile: Optional[Path] = None + + +class BuildPreset(Preset): + """ + Partial Build Preset specification + """ + + configurePreset: Optional[str] = None + inheritConfigureEnvironment: Optional[bool] = None + + +class TestPreset(Preset): + """ + Partial Test Preset specification + """ + + configurePreset: Optional[str] = None + inheritConfigureEnvironment: Optional[bool] = None + + +class CMakeVersion(BaseModel, extra=Extra.forbid): + """ + The version specification for CMake + """ + + major: int = 3 + minor: int = 23 + patch: int = 1 + + +class CMakePresets(BaseModel, extra=Extra.forbid): + """ + The schema for the CMakePresets and CMakeUserPresets files + """ + + version: int = Field(default=4, const=True) + cmakeMinimumRequired: CMakeVersion = CMakeVersion() # TODO: 'version' compatability validation + include: list[Path] = [] + vendor: Optional[Any] = None + configurePresets: list[ConfigurePreset] = [] + buildPresets: list[BuildPreset] = [] + testPresets: list[TestPreset] = [] + + +@dataclass +class ProjectConfiguration: + """ + TODO + """ + + _verbosity: int = 0 + + @property + def verbosity(self) -> int: + """ + TODO + """ + return self._verbosity + + @verbosity.setter + def verbosity(self, value: int) -> None: + """ + TODO + """ + self._verbosity = min(max(value, 0), 2) + + +class API: + """ + Project API + """ + + @abstractmethod + def install(self) -> None: + """ + TODO + """ + raise NotImplementedError() + + @abstractmethod + def update(self) -> None: + """ + TODO + """ + + raise NotImplementedError() diff --git a/cppython/utility.py b/cppython/utility.py new file mode 100644 index 0000000..ff9a39f --- /dev/null +++ b/cppython/utility.py @@ -0,0 +1,25 @@ +""" +TODO +""" + +import json +from pathlib import Path + +from cppython.schema import CMakePresets + + +def read_preset(path: Path) -> CMakePresets: + """ + Reading routing + """ + + preset_path = path / "CMakePresets.json" + return CMakePresets.parse_file(path=preset_path) + + +def write_preset(path: Path, presets: CMakePresets) -> None: + """ + Writing routine + """ + with open(path / "CMakePresets.json", "w", encoding="utf8") as json_file: + json.dump(presets.dict(), json_file, ensure_ascii=False, indent=2) diff --git a/pdm.lock b/pdm.lock index 1ca852f..25d6f22 100644 --- a/pdm.lock +++ b/pdm.lock @@ -68,7 +68,7 @@ dependencies = [ [[package]] name = "cppython-core" -version = "0.3.3.dev11" +version = "0.3.3.dev14" requires_python = ">=3.10" summary = "Data definitions for CPPython" dependencies = [ @@ -200,7 +200,7 @@ dependencies = [ [[package]] name = "pytest-cppython" -version = "0.1.7.dev7" +version = "0.1.7.dev8" requires_python = ">=3.10" summary = "A pytest plugin that imports CPPython testing types" dependencies = [ @@ -340,9 +340,9 @@ content_hash = "sha256:9842348d0ddc71440b500499ed71e9b60aea9edcf388f5d8ef170c125 {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, ] -"cppython-core 0.3.3.dev11" = [ - {file = "cppython_core-0.3.3.dev11-py3-none-any.whl", hash = "sha256:0eabfb0b16d0eb189a9aa0fddc0ba4969544b0fa23ca0c645446cb2cf99739e6"}, - {file = "cppython-core-0.3.3.dev11.tar.gz", hash = "sha256:d70407fe819fdfc1afcf0229f12374a87a2306ee34e641f3fd79ca56b0cec0e5"}, +"cppython-core 0.3.3.dev14" = [ + {file = "cppython_core-0.3.3.dev14-py3-none-any.whl", hash = "sha256:e9735b3029c97b2a3d9ae92c587a734c90ff655ebbfb39d44164c728a499be09"}, + {file = "cppython-core-0.3.3.dev14.tar.gz", hash = "sha256:000e71af5cab85906781b3b32fd7a8d8c3179a8ee6a137a45ac9c3d219186cd8"}, ] "dill 0.3.4" = [ {file = "dill-0.3.4-py2.py3-none-any.whl", hash = "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f"}, @@ -476,9 +476,9 @@ content_hash = "sha256:9842348d0ddc71440b500499ed71e9b60aea9edcf388f5d8ef170c125 {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, ] -"pytest-cppython 0.1.7.dev7" = [ - {file = "pytest_cppython-0.1.7.dev7-py3-none-any.whl", hash = "sha256:9633695811b4db94861cbd7e50ffa14915c8b219189343b2314a24488f030f01"}, - {file = "pytest-cppython-0.1.7.dev7.tar.gz", hash = "sha256:40fa340a05f1eea5c25dbf5221573378b831ef06cd4e5f5a965f9b0fde31f7a1"}, +"pytest-cppython 0.1.7.dev8" = [ + {file = "pytest_cppython-0.1.7.dev8-py3-none-any.whl", hash = "sha256:e0d0475e7dc7f7c640f15257c7e50c186f2c29351a03d5c3b5820d33fb7c26c5"}, + {file = "pytest-cppython-0.1.7.dev8.tar.gz", hash = "sha256:603e9b6fc1ae7cc3e1e1133d88b8f258870373e6354aecbf86058ea8593cecce"}, ] "pytest-mock 3.7.0" = [ {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, diff --git a/tests/unit/test_interface.py b/tests/unit/test_interface.py index a241ee8..89ff164 100644 --- a/tests/unit/test_interface.py +++ b/tests/unit/test_interface.py @@ -5,7 +5,6 @@ import pytest from click.testing import CliRunner from cppython_core.schema import ( - API, PEP621, CPPythonData, InterfaceConfiguration, @@ -17,11 +16,12 @@ from pytest_mock.plugin import MockerFixture from cppython.console import Config, ConsoleInterface, cli +from cppython.schema import API default_pep621 = PEP621(name="test_name", version="1.0") -default_cppython_data = CPPythonData(**{"target": TargetEnum.EXE}) -default_tool_data = ToolData(**{"cppython": default_cppython_data}) -default_pyproject = PyProject(**{"project": default_pep621, "tool": default_tool_data}) +default_cppython_data = CPPythonData(target=TargetEnum.EXE) +default_tool_data = ToolData(cppython=default_cppython_data) +default_pyproject = PyProject(project=default_pep621, tool=default_tool_data) class TestCLIInterface(InterfaceUnitTests): diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index c140605..933e8b7 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -2,8 +2,6 @@ Test the functions related to the internal interface implementation and the 'Interface' interface itself """ -import logging - from cppython_core.schema import ( PEP621, CPPythonData, diff --git a/tests/unit/test_utility.py b/tests/unit/test_utility.py new file mode 100644 index 0000000..6b298e7 --- /dev/null +++ b/tests/unit/test_utility.py @@ -0,0 +1,24 @@ +""" +TODO +""" +from pathlib import Path + +from cppython.schema import CMakePresets +from cppython.utility import read_preset, write_preset + + +class TestBuilder: + """ + TODO + """ + + def test_preset_read_write(self, tmpdir: Path): + """ + TODO + """ + + presets = CMakePresets() + write_preset(tmpdir, presets) + output = read_preset(tmpdir) + + assert presets == output