From 5f7fbd722380d35ffa05ca7f06b37ee56236a64a Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 11 Apr 2025 17:14:29 -0400 Subject: [PATCH 01/10] Update CMake Example Tests --- cppython/plugins/cmake/builder.py | 2 +- cppython/plugins/cmake/resolution.py | 10 ++- cppython/plugins/conan/builder.py | 55 ++++++------- examples/conan_cmake/inject/pyproject.toml | 6 +- examples/conan_cmake/simple/pyproject.toml | 7 +- examples/vcpkg_cmake/simple/pyproject.toml | 7 +- pyproject.toml | 3 +- tests/fixtures/cli.py | 25 ++++++ .../integration/examples/test_conan_cmake.py | 30 +++---- .../integration/examples/test_vcpkg_cmake.py | 18 ++--- tests/unit/plugins/conan/test_ast.py | 80 +++++++++++++++++++ 11 files changed, 166 insertions(+), 77 deletions(-) create mode 100644 tests/unit/plugins/conan/test_ast.py diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index 82e2f33..fb93a3b 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -109,7 +109,7 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None: else: # If the file doesn't exist, we need to default it for the user - # Forward the tool's build directory + # TODO: Forward the tool's build directory default_configure_preset = ConfigurePreset(name='default', inherits='cppython', binaryDir='build') root_preset = CMakePresets(configurePresets=[default_configure_preset]) diff --git a/cppython/plugins/cmake/resolution.py b/cppython/plugins/cmake/resolution.py index a36f584..aecad09 100644 --- a/cppython/plugins/cmake/resolution.py +++ b/cppython/plugins/cmake/resolution.py @@ -20,8 +20,10 @@ def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMake root_directory = core_data.project_data.project_root.absolute() - modified_preset_dir = parsed_data.preset_file - if not modified_preset_dir.is_absolute(): - modified_preset_dir = root_directory / modified_preset_dir + modified_preset_file = parsed_data.preset_file + if not modified_preset_file.is_absolute(): + modified_preset_file = root_directory / modified_preset_file - return CMakeData(preset_file=modified_preset_dir, configuration_name=parsed_data.configuration_name) + + + return CMakeData(preset_file=modified_preset_file, configuration_name=parsed_data.configuration_name) diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 99f2afb..1712b07 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -20,10 +20,10 @@ def __init__(self, dependencies: list[ConanDependency]) -> None: def _create_requires_assignment(self) -> cst.Assign: """Create a `requires` assignment statement.""" return cst.Assign( - targets=[cst.AssignTarget(cst.Name('requires'))], - value=cst.List([ - cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies - ]), + targets=[cst.AssignTarget(cst.Name(value='requires'))], + value=cst.List( + [cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies] + ), ) def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.BaseStatement: @@ -56,24 +56,23 @@ def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef: for body_statement_line in updated_node.body.body: if not isinstance(body_statement_line, cst.SimpleStatementLine): continue - - assignment_statement = body_statement_line.body[0] - if not isinstance(assignment_statement, cst.Assign): - continue - - for target in assignment_statement.targets: - if not isinstance(target.target, cst.Name) or target.target.value != 'requires': + for assignment_statement in body_statement_line.body: + if not isinstance(assignment_statement, cst.Assign): continue - - return self._replace_requires(updated_node, body_statement_line, assignment_statement) + for target in assignment_statement.targets: + if not isinstance(target.target, cst.Name) or target.target.value != 'requires': + continue + # Replace only the assignment within the SimpleStatementLine + return self._replace_requires(updated_node, body_statement_line, assignment_statement) # Find the last attribute assignment before methods last_attribute = None for body_statement_line in updated_node.body.body: if not isinstance(body_statement_line, cst.SimpleStatementLine): break - assignment_statement = body_statement_line.body[0] - if not isinstance(assignment_statement, cst.Assign): + if not body_statement_line.body: + break + if not isinstance(body_statement_line.body[0], cst.Assign): break last_attribute = body_statement_line @@ -89,29 +88,27 @@ def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef: new_body.insert(index + 1, new_statement) else: new_body = [new_statement] + list(updated_node.body.body) - return updated_node.with_changes(body=updated_node.body.with_changes(body=new_body)) def _replace_requires( self, updated_node: cst.ClassDef, body_statement_line: cst.SimpleStatementLine, assignment_statement: cst.Assign ) -> cst.ClassDef: - """Replace the existing 'requires' assignment with a new one. + """Replace the existing 'requires' assignment with a new one, preserving other statements on the same line.""" + new_value = cst.List( + [cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies] + ) + new_assignment = assignment_statement.with_changes(value=new_value) - Args: - updated_node (cst.ClassDef): The class definition to update. - body_statement_line (cst.SimpleStatementLine): The body item containing the assignment. - assignment_statement (cst.Assign): The existing assignment statement. + # Replace only the relevant assignment in the SimpleStatementLine + new_body = [ + new_assignment if statement is assignment_statement else statement for statement in body_statement_line.body + ] + new_statement_line = body_statement_line.with_changes(body=new_body) - Returns: - cst.ClassDef: The updated class definition. - """ - new_value = cst.List([ - cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies - ]) - new_assignment = assignment_statement.with_changes(value=new_value) + # Replace the statement line in the class body return updated_node.with_changes( body=updated_node.body.with_changes( - body=[new_assignment if item is body_statement_line else item for item in updated_node.body.body] + body=[new_statement_line if item is body_statement_line else item for item in updated_node.body.body] ) ) diff --git a/examples/conan_cmake/inject/pyproject.toml b/examples/conan_cmake/inject/pyproject.toml index 7d320b8..51f07ba 100644 --- a/examples/conan_cmake/inject/pyproject.toml +++ b/examples/conan_cmake/inject/pyproject.toml @@ -3,13 +3,13 @@ description = "A simple project showing how to use conan with CPPython" name = "cppython-conan-cmake-simple" version = "1.0.0" -license = {text = "MIT"} +license = { text = "MIT" } -authors = [{name = "Synodic Software", email = "contact@synodic.software"}] +authors = [{ name = "Synodic Software", email = "contact@synodic.software" }] requires-python = ">=3.13" -dependencies = ["cppython[conan, cmake]>=0.1.0"] +dependencies = ["cppython[conan, cmake, git]>=0.9.0"] [tool.cppython] generator-name = "cmake" diff --git a/examples/conan_cmake/simple/pyproject.toml b/examples/conan_cmake/simple/pyproject.toml index 7d320b8..d97b190 100644 --- a/examples/conan_cmake/simple/pyproject.toml +++ b/examples/conan_cmake/simple/pyproject.toml @@ -3,13 +3,14 @@ description = "A simple project showing how to use conan with CPPython" name = "cppython-conan-cmake-simple" version = "1.0.0" -license = {text = "MIT"} +license = { text = "MIT" } -authors = [{name = "Synodic Software", email = "contact@synodic.software"}] +authors = [{ name = "Synodic Software", email = "contact@synodic.software" }] requires-python = ">=3.13" -dependencies = ["cppython[conan, cmake]>=0.1.0"] +dependencies = ["cppython[conan, cmake, git]>=0.9.0"] + [tool.cppython] generator-name = "cmake" diff --git a/examples/vcpkg_cmake/simple/pyproject.toml b/examples/vcpkg_cmake/simple/pyproject.toml index c2bf184..47bae63 100644 --- a/examples/vcpkg_cmake/simple/pyproject.toml +++ b/examples/vcpkg_cmake/simple/pyproject.toml @@ -3,13 +3,14 @@ description = "A simple project showing how to use vcpkg with CPPython" name = "cppython-vcpkg-cmake-simple" version = "1.0.0" -license = {text = "MIT"} +license = { text = "MIT" } -authors = [{name = "Synodic Software", email = "contact@synodic.software"}] +authors = [{ name = "Synodic Software", email = "contact@synodic.software" }] requires-python = ">=3.13" -dependencies = ["cppython[vcpkg, cmake]>=0.1.0"] +dependencies = ["cppython[vcpkg, cmake, git]>=0.9.0"] + [tool.cppython] generator-name = "cmake" diff --git a/pyproject.toml b/pyproject.toml index 1e2ab2e..e9e9495 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,8 @@ skip_empty = true plugins = ["-e file:///${PROJECT_ROOT}"] [tool.pdm.options] -update = ["--update-all", "--unconstrained"] +install = ["-G:all"] +update = ["--update-all"] [tool.pdm.version] source = "scm" diff --git a/tests/fixtures/cli.py b/tests/fixtures/cli.py index 25ee2e4..8f8688f 100644 --- a/tests/fixtures/cli.py +++ b/tests/fixtures/cli.py @@ -1,5 +1,8 @@ """Fixtures for interfacing with the CLI.""" +import os +import platform + import pytest from typer.testing import CliRunner @@ -13,3 +16,25 @@ def fixture_typer_runner() -> CliRunner: runner = CliRunner() return runner + + +@pytest.fixture( + name='fresh_environment', + scope='session', +) +def fixture_fresh_environment(request: pytest.FixtureRequest) -> dict[str, str]: + """Create a fresh environment for subprocess calls.""" + # Start with a minimal environment + new_env = {} + + # Copy only variables you need + if platform.system() == 'Windows': + new_env['SystemRoot'] = os.environ['SystemRoot'] # noqa: SIM112 + + # Provide a PATH that doesn't contain venv references + new_env['PATH'] = os.environ['PATH'] + + # Set the Cppython root directory + new_env['CPPYTHON_ROOT'] = str(request.config.rootpath.resolve()) + + return new_env diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index 13626f5..ca06bca 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -9,8 +9,6 @@ from typer.testing import CliRunner -from cppython.console.entry import app - pytest_plugins = ['tests.fixtures.example'] @@ -20,19 +18,15 @@ class TestConanCMake: @staticmethod def test_simple(example_runner: CliRunner) -> None: """Simple project""" - result = example_runner.invoke( - app, - [ - 'install', - ], - ) + # By nature of running the test, we require PDM to develop the project and so it will be installed + result = subprocess.run(['pdm', 'install'], capture_output=True, text=True, check=False) - assert result.exit_code == 0, result.output + assert result.returncode == 0, f'PDM install failed: {result.stderr}' # Run the CMake configuration command - cmake_result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) + result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) - assert cmake_result.returncode == 0, f'CMake configuration failed: {cmake_result.stderr}' + assert result.returncode == 0, f'Cmake failed: {result.stderr}' # Verify that the build directory contains the expected files assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found' @@ -40,19 +34,15 @@ def test_simple(example_runner: CliRunner) -> None: @staticmethod def test_inject(example_runner: CliRunner) -> None: """Inject""" - result = example_runner.invoke( - app, - [ - 'install', - ], - ) + # By nature of running the test, we require PDM to develop the project and so it will be installed + result = subprocess.run(['pdm', 'install'], capture_output=True, text=True, check=False) - assert result.exit_code == 0, result.output + assert result.returncode == 0, f'PDM install failed: {result.stderr}' # Run the CMake configuration command - cmake_result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) + result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) - assert cmake_result.returncode == 0, f'CMake configuration failed: {cmake_result.stderr}' + assert result.returncode == 0, f'Cmake failed: {result.stderr}' # Verify that the build directory contains the expected files assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found' diff --git a/tests/integration/examples/test_vcpkg_cmake.py b/tests/integration/examples/test_vcpkg_cmake.py index 724844f..f7486a3 100644 --- a/tests/integration/examples/test_vcpkg_cmake.py +++ b/tests/integration/examples/test_vcpkg_cmake.py @@ -10,8 +10,6 @@ import pytest from typer.testing import CliRunner -from cppython.console.entry import app - pytest_plugins = ['tests.fixtures.example'] @@ -22,21 +20,15 @@ class TestVcpkgCMake: @pytest.mark.skip(reason='TODO') def test_simple(example_runner: CliRunner) -> None: """Simple project""" - result = example_runner.invoke( - app, - [ - 'install', - ], - ) + # By nature of running the test, we require PDM to develop the project and so it will be installed + result = subprocess.run(['pdm', 'install'], capture_output=True, text=True, check=False) - assert result.exit_code == 0, result.output + assert result.returncode == 0, f'PDM install failed: {result.stderr}' # Run the CMake configuration command - cmake_result = subprocess.run( - ['cmake', '--preset=default', '-B', 'build'], capture_output=True, text=True, check=False - ) + result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) - assert cmake_result.returncode == 0, f'CMake configuration failed: {cmake_result.stderr}' + assert result.returncode == 0, f'Cmake failed: {result.stderr}' # Verify that the build directory contains the expected files assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found' diff --git a/tests/unit/plugins/conan/test_ast.py b/tests/unit/plugins/conan/test_ast.py new file mode 100644 index 0000000..591bbfa --- /dev/null +++ b/tests/unit/plugins/conan/test_ast.py @@ -0,0 +1,80 @@ +"""Tests for the AST transformer that modifies ConanFile classes.""" + +import ast +from textwrap import dedent + +import libcst as cst + +from cppython.plugins.conan.builder import RequiresTransformer +from cppython.plugins.conan.schema import ConanDependency + + +class TestTransformer: + """Unit tests for the RequiresTransformer.""" + + class MockDependency(ConanDependency): + """A dummy dependency class for testing.""" + + @staticmethod + def requires() -> str: + """Return a dummy requires string.""" + return 'test/1.2.3' + + @staticmethod + def test_add_requires_when_missing() -> None: + """Test that the transformer adds requires when missing.""" + dependency = TestTransformer.MockDependency(name='test') + + code = """ + class MyFile(ConanFile): + name = "test" + version = "1.0" + """ + + module = cst.parse_module(dedent(code)) + transformer = RequiresTransformer([dependency]) + modified = module.visit(transformer) + assert 'requires = ["test/1.2.3"]' in modified.code + + # Verify the resulting code is valid Python syntax + ast.parse(modified.code) + + @staticmethod + def test_replace_existing_requires() -> None: + """Test that the transformer replaces existing requires.""" + dependency = TestTransformer.MockDependency(name='test') + + code = """ + class MyFile(ConanFile): + name = "test" + requires = ["old/0.1"] + version = "1.0" + """ + + module = cst.parse_module(dedent(code)) + transformer = RequiresTransformer([dependency]) + modified = module.visit(transformer) + assert 'requires = ["test/1.2.3"]' in modified.code + assert 'old/0.1' not in modified.code + + # Verify the resulting code is valid Python syntax + ast.parse(modified.code) + + @staticmethod + def test_no_conanfile_class() -> None: + """Test that the transformer does not modify non-ConanFile classes.""" + dependency = TestTransformer.MockDependency(name='test') + + code = """ + class NotConan: + pass + """ + + module = cst.parse_module(dedent(code)) + transformer = RequiresTransformer([dependency]) + modified = module.visit(transformer) + # Should not add requires to non-ConanFile classes + assert 'requires' not in modified.code + + # Verify the resulting code is valid Python syntax + ast.parse(modified.code) From f6eb1467544e3ae2f209a5664d92efbbf25ede10 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 16 Apr 2025 13:26:26 -0400 Subject: [PATCH 02/10] Refactor Builder Generation --- cppython/plugins/cmake/builder.py | 79 +++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index fb93a3b..43df7fd 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -12,8 +12,8 @@ def __init__(self) -> None: """Initialize the builder""" @staticmethod - def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData) -> None: - """Writes a provider preset from input sync data + def generate_provider_preset(provider_data: CMakeSyncData) -> CMakePresets: + """Generates a provider preset from input sync data Args: provider_directory: The base directory to place the preset files @@ -26,7 +26,17 @@ def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData 'CMAKE_PROJECT_TOP_LEVEL_INCLUDES': str(provider_data.top_level_includes.as_posix()), } - generated_preset = CMakePresets(configurePresets=[generated_configure_preset]) + return CMakePresets(configurePresets=[generated_configure_preset]) + + @staticmethod + def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData) -> None: + """Writes a provider preset from input sync data + + Args: + provider_directory: The base directory to place the preset files + provider_data: The providers synchronization data + """ + generated_preset = Builder.generate_provider_preset(provider_data) provider_preset_file = provider_directory / f'{provider_data.provider_name}.json' @@ -44,10 +54,10 @@ def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData file.write(serialized) @staticmethod - def write_cppython_preset( + def generate_cppython_preset( cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData - ) -> Path: - """Write the cppython presets which inherit from the provider presets + ) -> CMakePresets: + """Generates the cppython preset which inherits from the provider presets Args: cppython_preset_directory: The tool directory @@ -55,7 +65,7 @@ def write_cppython_preset( provider_data: The provider's synchronization data Returns: - A file path to the written data + A CMakePresets object """ generated_configure_preset = ConfigurePreset(name='cppython', inherits=provider_data.provider_name) generated_preset = CMakePresets(configurePresets=[generated_configure_preset]) @@ -66,7 +76,25 @@ def write_cppython_preset( # Set the data generated_preset.include = [relative_preset] + return generated_preset + @staticmethod + def write_cppython_preset( + cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData + ) -> Path: + """Write the cppython presets which inherit from the provider presets + + Args: + cppython_preset_directory: The tool directory + provider_directory: The base directory containing provider presets + provider_data: The provider's synchronization data + + Returns: + A file path to the written data + """ + generated_preset = Builder.generate_cppython_preset( + cppython_preset_directory, provider_directory, provider_data + ) cppython_preset_file = cppython_preset_directory / 'cppython.json' initial_preset = None @@ -86,17 +114,15 @@ def write_cppython_preset( return cppython_preset_file @staticmethod - def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None: - """Read the top level json file and insert the include reference. - - Receives a relative path to the tool cmake json file - - Raises: - ConfigError: If key files do not exists + def generate_root_preset(preset_file: Path, cppython_preset_file: Path) -> CMakePresets: + """Generates the top level root preset with the include reference. Args: preset_file: Preset file to modify cppython_preset_file: Path to the cppython preset file to include + + Returns: + A CMakePresets object """ initial_root_preset = None @@ -108,7 +134,6 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None: root_preset = initial_root_preset.model_copy(deep=True) else: # If the file doesn't exist, we need to default it for the user - # TODO: Forward the tool's build directory default_configure_preset = ConfigurePreset(name='default', inherits='cppython', binaryDir='build') root_preset = CMakePresets(configurePresets=[default_configure_preset]) @@ -125,6 +150,30 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None: if str(relative_preset) not in root_preset.include: root_preset.include.append(str(relative_preset)) + return root_preset + + @staticmethod + def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None: + """Read the top level json file and insert the include reference. + + Receives a relative path to the tool cmake json file + + Raises: + ConfigError: If key files do not exists + + Args: + preset_file: Preset file to modify + cppython_preset_file: Path to the cppython preset file to include + """ + initial_root_preset = None + + if preset_file.exists(): + with open(preset_file, encoding='utf-8') as file: + initial_json = file.read() + initial_root_preset = CMakePresets.model_validate_json(initial_json) + + root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file) + # Only write the file if the data has changed if root_preset != initial_root_preset: with open(preset_file, 'w', encoding='utf-8') as file: From 076c2b6204db05d03a8cdcbf4c70be8294b36dc6 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 16 Apr 2025 13:57:56 -0400 Subject: [PATCH 03/10] Split CMake Tests --- tests/unit/plugins/cmake/test_generator.py | 109 ------------------- tests/unit/plugins/cmake/test_presets.py | 115 +++++++++++++++++++++ 2 files changed, 115 insertions(+), 109 deletions(-) create mode 100644 tests/unit/plugins/cmake/test_presets.py diff --git a/tests/unit/plugins/cmake/test_generator.py b/tests/unit/plugins/cmake/test_generator.py index aaec88b..96f434a 100644 --- a/tests/unit/plugins/cmake/test_generator.py +++ b/tests/unit/plugins/cmake/test_generator.py @@ -1,20 +1,15 @@ """Unit test the provider plugin""" -from pathlib import Path from typing import Any import pytest -from cppython.plugins.cmake.builder import Builder from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import ( CMakeConfiguration, - CMakePresets, - CMakeSyncData, ) from cppython.test.pytest.tests import GeneratorUnitTests from cppython.test.schema import Variant -from cppython.utility.utility import TypeName pytest_plugins = ['tests.fixtures.cmake'] @@ -44,107 +39,3 @@ def fixture_plugin_type() -> type[CMakeGenerator]: The type of the Generator """ return CMakeGenerator - - @staticmethod - def test_provider_write(tmp_path: Path) -> None: - """Verifies that the provider preset writing works as intended - - Args: - tmp_path: The input path the use - """ - builder = Builder() - - includes_file = tmp_path / 'includes.cmake' - with includes_file.open('w', encoding='utf-8') as file: - file.write('example contents') - - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(tmp_path, data) - - @staticmethod - def test_cppython_write(tmp_path: Path) -> None: - """Verifies that the cppython preset writing works as intended - - Args: - tmp_path: The input path the use - """ - builder = Builder() - - provider_directory = tmp_path / 'providers' - provider_directory.mkdir(parents=True, exist_ok=True) - - includes_file = provider_directory / 'includes.cmake' - with includes_file.open('w', encoding='utf-8') as file: - file.write('example contents') - - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(provider_directory, data) - - builder.write_cppython_preset(tmp_path, provider_directory, data) - - @staticmethod - def test_root_write(tmp_path: Path) -> None: - """Verifies that the root preset writing works as intended - - Args: - tmp_path: The input path the use - """ - builder = Builder() - - cppython_preset_directory = tmp_path / 'cppython' - cppython_preset_directory.mkdir(parents=True, exist_ok=True) - - provider_directory = cppython_preset_directory / 'providers' - provider_directory.mkdir(parents=True, exist_ok=True) - - includes_file = provider_directory / 'includes.cmake' - with includes_file.open('w', encoding='utf-8') as file: - file.write('example contents') - - root_file = tmp_path / 'CMakePresets.json' - presets = CMakePresets() - - serialized = presets.model_dump_json(exclude_none=True, by_alias=False, indent=4) - with open(root_file, 'w', encoding='utf8') as file: - file.write(serialized) - - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(provider_directory, data) - - cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) - - builder.write_root_presets(root_file, cppython_preset_file) - - @staticmethod - def test_relative_root_write(tmp_path: Path) -> None: - """Verifies that the root preset writing works as intended - - Args: - tmp_path: The input path the use - """ - builder = Builder() - - cppython_preset_directory = tmp_path / 'tool' / 'cppython' - cppython_preset_directory.mkdir(parents=True, exist_ok=True) - - provider_directory = cppython_preset_directory / 'providers' - provider_directory.mkdir(parents=True, exist_ok=True) - - includes_file = provider_directory / 'includes.cmake' - with includes_file.open('w', encoding='utf-8') as file: - file.write('example contents') - - relative_indirection = tmp_path / 'nested' - relative_indirection.mkdir(parents=True, exist_ok=True) - - root_file = relative_indirection / 'CMakePresets.json' - presets = CMakePresets() - serialized = presets.model_dump_json(exclude_none=True, by_alias=False, indent=4) - with open(root_file, 'w', encoding='utf8') as file: - file.write(serialized) - - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(provider_directory, data) - - cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) - builder.write_root_presets(root_file, cppython_preset_file) diff --git a/tests/unit/plugins/cmake/test_presets.py b/tests/unit/plugins/cmake/test_presets.py new file mode 100644 index 0000000..a0f36ae --- /dev/null +++ b/tests/unit/plugins/cmake/test_presets.py @@ -0,0 +1,115 @@ +"""Tests for CMakePresets""" + +from pathlib import Path + +from cppython.plugins.cmake.builder import Builder +from cppython.plugins.cmake.schema import CMakePresets, CMakeSyncData +from cppython.utility.utility import TypeName + + +class TestCMakePresets: + """Tests for the CMakePresets class""" + + @staticmethod + def test_provider_write(tmp_path: Path) -> None: + """Verifies that the provider preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + includes_file = tmp_path / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(tmp_path, data) + + @staticmethod + def test_cppython_write(tmp_path: Path) -> None: + """Verifies that the cppython preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + provider_directory = tmp_path / 'providers' + provider_directory.mkdir(parents=True, exist_ok=True) + + includes_file = provider_directory / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(provider_directory, data) + + builder.write_cppython_preset(tmp_path, provider_directory, data) + + @staticmethod + def test_root_write(tmp_path: Path) -> None: + """Verifies that the root preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + cppython_preset_directory = tmp_path / 'cppython' + cppython_preset_directory.mkdir(parents=True, exist_ok=True) + + provider_directory = cppython_preset_directory / 'providers' + provider_directory.mkdir(parents=True, exist_ok=True) + + includes_file = provider_directory / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + root_file = tmp_path / 'CMakePresets.json' + presets = CMakePresets() + + serialized = presets.model_dump_json(exclude_none=True, by_alias=False, indent=4) + with open(root_file, 'w', encoding='utf8') as file: + file.write(serialized) + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(provider_directory, data) + + cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + + builder.write_root_presets(root_file, cppython_preset_file) + + @staticmethod + def test_relative_root_write(tmp_path: Path) -> None: + """Verifies that the root preset writing works as intended + + Args: + tmp_path: The input path the use + """ + builder = Builder() + + cppython_preset_directory = tmp_path / 'tool' / 'cppython' + cppython_preset_directory.mkdir(parents=True, exist_ok=True) + + provider_directory = cppython_preset_directory / 'providers' + provider_directory.mkdir(parents=True, exist_ok=True) + + includes_file = provider_directory / 'includes.cmake' + with includes_file.open('w', encoding='utf-8') as file: + file.write('example contents') + + relative_indirection = tmp_path / 'nested' + relative_indirection.mkdir(parents=True, exist_ok=True) + + root_file = relative_indirection / 'CMakePresets.json' + presets = CMakePresets() + serialized = presets.model_dump_json(exclude_none=True, by_alias=False, indent=4) + with open(root_file, 'w', encoding='utf8') as file: + file.write(serialized) + + data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) + builder.write_provider_preset(provider_directory, data) + + cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + builder.write_root_presets(root_file, cppython_preset_file) From 3211d92ad50458020088b19a9fb1d16b5691ff27 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 16 Apr 2025 14:05:01 -0400 Subject: [PATCH 04/10] Rename Pytest Helper Files --- cppython/test/pytest/{shared.py => base_classes.py} | 0 cppython/test/pytest/{tests.py => classes.py} | 2 +- tests/integration/plugins/cmake/test_generator.py | 2 +- tests/integration/plugins/conan/test_interface.py | 2 +- tests/integration/plugins/git/test_interface.py | 2 +- tests/integration/plugins/vcpkg/test_provider.py | 2 +- tests/integration/test/test_generator.py | 2 +- tests/integration/test/test_provider.py | 2 +- tests/integration/test/test_scm.py | 2 +- tests/unit/plugins/cmake/test_generator.py | 2 +- tests/unit/plugins/conan/test_interface.py | 2 +- tests/unit/plugins/git/test_version_control.py | 2 +- tests/unit/plugins/vcpkg/test_provider.py | 2 +- tests/unit/test/test_generator.py | 2 +- tests/unit/test/test_provider.py | 2 +- tests/unit/test/test_scm.py | 2 +- 16 files changed, 15 insertions(+), 15 deletions(-) rename cppython/test/pytest/{shared.py => base_classes.py} (100%) rename cppython/test/pytest/{tests.py => classes.py} (95%) diff --git a/cppython/test/pytest/shared.py b/cppython/test/pytest/base_classes.py similarity index 100% rename from cppython/test/pytest/shared.py rename to cppython/test/pytest/base_classes.py diff --git a/cppython/test/pytest/tests.py b/cppython/test/pytest/classes.py similarity index 95% rename from cppython/test/pytest/tests.py rename to cppython/test/pytest/classes.py index dd70a62..1023882 100644 --- a/cppython/test/pytest/tests.py +++ b/cppython/test/pytest/classes.py @@ -9,7 +9,7 @@ from cppython.core.plugin_schema.generator import Generator from cppython.core.plugin_schema.provider import Provider from cppython.core.plugin_schema.scm import SCM -from cppython.test.pytest.shared import ( +from cppython.test.pytest.base_classes import ( DataPluginIntegrationTests, DataPluginUnitTests, GeneratorTests, diff --git a/tests/integration/plugins/cmake/test_generator.py b/tests/integration/plugins/cmake/test_generator.py index 993338c..0058dcf 100644 --- a/tests/integration/plugins/cmake/test_generator.py +++ b/tests/integration/plugins/cmake/test_generator.py @@ -6,7 +6,7 @@ from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import CMakeConfiguration -from cppython.test.pytest.tests import GeneratorIntegrationTests +from cppython.test.pytest.classes import GeneratorIntegrationTests from cppython.test.schema import Variant pytest_plugins = ['tests.fixtures.cmake'] diff --git a/tests/integration/plugins/conan/test_interface.py b/tests/integration/plugins/conan/test_interface.py index 556a0be..fd01cf6 100644 --- a/tests/integration/plugins/conan/test_interface.py +++ b/tests/integration/plugins/conan/test_interface.py @@ -5,7 +5,7 @@ import pytest from cppython.plugins.conan.plugin import ConanProvider -from cppython.test.pytest.tests import ProviderIntegrationTests +from cppython.test.pytest.classes import ProviderIntegrationTests class TestCPPythonProvider(ProviderIntegrationTests[ConanProvider]): diff --git a/tests/integration/plugins/git/test_interface.py b/tests/integration/plugins/git/test_interface.py index 3664017..cb3eca6 100644 --- a/tests/integration/plugins/git/test_interface.py +++ b/tests/integration/plugins/git/test_interface.py @@ -3,7 +3,7 @@ import pytest from cppython.plugins.git.plugin import GitSCM -from cppython.test.pytest.tests import SCMIntegrationTests +from cppython.test.pytest.classes import SCMIntegrationTests class TestGitInterface(SCMIntegrationTests[GitSCM]): diff --git a/tests/integration/plugins/vcpkg/test_provider.py b/tests/integration/plugins/vcpkg/test_provider.py index 1f08637..feafdde 100644 --- a/tests/integration/plugins/vcpkg/test_provider.py +++ b/tests/integration/plugins/vcpkg/test_provider.py @@ -5,7 +5,7 @@ import pytest from cppython.plugins.vcpkg.plugin import VcpkgProvider -from cppython.test.pytest.tests import ProviderIntegrationTests +from cppython.test.pytest.classes import ProviderIntegrationTests class TestCPPythonProvider(ProviderIntegrationTests[VcpkgProvider]): diff --git a/tests/integration/test/test_generator.py b/tests/integration/test/test_generator.py index f34820d..3bdf302 100644 --- a/tests/integration/test/test_generator.py +++ b/tests/integration/test/test_generator.py @@ -5,7 +5,7 @@ import pytest from cppython.test.mock.generator import MockGenerator -from cppython.test.pytest.tests import GeneratorIntegrationTests +from cppython.test.pytest.classes import GeneratorIntegrationTests class TestCPPythonGenerator(GeneratorIntegrationTests[MockGenerator]): diff --git a/tests/integration/test/test_provider.py b/tests/integration/test/test_provider.py index 71752ab..d79c6d3 100644 --- a/tests/integration/test/test_provider.py +++ b/tests/integration/test/test_provider.py @@ -5,7 +5,7 @@ import pytest from cppython.test.mock.provider import MockProvider -from cppython.test.pytest.tests import ProviderIntegrationTests +from cppython.test.pytest.classes import ProviderIntegrationTests class TestMockProvider(ProviderIntegrationTests[MockProvider]): diff --git a/tests/integration/test/test_scm.py b/tests/integration/test/test_scm.py index 76ab30e..da69979 100644 --- a/tests/integration/test/test_scm.py +++ b/tests/integration/test/test_scm.py @@ -5,7 +5,7 @@ import pytest from cppython.test.mock.scm import MockSCM -from cppython.test.pytest.tests import SCMIntegrationTests +from cppython.test.pytest.classes import SCMIntegrationTests class TestCPPythonSCM(SCMIntegrationTests[MockSCM]): diff --git a/tests/unit/plugins/cmake/test_generator.py b/tests/unit/plugins/cmake/test_generator.py index 96f434a..84d0270 100644 --- a/tests/unit/plugins/cmake/test_generator.py +++ b/tests/unit/plugins/cmake/test_generator.py @@ -8,7 +8,7 @@ from cppython.plugins.cmake.schema import ( CMakeConfiguration, ) -from cppython.test.pytest.tests import GeneratorUnitTests +from cppython.test.pytest.classes import GeneratorUnitTests from cppython.test.schema import Variant pytest_plugins = ['tests.fixtures.cmake'] diff --git a/tests/unit/plugins/conan/test_interface.py b/tests/unit/plugins/conan/test_interface.py index c9b2793..47c4258 100644 --- a/tests/unit/plugins/conan/test_interface.py +++ b/tests/unit/plugins/conan/test_interface.py @@ -5,7 +5,7 @@ import pytest from cppython.plugins.conan.plugin import ConanProvider -from cppython.test.pytest.tests import ProviderUnitTests +from cppython.test.pytest.classes import ProviderUnitTests class TestCPPythonProvider(ProviderUnitTests[ConanProvider]): diff --git a/tests/unit/plugins/git/test_version_control.py b/tests/unit/plugins/git/test_version_control.py index 9319e4f..0d5cbc0 100644 --- a/tests/unit/plugins/git/test_version_control.py +++ b/tests/unit/plugins/git/test_version_control.py @@ -3,7 +3,7 @@ import pytest from cppython.plugins.git.plugin import GitSCM -from cppython.test.pytest.tests import SCMUnitTests +from cppython.test.pytest.classes import SCMUnitTests class TestGitInterface(SCMUnitTests[GitSCM]): diff --git a/tests/unit/plugins/vcpkg/test_provider.py b/tests/unit/plugins/vcpkg/test_provider.py index e73853a..7c11734 100644 --- a/tests/unit/plugins/vcpkg/test_provider.py +++ b/tests/unit/plugins/vcpkg/test_provider.py @@ -5,7 +5,7 @@ import pytest from cppython.plugins.vcpkg.plugin import VcpkgProvider -from cppython.test.pytest.tests import ProviderUnitTests +from cppython.test.pytest.classes import ProviderUnitTests class TestCPPythonProvider(ProviderUnitTests[VcpkgProvider]): diff --git a/tests/unit/test/test_generator.py b/tests/unit/test/test_generator.py index 4ca34d9..a9b9b95 100644 --- a/tests/unit/test/test_generator.py +++ b/tests/unit/test/test_generator.py @@ -5,7 +5,7 @@ import pytest from cppython.test.mock.generator import MockGenerator -from cppython.test.pytest.tests import GeneratorUnitTests +from cppython.test.pytest.classes import GeneratorUnitTests class TestCPPythonGenerator(GeneratorUnitTests[MockGenerator]): diff --git a/tests/unit/test/test_provider.py b/tests/unit/test/test_provider.py index 695e688..73297bf 100644 --- a/tests/unit/test/test_provider.py +++ b/tests/unit/test/test_provider.py @@ -7,7 +7,7 @@ from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider -from cppython.test.pytest.tests import ProviderUnitTests +from cppython.test.pytest.classes import ProviderUnitTests class TestMockProvider(ProviderUnitTests[MockProvider]): diff --git a/tests/unit/test/test_scm.py b/tests/unit/test/test_scm.py index 38aa4f0..ef07354 100644 --- a/tests/unit/test/test_scm.py +++ b/tests/unit/test/test_scm.py @@ -5,7 +5,7 @@ import pytest from cppython.test.mock.scm import MockSCM -from cppython.test.pytest.tests import SCMUnitTests +from cppython.test.pytest.classes import SCMUnitTests class TestCPPythonSCM(SCMUnitTests[MockSCM]): From 2db0ef768d99c58665700280fa34837f182a2d6a Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 04:45:37 -0400 Subject: [PATCH 05/10] Replace Variant/Parameterization Usage Parameterization can now happen on a per-test basis --- cppython/test/data/variants.py | 93 ------------------- cppython/test/pytest/base_classes.py | 5 +- cppython/test/pytest/fixtures.py | 83 +++++------------ cppython/test/schema.py | 24 ----- tests/fixtures/cmake.py | 16 +--- .../plugins/cmake/test_generator.py | 5 +- tests/unit/core/test_resolution.py | 25 +++-- tests/unit/plugins/cmake/test_generator.py | 5 +- tests/unit/test_builder.py | 19 ++-- tests/unit/test_data.py | 13 +-- 10 files changed, 60 insertions(+), 228 deletions(-) delete mode 100644 cppython/test/data/variants.py delete mode 100644 cppython/test/schema.py diff --git a/cppython/test/data/variants.py b/cppython/test/data/variants.py deleted file mode 100644 index 9a0d807..0000000 --- a/cppython/test/data/variants.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Data definitions""" - -from pathlib import Path - -from cppython.core.schema import ( - CPPythonGlobalConfiguration, - CPPythonLocalConfiguration, - PEP621Configuration, - ProjectConfiguration, -) -from cppython.test.schema import Variant, Variants - - -def _pep621_configuration_list() -> Variants[PEP621Configuration]: - """Creates a list of mocked configuration types - - Returns: - A list of variants to test - """ - data = Variants[PEP621Configuration]() - - # Default - default = PEP621Configuration(name='default-test', version='1.0.0') - default_variant = Variant[PEP621Configuration](configuration=default) - - data.variants.append(default_variant) - - return data - - -def _cppython_local_configuration_list() -> Variants[CPPythonLocalConfiguration]: - """Mocked list of local configuration data - - Returns: - A list of variants to test - """ - data = Variants[CPPythonLocalConfiguration]() - - # Default - default = CPPythonLocalConfiguration() - default_variant = Variant[CPPythonLocalConfiguration](configuration=default) - - data.variants.append(default_variant) - - return data - - -def _cppython_global_configuration_list() -> Variants[CPPythonGlobalConfiguration]: - """Mocked list of global configuration data - - Returns: - A list of variants to test - """ - data = Variants[CPPythonGlobalConfiguration]() - - # Default - default = CPPythonGlobalConfiguration() - default_variant = Variant[CPPythonGlobalConfiguration](configuration=default) - - # Check off - check_off_data = {'current-check': False} - check_off = CPPythonGlobalConfiguration(**check_off_data) - check_off_variant = Variant[CPPythonGlobalConfiguration](configuration=check_off) - - data.variants.append(default_variant) - data.variants.append(check_off_variant) - - return data - - -def _project_configuration_list() -> Variants[ProjectConfiguration]: - """Mocked list of project configuration data - - Returns: - A list of variants to test - """ - data = Variants[ProjectConfiguration]() - - # NOTE: pyproject_file will be overridden by fixture - - # Default - default = ProjectConfiguration(project_root=Path(), version='0.1.0') - default_variant = Variant[ProjectConfiguration](configuration=default) - - data.variants.append(default_variant) - - return data - - -pep621_variants = _pep621_configuration_list() -cppython_local_variants = _cppython_local_configuration_list() -cppython_global_variants = _cppython_global_configuration_list() -project_variants = _project_configuration_list() diff --git a/cppython/test/pytest/base_classes.py b/cppython/test/pytest/base_classes.py index cf07e07..884542f 100644 --- a/cppython/test/pytest/base_classes.py +++ b/cppython/test/pytest/base_classes.py @@ -32,7 +32,6 @@ provider_variants, scm_variants, ) -from cppython.test.schema import Variant class BaseTests[T: Plugin](metaclass=ABCMeta): @@ -131,7 +130,7 @@ class BaseUnitTests[T: Plugin](BaseTests[T], metaclass=ABCMeta): """Unit testing information for all plugin test classes""" @staticmethod - def test_feature_extraction(plugin_type: type[T], project_configuration: Variant[ProjectConfiguration]) -> None: + def test_feature_extraction(plugin_type: type[T], project_configuration: ProjectConfiguration) -> None: """Test the feature extraction of a plugin. This method tests the feature extraction functionality of a plugin by asserting that the features @@ -141,7 +140,7 @@ def test_feature_extraction(plugin_type: type[T], project_configuration: Variant plugin_type: The type of plugin to test. project_configuration: The project configuration to use for testing. """ - assert plugin_type.features(project_configuration.configuration.project_root) + assert plugin_type.features(project_configuration.project_root) @staticmethod def test_information(plugin_type: type[T]) -> None: diff --git a/cppython/test/pytest/fixtures.py b/cppython/test/pytest/fixtures.py index 194720e..754d4c2 100644 --- a/cppython/test/pytest/fixtures.py +++ b/cppython/test/pytest/fixtures.py @@ -2,7 +2,6 @@ # from pathlib import Path from pathlib import Path -from typing import cast import pytest @@ -28,13 +27,7 @@ PyProject, ToolData, ) -from cppython.test.data.variants import ( - cppython_global_variants, - cppython_local_variants, - pep621_variants, - project_variants, -) -from cppython.test.schema import Variant +from cppython.utility.utility import TypeName @pytest.fixture( @@ -58,18 +51,14 @@ def fixture_install_path(tmp_path_factory: pytest.TempPathFactory) -> Path: @pytest.fixture( name='pep621_configuration', scope='session', - params=pep621_variants.variants, ) -def fixture_pep621_configuration(request: pytest.FixtureRequest) -> Variant[PEP621Configuration]: +def fixture_pep621_configuration() -> PEP621Configuration: """Fixture defining all testable variations of PEP621 - Args: - request: Parameterization list - Returns: PEP621 variant """ - return cast(Variant[PEP621Configuration], request.param) + return PEP621Configuration(name='unnamed', version='1.0.0') @pytest.fixture( @@ -77,7 +66,7 @@ def fixture_pep621_configuration(request: pytest.FixtureRequest) -> Variant[PEP6 scope='session', ) def fixture_pep621_data( - pep621_configuration: Variant[PEP621Configuration], project_configuration: Variant[ProjectConfiguration] + pep621_configuration: PEP621Configuration, project_configuration: ProjectConfiguration ) -> PEP621Data: """Resolved project table fixture @@ -88,58 +77,40 @@ def fixture_pep621_data( Returns: The resolved project table """ - return resolve_pep621(pep621_configuration.configuration, project_configuration.configuration, None) + return resolve_pep621(pep621_configuration, project_configuration, None) @pytest.fixture( name='cppython_local_configuration', scope='session', - params=cppython_local_variants.variants, ) -def fixture_cppython_local_configuration( - request: pytest.FixtureRequest, install_path: Path -) -> Variant[CPPythonLocalConfiguration]: +def fixture_cppython_local_configuration(install_path: Path) -> CPPythonLocalConfiguration: """Fixture defining all testable variations of CPPythonData Args: - request: Parameterization list install_path: The temporary install directory Returns: Variation of CPPython data """ - cppython_local_configuration = cast(Variant[CPPythonLocalConfiguration], request.param) - - data = cppython_local_configuration.configuration.model_dump(by_alias=True) - - # Pin the install location to the base temporary directory - data['install-path'] = install_path - - # Fill the plugin names with mocked values - data['provider-name'] = 'mock' - data['generator-name'] = 'mock' + cppython_local_configuration = CPPythonLocalConfiguration( + install_path=install_path, provider_name=TypeName('mock'), generator_name=TypeName('mock') + ) - new_configuration = CPPythonLocalConfiguration(**data) - return Variant[CPPythonLocalConfiguration](configuration=new_configuration) + return cppython_local_configuration @pytest.fixture( name='cppython_global_configuration', scope='session', - params=cppython_global_variants.variants, ) -def fixture_cppython_global_configuration(request: pytest.FixtureRequest) -> Variant[CPPythonGlobalConfiguration]: +def fixture_cppython_global_configuration() -> CPPythonGlobalConfiguration: """Fixture defining all testable variations of CPPythonData - Args: - request: Parameterization list - Returns: Variation of CPPython data """ - cppython_global_configuration = cast(Variant[CPPythonGlobalConfiguration], request.param) - - return cppython_global_configuration + return CPPythonGlobalConfiguration() @pytest.fixture( @@ -193,8 +164,8 @@ def fixture_plugin_cppython_data( scope='session', ) def fixture_cppython_data( - cppython_local_configuration: Variant[CPPythonLocalConfiguration], - cppython_global_configuration: Variant[CPPythonGlobalConfiguration], + cppython_local_configuration: CPPythonLocalConfiguration, + cppython_global_configuration: CPPythonGlobalConfiguration, project_data: ProjectData, plugin_cppython_data: PluginCPPythonData, ) -> CPPythonData: @@ -210,8 +181,8 @@ def fixture_cppython_data( The resolved CPPython table """ return resolve_cppython( - cppython_local_configuration.configuration, - cppython_global_configuration.configuration, + cppython_local_configuration, + cppython_global_configuration, project_data, plugin_cppython_data, ) @@ -236,29 +207,23 @@ def fixture_core_data(cppython_data: CPPythonData, project_data: ProjectData) -> @pytest.fixture( name='project_configuration', scope='session', - params=project_variants.variants, ) -def fixture_project_configuration(request: pytest.FixtureRequest) -> Variant[ProjectConfiguration]: +def fixture_project_configuration() -> ProjectConfiguration: """Project configuration fixture. Here we provide overrides on the input variants so that we can use a temporary directory for testing purposes. - Args: - request: Parameterized configuration data - tmp_path_factory: Factory for centralized temporary directories - Returns: Configuration with temporary directory capabilities """ - configuration = cast(Variant[ProjectConfiguration], request.param) - return configuration + return ProjectConfiguration(project_root=Path(), version='0.1.0') @pytest.fixture( name='project_data', scope='session', ) -def fixture_project_data(project_configuration: Variant[ProjectConfiguration]) -> ProjectData: +def fixture_project_data(project_configuration: ProjectConfiguration) -> ProjectData: """Fixture that creates a project space at 'workspace/test_project/pyproject.toml' Args: @@ -267,13 +232,13 @@ def fixture_project_data(project_configuration: Variant[ProjectConfiguration]) - Returns: A project data object that has populated a function level temporary directory """ - return resolve_project_configuration(project_configuration.configuration) + return resolve_project_configuration(project_configuration) @pytest.fixture(name='project') def fixture_project( - cppython_local_configuration: Variant[CPPythonLocalConfiguration], - pep621_configuration: Variant[PEP621Configuration], + cppython_local_configuration: CPPythonLocalConfiguration, + pep621_configuration: PEP621Configuration, ) -> PyProject: """Parameterized construction of PyProject data @@ -284,5 +249,5 @@ def fixture_project( Returns: All the data as one object """ - tool = ToolData(cppython=cppython_local_configuration.configuration) - return PyProject(project=pep621_configuration.configuration, tool=tool) + tool = ToolData(cppython=cppython_local_configuration) + return PyProject(project=pep621_configuration, tool=tool) diff --git a/cppython/test/schema.py b/cppython/test/schema.py deleted file mode 100644 index b010726..0000000 --- a/cppython/test/schema.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Data schemas for plugin testing""" - -from pathlib import Path -from typing import Annotated - -from pydantic import Field - -from cppython.core.schema import CPPythonModel - - -class Variant[T: CPPythonModel](CPPythonModel): - """A configuration variant for a schema type""" - - configuration: Annotated[T, Field(description='The configuration data')] - directory: Annotated[ - Path | None, - Field(description='The directory to mount alongside the configuration. `tests/build/`'), - ] = None - - -class Variants[T: CPPythonModel](CPPythonModel): - """A group of variants""" - - variants: Annotated[list[Variant[T]], Field(description='Data variants')] = [] diff --git a/tests/fixtures/cmake.py b/tests/fixtures/cmake.py index 4ff4288..10c1767 100644 --- a/tests/fixtures/cmake.py +++ b/tests/fixtures/cmake.py @@ -6,29 +6,21 @@ import pytest from cppython.plugins.cmake.schema import CMakeConfiguration -from cppython.test.schema import Variant, Variants -def _cmake_data_list() -> Variants[CMakeConfiguration]: +def _cmake_data_list() -> list[CMakeConfiguration]: """Creates a list of mocked configuration types Returns: A list of variants to test """ - data = Variants[CMakeConfiguration]() - # Default default = CMakeConfiguration(configuration_name='default') - default_variant = Variant[CMakeConfiguration](configuration=default) # Non-root preset file config = CMakeConfiguration(preset_file=Path('inner/CMakePresets.json'), configuration_name='default') - config_variant = Variant[CMakeConfiguration](configuration=config, directory=Path('cmake/non-root')) - - data.variants.append(default_variant) - data.variants.append(config_variant) - return data + return [default, config] @pytest.fixture( @@ -36,7 +28,7 @@ def _cmake_data_list() -> Variants[CMakeConfiguration]: scope='session', params=_cmake_data_list(), ) -def fixture_cmake_data(request: pytest.FixtureRequest) -> Variant[CMakeConfiguration]: +def fixture_cmake_data(request: pytest.FixtureRequest) -> CMakeConfiguration: """A fixture to provide a list of configuration types Args: @@ -45,4 +37,4 @@ def fixture_cmake_data(request: pytest.FixtureRequest) -> Variant[CMakeConfigura Returns: A configuration type instance """ - return cast(Variant[CMakeConfiguration], request.param) + return cast(CMakeConfiguration, request.param) diff --git a/tests/integration/plugins/cmake/test_generator.py b/tests/integration/plugins/cmake/test_generator.py index 0058dcf..5ffe801 100644 --- a/tests/integration/plugins/cmake/test_generator.py +++ b/tests/integration/plugins/cmake/test_generator.py @@ -7,7 +7,6 @@ from cppython.plugins.cmake.plugin import CMakeGenerator from cppython.plugins.cmake.schema import CMakeConfiguration from cppython.test.pytest.classes import GeneratorIntegrationTests -from cppython.test.schema import Variant pytest_plugins = ['tests.fixtures.cmake'] @@ -17,7 +16,7 @@ class TestCPPythonGenerator(GeneratorIntegrationTests[CMakeGenerator]): @staticmethod @pytest.fixture(name='plugin_data', scope='session') - def fixture_plugin_data(cmake_data: Variant[CMakeConfiguration]) -> dict[str, Any]: + def fixture_plugin_data(cmake_data: CMakeConfiguration) -> dict[str, Any]: """A required testing hook that allows data generation Args: @@ -26,7 +25,7 @@ def fixture_plugin_data(cmake_data: Variant[CMakeConfiguration]) -> dict[str, An Returns: The constructed plugin data """ - return cmake_data.configuration.model_dump() + return cmake_data.model_dump() @staticmethod @pytest.fixture(name='plugin_type', scope='session') diff --git a/tests/unit/core/test_resolution.py b/tests/unit/core/test_resolution.py index 6583c74..3a9b49f 100644 --- a/tests/unit/core/test_resolution.py +++ b/tests/unit/core/test_resolution.py @@ -29,7 +29,6 @@ ProjectConfiguration, ProjectData, ) -from cppython.test.schema import Variant from cppython.utility.utility import TypeName @@ -37,10 +36,10 @@ class TestResolve: """Test resolution of data""" @staticmethod - def test_pep621_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: + def test_pep621_resolve(project_configuration: ProjectConfiguration) -> None: """Test the PEP621 schema resolve function""" data = PEP621Configuration(name='pep621-resolve-test', dynamic=['version']) - resolved = resolve_pep621(data, project_configuration.configuration, None) + resolved = resolve_pep621(data, project_configuration, None) class_variables = vars(resolved) @@ -48,17 +47,17 @@ def test_pep621_resolve(project_configuration: Variant[ProjectConfiguration]) -> assert None not in class_variables.values() @staticmethod - def test_project_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: + def test_project_resolve(project_configuration: ProjectConfiguration) -> None: """Tests project configuration resolution""" - assert resolve_project_configuration(project_configuration.configuration) + assert resolve_project_configuration(project_configuration) @staticmethod - def test_cppython_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: + def test_cppython_resolve(project_configuration: ProjectConfiguration) -> None: """Tests cppython configuration resolution""" cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - project_data = resolve_project_configuration(project_configuration.configuration) + project_data = resolve_project_configuration(project_configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') @@ -89,13 +88,13 @@ class MockModel(CPPythonModel): resolve_model(MockModel, good_data) @staticmethod - def test_generator_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: + def test_generator_resolve(project_configuration: ProjectConfiguration) -> None: """Test generator resolution""" project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - project_data = resolve_project_configuration(project_configuration.configuration) + project_data = resolve_project_configuration(project_configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') @@ -112,13 +111,13 @@ def test_generator_resolve(project_configuration: Variant[ProjectConfiguration]) assert resolve_generator(project_data, cppython_plugin_data) @staticmethod - def test_provider_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: + def test_provider_resolve(project_configuration: ProjectConfiguration) -> None: """Test provider resolution""" project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - project_data = resolve_project_configuration(project_configuration.configuration) + project_data = resolve_project_configuration(project_configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') @@ -135,13 +134,13 @@ def test_provider_resolve(project_configuration: Variant[ProjectConfiguration]) assert resolve_provider(project_data, cppython_plugin_data) @staticmethod - def test_scm_resolve(project_configuration: Variant[ProjectConfiguration]) -> None: + def test_scm_resolve(project_configuration: ProjectConfiguration) -> None: """Test scm resolution""" project_data = ProjectData(project_root=Path()) cppython_local_configuration = CPPythonLocalConfiguration() cppython_global_configuration = CPPythonGlobalConfiguration() - project_data = resolve_project_configuration(project_configuration.configuration) + project_data = resolve_project_configuration(project_configuration) plugin_build_data = PluginCPPythonData( generator_name=TypeName('generator'), provider_name=TypeName('provider'), scm_name=TypeName('scm') diff --git a/tests/unit/plugins/cmake/test_generator.py b/tests/unit/plugins/cmake/test_generator.py index 84d0270..407a645 100644 --- a/tests/unit/plugins/cmake/test_generator.py +++ b/tests/unit/plugins/cmake/test_generator.py @@ -9,7 +9,6 @@ CMakeConfiguration, ) from cppython.test.pytest.classes import GeneratorUnitTests -from cppython.test.schema import Variant pytest_plugins = ['tests.fixtures.cmake'] @@ -19,7 +18,7 @@ class TestCPPythonGenerator(GeneratorUnitTests[CMakeGenerator]): @staticmethod @pytest.fixture(name='plugin_data', scope='session') - def fixture_plugin_data(cmake_data: Variant[CMakeConfiguration]) -> dict[str, Any]: + def fixture_plugin_data(cmake_data: CMakeConfiguration) -> dict[str, Any]: """A required testing hook that allows data generation Args: @@ -28,7 +27,7 @@ def fixture_plugin_data(cmake_data: Variant[CMakeConfiguration]) -> dict[str, An Returns: The constructed plugin data """ - return cmake_data.configuration.model_dump() + return cmake_data.model_dump() @staticmethod @pytest.fixture(name='plugin_type', scope='session') diff --git a/tests/unit/test_builder.py b/tests/unit/test_builder.py index 8b5e647..9a9fbb6 100644 --- a/tests/unit/test_builder.py +++ b/tests/unit/test_builder.py @@ -15,7 +15,6 @@ from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider from cppython.test.mock.scm import MockSCM -from cppython.test.schema import Variant class TestBuilder: @@ -23,9 +22,9 @@ class TestBuilder: @staticmethod def test_build( - project_configuration: Variant[ProjectConfiguration], - pep621_configuration: Variant[PEP621Configuration], - cppython_local_configuration: Variant[CPPythonLocalConfiguration], + project_configuration: ProjectConfiguration, + pep621_configuration: PEP621Configuration, + cppython_local_configuration: CPPythonLocalConfiguration, mocker: MockerFixture, ) -> None: """Verifies that the builder can build a project with all test variants @@ -37,7 +36,7 @@ def test_build( mocker: Pytest mocker fixture """ logger = logging.getLogger() - builder = Builder(project_configuration.configuration, logger) + builder = Builder(project_configuration, logger) # Insert ourself into the builder and load the mock plugins by returning them directly in the expected order # they will be built @@ -47,7 +46,7 @@ def test_build( ) mocker.patch.object(metadata.EntryPoint, 'load', side_effect=[MockGenerator, MockProvider, MockSCM]) - assert builder.build(pep621_configuration.configuration, cppython_local_configuration.configuration) + assert builder.build(pep621_configuration, cppython_local_configuration) class TestResolver: @@ -55,8 +54,8 @@ class TestResolver: @staticmethod def test_generate_plugins( - project_configuration: Variant[ProjectConfiguration], - cppython_local_configuration: Variant[CPPythonLocalConfiguration], + project_configuration: ProjectConfiguration, + cppython_local_configuration: CPPythonLocalConfiguration, project_data: ProjectData, ) -> None: """Verifies that the resolver can generate plugins @@ -67,6 +66,6 @@ def test_generate_plugins( project_data: Variant fixture for the project data """ logger = logging.getLogger() - resolver = Resolver(project_configuration.configuration, logger) + resolver = Resolver(project_configuration, logger) - assert resolver.generate_plugins(cppython_local_configuration.configuration, project_data) + assert resolver.generate_plugins(cppython_local_configuration, project_data) diff --git a/tests/unit/test_data.py b/tests/unit/test_data.py index 2d58b2e..0357d6d 100644 --- a/tests/unit/test_data.py +++ b/tests/unit/test_data.py @@ -15,7 +15,6 @@ from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider from cppython.test.mock.scm import MockSCM -from cppython.test.schema import Variant class TestData: @@ -27,9 +26,9 @@ class TestData: scope='session', ) def fixture_data( - project_configuration: Variant[ProjectConfiguration], - pep621_configuration: Variant[PEP621Configuration], - cppython_local_configuration: Variant[CPPythonLocalConfiguration], + project_configuration: ProjectConfiguration, + pep621_configuration: PEP621Configuration, + cppython_local_configuration: CPPythonLocalConfiguration, ) -> Data: """Creates a mock plugins fixture. @@ -46,13 +45,11 @@ def fixture_data( """ logger = logging.getLogger() - builder = Builder(project_configuration.configuration, logger) + builder = Builder(project_configuration, logger) plugin_build_data = PluginBuildData(generator_type=MockGenerator, provider_type=MockProvider, scm_type=MockSCM) - return builder.build( - pep621_configuration.configuration, cppython_local_configuration.configuration, plugin_build_data - ) + return builder.build(pep621_configuration, cppython_local_configuration, plugin_build_data) @staticmethod def test_sync(data: Data) -> None: From 63f2334893b00a4b31008e97d13c3ba7ef119767 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 05:04:34 -0400 Subject: [PATCH 06/10] Replace Custom Subprocess Wrapper Obfuscates the functionality of the built-in library --- cppython/plugins/vcpkg/plugin.py | 119 ++++++++++------------ cppython/project.py | 7 -- cppython/utility/exception.py | 23 ----- cppython/utility/subprocess.py | 44 -------- tests/unit/utility/test_utility.py | 155 ----------------------------- 5 files changed, 55 insertions(+), 293 deletions(-) delete mode 100644 cppython/utility/subprocess.py diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index 7c14ddb..063adda 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -1,5 +1,6 @@ """The vcpkg provider implementation""" +import subprocess from logging import getLogger from os import name as system_name from pathlib import Path, PosixPath, WindowsPath @@ -16,8 +17,7 @@ from cppython.plugins.cmake.schema import CMakeSyncData from cppython.plugins.vcpkg.resolution import generate_manifest, resolve_vcpkg_data from cppython.plugins.vcpkg.schema import VcpkgData -from cppython.utility.exception import NotSupportedError, ProcessError -from cppython.utility.subprocess import invoke as subprocess_call +from cppython.utility.exception import NotSupportedError from cppython.utility.utility import TypeName @@ -76,19 +76,25 @@ def _update_provider(cls, path: Path) -> None: try: if system_name == 'nt': - subprocess_call( - str(WindowsPath('bootstrap-vcpkg.bat')), ['-disableMetrics'], logger=logger, cwd=path, shell=True + subprocess.run( + [str(WindowsPath('bootstrap-vcpkg.bat')), '-disableMetrics'], + cwd=path, + shell=True, + check=True, + capture_output=True, ) elif system_name == 'posix': - subprocess_call( - './' + str(PosixPath('bootstrap-vcpkg.sh')), - ['-disableMetrics'], - logger=logger, + subprocess.run( + ['./' + str(PosixPath('bootstrap-vcpkg.sh')), '-disableMetrics'], cwd=path, shell=True, + check=True, + capture_output=True, ) - except ProcessError: - logger.error('Unable to bootstrap the vcpkg repository', exc_info=True) + except subprocess.CalledProcessError as e: + logger.error( + 'Unable to bootstrap the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e), exc_info=True + ) raise def sync_data(self, consumer: SyncConsumer) -> SyncData: @@ -119,25 +125,17 @@ def tooling_downloaded(cls, path: Path) -> bool: Args: path: The directory to check for downloaded tooling - Raises: - ProcessError: Failed vcpkg calls - Returns: Whether the tooling has been downloaded or not """ - logger = getLogger('cppython.vcpkg') - try: - # Hide output, given an error output is a logic conditional - subprocess_call( - 'git', - ['rev-parse', '--is-inside-work-tree'], - logger=logger, - suppress=True, + subprocess.run( + ['git', 'rev-parse', '--is-inside-work-tree'], cwd=path, + check=True, + capture_output=True, ) - - except ProcessError: + except subprocess.CalledProcessError: return False return True @@ -148,9 +146,6 @@ async def download_tooling(cls, directory: Path) -> None: Args: directory: The directory to download any extra tooling to - - Raises: - ProcessError: Failed vcpkg calls """ logger = getLogger('cppython.vcpkg') @@ -159,35 +154,41 @@ async def download_tooling(cls, directory: Path) -> None: logger.debug("Updating the vcpkg repository at '%s'", directory.absolute()) # The entire history is need for vcpkg 'baseline' information - subprocess_call('git', ['fetch', 'origin'], logger=logger, cwd=directory) - subprocess_call('git', ['pull'], logger=logger, cwd=directory) - except ProcessError: - logger.exception('Unable to update the vcpkg repository') + subprocess.run( + ['git', 'fetch', 'origin'], + cwd=directory, + check=True, + capture_output=True, + ) + subprocess.run( + ['git', 'pull'], + cwd=directory, + check=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + logger.exception('Unable to update the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e)) raise else: try: logger.debug("Cloning the vcpkg repository to '%s'", directory.absolute()) # The entire history is need for vcpkg 'baseline' information - subprocess_call( - 'git', - ['clone', 'https://github.com/microsoft/vcpkg', '.'], - logger=logger, + subprocess.run( + ['git', 'clone', 'https://github.com/microsoft/vcpkg', '.'], cwd=directory, + check=True, + capture_output=True, ) - except ProcessError: - logger.exception('Unable to clone the vcpkg repository') + except subprocess.CalledProcessError as e: + logger.exception('Unable to clone the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e)) raise cls._update_provider(directory) def install(self) -> None: - """Called when dependencies need to be installed from a lock file. - - Raises: - ProcessError: Failed vcpkg calls - """ + """Called when dependencies need to be installed from a lock file.""" manifest_directory = self.core_data.project_data.project_root manifest = generate_manifest(self.core_data, self.data) @@ -199,25 +200,18 @@ def install(self) -> None: executable = self.core_data.cppython_data.install_path / 'vcpkg' logger = getLogger('cppython.vcpkg') try: - subprocess_call( - executable, - [ - 'install', - f'--x-install-root={self.data.install_directory}', - ], - logger=logger, + subprocess.run( + [str(executable), 'install', f'--x-install-root={self.data.install_directory}'], cwd=self.core_data.cppython_data.build_path, + check=True, + capture_output=True, ) - except ProcessError: - logger.exception('Unable to install project dependencies') + except subprocess.CalledProcessError as e: + logger.exception('Unable to install project dependencies: %s', e.stderr.decode() if e.stderr else str(e)) raise def update(self) -> None: - """Called when dependencies need to be updated and written to the lock file. - - Raises: - ProcessError: Failed vcpkg calls - """ + """Called when dependencies need to be updated and written to the lock file.""" manifest_directory = self.core_data.project_data.project_root manifest = generate_manifest(self.core_data, self.data) @@ -230,15 +224,12 @@ def update(self) -> None: executable = self.core_data.cppython_data.install_path / 'vcpkg' logger = getLogger('cppython.vcpkg') try: - subprocess_call( - executable, - [ - 'install', - f'--x-install-root={self.data.install_directory}', - ], - logger=logger, + subprocess.run( + [str(executable), 'install', f'--x-install-root={self.data.install_directory}'], cwd=self.core_data.cppython_data.build_path, + check=True, + capture_output=True, ) - except ProcessError: - logger.exception('Unable to install project dependencies') + except subprocess.CalledProcessError as e: + logger.exception('Unable to install project dependencies: %s', e.stderr.decode() if e.stderr else str(e)) raise diff --git a/cppython/project.py b/cppython/project.py index 075c0a2..c3404b1 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -9,7 +9,6 @@ from cppython.core.resolution import resolve_model from cppython.core.schema import Interface, ProjectConfiguration, PyProject from cppython.schema import API -from cppython.utility.exception import ProcessError class Project(API): @@ -71,9 +70,6 @@ def install(self) -> None: try: self._data.plugins.provider.install() - except ProcessError as error: - self.logger.error('Installation failed: %s', error.error) - raise SystemExit('Error: Provider installation failed. Please check the logs.') from None except Exception as exception: self.logger.error('Unexpected error during installation: %s', str(exception)) raise SystemExit('Error: An unexpected error occurred during installation.') from None @@ -98,9 +94,6 @@ def update(self) -> None: try: self._data.plugins.provider.update() - except ProcessError as error: - self.logger.error('Update failed: %s', error.error) - raise SystemExit('Error: Provider update failed. Please check the logs.') from None except Exception as exception: self.logger.error('Unexpected error during update: %s', str(exception)) raise SystemExit('Error: An unexpected error occurred during update.') from None diff --git a/cppython/utility/exception.py b/cppython/utility/exception.py index 2c43681..3f90fb7 100644 --- a/cppython/utility/exception.py +++ b/cppython/utility/exception.py @@ -1,29 +1,6 @@ """Exception definitions""" -class ProcessError(Exception): - """Raised when there is a configuration error""" - - def __init__(self, error: str) -> None: - """Initializes the error - - Args: - error: The error message - """ - self._error = error - - super().__init__(error) - - @property - def error(self) -> str: - """Returns the underlying error - - Returns: - str -- The underlying error - """ - return self._error - - class PluginError(Exception): """Raised when there is a plugin error""" diff --git a/cppython/utility/subprocess.py b/cppython/utility/subprocess.py deleted file mode 100644 index d2d60b6..0000000 --- a/cppython/utility/subprocess.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Subprocess definitions""" - -import logging -import subprocess -from pathlib import Path -from typing import Any - -from cppython.utility.exception import ProcessError - - -def invoke( - executable: str | Path, - arguments: list[str | Path], - logger: logging.Logger, - log_level: int = logging.WARNING, - suppress: bool = False, - **kwargs: Any, -) -> None: - """Executes a subprocess call with logger and utility attachments. Captures STDOUT and STDERR - - Args: - executable: The executable to call - arguments: Arguments to pass to Popen - logger: The logger to log the process pipes to - log_level: The level to log to. Defaults to logging.WARNING. - suppress: Mutes logging output. Defaults to False. - kwargs: Keyword arguments to pass to subprocess.Popen - - Raises: - ProcessError: If the underlying process fails - """ - with subprocess.Popen( - [executable] + arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, **kwargs - ) as process: - if process.stdout is None: - return - - with process.stdout as pipe: - for line in iter(pipe.readline, ''): - if not suppress: - logger.log(log_level, line.rstrip()) - - if process.returncode != 0: - raise ProcessError('Subprocess task failed') diff --git a/tests/unit/utility/test_utility.py b/tests/unit/utility/test_utility.py index 537bf30..f4d39a1 100644 --- a/tests/unit/utility/test_utility.py +++ b/tests/unit/utility/test_utility.py @@ -3,13 +3,8 @@ import logging from logging import StreamHandler from pathlib import Path -from sys import executable from typing import NamedTuple -import pytest - -from cppython.utility.exception import ProcessError -from cppython.utility.subprocess import invoke from cppython.utility.utility import canonicalize_name cppython_logger = logging.getLogger('cppython') @@ -70,153 +65,3 @@ def test_name_multi_caps() -> None: test = canonicalize_name('NAmeGroup') assert test.group == 'group' assert test.name == 'name' - - -@pytest.mark.skip(reason='Breaks debugging tests') -class TestSubprocess: - """Subprocess testing""" - - @staticmethod - def test_subprocess_stdout(caplog: pytest.LogCaptureFixture) -> None: - """Test subprocess_call - - Args: - caplog: Fixture for capturing logging input - """ - python = Path(executable) - - with caplog.at_level(logging.INFO): - invoke( - python, - ['-c', "import sys; print('Test Out', file = sys.stdout)"], - cppython_logger, - ) - - assert len(caplog.records) == 1 - assert caplog.records[0].message == 'Test Out' - - @staticmethod - def test_subprocess_stderr(caplog: pytest.LogCaptureFixture) -> None: - """Test subprocess_call - - Args: - caplog: Fixture for capturing logging input - """ - python = Path(executable) - - with caplog.at_level(logging.INFO): - invoke( - python, - ['-c', "import sys; print('Test Error', file = sys.stderr)"], - cppython_logger, - ) - - assert len(caplog.records) == 1 - assert caplog.records[0].message == 'Test Error' - - @staticmethod - def test_subprocess_suppression(caplog: pytest.LogCaptureFixture) -> None: - """Test subprocess_call suppression flag - - Args: - caplog: Fixture for capturing logging input - """ - python = Path(executable) - - with caplog.at_level(logging.INFO): - invoke( - python, - ['-c', "import sys; print('Test Out', file = sys.stdout)"], - cppython_logger, - suppress=True, - ) - assert len(caplog.records) == 0 - - @staticmethod - def test_subprocess_exit(caplog: pytest.LogCaptureFixture) -> None: - """Test subprocess_call exception output - - Args: - caplog: Fixture for capturing logging input - """ - python = Path(executable) - - with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - invoke( - python, - ['-c', "import sys; sys.exit('Test Exit Output')"], - cppython_logger, - ) - - assert len(caplog.records) == 1 - assert caplog.records[0].message == 'Test Exit Output' - - assert 'Subprocess task failed' in str(exec_info.value) - - @staticmethod - def test_subprocess_exception(caplog: pytest.LogCaptureFixture) -> None: - """Test subprocess_call exception output - - Args: - caplog: Fixture for capturing logging input - """ - python = Path(executable) - - with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - invoke( - python, - ['-c', "import sys; raise Exception('Test Exception Output')"], - cppython_logger, - ) - - assert 'Subprocess task failed' in str(exec_info.value) - - @staticmethod - def test_stderr_exception(caplog: pytest.LogCaptureFixture) -> None: - """Verify print and exit - - Args: - caplog: Fixture for capturing logging input - """ - python = Path(executable) - with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - invoke( - python, - [ - '-c', - "import sys; print('Test Out', file = sys.stdout); sys.exit('Test Exit Out')", - ], - cppython_logger, - ) - - LOG_COUNT = 2 - assert len(caplog.records) == LOG_COUNT - assert caplog.records[0].message == 'Test Out' - assert caplog.records[1].message == 'Test Exit Out' - - assert 'Subprocess task failed' in str(exec_info.value) - - @staticmethod - def test_stdout_exception(caplog: pytest.LogCaptureFixture) -> None: - """Verify print and exit - - Args: - caplog: Fixture for capturing logging input - """ - python = Path(executable) - with pytest.raises(ProcessError) as exec_info, caplog.at_level(logging.INFO): - invoke( - python, - [ - '-c', - "import sys; print('Test Error', file = sys.stderr); sys.exit('Test Exit Error')", - ], - cppython_logger, - ) - - LOG_COUNT = 2 - assert len(caplog.records) == LOG_COUNT - assert caplog.records[0].message == 'Test Error' - assert caplog.records[1].message == 'Test Exit Error' - - assert 'Subprocess task failed' in str(exec_info.value) From bebf03b91ca86700adb9d8912ef4ba80c68a51ab Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 05:08:01 -0400 Subject: [PATCH 07/10] Update Chore --- pdm.lock | 12 ++++++------ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pdm.lock b/pdm.lock index 5c79235..febcc9b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "cmake", "conan", "git", "lint", "pdm", "pytest", "test"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:90f922a571d25ecf4ebcbf4f9f973332d3a18d433d2ced3a15230cb5f770d3fe" +content_hash = "sha256:6e9ed604be2a15d13ab7b237c5a867e4e0c880b8b474a53afa34af3ede702751" [[metadata.targets]] requires_python = ">=3.13" @@ -146,7 +146,7 @@ files = [ [[package]] name = "conan" -version = "2.15.0" +version = "2.15.1" requires_python = ">=3.6" summary = "Conan C/C++ package manager" groups = ["conan"] @@ -162,7 +162,7 @@ dependencies = [ "urllib3<2.1,>=1.26.6", ] files = [ - {file = "conan-2.15.0.tar.gz", hash = "sha256:c984c5fb0623ea60b39c2feb351886a3e2df8f6a1f99e3f322125a24198f05da"}, + {file = "conan-2.15.1.tar.gz", hash = "sha256:c4114e197f7908409766ad16cea758f088ebc926f8426212b2a6a62829f996a3"}, ] [[package]] @@ -234,7 +234,7 @@ files = [ [[package]] name = "dep-logic" -version = "0.4.11" +version = "0.5.0" requires_python = ">=3.8" summary = "Python dependency specifications supporting logical operations" groups = ["pdm"] @@ -242,8 +242,8 @@ dependencies = [ "packaging>=22", ] files = [ - {file = "dep_logic-0.4.11-py3-none-any.whl", hash = "sha256:44cf69b3e0e369315e7d4cafa4a5050aa70666753831bf0c27b7f3eadbcf70ce"}, - {file = "dep_logic-0.4.11.tar.gz", hash = "sha256:6084c81ce683943a60394ca0f8531ff803dfd955b5d7f52fb0571b53b930a182"}, + {file = "dep_logic-0.5.0-py3-none-any.whl", hash = "sha256:f16a73ec5baf1f126e253f6a6249c80999818e608f84677736591ac623c516a6"}, + {file = "dep_logic-0.5.0.tar.gz", hash = "sha256:be92e772f15d2563edd6b8694a6818846ad2822310dfc8f39cd20ebb0b03e329"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index e9e9495..45c7ee6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ pdm = ["pdm>=2.23.1"] cmake = ["cmake>=4.0.0"] -conan = ["conan>=2.15.0", "libcst>=1.7.0"] +conan = ["conan>=2.15.1", "libcst>=1.7.0"] [project.urls] homepage = "https://github.com/Synodic-Software/CPPython" From f9b1450306ff3d65ec7fd5fb4890fe920bfb60cf Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 05:23:17 -0400 Subject: [PATCH 08/10] Remove Inject Example Test case now covers it --- examples/conan_cmake/inject/CMakeLists.txt | 10 ------- examples/conan_cmake/inject/conanfile.py | 28 ------------------- examples/conan_cmake/inject/main.cpp | 7 ----- examples/conan_cmake/inject/pyproject.toml | 27 ------------------ .../integration/examples/test_conan_cmake.py | 16 ----------- 5 files changed, 88 deletions(-) delete mode 100644 examples/conan_cmake/inject/CMakeLists.txt delete mode 100644 examples/conan_cmake/inject/conanfile.py delete mode 100644 examples/conan_cmake/inject/main.cpp delete mode 100644 examples/conan_cmake/inject/pyproject.toml diff --git a/examples/conan_cmake/inject/CMakeLists.txt b/examples/conan_cmake/inject/CMakeLists.txt deleted file mode 100644 index 7cf1ecc..0000000 --- a/examples/conan_cmake/inject/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -cmake_minimum_required(VERSION 3.24) - -project(FormatOutput LANGUAGES CXX C) - -set(CMAKE_CXX_STANDARD 14) - -find_package(fmt REQUIRED) - -add_executable(main main.cpp) -target_link_libraries(main PRIVATE fmt::fmt) \ No newline at end of file diff --git a/examples/conan_cmake/inject/conanfile.py b/examples/conan_cmake/inject/conanfile.py deleted file mode 100644 index b2f756c..0000000 --- a/examples/conan_cmake/inject/conanfile.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Preexisting CMake project with Conan integration.""" - -from conan import ConanFile # type: ignore -from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout # type: ignore - - -class MyProject(ConanFile): # type: ignore - """Conan file for a simple CMake project.""" - - name = 'myproject' - version = '1.0' - settings = 'os', 'compiler', 'build_type', 'arch' - generators = 'CMakeDeps' - - def layout(self) -> None: - """Define the layout of the project.""" - cmake_layout(self) - - def generate(self) -> None: - """Generate the CMake toolchain file.""" - tc = CMakeToolchain(self) - tc.generate() - - def build(self) -> None: - """Build the project using CMake.""" - cmake = CMake(self) - cmake.configure() - cmake.build() diff --git a/examples/conan_cmake/inject/main.cpp b/examples/conan_cmake/inject/main.cpp deleted file mode 100644 index 4de3567..0000000 --- a/examples/conan_cmake/inject/main.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "fmt/color.h" - -int main() -{ - fmt::print(fg(fmt::terminal_color::cyan), "Hello fmt {}!\n", FMT_VERSION); - return 0; -} \ No newline at end of file diff --git a/examples/conan_cmake/inject/pyproject.toml b/examples/conan_cmake/inject/pyproject.toml deleted file mode 100644 index 51f07ba..0000000 --- a/examples/conan_cmake/inject/pyproject.toml +++ /dev/null @@ -1,27 +0,0 @@ -[project] -description = "A simple project showing how to use conan with CPPython" -name = "cppython-conan-cmake-simple" -version = "1.0.0" - -license = { text = "MIT" } - -authors = [{ name = "Synodic Software", email = "contact@synodic.software" }] - -requires-python = ">=3.13" - -dependencies = ["cppython[conan, cmake, git]>=0.9.0"] - -[tool.cppython] -generator-name = "cmake" -provider-name = "conan" - -install-path = "install" - -dependencies = ["fmt>=11.0.1"] - -[tool.cppython.generator] - -[tool.cppython.provider] - -[tool.pdm] -distribution = false diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index ca06bca..7da3066 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -30,19 +30,3 @@ def test_simple(example_runner: CliRunner) -> None: # Verify that the build directory contains the expected files assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found' - - @staticmethod - def test_inject(example_runner: CliRunner) -> None: - """Inject""" - # By nature of running the test, we require PDM to develop the project and so it will be installed - result = subprocess.run(['pdm', 'install'], capture_output=True, text=True, check=False) - - assert result.returncode == 0, f'PDM install failed: {result.stderr}' - - # Run the CMake configuration command - result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) - - assert result.returncode == 0, f'Cmake failed: {result.stderr}' - - # Verify that the build directory contains the expected files - assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found' From 001711773143ac514865b9bc144bcad4dde9108c Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 07:55:29 -0400 Subject: [PATCH 09/10] Remove CMake BinaryDir Req --- cppython/plugins/cmake/schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cppython/plugins/cmake/schema.py b/cppython/plugins/cmake/schema.py index ab20994..dba1005 100644 --- a/cppython/plugins/cmake/schema.py +++ b/cppython/plugins/cmake/schema.py @@ -50,7 +50,6 @@ class ConfigurePreset(CPPythonModel, extra='allow'): inherits: Annotated[ str | list[str] | None, Field(description='The inherits field allows inheriting from other presets.') ] = None - binaryDir: Annotated[str | None, Field(description='The binary directory for the build output.')] = None cacheVariables: dict[str, None | bool | str | CacheVariable] | None = None @@ -92,4 +91,4 @@ class CMakeConfiguration(CPPythonModel): ] = Path('CMakePresets.json') configuration_name: Annotated[ str, Field(description='The CMake configuration preset to look for and override inside the given `preset_file`') - ] = 'cppython' + ] = 'default' From f911782c342aa6cd5b78c044f67f4ac62ac0d227 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Apr 2025 07:55:52 -0400 Subject: [PATCH 10/10] Add CMake Conditional Inheritance --- cppython/plugins/cmake/builder.py | 36 ++++++++++++----- cppython/plugins/cmake/plugin.py | 2 +- tests/unit/plugins/cmake/test_presets.py | 50 ++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index 43df7fd..d489c2a 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -2,7 +2,7 @@ from pathlib import Path -from cppython.plugins.cmake.schema import CMakePresets, CMakeSyncData, ConfigurePreset +from cppython.plugins.cmake.schema import CMakeData, CMakePresets, CMakeSyncData, ConfigurePreset class Builder: @@ -114,7 +114,7 @@ def write_cppython_preset( return cppython_preset_file @staticmethod - def generate_root_preset(preset_file: Path, cppython_preset_file: Path) -> CMakePresets: + def generate_root_preset(preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData) -> CMakePresets: """Generates the top level root preset with the include reference. Args: @@ -124,18 +124,34 @@ def generate_root_preset(preset_file: Path, cppython_preset_file: Path) -> CMake Returns: A CMakePresets object """ - initial_root_preset = None + default_configure_preset = ConfigurePreset( + name=cmake_data.configuration_name, + inherits='cppython', + ) - # If the file already exists, we need to compare it if preset_file.exists(): with open(preset_file, encoding='utf-8') as file: initial_json = file.read() - initial_root_preset = CMakePresets.model_validate_json(initial_json) - root_preset = initial_root_preset.model_copy(deep=True) + root_preset = CMakePresets.model_validate_json(initial_json) + + if root_preset.configurePresets is None: + root_preset.configurePresets = [default_configure_preset] + + # Set defaults + preset = next((p for p in root_preset.configurePresets if p.name == default_configure_preset.name), None) + if preset: + # If the name matches, we need to verify it inherits from cppython + if preset.inherits is None: + preset.inherits = 'cppython' + elif isinstance(preset.inherits, str) and preset.inherits != 'cppython': + preset.inherits = [preset.inherits, 'cppython'] + elif isinstance(preset.inherits, list) and 'cppython' not in preset.inherits: + preset.inherits.append('cppython') + else: + root_preset.configurePresets.append(default_configure_preset) + else: # If the file doesn't exist, we need to default it for the user - # TODO: Forward the tool's build directory - default_configure_preset = ConfigurePreset(name='default', inherits='cppython', binaryDir='build') root_preset = CMakePresets(configurePresets=[default_configure_preset]) # Get the relative path to the cppython preset file @@ -153,7 +169,7 @@ def generate_root_preset(preset_file: Path, cppython_preset_file: Path) -> CMake return root_preset @staticmethod - def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None: + def write_root_presets(preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData) -> None: """Read the top level json file and insert the include reference. Receives a relative path to the tool cmake json file @@ -172,7 +188,7 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None: initial_json = file.read() initial_root_preset = CMakePresets.model_validate_json(initial_json) - root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file) + root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data) # Only write the file if the data has changed if root_preset != initial_root_preset: diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index 15fdc62..374e5c0 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -71,6 +71,6 @@ def sync(self, sync_data: SyncData) -> None: self._cppython_preset_directory, self._provider_directory, sync_data ) - self.builder.write_root_presets(self.data.preset_file, cppython_preset_file) + self.builder.write_root_presets(self.data.preset_file, cppython_preset_file, self.data) case _: raise ValueError('Unsupported sync data type') diff --git a/tests/unit/plugins/cmake/test_presets.py b/tests/unit/plugins/cmake/test_presets.py index a0f36ae..4d5852a 100644 --- a/tests/unit/plugins/cmake/test_presets.py +++ b/tests/unit/plugins/cmake/test_presets.py @@ -3,13 +3,51 @@ from pathlib import Path from cppython.plugins.cmake.builder import Builder -from cppython.plugins.cmake.schema import CMakePresets, CMakeSyncData +from cppython.plugins.cmake.schema import CMakeData, CMakePresets, CMakeSyncData from cppython.utility.utility import TypeName -class TestCMakePresets: +class TestBuilder: """Tests for the CMakePresets class""" + @staticmethod + def test_generate_root_preset_new(tmp_path: Path) -> None: + """Test generate_root_preset when the preset file does not exist""" + builder = Builder() + preset_file = tmp_path / 'CMakePresets.json' + cppython_preset_file = tmp_path / 'cppython.json' + cmake_data = CMakeData(preset_file=preset_file, configuration_name='test-configuration') + + # The function should create a new preset with the correct name and inheritance + result = builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data) + assert result.configurePresets is not None + assert any(p.name == 'test-configuration' for p in result.configurePresets) + + preset = next(p for p in result.configurePresets if p.name == 'test-configuration') + assert preset.inherits == 'cppython' + + @staticmethod + def test_generate_root_preset_existing(tmp_path: Path) -> None: + """Test generate_root_preset when the preset file already exists""" + builder = Builder() + preset_file = tmp_path / 'CMakePresets.json' + cppython_preset_file = tmp_path / 'cppython.json' + cmake_data = CMakeData(preset_file=preset_file, configuration_name='test-configuration') + + # Create an initial preset file with a different preset + initial_presets = CMakePresets(configurePresets=[]) + with open(preset_file, 'w', encoding='utf-8') as f: + f.write(initial_presets.model_dump_json(exclude_none=True, by_alias=False, indent=4)) + + # Should add the new preset and include + result = builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data) + assert result.configurePresets is not None + assert any(p.name == 'test-configuration' for p in result.configurePresets) + + +class TestWrites: + """Tests for writing the CMakePresets class""" + @staticmethod def test_provider_write(tmp_path: Path) -> None: """Verifies that the provider preset writing works as intended @@ -78,7 +116,9 @@ def test_root_write(tmp_path: Path) -> None: cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) - builder.write_root_presets(root_file, cppython_preset_file) + builder.write_root_presets( + root_file, cppython_preset_file, CMakeData(preset_file=root_file, configuration_name='default') + ) @staticmethod def test_relative_root_write(tmp_path: Path) -> None: @@ -112,4 +152,6 @@ def test_relative_root_write(tmp_path: Path) -> None: builder.write_provider_preset(provider_directory, data) cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) - builder.write_root_presets(root_file, cppython_preset_file) + builder.write_root_presets( + root_file, cppython_preset_file, CMakeData(preset_file=root_file, configuration_name='default') + )