From 0ffeeed976ff0f179efc20278032009b403a239d Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 14 Jul 2025 21:07:15 -0400 Subject: [PATCH 01/20] Move Local Cache Fixture --- tests/fixtures/conan.py | 17 +++++++++++++++++ tests/integration/examples/test_conan_cmake.py | 4 +++- .../integration/plugins/conan/test_provider.py | 18 +----------------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py index 9571c24f..b658e4ac 100644 --- a/tests/fixtures/conan.py +++ b/tests/fixtures/conan.py @@ -11,6 +11,23 @@ from cppython.plugins.conan.schema import ConanDependency +@pytest.fixture(autouse=True) +def clean_conan_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Sets CONAN_HOME to a temporary directory for each test. + + This ensures all tests run with a clean Conan cache. + + Args: + tmp_path: Pytest temporary directory fixture + monkeypatch: Pytest monkeypatch fixture for environment variable manipulation + """ + conan_home = tmp_path / 'conan_home' + conan_home.mkdir() + + # Set CONAN_HOME to the temporary directory + monkeypatch.setenv('CONAN_HOME', str(conan_home)) + + @pytest.fixture(name='conan_mock_api') def fixture_conan_mock_api(mocker: MockerFixture) -> Mock: """Creates a mock ConanAPI instance for install/update operations diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index a9a8f6cc..cd99fd9b 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -14,7 +14,7 @@ from cppython.core.schema import ProjectConfiguration from cppython.project import Project -pytest_plugins = ['tests.fixtures.example'] +pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.conan'] class TestConanCMake: @@ -49,3 +49,5 @@ def test_simple(example_runner: CliRunner) -> None: # Verify that the build directory contains the expected files assert (path / 'CMakeCache.txt').exists(), f'{path / "CMakeCache.txt"} not found' + + # Publish the project to the local cache diff --git a/tests/integration/plugins/conan/test_provider.py b/tests/integration/plugins/conan/test_provider.py index c0ba43e0..761fff49 100644 --- a/tests/integration/plugins/conan/test_provider.py +++ b/tests/integration/plugins/conan/test_provider.py @@ -1,6 +1,5 @@ """Integration tests for the provider""" -from pathlib import Path from typing import Any import pytest @@ -8,22 +7,7 @@ from cppython.plugins.conan.plugin import ConanProvider from cppython.test.pytest.contracts import ProviderIntegrationTestContract - -@pytest.fixture(autouse=True) -def clean_conan_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): - """Sets CONAN_HOME to a temporary directory for each test. - - This ensures all tests run with a clean Conan cache. - - Args: - tmp_path: Pytest temporary directory fixture - monkeypatch: Pytest monkeypatch fixture for environment variable manipulation - """ - conan_home = tmp_path / 'conan_home' - conan_home.mkdir() - - # Set CONAN_HOME to the temporary directory - monkeypatch.setenv('CONAN_HOME', str(conan_home)) +pytest_plugins = ['tests.fixtures.conan'] class TestConanProvider(ProviderIntegrationTestContract[ConanProvider]): From ad02ab38979fc301a8f8174982b9a34c386047ed Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 14 Jul 2025 21:30:26 -0400 Subject: [PATCH 02/20] Update test_conan_cmake.py --- tests/integration/examples/test_conan_cmake.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index cd99fd9b..b3979632 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -50,4 +50,12 @@ def test_simple(example_runner: CliRunner) -> None: # Verify that the build directory contains the expected files assert (path / 'CMakeCache.txt').exists(), f'{path / "CMakeCache.txt"} not found' + # --- Setup for Publish with modified config --- + # Modify the in-memory representation of the pyproject data + pyproject_data['tool']['cppython']['provider']['remotes'] = [] + + # Create a new project instance with the modified configuration for the 'publish' step + publish_project = Project(project_configuration, interface, pyproject_data) + # Publish the project to the local cache + publish_project.publish() From 91f1d166e59773224874a4cf67a97816f50b0709 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 16 Jul 2025 23:16:37 -0400 Subject: [PATCH 03/20] Profile Schema --- cppython/plugins/conan/plugin.py | 74 +---- cppython/plugins/conan/resolution.py | 124 +++++++- cppython/plugins/conan/schema.py | 17 ++ tests/unit/plugins/conan/test_install.py | 22 +- tests/unit/plugins/conan/test_publish.py | 13 +- tests/unit/plugins/conan/test_resolution.py | 298 ++++++++++++++++++++ 6 files changed, 459 insertions(+), 89 deletions(-) create mode 100644 tests/unit/plugins/conan/test_resolution.py diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index a30d4b2c..aa80fcb3 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -12,7 +12,6 @@ import requests from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern -from conan.internal.model.profile import Profile from cppython.core.plugin_schema.generator import SyncConsumer from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures @@ -109,8 +108,8 @@ def _install_dependencies(self, *, update: bool = False) -> None: all_remotes = conan_api.remotes.list() logger.debug('Available remotes: %s', [remote.name for remote in all_remotes]) - # Get profiles with fallback to auto-detection - profile_host, profile_build = self._get_profiles(conan_api) + # Get profiles from resolved data + profile_host, profile_build = self.data.host_profile, self.data.build_profile path = str(conanfile_path) remotes = all_remotes @@ -249,8 +248,8 @@ def publish(self) -> None: remotes=all_remotes, # Use all remotes for dependency resolution during export ) - # Step 2: Get profiles with fallback to auto-detection - profile_host, profile_build = self._get_profiles(conan_api) + # Step 2: Get profiles from resolved data + profile_host, profile_build = self.data.host_profile, self.data.build_profile # Step 3: Build dependency graph for the package - prepare parameters path = str(conanfile_path) @@ -305,68 +304,3 @@ def publish(self) -> None: ) else: raise ProviderInstallationError('conan', 'No packages found to upload') - - def _apply_profile_processing(self, profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any) -> None: - """Apply profile plugin and settings processing to a list of profiles. - - Args: - profiles: List of profiles to process - conan_api: The Conan API instance - cache_settings: The settings configuration - """ - logger = logging.getLogger('cppython.conan') - - # Apply profile plugin processing - try: - profile_plugin = conan_api.profiles._load_profile_plugin() - if profile_plugin is not None: - for profile in profiles: - try: - profile_plugin(profile) - except Exception as plugin_error: - logger.warning('Profile plugin failed for profile: %s', str(plugin_error)) - except (AttributeError, Exception): - logger.debug('Profile plugin not available or failed to load') - - # Process settings to initialize processed_settings - for profile in profiles: - try: - profile.process_settings(cache_settings) - except (AttributeError, Exception) as settings_error: - logger.debug('Settings processing failed for profile: %s', str(settings_error)) - - def _get_profiles(self, conan_api: ConanAPI) -> tuple[Profile, Profile]: - """Get Conan profiles with fallback to auto-detection. - - Args: - conan_api: The Conan API instance - - Returns: - A tuple of (profile_host, profile_build) objects - """ - logger = logging.getLogger('cppython.conan') - - try: - # Gather default profile paths, these can raise exceptions if not available - profile_host_path = conan_api.profiles.get_default_host() - profile_build_path = conan_api.profiles.get_default_build() - - # Load the actual profile objects, can raise if data is invalid - profile_host = conan_api.profiles.get_profile([profile_host_path]) - profile_build = conan_api.profiles.get_profile([profile_build_path]) - - logger.debug('Using existing default profiles') - return profile_host, profile_build - - except Exception as e: - logger.warning('Default profiles not available, using auto-detection. Conan message: %s', str(e)) - - # Create auto-detected profiles - profiles = [conan_api.profiles.detect(), conan_api.profiles.detect()] - cache_settings = conan_api.config.settings_yml - - # Apply profile plugin processing to both profiles - self._apply_profile_processing(profiles, conan_api, cache_settings) - - logger.debug('Auto-detected profiles with plugin processing applied') - return profiles[0], profiles[1] diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index a287b803..b3e8c828 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -1,12 +1,124 @@ """Provides functionality to resolve Conan-specific data for the CPPython project.""" +import logging from typing import Any +from conan.api.conan_api import ConanAPI +from conan.internal.model.profile import Profile from packaging.requirements import Requirement from cppython.core.exception import ConfigException from cppython.core.schema import CorePluginData from cppython.plugins.conan.schema import ConanConfiguration, ConanData, ConanDependency +from cppython.utility.exception import ProviderConfigurationError + + +def _profile_post_process(profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any) -> None: + """Apply profile plugin and settings processing to a list of profiles. + + Args: + profiles: List of profiles to process + conan_api: The Conan API instance + cache_settings: The settings configuration + """ + logger = logging.getLogger('cppython.conan') + + # Apply profile plugin processing + try: + profile_plugin = conan_api.profiles._load_profile_plugin() + if profile_plugin is not None: + for profile in profiles: + try: + profile_plugin(profile) + except Exception as plugin_error: + logger.warning('Profile plugin failed for profile: %s', str(plugin_error)) + except (AttributeError, Exception): + logger.debug('Profile plugin not available or failed to load') + + # Process settings to initialize processed_settings + for profile in profiles: + try: + profile.process_settings(cache_settings) + except (AttributeError, Exception) as settings_error: + logger.debug('Settings processing failed for profile: %s', str(settings_error)) + + +def _resolve_profiles( + host_profile_name: str | None, build_profile_name: str | None, conan_api: ConanAPI +) -> tuple[Profile, Profile]: + """Resolve host and build profiles, with fallback to auto-detection. + + Args: + host_profile_name: The host profile name to resolve, or None for auto-detection + build_profile_name: The build profile name to resolve, or None for auto-detection + conan_api: The Conan API instance + + Returns: + A tuple of (host_profile, build_profile) + """ + logger = logging.getLogger('cppython.conan') + + def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile: + """Helper to resolve a single profile.""" + profile_type = 'host' if is_host else 'build' + + if profile_name is not None and profile_name != 'default': + # Explicitly specified profile name (not the default) - fail if not found + try: + logger.debug('Loading %s profile: %s', profile_type, profile_name) + profile = conan_api.profiles.get_profile([profile_name]) + logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name) + return profile + except Exception as e: + logger.error('Failed to load %s profile %s: %s', profile_type, profile_name, str(e)) + raise ProviderConfigurationError( + 'conan', + f'Failed to load {profile_type} profile {profile_name}: {str(e)}', + f'{profile_type}_profile', + ) from e + elif profile_name == 'default': + # Try to load default profile, but fall back to auto-detection if it fails + try: + logger.debug('Loading %s profile: %s', profile_type, profile_name) + profile = conan_api.profiles.get_profile([profile_name]) + logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name) + return profile + except Exception as e: + logger.debug( + 'Failed to load %s profile %s: %s. Falling back to auto-detection.', + profile_type, + profile_name, + str(e), + ) + # Fall back to auto-detection + + try: + if is_host: + default_profile_path = conan_api.profiles.get_default_host() + else: + default_profile_path = conan_api.profiles.get_default_build() + + profile = conan_api.profiles.get_profile([default_profile_path]) + logger.debug('Using default %s profile', profile_type) + return profile + except Exception as e: + logger.warning('Default %s profile not available, using auto-detection: %s', profile_type, str(e)) + + # Create auto-detected profile + profile = conan_api.profiles.detect() + cache_settings = conan_api.config.settings_yml + + # Apply profile plugin processing + _profile_post_process([profile], conan_api, cache_settings) + + logger.debug('Auto-detected %s profile with plugin processing applied', profile_type) + return profile + + # Resolve both profiles + host_profile = _resolve_profile(host_profile_name, is_host=True) + build_profile = _resolve_profile(build_profile_name, is_host=False) + + return host_profile, build_profile def resolve_conan_dependency(requirement: Requirement) -> ConanDependency: @@ -43,4 +155,14 @@ def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> Conan """ parsed_data = ConanConfiguration(**data) - return ConanData(remotes=parsed_data.remotes) + # Initialize Conan API for profile resolution + conan_api = ConanAPI() + + # Resolve profiles + host_profile, build_profile = _resolve_profiles(parsed_data.host_profile, parsed_data.build_profile, conan_api) + + return ConanData( + remotes=parsed_data.remotes, + host_profile=host_profile, + build_profile=build_profile, + ) diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py index 9f30206d..42fd0e42 100644 --- a/cppython/plugins/conan/schema.py +++ b/cppython/plugins/conan/schema.py @@ -7,6 +7,7 @@ from typing import Annotated +from conan.internal.model.profile import Profile from pydantic import Field from cppython.core.schema import CPPythonModel @@ -31,6 +32,8 @@ class ConanData(CPPythonModel): """Resolved conan data""" remotes: list[str] + host_profile: Profile + build_profile: Profile @property def local_only(self) -> bool: @@ -45,3 +48,17 @@ class ConanConfiguration(CPPythonModel): list[str], Field(description='List of remotes to upload to. Empty list means the local conan cache will be used.'), ] = ['conancenter'] + host_profile: Annotated[ + str | None, + Field( + description='Conan host profile defining the target platform where the built software will run. ' + 'Used for cross-compilation scenarios.' + ), + ] = 'default' + build_profile: Annotated[ + str | None, + Field( + description='Conan build profile defining the platform where the compilation process executes. ' + 'Typically matches the development machine.' + ), + ] = 'default' diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py index 04a157cd..509059da 100644 --- a/tests/unit/plugins/conan/test_install.py +++ b/tests/unit/plugins/conan/test_install.py @@ -136,7 +136,7 @@ def mock_resolve(requirement: Requirement) -> ConanDependency: # Verify Conan API was attempted mock_conan_api_constructor.assert_called_once() - def test_install_with_profile_exception( + def test_install_with_default_profiles( self, plugin: ConanProvider, conan_temp_conanfile: Path, @@ -144,7 +144,7 @@ def test_install_with_profile_exception( conan_setup_mocks: dict[str, Mock], conan_mock_api: Mock, ) -> None: - """Test install method when profile operations throw exceptions but detect() works + """Test install method uses pre-resolved profiles from plugin construction Args: plugin: The plugin instance @@ -153,23 +153,21 @@ def test_install_with_profile_exception( conan_setup_mocks: Dictionary containing all mocks conan_mock_api: Mock ConanAPI instance """ - # Configure the API mock to throw exception on profile calls but detect() works - conan_mock_api.profiles.get_default_host.side_effect = Exception('Profile not found') - # Setup dependencies plugin.core_data.cppython_data.dependencies = conan_mock_dependencies - # Execute - should succeed using fallback detect profiles + # Execute - should use the profiles resolved during plugin construction plugin.install() - # Verify that the fallback was used + # Verify that the API was used for installation conan_setup_mocks['conan_api_constructor'].assert_called_once() - conan_mock_api.profiles.get_default_host.assert_called_once() - - # Verify detect was called for fallback (should be called twice for fallback) - assert conan_mock_api.profiles.detect.call_count >= EXPECTED_PROFILE_CALLS - # Verify the rest of the process continued + # Verify the rest of the process continued with resolved profiles conan_mock_api.graph.load_graph_consumer.assert_called_once() conan_mock_api.install.install_binaries.assert_called_once() conan_mock_api.install.install_consumer.assert_called_once() + + # Verify that the resolved profiles were used in the graph loading + call_args = conan_mock_api.graph.load_graph_consumer.call_args + assert call_args.kwargs['profile_host'] == plugin.data.host_profile + assert call_args.kwargs['profile_build'] == plugin.data.build_profile diff --git a/tests/unit/plugins/conan/test_publish.py b/tests/unit/plugins/conan/test_publish.py index b21f8f77..e395a4c3 100644 --- a/tests/unit/plugins/conan/test_publish.py +++ b/tests/unit/plugins/conan/test_publish.py @@ -179,10 +179,10 @@ def test_publish_no_packages_found( with pytest.raises(ProviderInstallationError, match='No packages found to upload'): plugin.publish() - def test_publish_uses_default_profiles( + def test_publish_with_default_profiles( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: - """Test that publish uses default profiles from API + """Test that publish uses pre-resolved profiles from plugin construction Args: plugin: The plugin instance @@ -203,10 +203,11 @@ def test_publish_uses_default_profiles( # Execute publish plugin.publish() - # Verify profiles were obtained from API - conan_mock_api_publish.profiles.get_default_host.assert_called_once() - conan_mock_api_publish.profiles.get_default_build.assert_called_once() - conan_mock_api_publish.profiles.get_profile.assert_called() + # Verify that the resolved profiles were used in the graph loading + conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() + call_args = conan_mock_api_publish.graph.load_graph_consumer.call_args + assert call_args.kwargs['profile_host'] == plugin.data.host_profile + assert call_args.kwargs['profile_build'] == plugin.data.build_profile def test_publish_upload_parameters( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py new file mode 100644 index 00000000..ae070b47 --- /dev/null +++ b/tests/unit/plugins/conan/test_resolution.py @@ -0,0 +1,298 @@ +"""Unit tests for Conan resolution functionality.""" + +import logging +from unittest.mock import Mock, patch + +import pytest +from conan.internal.model.profile import Profile +from packaging.requirements import Requirement + +from cppython.core.exception import ConfigException +from cppython.core.schema import CorePluginData +from cppython.plugins.conan.resolution import ( + _profile_post_process, + _resolve_profiles, + resolve_conan_data, + resolve_conan_dependency, +) +from cppython.plugins.conan.schema import ConanData +from cppython.utility.exception import ProviderConfigurationError + +# Constants for test validation +EXPECTED_PROFILE_CALL_COUNT = 2 + + +class TestResolveDependency: + """Test dependency resolution.""" + + def test_resolve_dependency_with_version(self) -> None: + """Test resolving a dependency with a version specifier.""" + requirement = Requirement('boost>=1.80.0') + + result = resolve_conan_dependency(requirement) + + assert result.name == 'boost' + assert result.version_ge == '1.80.0' + + def test_resolve_dependency_without_version(self) -> None: + """Test resolving a dependency without a version specifier.""" + requirement = Requirement('boost') + + result = resolve_conan_dependency(requirement) + + assert result.name == 'boost' + assert result.version_ge is None + + def test_resolve_dependency_multiple_specifiers(self) -> None: + """Test that multiple specifiers raise an error.""" + requirement = Requirement('boost>=1.80.0,<2.0.0') + + with pytest.raises(ConfigException, match='Multiple specifiers are not supported'): + resolve_conan_dependency(requirement) + + def test_resolve_dependency_unsupported_operator(self) -> None: + """Test that unsupported operators raise an error.""" + requirement = Requirement('boost==1.80.0') + + with pytest.raises(ConfigException, match="Unsupported specifier '=='"): + resolve_conan_dependency(requirement) + + +class TestProfileProcessing: + """Test profile processing functionality.""" + + def test_apply_profile_processing_success(self) -> None: + """Test successful profile processing.""" + mock_conan_api = Mock() + mock_profile = Mock() + mock_cache_settings = Mock() + mock_plugin = Mock() + + mock_conan_api.profiles._load_profile_plugin.return_value = mock_plugin + profiles = [mock_profile] + + _profile_post_process(profiles, mock_conan_api, mock_cache_settings) + + mock_plugin.assert_called_once_with(mock_profile) + mock_profile.process_settings.assert_called_once_with(mock_cache_settings) + + def test_apply_profile_processing_no_plugin(self) -> None: + """Test profile processing when no plugin is available.""" + mock_conan_api = Mock() + mock_profile = Mock() + mock_cache_settings = Mock() + + mock_conan_api.profiles._load_profile_plugin.return_value = None + profiles = [mock_profile] + + _profile_post_process(profiles, mock_conan_api, mock_cache_settings) + + mock_profile.process_settings.assert_called_once_with(mock_cache_settings) + + def test_apply_profile_processing_plugin_failure(self, caplog: pytest.LogCaptureFixture) -> None: + """Test profile processing when plugin fails.""" + mock_conan_api = Mock() + mock_profile = Mock() + mock_cache_settings = Mock() + mock_plugin = Mock() + + mock_conan_api.profiles._load_profile_plugin.return_value = mock_plugin + mock_plugin.side_effect = Exception('Plugin failed') + profiles = [mock_profile] + + with caplog.at_level(logging.WARNING): + _profile_post_process(profiles, mock_conan_api, mock_cache_settings) + + assert 'Profile plugin failed for profile' in caplog.text + mock_profile.process_settings.assert_called_once_with(mock_cache_settings) + + def test_apply_profile_processing_settings_failure(self, caplog: pytest.LogCaptureFixture) -> None: + """Test profile processing when settings processing fails.""" + mock_conan_api = Mock() + mock_profile = Mock() + mock_cache_settings = Mock() + + mock_conan_api.profiles._load_profile_plugin.return_value = None + mock_profile.process_settings.side_effect = Exception('Settings failed') + profiles = [mock_profile] + + with caplog.at_level(logging.DEBUG): + _profile_post_process(profiles, mock_conan_api, mock_cache_settings) + + assert 'Settings processing failed for profile' in caplog.text + + +class TestResolveProfiles: + """Test profile resolution functionality.""" + + def test_resolve_profiles_by_name(self) -> None: + """Test resolving profiles by name.""" + mock_conan_api = Mock() + mock_host_profile = Mock() + mock_build_profile = Mock() + mock_conan_api.profiles.get_profile.side_effect = [mock_host_profile, mock_build_profile] + + host_result, build_result = _resolve_profiles('host-profile', 'build-profile', mock_conan_api) + + assert host_result == mock_host_profile + assert build_result == mock_build_profile + assert mock_conan_api.profiles.get_profile.call_count == 2 + mock_conan_api.profiles.get_profile.assert_any_call(['host-profile']) + mock_conan_api.profiles.get_profile.assert_any_call(['build-profile']) + + def test_resolve_profiles_by_name_failure(self) -> None: + """Test resolving profiles by name when host profile fails.""" + mock_conan_api = Mock() + mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') + + with pytest.raises(ProviderConfigurationError, match='Failed to load host profile'): + _resolve_profiles('missing-profile', 'other-profile', mock_conan_api) + + def test_resolve_profiles_auto_detect(self) -> None: + """Test auto-detecting profiles.""" + mock_conan_api = Mock() + mock_host_profile = Mock() + mock_build_profile = Mock() + mock_host_default_path = 'host-default' + mock_build_default_path = 'build-default' + + mock_conan_api.profiles.get_default_host.return_value = mock_host_default_path + mock_conan_api.profiles.get_default_build.return_value = mock_build_default_path + mock_conan_api.profiles.get_profile.side_effect = [mock_host_profile, mock_build_profile] + + host_result, build_result = _resolve_profiles(None, None, mock_conan_api) + + assert host_result == mock_host_profile + assert build_result == mock_build_profile + mock_conan_api.profiles.get_default_host.assert_called_once() + mock_conan_api.profiles.get_default_build.assert_called_once() + mock_conan_api.profiles.get_profile.assert_any_call([mock_host_default_path]) + mock_conan_api.profiles.get_profile.assert_any_call([mock_build_default_path]) + + @patch('cppython.plugins.conan.resolution._profile_post_process') + def test_resolve_profiles_fallback_to_detect(self, mock_post_process: Mock) -> None: + """Test falling back to profile detection when defaults fail.""" + mock_conan_api = Mock() + mock_host_profile = Mock() + mock_build_profile = Mock() + mock_cache_settings = Mock() + + # Mock the default profile methods to fail + mock_conan_api.profiles.get_default_host.side_effect = Exception('No default profile') + mock_conan_api.profiles.get_default_build.side_effect = Exception('No default profile') + mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') + + # Mock detect to succeed + mock_conan_api.profiles.detect.side_effect = [mock_host_profile, mock_build_profile] + mock_conan_api.config.settings_yml = mock_cache_settings + + host_result, build_result = _resolve_profiles(None, None, mock_conan_api) + + assert host_result == mock_host_profile + assert build_result == mock_build_profile + assert mock_conan_api.profiles.detect.call_count == 2 + assert mock_post_process.call_count == 2 + mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings) + mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings) + + @patch('cppython.plugins.conan.resolution._profile_post_process') + def test_resolve_profiles_default_fallback_to_detect(self, mock_post_process: Mock) -> None: + """Test falling back to profile detection when default profile fails.""" + mock_conan_api = Mock() + mock_host_profile = Mock() + mock_build_profile = Mock() + mock_cache_settings = Mock() + + # Mock the default profile to fail (this simulates the "default" profile not existing) + mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') + mock_conan_api.profiles.get_default_host.side_effect = Exception('No default profile') + mock_conan_api.profiles.get_default_build.side_effect = Exception('No default profile') + + # Mock detect to succeed + mock_conan_api.profiles.detect.side_effect = [mock_host_profile, mock_build_profile] + mock_conan_api.config.settings_yml = mock_cache_settings + + host_result, build_result = _resolve_profiles('default', 'default', mock_conan_api) + + assert host_result == mock_host_profile + assert build_result == mock_build_profile + assert mock_conan_api.profiles.detect.call_count == 2 + assert mock_post_process.call_count == 2 + mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings) + mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings) + + +class TestResolveConanData: + """Test Conan data resolution.""" + + @patch('cppython.plugins.conan.resolution.ConanAPI') + @patch('cppython.plugins.conan.resolution._resolve_profiles') + def test_resolve_conan_data_with_profiles(self, mock_resolve_profiles: Mock, mock_conan_api_class: Mock) -> None: + """Test resolving ConanData with profile configuration.""" + mock_conan_api = Mock() + mock_conan_api_class.return_value = mock_conan_api + + mock_host_profile = Mock(spec=Profile) + mock_build_profile = Mock(spec=Profile) + mock_resolve_profiles.return_value = (mock_host_profile, mock_build_profile) + + data = {'host_profile': 'linux-x64', 'build_profile': 'linux-gcc11', 'remotes': ['conancenter']} + core_data = Mock(spec=CorePluginData) + + result = resolve_conan_data(data, core_data) + + assert isinstance(result, ConanData) + assert result.host_profile == mock_host_profile + assert result.build_profile == mock_build_profile + assert result.remotes == ['conancenter'] + + # Verify profile resolution was called correctly + mock_resolve_profiles.assert_called_once_with('linux-x64', 'linux-gcc11', mock_conan_api) + + @patch('cppython.plugins.conan.resolution.ConanAPI') + @patch('cppython.plugins.conan.resolution._resolve_profiles') + def test_resolve_conan_data_default_profiles(self, mock_resolve_profiles: Mock, mock_conan_api_class: Mock) -> None: + """Test resolving ConanData with default profile configuration.""" + mock_conan_api = Mock() + mock_conan_api_class.return_value = mock_conan_api + + mock_host_profile = Mock(spec=Profile) + mock_build_profile = Mock(spec=Profile) + mock_resolve_profiles.return_value = (mock_host_profile, mock_build_profile) + + data = {} # Empty data should use defaults + core_data = Mock(spec=CorePluginData) + + result = resolve_conan_data(data, core_data) + + assert isinstance(result, ConanData) + assert result.host_profile == mock_host_profile + assert result.build_profile == mock_build_profile + assert result.remotes == ['conancenter'] # Default remote + + # Verify profile resolution was called with default values + mock_resolve_profiles.assert_called_once_with('default', 'default', mock_conan_api) + + @patch('cppython.plugins.conan.resolution.ConanAPI') + @patch('cppython.plugins.conan.resolution._resolve_profiles') + def test_resolve_conan_data_null_profiles(self, mock_resolve_profiles: Mock, mock_conan_api_class: Mock) -> None: + """Test resolving ConanData with null profile configuration.""" + mock_conan_api = Mock() + mock_conan_api_class.return_value = mock_conan_api + + mock_host_profile = Mock(spec=Profile) + mock_build_profile = Mock(spec=Profile) + mock_resolve_profiles.return_value = (mock_host_profile, mock_build_profile) + + data = {'host_profile': None, 'build_profile': None, 'remotes': []} + core_data = Mock(spec=CorePluginData) + + result = resolve_conan_data(data, core_data) + + assert isinstance(result, ConanData) + assert result.host_profile == mock_host_profile + assert result.build_profile == mock_build_profile + assert result.remotes == [] + + # Verify profile resolution was called with None values + mock_resolve_profiles.assert_called_once_with(None, None, mock_conan_api) From 6724a5dc7ddcc5172f92f7ccf134084758ec4fe7 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Thu, 17 Jul 2025 11:11:03 -0400 Subject: [PATCH 04/20] Stub New Files --- .../conan_cmake/build_requires/CMakeLists.txt | 10 + examples/conan_cmake/build_requires/README.md | 0 examples/conan_cmake/build_requires/main.cpp | 7 + examples/conan_cmake/build_requires/pdm.lock | 537 ++++++++++++++++++ .../conan_cmake/build_requires/pyproject.toml | 28 + examples/conan_cmake/simple/README.md | 0 6 files changed, 582 insertions(+) create mode 100644 examples/conan_cmake/build_requires/CMakeLists.txt create mode 100644 examples/conan_cmake/build_requires/README.md create mode 100644 examples/conan_cmake/build_requires/main.cpp create mode 100644 examples/conan_cmake/build_requires/pdm.lock create mode 100644 examples/conan_cmake/build_requires/pyproject.toml create mode 100644 examples/conan_cmake/simple/README.md diff --git a/examples/conan_cmake/build_requires/CMakeLists.txt b/examples/conan_cmake/build_requires/CMakeLists.txt new file mode 100644 index 00000000..7cf1eccb --- /dev/null +++ b/examples/conan_cmake/build_requires/CMakeLists.txt @@ -0,0 +1,10 @@ +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/build_requires/README.md b/examples/conan_cmake/build_requires/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/conan_cmake/build_requires/main.cpp b/examples/conan_cmake/build_requires/main.cpp new file mode 100644 index 00000000..4de35678 --- /dev/null +++ b/examples/conan_cmake/build_requires/main.cpp @@ -0,0 +1,7 @@ +#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/build_requires/pdm.lock b/examples/conan_cmake/build_requires/pdm.lock new file mode 100644 index 00000000..1eb1bc96 --- /dev/null +++ b/examples/conan_cmake/build_requires/pdm.lock @@ -0,0 +1,537 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = [] +lock_version = "4.5.0" +content_hash = "sha256:618cf02c62d23783da0e2d36b9ea92bec82152cbc38a811e71f5dcf02c5eeffc" + +[[metadata.targets]] +requires_python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +summary = "" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "certifi" +version = "2025.7.9" +summary = "" +files = [ + {file = "certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39"}, + {file = "certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +summary = "" +files = [ + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] + +[[package]] +name = "click" +version = "8.2.1" +summary = "" +dependencies = [ + "colorama; sys_platform == \"win32\"", +] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[[package]] +name = "cmake" +version = "4.0.3" +summary = "" +files = [ + {file = "cmake-4.0.3-py3-none-macosx_10_10_universal2.whl", hash = "sha256:f2adfb459747025f40f9d3bdd1f3a485d43e866c0c4eb66373d1fcd666b13e4a"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04c40c92fdcaa96c66a5731b5b3fbbdf87da99cc68fdd30ff30b90c34d222986"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d41b83d061bcc375a7a5f2942ba523a7563368d296d91260f9d8a53a10f5e5e5"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:434f84fdf1e21578974876b8414dc47afeaea62027d9adc37a943a6bb08eb053"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beec48371a4b906fe398758ded5df57fc16e9bb14fd34244d9d66ee35862fb9f"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47dc28bee6cfb4de00c7cf7e87d565b5c86eb4088da81b60a49e214fcdd4ffda"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10fdc972b3211915b65cc89e8cd24e1a26c9bd684ee71c3f369fb488f2c4388"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d840e780c48c5df1330879d50615176896e8e6eee554507d21ce8e2f1a5f0ff8"}, + {file = "cmake-4.0.3-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:6ef63bbabcbe3b89c1d80547913b6caceaad57987a27e7afc79ebc88ecd829e4"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:67103f2bcce8f57b8705ba8e353f18fdc3684a346eee97dc5f94d11575a424c6"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:880a1e1ae26d440d7e4f604fecbf839728ca7b096c870f2e7359855cc4828532"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:c403b660bbff1fd4d7f1c5d9e015ea27566e49ca9461e260c9758f2fd4e5e813"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:2a66ecdd4c3238484cb0c377d689c086a9b8b533e25329f73d21bd1c38f1ae86"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:004e58b1a1a384c2ca799c9c41ac4ed86ac3b80129462992c43c1121f8729ffd"}, + {file = "cmake-4.0.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:133dbc33f995cb97a4456d83d67fa0a7a798f53f979454359140588baa928f43"}, + {file = "cmake-4.0.3-py3-none-win32.whl", hash = "sha256:3e07bdd14e69ea67d1e67a4f5225ac2fd91ee9e349c440143cdddd7368be1f46"}, + {file = "cmake-4.0.3-py3-none-win_amd64.whl", hash = "sha256:9a349ff2b4a7c63c896061676bc0f4e6994f373d54314d79ba3608ee7fa75442"}, + {file = "cmake-4.0.3-py3-none-win_arm64.whl", hash = "sha256:94a52e67b264a51089907c9e74ca5a9e2f3e65c57c457e0f40f02629a0de74d8"}, + {file = "cmake-4.0.3.tar.gz", hash = "sha256:215732f09ea8a7088fe1ab46bbd61669437217278d709fd3851bf8211e8c59e3"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +summary = "" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "conan" +version = "2.18.1" +summary = "" +dependencies = [ + "colorama", + "distro; platform_system == \"FreeBSD\" or sys_platform == \"linux\"", + "fasteners", + "jinja2", + "patch-ng", + "python-dateutil", + "pyyaml", + "requests", + "urllib3", +] +files = [ + {file = "conan-2.18.1.tar.gz", hash = "sha256:5d8e9fac7614de9297933f65de8f17db14851a871cebc962f4856b7c294f43c5"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +summary = "" +dependencies = [ + "packaging", + "pydantic", + "requests", + "typer", + "types-requests", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +extras = ["cmake"] +summary = "" +dependencies = [ + "cmake", + "cppython==0.9.2", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +extras = ["conan"] +summary = "" +dependencies = [ + "conan", + "cppython==0.9.2", + "libcst", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "cppython" +version = "0.9.2" +extras = ["git"] +summary = "" +dependencies = [ + "cppython==0.9.2", + "dulwich", +] +files = [ + {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, + {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, +] + +[[package]] +name = "distro" +version = "1.8.0" +summary = "" +files = [ + {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, + {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, +] + +[[package]] +name = "dulwich" +version = "0.23.2" +summary = "" +dependencies = [ + "urllib3", +] +files = [ + {file = "dulwich-0.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e44dec7e36bc035da0ec3df6c1564810699e319ba41b71d17750dd7452e1b2fc"}, + {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:398ba1c0e1581071cdcb38a681e0ff1e046aa8f31bad3bc368266f499c4ddf9e"}, + {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:432c6eeac5edf97ff7090fbac7cda708167ee90e5afa78652d252e87e397f425"}, + {file = "dulwich-0.23.2-cp313-cp313-win32.whl", hash = "sha256:8555980e8509d7f76e80de58d1eb7bd2c1c317942b7a3c9c113d81dfc287f4c0"}, + {file = "dulwich-0.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:2b042dca31de4d4a0e88e4dbe20afe804a640c8882eec0de5093bffb34b75370"}, + {file = "dulwich-0.23.2-py3-none-any.whl", hash = "sha256:0b0439d309cf808f7955f74776981d9ac9dc1ec715aa39798de9b22bb95ac163"}, + {file = "dulwich-0.23.2.tar.gz", hash = "sha256:a152ebb0e95bc0f23768be563f80ff1e719bf5c4f5c2696be4fa8ab625a39879"}, +] + +[[package]] +name = "fasteners" +version = "0.19" +summary = "" +files = [ + {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, + {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, +] + +[[package]] +name = "idna" +version = "3.10" +summary = "" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +summary = "" +dependencies = [ + "markupsafe", +] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[[package]] +name = "libcst" +version = "1.8.2" +summary = "" +dependencies = [ + "pyyaml-ft", +] +files = [ + {file = "libcst-1.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08e9dca4ab6f8551794ce7ec146f86def6a82da41750cbed2c07551345fa10d3"}, + {file = "libcst-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8310521f2ccb79b5c4345750d475b88afa37bad930ab5554735f85ad5e3add30"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:da2d8b008aff72acd5a4a588491abdda1b446f17508e700f26df9be80d8442ae"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:be821d874ce8b26cbadd7277fa251a9b37f6d2326f8b5682b6fc8966b50a3a59"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f74b0bc7378ad5afcf25ac9d0367b4dbba50f6f6468faa41f5dfddcf8bf9c0f8"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b68ea4a6018abfea1f68d50f74de7d399172684c264eb09809023e2c8696fc23"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e264307ec49b2c72480422abafe80457f90b4e6e693b7ddf8a23d24b5c24001"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5d5519962ce7c72d81888fb0c09e58e308ba4c376e76bcd853b48151063d6a8"}, + {file = "libcst-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:b62aa11d6b74ed5545e58ac613d3f63095e5fd0254b3e0d1168fda991b9a6b41"}, + {file = "libcst-1.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9c2bd4ac288a9cdb7ffc3229a9ce8027a66a3fd3f2ab9e13da60f5fbfe91f3b2"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:08a8c7d9922ca6eed24e2c13a3c552b3c186af8fc78e5d4820b58487d780ec19"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bba7c2b5063e8ada5a5477f9fa0c01710645426b5a8628ec50d558542a0a292e"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d97c9fe13aacfbefded6861f5200dcb8e837da7391a9bdeb44ccb133705990af"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d2194ae959630aae4176a4b75bd320b3274c20bef2a5ca6b8d6fc96d3c608edf"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0be639f5b2e1999a4b4a82a0f4633969f97336f052d0c131627983589af52f56"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6753e50904e05c27915933da41518ecd7a8ca4dd3602112ba44920c6e353a455"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:706d07106af91c343150be86caeae1ea3851b74aa0730fcbbf8cd089e817f818"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd4310ea8ddc49cc8872e083737cf806299b17f93159a1f354d59aa08993e876"}, + {file = "libcst-1.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:51bbafdd847529e8a16d1965814ed17831af61452ee31943c414cb23451de926"}, + {file = "libcst-1.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:4f14f5045766646ed9e8826b959c6d07194788babed1e0ba08c94ea4f39517e3"}, + {file = "libcst-1.8.2.tar.gz", hash = "sha256:66e82cedba95a6176194a817be4232c720312f8be6d2c8f3847f3317d95a0c7f"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +summary = "" +dependencies = [ + "mdurl", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +summary = "" +files = [ + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +summary = "" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "packaging" +version = "25.0" +summary = "" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "patch-ng" +version = "1.18.1" +summary = "" +files = [ + {file = "patch-ng-1.18.1.tar.gz", hash = "sha256:52fd46ee46f6c8667692682c1fd7134edc65a2d2d084ebec1d295a6087fc0291"}, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +summary = "" +dependencies = [ + "annotated-types", + "pydantic-core", + "typing-extensions", + "typing-inspection", +] +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +summary = "" +dependencies = [ + "typing-extensions", +] +files = [ + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +summary = "" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +summary = "" +dependencies = [ + "six", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +summary = "" +files = [ + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +summary = "" +files = [ + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, + {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +summary = "" +dependencies = [ + "certifi", + "charset-normalizer", + "idna", + "urllib3", +] +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[[package]] +name = "rich" +version = "14.0.0" +summary = "" +dependencies = [ + "markdown-it-py", + "pygments", +] +files = [ + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +summary = "" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +summary = "" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "typer" +version = "0.16.0" +summary = "" +dependencies = [ + "click", + "rich", + "shellingham", + "typing-extensions", +] +files = [ + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +summary = "" +dependencies = [ + "urllib3", +] +files = [ + {file = "types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072"}, + {file = "types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +summary = "" +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +summary = "" +dependencies = [ + "typing-extensions", +] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +summary = "" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] diff --git a/examples/conan_cmake/build_requires/pyproject.toml b/examples/conan_cmake/build_requires/pyproject.toml new file mode 100644 index 00000000..6803860d --- /dev/null +++ b/examples/conan_cmake/build_requires/pyproject.toml @@ -0,0 +1,28 @@ +[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.2.0"] + +[tool.cppython.generator] + +[tool.cppython.provider] + +[tool.pdm] +distribution = false diff --git a/examples/conan_cmake/simple/README.md b/examples/conan_cmake/simple/README.md new file mode 100644 index 00000000..e69de29b From 584592a3765fe43d561d9e7d186548335018f457 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 18 Jul 2025 15:01:04 -0400 Subject: [PATCH 05/20] Build Requires Example --- .../conan_cmake/build_requires/CMakeLists.txt | 17 ++++-- .../conan_cmake/build_requires/conanfile.py | 54 +++++++++++++++++++ examples/conan_cmake/build_requires/main.cpp | 22 +++++++- .../conan_cmake/build_requires/pyproject.toml | 10 ++-- .../integration/examples/test_conan_cmake.py | 40 ++++++++++++++ 5 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 examples/conan_cmake/build_requires/conanfile.py diff --git a/examples/conan_cmake/build_requires/CMakeLists.txt b/examples/conan_cmake/build_requires/CMakeLists.txt index 7cf1eccb..4ccf53ee 100644 --- a/examples/conan_cmake/build_requires/CMakeLists.txt +++ b/examples/conan_cmake/build_requires/CMakeLists.txt @@ -1,10 +1,19 @@ cmake_minimum_required(VERSION 3.24) -project(FormatOutput LANGUAGES CXX C) +project(BuildRequiresExample LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(fmt REQUIRED) +# Find the abseil package (provided by CMakeDeps generator) +find_package(absl REQUIRED) +# Create the executable add_executable(main main.cpp) -target_link_libraries(main PRIVATE fmt::fmt) \ No newline at end of file + +# Link against abseil libraries +target_link_libraries(main PRIVATE + absl::strings + absl::str_format + absl::base +) \ No newline at end of file diff --git a/examples/conan_cmake/build_requires/conanfile.py b/examples/conan_cmake/build_requires/conanfile.py new file mode 100644 index 00000000..06542029 --- /dev/null +++ b/examples/conan_cmake/build_requires/conanfile.py @@ -0,0 +1,54 @@ +"""Conan recipe demonstrating tool_requires with CMake and abseil dependency. + +This example shows how to use tool_requires to specify build-time dependencies +like CMake, while using regular requires for runtime dependencies like abseil. +""" + +from conan import ConanFile +from conan.tools.cmake import CMake, cmake_layout + + +class BuildRequiresExample(ConanFile): + """Example Conan recipe demonstrating tool_requires with CMake and abseil.""" + + name = 'build_requires_example' + version = '1.0' + + # Basic package configuration + settings = 'os', 'compiler', 'build_type', 'arch' + + # Regular dependencies - libraries we link against + requires = 'abseil/20240116.2' + + # Build dependencies - tools needed during build + tool_requires = 'cmake/[>=3.24]' + + # Generators for CMake integration + generators = 'CMakeToolchain', 'CMakeDeps' + + def layout(self): + """Define the layout for the project.""" + cmake_layout(self) + + def configure(self): + """Configure package options based on settings.""" + # Example: Configure abseil options if needed + # self.options["abseil"].shared = False + pass + + def build_requirements(self): + """Additional build requirements logic if needed.""" + # This method can be used for conditional build requirements + # For example, only require certain tools on specific platforms + pass + + def build(self): + """Build the project using CMake.""" + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + """Package the built artifacts.""" + cmake = CMake(self) + cmake.install() diff --git a/examples/conan_cmake/build_requires/main.cpp b/examples/conan_cmake/build_requires/main.cpp index 4de35678..3b7a8b26 100644 --- a/examples/conan_cmake/build_requires/main.cpp +++ b/examples/conan_cmake/build_requires/main.cpp @@ -1,7 +1,25 @@ -#include "fmt/color.h" +#include +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "absl/base/macros.h" int main() { - fmt::print(fg(fmt::terminal_color::cyan), "Hello fmt {}!\n", FMT_VERSION); + // Demonstrate abseil string formatting + absl::string_view greeting = "Hello"; + std::string name = "Abseil"; + + // Use absl::StrFormat instead of printf-style formatting + std::string formatted = absl::StrFormat("%s %s! Version: %s", + greeting, name, + ABSL_PACKAGE_VERSION); + + std::cout << formatted << std::endl; + + // Demonstrate some abseil string operations + std::string message = absl::StrFormat("This example shows tool_requires with CMake %s", + "working correctly!"); + std::cout << message << std::endl; + return 0; } \ No newline at end of file diff --git a/examples/conan_cmake/build_requires/pyproject.toml b/examples/conan_cmake/build_requires/pyproject.toml index 6803860d..f4c353c1 100644 --- a/examples/conan_cmake/build_requires/pyproject.toml +++ b/examples/conan_cmake/build_requires/pyproject.toml @@ -1,6 +1,6 @@ [project] -description = "A simple project showing how to use conan with CPPython" -name = "cppython-conan-cmake-simple" +description = "Example showing tool_requires with CMake and abseil dependency" +name = "cppython-conan-cmake-build-requires" version = "1.0.0" license = { text = "MIT" } @@ -18,7 +18,11 @@ provider-name = "conan" install-path = "install" -dependencies = ["fmt>=11.2.0"] +dependencies = ["abseil/20240116.2"] + +# Future tool_requires support (not yet implemented in CPPython): +# [tool.cppython.provider.tool_requires] +# cmake = ">=3.20" [tool.cppython.generator] diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index b3979632..41faf046 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -59,3 +59,43 @@ def test_simple(example_runner: CliRunner) -> None: # Publish the project to the local cache publish_project.publish() + + @staticmethod + def test_build_requires(example_runner: CliRunner) -> None: + """build_requires project""" + # Create project configuration + project_root = Path.cwd() + project_configuration = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True) + + # Create console interface + interface = ConsoleInterface() + + # Load pyproject.toml data + pyproject_path = project_root / 'pyproject.toml' + pyproject_data = loads(pyproject_path.read_text(encoding='utf-8')) + + # Create and use the project directly + project = Project(project_configuration, interface, pyproject_data) + + # Call install directly to get structured results + project.install() + + # 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}' + + path = Path('build').absolute() + + # Verify that the build directory contains the expected files + assert (path / 'CMakeCache.txt').exists(), f'{path / "CMakeCache.txt"} not found' + + # --- Setup for Publish with modified config --- + # Modify the in-memory representation of the pyproject data + pyproject_data['tool']['cppython']['provider']['remotes'] = [] + + # Create a new project instance with the modified configuration for the 'publish' step + publish_project = Project(project_configuration, interface, pyproject_data) + + # Publish the project to the local cache + publish_project.publish() From 50df16094fe333afe21c3232ab531eaece65515d Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 18 Jul 2025 16:28:07 -0400 Subject: [PATCH 06/20] Update Conan Version Schema --- cppython/plugins/conan/resolution.py | 102 ++++++- cppython/plugins/conan/schema.py | 279 +++++++++++++++++- .../conan_cmake/build_requires/pyproject.toml | 2 +- tests/unit/plugins/conan/test_resolution.py | 166 ++++++++++- 4 files changed, 514 insertions(+), 35 deletions(-) diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index b3e8c828..9efd5410 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -9,7 +9,13 @@ from cppython.core.exception import ConfigException from cppython.core.schema import CorePluginData -from cppython.plugins.conan.schema import ConanConfiguration, ConanData, ConanDependency +from cppython.plugins.conan.schema import ( + ConanConfiguration, + ConanData, + ConanDependency, + ConanVersion, + ConanVersionRange, +) from cppython.utility.exception import ProviderConfigurationError @@ -121,26 +127,90 @@ def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile: return host_profile, build_profile +def _handle_single_specifier(name: str, specifier) -> ConanDependency: + """Handle a single version specifier.""" + MINIMUM_VERSION_PARTS = 2 + + operator_handlers = { + '==': lambda v: ConanDependency(name=name, version=ConanVersion.from_string(v)), + '>=': lambda v: ConanDependency(name=name, version_range=ConanVersionRange(expression=f'>={v}')), + '>': lambda v: ConanDependency(name=name, version_range=ConanVersionRange(expression=f'>{v}')), + '<': lambda v: ConanDependency(name=name, version_range=ConanVersionRange(expression=f'<{v}')), + '<=': lambda v: ConanDependency(name=name, version_range=ConanVersionRange(expression=f'<={v}')), + '!=': lambda v: ConanDependency(name=name, version_range=ConanVersionRange(expression=f'!={v}')), + } + + if specifier.operator in operator_handlers: + return operator_handlers[specifier.operator](specifier.version) + elif specifier.operator == '~=': + # Compatible release - convert to Conan tilde syntax + version_parts = specifier.version.split('.') + if len(version_parts) >= MINIMUM_VERSION_PARTS: + conan_version = '.'.join(version_parts[:MINIMUM_VERSION_PARTS]) + return ConanDependency(name=name, version_range=ConanVersionRange(expression=f'~{conan_version}')) + else: + return ConanDependency(name=name, version_range=ConanVersionRange(expression=f'>={specifier.version}')) + else: + raise ConfigException( + f"Unsupported single specifier '{specifier.operator}'. Supported: '==', '>=', '>', '<', '<=', '!=', '~='", + [], + ) + + def resolve_conan_dependency(requirement: Requirement) -> ConanDependency: - """Resolves a Conan dependency from a requirement""" + """Resolves a Conan dependency from a Python requirement string. + + Converts Python packaging requirements to Conan version specifications: + - package>=1.0.0 -> package/[>=1.0.0] + - package==1.0.0 -> package/1.0.0 + - package~=1.2.0 -> package/[~1.2] + - package>=1.0,<2.0 -> package/[>=1.0 <2.0] + """ specifiers = requirement.specifier - # If the length of specifiers is greater than one, raise a configuration error - if len(specifiers) > 1: - raise ConfigException('Multiple specifiers are not supported. Please provide a single specifier.', []) + # Handle no version specifiers + if not specifiers: + return ConanDependency(name=requirement.name) - # Extract the version from the single specifier - min_version = None + # Handle single specifier (most common case) if len(specifiers) == 1: - specifier = next(iter(specifiers)) - if specifier.operator != '>=': - raise ConfigException(f"Unsupported specifier '{specifier.operator}'. Only '>=' is supported.", []) - min_version = specifier.version - - return ConanDependency( - name=requirement.name, - version_ge=min_version, - ) + return _handle_single_specifier(requirement.name, next(iter(specifiers))) + + # Handle multiple specifiers - convert to Conan range syntax + range_parts = [] + + # Define order for operators to ensure consistent output + operator_order = ['>=', '>', '<=', '<', '!='] + + # Group specifiers by operator to ensure consistent ordering + specifier_groups = {op: [] for op in operator_order} + + for specifier in specifiers: + if specifier.operator in ('>=', '>', '<', '<=', '!='): + specifier_groups[specifier.operator].append(specifier.version) + elif specifier.operator == '==': + # Multiple == operators would be contradictory + raise ConfigException( + "Multiple '==' specifiers are contradictory. Use a single '==' or range operators.", [] + ) + elif specifier.operator == '~=': + # ~= with other operators is complex, for now treat as >= + specifier_groups['>='].append(specifier.version) + else: + raise ConfigException( + f"Unsupported specifier '{specifier.operator}' in multi-specifier requirement. " + f"Supported: '>=', '>', '<', '<=', '!='", + [], + ) + + # Build range parts in consistent order + for operator in operator_order: + for version in specifier_groups[operator]: + range_parts.append(f'{operator}{version}') + + # Join range parts with spaces (Conan AND syntax) + version_range = ' '.join(range_parts) + return ConanDependency(name=requirement.name, version_range=ConanVersionRange(expression=version_range)) def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> ConanData: diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py index 42fd0e42..08129c05 100644 --- a/cppython/plugins/conan/schema.py +++ b/cppython/plugins/conan/schema.py @@ -5,27 +5,288 @@ provide structured configuration and data needed by the Conan Provider. """ +import re from typing import Annotated from conan.internal.model.profile import Profile -from pydantic import Field +from pydantic import Field, field_validator from cppython.core.schema import CPPythonModel +class ConanVersion(CPPythonModel): + """Represents a single Conan version with optional pre-release suffix.""" + + major: int + minor: int + patch: int | None = None + prerelease: str | None = None + + @field_validator('major', 'minor', mode='before') # type: ignore + @classmethod + def validate_version_parts(cls, v: int) -> int: + """Validate version parts are non-negative integers.""" + if v < 0: + raise ValueError('Version parts must be non-negative') + return v + + @field_validator('patch', mode='before') # type: ignore + @classmethod + def validate_patch(cls, v: int | None) -> int | None: + """Validate patch is non-negative integer or None.""" + if v is not None and v < 0: + raise ValueError('Version parts must be non-negative') + return v + + @field_validator('prerelease', mode='before') # type: ignore + @classmethod + def validate_prerelease(cls, v: str | None) -> str | None: + """Validate prerelease is not an empty string.""" + if v is not None and not v.strip(): + raise ValueError('Pre-release cannot be empty string') + return v + + def __str__(self) -> str: + """String representation of the version.""" + version = f'{self.major}.{self.minor}.{self.patch}' if self.patch is not None else f'{self.major}.{self.minor}' + + if self.prerelease: + version += f'-{self.prerelease}' + return version + + @classmethod + def from_string(cls, version_str: str) -> 'ConanVersion': + """Parse a version string into a ConanVersion.""" + if '-' in version_str: + version_part, prerelease = version_str.split('-', 1) + else: + version_part = version_str + prerelease = None + + parts = version_part.split('.') + + # Parse parts based on what's actually provided + MAJOR_INDEX = 0 + MINOR_INDEX = 1 + PATCH_INDEX = 2 + + major = int(parts[MAJOR_INDEX]) + minor = int(parts[MINOR_INDEX]) if len(parts) > MINOR_INDEX else 0 + patch = int(parts[PATCH_INDEX]) if len(parts) > PATCH_INDEX else None + + return cls( + major=major, + minor=minor, + patch=patch, + prerelease=prerelease, + ) + + +class ConanVersionRange(CPPythonModel): + """Represents a Conan version range expression like '>=1.0 <2.0' or complex expressions.""" + + expression: str + + @field_validator('expression') # type: ignore + @classmethod + def validate_expression(cls, v: str) -> str: + """Validate the version range expression contains valid operators.""" + if not v.strip(): + raise ValueError('Version range expression cannot be empty') + + # Basic validation - ensure it contains valid operators + valid_operators = {'>=', '>', '<=', '<', '!=', '~', '||', '&&'} + + # Split by spaces and logical operators to get individual components + tokens = re.split(r'(\|\||&&|\s+)', v) + + for token in tokens: + current_token = token.strip() + if not current_token or current_token in {'||', '&&'}: + continue + + # Check if token starts with a valid operator + has_valid_operator = any(current_token.startswith(op) for op in valid_operators) + if not has_valid_operator: + raise ValueError(f'Invalid operator in version range: {current_token}') + + return v + + def __str__(self) -> str: + """Return the version range expression.""" + return self.expression + + +class ConanUserChannel(CPPythonModel): + """Represents a Conan user/channel pair.""" + + user: str + channel: str | None = None + + @field_validator('user') # type: ignore + @classmethod + def validate_user(cls, v: str) -> str: + """Validate user is not empty.""" + if not v.strip(): + raise ValueError('User cannot be empty') + return v.strip() + + @field_validator('channel') # type: ignore + @classmethod + def validate_channel(cls, v: str | None) -> str | None: + """Validate channel is not an empty string.""" + if v is not None and not v.strip(): + raise ValueError('Channel cannot be empty string') + return v.strip() if v else None + + def __str__(self) -> str: + """String representation for use in requires().""" + if self.channel: + return f'{self.user}/{self.channel}' + return f'{self.user}/_' + + +class ConanRevision(CPPythonModel): + """Represents a Conan revision identifier.""" + + revision: str + + @field_validator('revision') # type: ignore + @classmethod + def validate_revision(cls, v: str) -> str: + """Validate revision is not empty.""" + if not v.strip(): + raise ValueError('Revision cannot be empty') + return v.strip() + + def __str__(self) -> str: + """Return the revision identifier.""" + return self.revision + + class ConanDependency(CPPythonModel): - """Dependency information""" + """Dependency information following Conan's full version specification. + + Supports: + - Exact versions: package/1.0.0 + - Pre-release versions: package/1.0.0-alpha1 + - Version ranges: package/[>1.0 <2.0] + - Revisions: package/1.0.0#revision + - User/channel: package/1.0.0@user/channel + - Complex expressions: package/[>=1.0 <2.0 || >=3.0] + - Pre-release handling: resolve_prereleases setting + """ name: str - version_ge: str | None = None - include_prerelease: bool | None = None + version: ConanVersion | None = None + version_range: ConanVersionRange | None = None + user_channel: ConanUserChannel | None = None + revision: ConanRevision | None = None + + # Pre-release handling + resolve_prereleases: bool | None = None def requires(self) -> str: - """Generate the requires attribute for Conan""" - # TODO: Implement lower and upper bounds per conan documentation - if self.version_ge: - return f'{self.name}/[>={self.version_ge}]' - return self.name + """Generate the requires attribute for Conan following the full specification. + + Examples: + - package -> package + - package/1.0.0 -> package/1.0.0 + - package/1.0.0-alpha1 -> package/1.0.0-alpha1 + - package/[>=1.0 <2.0] -> package/[>=1.0 <2.0] + - package/1.0.0@user/channel -> package/1.0.0@user/channel + - package/1.0.0#revision -> package/1.0.0#revision + - package/1.0.0@user/channel#revision -> package/1.0.0@user/channel#revision + """ + result = self.name + + # Add version or version range + if self.version_range: + # Complex version range + result += f'/[{self.version_range}]' + elif self.version: + # Simple version (can include pre-release suffixes) + result += f'/{self.version}' + + # Add user/channel + if self.user_channel: + result += f'@{self.user_channel}' + + # Add revision + if self.revision: + result += f'#{self.revision}' + + return result + + @classmethod + def from_conan_reference(cls, reference: str) -> 'ConanDependency': + """Parse a Conan reference string into a ConanDependency. + + Examples: + - package -> ConanDependency(name='package') + - package/1.0.0 -> ConanDependency(name='package', version=ConanVersion.from_string('1.0.0')) + - package/[>=1.0 <2.0] -> ConanDependency(name='package', version_range=ConanVersionRange('>=1.0 <2.0')) + - package/1.0.0@user/channel -> ConanDependency(name='package', version=..., user_channel=ConanUserChannel(...)) + - package/1.0.0#revision -> ConanDependency(name='package', version=..., revision=ConanRevision('revision')) + """ + # Split revision first (everything after #) + revision_obj = None + if '#' in reference: + reference, revision_str = reference.rsplit('#', 1) + revision_obj = ConanRevision(revision=revision_str) + + # Split user/channel (everything after @) + user_channel_obj = None + if '@' in reference: + reference, user_channel_str = reference.rsplit('@', 1) + if '/' in user_channel_str: + user, channel = user_channel_str.split('/', 1) + if channel == '_': + channel = None + else: + user = user_channel_str + channel = None + user_channel_obj = ConanUserChannel(user=user, channel=channel) + + # Split name and version + name = reference + version_obj = None + version_range_obj = None + + if '/' in reference: + name, version_part = reference.split('/', 1) + + # Check if it's a version range (enclosed in brackets) + if version_part.startswith('[') and version_part.endswith(']'): + version_range_obj = ConanVersionRange(expression=version_part[1:-1]) # Remove brackets + else: + version_obj = ConanVersion.from_string(version_part) + + return cls( + name=name, + version=version_obj, + version_range=version_range_obj, + user_channel=user_channel_obj, + revision=revision_obj, + ) + + def is_prerelease(self) -> bool: + """Check if this dependency specifies a pre-release version. + + Pre-release versions contain hyphens followed by pre-release identifiers + like: 1.0.0-alpha1, 1.0.0-beta2, 1.0.0-rc1, 1.0.0-dev, etc. + """ + # Check version object for pre-release + if self.version and self.version.prerelease: + prerelease_keywords = {'alpha', 'beta', 'rc', 'dev', 'pre', 'snapshot'} + return any(keyword in self.version.prerelease.lower() for keyword in prerelease_keywords) + + # Also check version_range for pre-release patterns + if self.version_range and '-' in self.version_range.expression: + prerelease_keywords = {'alpha', 'beta', 'rc', 'dev', 'pre', 'snapshot'} + return any(keyword in self.version_range.expression.lower() for keyword in prerelease_keywords) + + return False class ConanData(CPPythonModel): diff --git a/examples/conan_cmake/build_requires/pyproject.toml b/examples/conan_cmake/build_requires/pyproject.toml index f4c353c1..812d2bfd 100644 --- a/examples/conan_cmake/build_requires/pyproject.toml +++ b/examples/conan_cmake/build_requires/pyproject.toml @@ -18,7 +18,7 @@ provider-name = "conan" install-path = "install" -dependencies = ["abseil/20240116.2"] +dependencies = ["abseil==20240116.2"] # Future tool_requires support (not yet implemented in CPPython): # [tool.cppython.provider.tool_requires] diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py index ae070b47..4385d0a5 100644 --- a/tests/unit/plugins/conan/test_resolution.py +++ b/tests/unit/plugins/conan/test_resolution.py @@ -15,7 +15,14 @@ resolve_conan_data, resolve_conan_dependency, ) -from cppython.plugins.conan.schema import ConanData +from cppython.plugins.conan.schema import ( + ConanData, + ConanDependency, + ConanRevision, + ConanUserChannel, + ConanVersion, + ConanVersionRange, +) from cppython.utility.exception import ProviderConfigurationError # Constants for test validation @@ -26,13 +33,26 @@ class TestResolveDependency: """Test dependency resolution.""" def test_resolve_dependency_with_version(self) -> None: - """Test resolving a dependency with a version specifier.""" + """Test resolving a dependency with a >= version specifier.""" requirement = Requirement('boost>=1.80.0') result = resolve_conan_dependency(requirement) assert result.name == 'boost' - assert result.version_ge == '1.80.0' + assert result.version_range is not None + assert result.version_range.expression == '>=1.80.0' + assert result.version is None + + def test_resolve_dependency_with_exact_version(self) -> None: + """Test resolving a dependency with an exact version specifier.""" + requirement = Requirement('abseil==20240116.2') + + result = resolve_conan_dependency(requirement) + + assert result.name == 'abseil' + assert result.version is not None + assert str(result.version) == '20240116.2' + assert result.version_range is None def test_resolve_dependency_without_version(self) -> None: """Test resolving a dependency without a version specifier.""" @@ -41,22 +61,150 @@ def test_resolve_dependency_without_version(self) -> None: result = resolve_conan_dependency(requirement) assert result.name == 'boost' - assert result.version_ge is None + assert result.version is None + assert result.version_range is None + + def test_resolve_dependency_compatible_release(self) -> None: + """Test resolving a dependency with ~= (compatible release) operator.""" + requirement = Requirement('package~=1.2.3') + + result = resolve_conan_dependency(requirement) + + assert result.name == 'package' + assert result.version_range is not None + assert result.version_range.expression == '~1.2' + assert result.version is None def test_resolve_dependency_multiple_specifiers(self) -> None: - """Test that multiple specifiers raise an error.""" + """Test resolving a dependency with multiple specifiers.""" requirement = Requirement('boost>=1.80.0,<2.0.0') - with pytest.raises(ConfigException, match='Multiple specifiers are not supported'): - resolve_conan_dependency(requirement) + result = resolve_conan_dependency(requirement) + + assert result.name == 'boost' + assert result.version_range is not None + assert result.version_range.expression == '>=1.80.0 <2.0.0' + assert result.version is None def test_resolve_dependency_unsupported_operator(self) -> None: """Test that unsupported operators raise an error.""" - requirement = Requirement('boost==1.80.0') + requirement = Requirement('boost===1.80.0') - with pytest.raises(ConfigException, match="Unsupported specifier '=='"): + with pytest.raises(ConfigException, match="Unsupported single specifier '==='"): resolve_conan_dependency(requirement) + def test_resolve_dependency_contradictory_exact_versions(self) -> None: + """Test that multiple specifiers work correctly for valid ranges.""" + # Test our logic with a valid range instead of invalid syntax + requirement = Requirement('package>=1.0,<=2.0') # Valid range + result = resolve_conan_dependency(requirement) + + assert result.name == 'package' + assert result.version_range is not None + assert result.version_range.expression == '>=1.0 <=2.0' + + def test_conan_dependency_requires_exact_version(self) -> None: + """Test that ConanDependency generates correct requires for exact versions.""" + dependency = ConanDependency(name='abseil', version=ConanVersion.from_string('20240116.2')) + + assert dependency.requires() == 'abseil/20240116.2' + + def test_conan_dependency_requires_version_range(self) -> None: + """Test that ConanDependency generates correct requires for version ranges.""" + dependency = ConanDependency(name='boost', version_range=ConanVersionRange(expression='>=1.80.0 <2.0')) + + assert dependency.requires() == 'boost/[>=1.80.0 <2.0]' + + def test_conan_dependency_requires_legacy_minimum_version(self) -> None: + """Test that ConanDependency generates correct requires for legacy minimum versions.""" + dependency = ConanDependency(name='boost', version_range=ConanVersionRange(expression='>=1.80.0')) + + assert dependency.requires() == 'boost/[>=1.80.0]' + + def test_conan_dependency_requires_legacy_exact_version(self) -> None: + """Test that ConanDependency generates correct requires for legacy exact versions.""" + dependency = ConanDependency(name='abseil', version=ConanVersion.from_string('20240116.2')) + + assert dependency.requires() == 'abseil/20240116.2' + + def test_conan_dependency_requires_no_version(self) -> None: + """Test that ConanDependency generates correct requires for dependencies without version.""" + dependency = ConanDependency(name='somelib') + + assert dependency.requires() == 'somelib' + + def test_conan_dependency_with_user_channel(self) -> None: + """Test that ConanDependency handles user/channel correctly.""" + dependency = ConanDependency( + name='mylib', + version=ConanVersion.from_string('1.0.0'), + user_channel=ConanUserChannel(user='myuser', channel='stable'), + ) + + assert dependency.requires() == 'mylib/1.0.0@myuser/stable' + + def test_conan_dependency_with_revision(self) -> None: + """Test that ConanDependency handles revisions correctly.""" + dependency = ConanDependency( + name='mylib', version=ConanVersion.from_string('1.0.0'), revision=ConanRevision(revision='abc123') + ) + + assert dependency.requires() == 'mylib/1.0.0#abc123' + + def test_conan_dependency_full_reference(self) -> None: + """Test that ConanDependency handles full references correctly.""" + dependency = ConanDependency( + name='mylib', + version=ConanVersion.from_string('1.0.0'), + user_channel=ConanUserChannel(user='myuser', channel='stable'), + revision=ConanRevision(revision='abc123'), + ) + + assert dependency.requires() == 'mylib/1.0.0@myuser/stable#abc123' + + def test_from_conan_reference_simple(self) -> None: + """Test parsing a simple package name.""" + dependency = ConanDependency.from_conan_reference('mylib') + + assert dependency.name == 'mylib' + assert dependency.version is None + assert dependency.user_channel is None + assert dependency.revision is None + + def test_from_conan_reference_with_version(self) -> None: + """Test parsing a package with version.""" + dependency = ConanDependency.from_conan_reference('mylib/1.0.0') + + assert dependency.name == 'mylib' + assert dependency.version is not None + assert str(dependency.version) == '1.0.0' + assert dependency.user_channel is None + assert dependency.revision is None + + def test_from_conan_reference_with_version_range(self) -> None: + """Test parsing a package with version range.""" + dependency = ConanDependency.from_conan_reference('mylib/[>=1.0 <2.0]') + + assert dependency.name == 'mylib' + assert dependency.version is None + assert dependency.version_range is not None + assert dependency.version_range.expression == '>=1.0 <2.0' + assert dependency.user_channel is None + assert dependency.revision is None + + def test_from_conan_reference_full(self) -> None: + """Test parsing a full Conan reference.""" + dependency = ConanDependency.from_conan_reference('mylib/1.0.0@myuser/stable#abc123') + + assert dependency.name == 'mylib' + assert dependency.version is not None + assert str(dependency.version) == '1.0.0' + assert dependency.user_channel is not None + assert dependency.user_channel.user == 'myuser' + assert dependency.user_channel.channel == 'stable' + assert dependency.revision is not None + assert dependency.revision.revision == 'abc123' + class TestProfileProcessing: """Test profile processing functionality.""" From 9bef1789f0b8076a8c70d895687eadda309d6471 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 18 Jul 2025 19:13:17 -0400 Subject: [PATCH 07/20] Delete conanfile.py --- .../conan_cmake/build_requires/conanfile.py | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 examples/conan_cmake/build_requires/conanfile.py diff --git a/examples/conan_cmake/build_requires/conanfile.py b/examples/conan_cmake/build_requires/conanfile.py deleted file mode 100644 index 06542029..00000000 --- a/examples/conan_cmake/build_requires/conanfile.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Conan recipe demonstrating tool_requires with CMake and abseil dependency. - -This example shows how to use tool_requires to specify build-time dependencies -like CMake, while using regular requires for runtime dependencies like abseil. -""" - -from conan import ConanFile -from conan.tools.cmake import CMake, cmake_layout - - -class BuildRequiresExample(ConanFile): - """Example Conan recipe demonstrating tool_requires with CMake and abseil.""" - - name = 'build_requires_example' - version = '1.0' - - # Basic package configuration - settings = 'os', 'compiler', 'build_type', 'arch' - - # Regular dependencies - libraries we link against - requires = 'abseil/20240116.2' - - # Build dependencies - tools needed during build - tool_requires = 'cmake/[>=3.24]' - - # Generators for CMake integration - generators = 'CMakeToolchain', 'CMakeDeps' - - def layout(self): - """Define the layout for the project.""" - cmake_layout(self) - - def configure(self): - """Configure package options based on settings.""" - # Example: Configure abseil options if needed - # self.options["abseil"].shared = False - pass - - def build_requirements(self): - """Additional build requirements logic if needed.""" - # This method can be used for conditional build requirements - # For example, only require certain tools on specific platforms - pass - - def build(self): - """Build the project using CMake.""" - cmake = CMake(self) - cmake.configure() - cmake.build() - - def package(self): - """Package the built artifacts.""" - cmake = CMake(self) - cmake.install() From 210a422c309bc68cd2057877b1d7b98f16e1599b Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 18 Jul 2025 19:34:43 -0400 Subject: [PATCH 08/20] Remove Unused Import in Template --- cppython/plugins/conan/builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 957d1f17..2be317d6 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -125,7 +125,7 @@ def _create_conanfile(conan_file: Path, dependencies: list[ConanDependency]) -> """Creates a conanfile.py file with the necessary content.""" template_string = """ from conan import ConanFile - from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout + from conan.tools.cmake import CMake, cmake_layout class MyProject(ConanFile): name = "myproject" From f9b4a252503e4362f6ed022f08e07c2675a4d7a4 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 18 Jul 2025 22:39:17 -0400 Subject: [PATCH 09/20] Update Profile Again --- cppython/plugins/conan/resolution.py | 19 +++++++++++- tests/unit/plugins/conan/test_resolution.py | 33 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index 9efd5410..ee8f4735 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -29,6 +29,9 @@ def _profile_post_process(profiles: list[Profile], conan_api: ConanAPI, cache_se """ logger = logging.getLogger('cppython.conan') + # Get global configuration + global_conf = conan_api.config.global_conf + # Apply profile plugin processing try: profile_plugin = conan_api.profiles._load_profile_plugin() @@ -41,13 +44,27 @@ def _profile_post_process(profiles: list[Profile], conan_api: ConanAPI, cache_se except (AttributeError, Exception): logger.debug('Profile plugin not available or failed to load') - # Process settings to initialize processed_settings + # Apply the full profile processing pipeline for each profile for profile in profiles: + # Process settings to initialize processed_settings try: profile.process_settings(cache_settings) except (AttributeError, Exception) as settings_error: logger.debug('Settings processing failed for profile: %s', str(settings_error)) + # Validate configuration + try: + profile.conf.validate() + except (AttributeError, Exception) as conf_error: + logger.debug('Configuration validation failed for profile: %s', str(conf_error)) + + # Apply global configuration to the profile + try: + if global_conf is not None: + profile.conf.rebase_conf_definition(global_conf) + except (AttributeError, Exception) as rebase_error: + logger.debug('Configuration rebase failed for profile: %s', str(rebase_error)) + def _resolve_profiles( host_profile_name: str | None, build_profile_name: str | None, conan_api: ConanAPI diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py index 4385d0a5..ab888e42 100644 --- a/tests/unit/plugins/conan/test_resolution.py +++ b/tests/unit/plugins/conan/test_resolution.py @@ -444,3 +444,36 @@ def test_resolve_conan_data_null_profiles(self, mock_resolve_profiles: Mock, moc # Verify profile resolution was called with None values mock_resolve_profiles.assert_called_once_with(None, None, mock_conan_api) + + def test_auto_detected_profile_gets_post_processed(self, conan_mock_api: Mock): + """Test that auto-detected profiles get proper post-processing. + + Args: + conan_mock_api: Mock ConanAPI instance from fixture + """ + # Configure the mock to simulate no default profiles + conan_mock_api.profiles.get_default_host.side_effect = Exception('No default profile') + conan_mock_api.profiles.get_default_build.side_effect = Exception('No default profile') + + # Create a profile that simulates auto-detection + mock_profile = Mock() + mock_profile.settings = {'os': 'Windows', 'arch': 'x86_64'} + mock_profile.process_settings = Mock() + mock_profile.conf = Mock() + mock_profile.conf.validate = Mock() + mock_profile.conf.rebase_conf_definition = Mock() + + conan_mock_api.profiles.detect.return_value = mock_profile + conan_mock_api.config.global_conf = Mock() + + # Call the resolution - this should trigger auto-detection and post-processing + host_profile, build_profile = _resolve_profiles(None, None, conan_mock_api) + + # Verify that process_settings was called on both profiles + assert mock_profile.process_settings.call_count == EXPECTED_PROFILE_CALL_COUNT + + # Verify that conf validation was called on both profiles + assert mock_profile.conf.validate.call_count == EXPECTED_PROFILE_CALL_COUNT + + # Verify that conf rebase was called on both profiles + assert mock_profile.conf.rebase_conf_definition.call_count == EXPECTED_PROFILE_CALL_COUNT From 476fb9ca23268963455352171e53be9b11709f15 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Fri, 18 Jul 2025 23:09:43 -0400 Subject: [PATCH 10/20] Type Fix --- tests/fixtures/conan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py index b658e4ac..8fd06299 100644 --- a/tests/fixtures/conan.py +++ b/tests/fixtures/conan.py @@ -171,7 +171,7 @@ def fixture_conan_setup_mocks( # Mock resolve_conan_dependency def mock_resolve(requirement: Requirement) -> ConanDependency: - return ConanDependency(name=requirement.name, version_ge=None) + return ConanDependency(name=requirement.name) mock_resolve_conan_dependency = mocker.patch( 'cppython.plugins.conan.plugin.resolve_conan_dependency', side_effect=mock_resolve From 316949f0967148c661626ff795f5f6046c6e933e Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 00:36:00 -0400 Subject: [PATCH 11/20] Remove Example --- .../conan_cmake/build_requires/CMakeLists.txt | 19 - examples/conan_cmake/build_requires/README.md | 0 examples/conan_cmake/build_requires/main.cpp | 25 - examples/conan_cmake/build_requires/pdm.lock | 537 ------------------ .../conan_cmake/build_requires/pyproject.toml | 32 -- .../integration/examples/test_conan_cmake.py | 42 +- 6 files changed, 1 insertion(+), 654 deletions(-) delete mode 100644 examples/conan_cmake/build_requires/CMakeLists.txt delete mode 100644 examples/conan_cmake/build_requires/README.md delete mode 100644 examples/conan_cmake/build_requires/main.cpp delete mode 100644 examples/conan_cmake/build_requires/pdm.lock delete mode 100644 examples/conan_cmake/build_requires/pyproject.toml diff --git a/examples/conan_cmake/build_requires/CMakeLists.txt b/examples/conan_cmake/build_requires/CMakeLists.txt deleted file mode 100644 index 4ccf53ee..00000000 --- a/examples/conan_cmake/build_requires/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -cmake_minimum_required(VERSION 3.24) - -project(BuildRequiresExample LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Find the abseil package (provided by CMakeDeps generator) -find_package(absl REQUIRED) - -# Create the executable -add_executable(main main.cpp) - -# Link against abseil libraries -target_link_libraries(main PRIVATE - absl::strings - absl::str_format - absl::base -) \ No newline at end of file diff --git a/examples/conan_cmake/build_requires/README.md b/examples/conan_cmake/build_requires/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/conan_cmake/build_requires/main.cpp b/examples/conan_cmake/build_requires/main.cpp deleted file mode 100644 index 3b7a8b26..00000000 --- a/examples/conan_cmake/build_requires/main.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include -#include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" -#include "absl/base/macros.h" - -int main() -{ - // Demonstrate abseil string formatting - absl::string_view greeting = "Hello"; - std::string name = "Abseil"; - - // Use absl::StrFormat instead of printf-style formatting - std::string formatted = absl::StrFormat("%s %s! Version: %s", - greeting, name, - ABSL_PACKAGE_VERSION); - - std::cout << formatted << std::endl; - - // Demonstrate some abseil string operations - std::string message = absl::StrFormat("This example shows tool_requires with CMake %s", - "working correctly!"); - std::cout << message << std::endl; - - return 0; -} \ No newline at end of file diff --git a/examples/conan_cmake/build_requires/pdm.lock b/examples/conan_cmake/build_requires/pdm.lock deleted file mode 100644 index 1eb1bc96..00000000 --- a/examples/conan_cmake/build_requires/pdm.lock +++ /dev/null @@ -1,537 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default"] -strategy = [] -lock_version = "4.5.0" -content_hash = "sha256:618cf02c62d23783da0e2d36b9ea92bec82152cbc38a811e71f5dcf02c5eeffc" - -[[metadata.targets]] -requires_python = ">=3.13" - -[[package]] -name = "annotated-types" -version = "0.7.0" -summary = "" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "certifi" -version = "2025.7.9" -summary = "" -files = [ - {file = "certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39"}, - {file = "certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -summary = "" -files = [ - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - -[[package]] -name = "click" -version = "8.2.1" -summary = "" -dependencies = [ - "colorama; sys_platform == \"win32\"", -] -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[[package]] -name = "cmake" -version = "4.0.3" -summary = "" -files = [ - {file = "cmake-4.0.3-py3-none-macosx_10_10_universal2.whl", hash = "sha256:f2adfb459747025f40f9d3bdd1f3a485d43e866c0c4eb66373d1fcd666b13e4a"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04c40c92fdcaa96c66a5731b5b3fbbdf87da99cc68fdd30ff30b90c34d222986"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d41b83d061bcc375a7a5f2942ba523a7563368d296d91260f9d8a53a10f5e5e5"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:434f84fdf1e21578974876b8414dc47afeaea62027d9adc37a943a6bb08eb053"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beec48371a4b906fe398758ded5df57fc16e9bb14fd34244d9d66ee35862fb9f"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47dc28bee6cfb4de00c7cf7e87d565b5c86eb4088da81b60a49e214fcdd4ffda"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10fdc972b3211915b65cc89e8cd24e1a26c9bd684ee71c3f369fb488f2c4388"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d840e780c48c5df1330879d50615176896e8e6eee554507d21ce8e2f1a5f0ff8"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:6ef63bbabcbe3b89c1d80547913b6caceaad57987a27e7afc79ebc88ecd829e4"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:67103f2bcce8f57b8705ba8e353f18fdc3684a346eee97dc5f94d11575a424c6"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:880a1e1ae26d440d7e4f604fecbf839728ca7b096c870f2e7359855cc4828532"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:c403b660bbff1fd4d7f1c5d9e015ea27566e49ca9461e260c9758f2fd4e5e813"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:2a66ecdd4c3238484cb0c377d689c086a9b8b533e25329f73d21bd1c38f1ae86"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:004e58b1a1a384c2ca799c9c41ac4ed86ac3b80129462992c43c1121f8729ffd"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:133dbc33f995cb97a4456d83d67fa0a7a798f53f979454359140588baa928f43"}, - {file = "cmake-4.0.3-py3-none-win32.whl", hash = "sha256:3e07bdd14e69ea67d1e67a4f5225ac2fd91ee9e349c440143cdddd7368be1f46"}, - {file = "cmake-4.0.3-py3-none-win_amd64.whl", hash = "sha256:9a349ff2b4a7c63c896061676bc0f4e6994f373d54314d79ba3608ee7fa75442"}, - {file = "cmake-4.0.3-py3-none-win_arm64.whl", hash = "sha256:94a52e67b264a51089907c9e74ca5a9e2f3e65c57c457e0f40f02629a0de74d8"}, - {file = "cmake-4.0.3.tar.gz", hash = "sha256:215732f09ea8a7088fe1ab46bbd61669437217278d709fd3851bf8211e8c59e3"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -summary = "" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "conan" -version = "2.18.1" -summary = "" -dependencies = [ - "colorama", - "distro; platform_system == \"FreeBSD\" or sys_platform == \"linux\"", - "fasteners", - "jinja2", - "patch-ng", - "python-dateutil", - "pyyaml", - "requests", - "urllib3", -] -files = [ - {file = "conan-2.18.1.tar.gz", hash = "sha256:5d8e9fac7614de9297933f65de8f17db14851a871cebc962f4856b7c294f43c5"}, -] - -[[package]] -name = "cppython" -version = "0.9.2" -summary = "" -dependencies = [ - "packaging", - "pydantic", - "requests", - "typer", - "types-requests", -] -files = [ - {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, - {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, -] - -[[package]] -name = "cppython" -version = "0.9.2" -extras = ["cmake"] -summary = "" -dependencies = [ - "cmake", - "cppython==0.9.2", -] -files = [ - {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, - {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, -] - -[[package]] -name = "cppython" -version = "0.9.2" -extras = ["conan"] -summary = "" -dependencies = [ - "conan", - "cppython==0.9.2", - "libcst", -] -files = [ - {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, - {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, -] - -[[package]] -name = "cppython" -version = "0.9.2" -extras = ["git"] -summary = "" -dependencies = [ - "cppython==0.9.2", - "dulwich", -] -files = [ - {file = "cppython-0.9.2-py3-none-any.whl", hash = "sha256:b43997b0d7237e4098501bf73f5d25576f5ef08ec7ec9fe4a111be5654cf4dca"}, - {file = "cppython-0.9.2.tar.gz", hash = "sha256:d8468a612f663d47074059811a4a53c6489d4e7a2d20ad15d811b4133377f072"}, -] - -[[package]] -name = "distro" -version = "1.8.0" -summary = "" -files = [ - {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, - {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, -] - -[[package]] -name = "dulwich" -version = "0.23.2" -summary = "" -dependencies = [ - "urllib3", -] -files = [ - {file = "dulwich-0.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e44dec7e36bc035da0ec3df6c1564810699e319ba41b71d17750dd7452e1b2fc"}, - {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:398ba1c0e1581071cdcb38a681e0ff1e046aa8f31bad3bc368266f499c4ddf9e"}, - {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:432c6eeac5edf97ff7090fbac7cda708167ee90e5afa78652d252e87e397f425"}, - {file = "dulwich-0.23.2-cp313-cp313-win32.whl", hash = "sha256:8555980e8509d7f76e80de58d1eb7bd2c1c317942b7a3c9c113d81dfc287f4c0"}, - {file = "dulwich-0.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:2b042dca31de4d4a0e88e4dbe20afe804a640c8882eec0de5093bffb34b75370"}, - {file = "dulwich-0.23.2-py3-none-any.whl", hash = "sha256:0b0439d309cf808f7955f74776981d9ac9dc1ec715aa39798de9b22bb95ac163"}, - {file = "dulwich-0.23.2.tar.gz", hash = "sha256:a152ebb0e95bc0f23768be563f80ff1e719bf5c4f5c2696be4fa8ab625a39879"}, -] - -[[package]] -name = "fasteners" -version = "0.19" -summary = "" -files = [ - {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, - {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, -] - -[[package]] -name = "idna" -version = "3.10" -summary = "" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -summary = "" -dependencies = [ - "markupsafe", -] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[[package]] -name = "libcst" -version = "1.8.2" -summary = "" -dependencies = [ - "pyyaml-ft", -] -files = [ - {file = "libcst-1.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08e9dca4ab6f8551794ce7ec146f86def6a82da41750cbed2c07551345fa10d3"}, - {file = "libcst-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8310521f2ccb79b5c4345750d475b88afa37bad930ab5554735f85ad5e3add30"}, - {file = "libcst-1.8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:da2d8b008aff72acd5a4a588491abdda1b446f17508e700f26df9be80d8442ae"}, - {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:be821d874ce8b26cbadd7277fa251a9b37f6d2326f8b5682b6fc8966b50a3a59"}, - {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f74b0bc7378ad5afcf25ac9d0367b4dbba50f6f6468faa41f5dfddcf8bf9c0f8"}, - {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b68ea4a6018abfea1f68d50f74de7d399172684c264eb09809023e2c8696fc23"}, - {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e264307ec49b2c72480422abafe80457f90b4e6e693b7ddf8a23d24b5c24001"}, - {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5d5519962ce7c72d81888fb0c09e58e308ba4c376e76bcd853b48151063d6a8"}, - {file = "libcst-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:b62aa11d6b74ed5545e58ac613d3f63095e5fd0254b3e0d1168fda991b9a6b41"}, - {file = "libcst-1.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9c2bd4ac288a9cdb7ffc3229a9ce8027a66a3fd3f2ab9e13da60f5fbfe91f3b2"}, - {file = "libcst-1.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:08a8c7d9922ca6eed24e2c13a3c552b3c186af8fc78e5d4820b58487d780ec19"}, - {file = "libcst-1.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bba7c2b5063e8ada5a5477f9fa0c01710645426b5a8628ec50d558542a0a292e"}, - {file = "libcst-1.8.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d97c9fe13aacfbefded6861f5200dcb8e837da7391a9bdeb44ccb133705990af"}, - {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d2194ae959630aae4176a4b75bd320b3274c20bef2a5ca6b8d6fc96d3c608edf"}, - {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0be639f5b2e1999a4b4a82a0f4633969f97336f052d0c131627983589af52f56"}, - {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6753e50904e05c27915933da41518ecd7a8ca4dd3602112ba44920c6e353a455"}, - {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:706d07106af91c343150be86caeae1ea3851b74aa0730fcbbf8cd089e817f818"}, - {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd4310ea8ddc49cc8872e083737cf806299b17f93159a1f354d59aa08993e876"}, - {file = "libcst-1.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:51bbafdd847529e8a16d1965814ed17831af61452ee31943c414cb23451de926"}, - {file = "libcst-1.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:4f14f5045766646ed9e8826b959c6d07194788babed1e0ba08c94ea4f39517e3"}, - {file = "libcst-1.8.2.tar.gz", hash = "sha256:66e82cedba95a6176194a817be4232c720312f8be6d2c8f3847f3317d95a0c7f"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -summary = "" -dependencies = [ - "mdurl", -] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -summary = "" -files = [ - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -summary = "" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "packaging" -version = "25.0" -summary = "" -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "patch-ng" -version = "1.18.1" -summary = "" -files = [ - {file = "patch-ng-1.18.1.tar.gz", hash = "sha256:52fd46ee46f6c8667692682c1fd7134edc65a2d2d084ebec1d295a6087fc0291"}, -] - -[[package]] -name = "pydantic" -version = "2.11.7" -summary = "" -dependencies = [ - "annotated-types", - "pydantic-core", - "typing-extensions", - "typing-inspection", -] -files = [ - {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, - {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, -] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -summary = "" -dependencies = [ - "typing-extensions", -] -files = [ - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -summary = "" -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -summary = "" -dependencies = [ - "six", -] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -summary = "" -files = [ - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyyaml-ft" -version = "8.0.0" -summary = "" -files = [ - {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, - {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, - {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, -] - -[[package]] -name = "requests" -version = "2.32.4" -summary = "" -dependencies = [ - "certifi", - "charset-normalizer", - "idna", - "urllib3", -] -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[[package]] -name = "rich" -version = "14.0.0" -summary = "" -dependencies = [ - "markdown-it-py", - "pygments", -] -files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -summary = "" -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.17.0" -summary = "" -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "typer" -version = "0.16.0" -summary = "" -dependencies = [ - "click", - "rich", - "shellingham", - "typing-extensions", -] -files = [ - {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, - {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20250611" -summary = "" -dependencies = [ - "urllib3", -] -files = [ - {file = "types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072"}, - {file = "types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826"}, -] - -[[package]] -name = "typing-extensions" -version = "4.14.1" -summary = "" -files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.1" -summary = "" -dependencies = [ - "typing-extensions", -] -files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, -] - -[[package]] -name = "urllib3" -version = "2.0.7" -summary = "" -files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, -] diff --git a/examples/conan_cmake/build_requires/pyproject.toml b/examples/conan_cmake/build_requires/pyproject.toml deleted file mode 100644 index 812d2bfd..00000000 --- a/examples/conan_cmake/build_requires/pyproject.toml +++ /dev/null @@ -1,32 +0,0 @@ -[project] -description = "Example showing tool_requires with CMake and abseil dependency" -name = "cppython-conan-cmake-build-requires" -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 = ["abseil==20240116.2"] - -# Future tool_requires support (not yet implemented in CPPython): -# [tool.cppython.provider.tool_requires] -# cmake = ">=3.20" - -[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 41faf046..a796a904 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -52,47 +52,7 @@ def test_simple(example_runner: CliRunner) -> None: # --- Setup for Publish with modified config --- # Modify the in-memory representation of the pyproject data - pyproject_data['tool']['cppython']['provider']['remotes'] = [] - - # Create a new project instance with the modified configuration for the 'publish' step - publish_project = Project(project_configuration, interface, pyproject_data) - - # Publish the project to the local cache - publish_project.publish() - - @staticmethod - def test_build_requires(example_runner: CliRunner) -> None: - """build_requires project""" - # Create project configuration - project_root = Path.cwd() - project_configuration = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True) - - # Create console interface - interface = ConsoleInterface() - - # Load pyproject.toml data - pyproject_path = project_root / 'pyproject.toml' - pyproject_data = loads(pyproject_path.read_text(encoding='utf-8')) - - # Create and use the project directly - project = Project(project_configuration, interface, pyproject_data) - - # Call install directly to get structured results - project.install() - - # 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}' - - path = Path('build').absolute() - - # Verify that the build directory contains the expected files - assert (path / 'CMakeCache.txt').exists(), f'{path / "CMakeCache.txt"} not found' - - # --- Setup for Publish with modified config --- - # Modify the in-memory representation of the pyproject data - pyproject_data['tool']['cppython']['provider']['remotes'] = [] + pyproject_data['tool']['cppython']['providers']['conan']['remotes'] = [] # Create a new project instance with the modified configuration for the 'publish' step publish_project = Project(project_configuration, interface, pyproject_data) From d649d4bea4773e9d4e3bbcd25356e64e14ec367b Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 00:43:55 -0400 Subject: [PATCH 12/20] Update Plugin Config Table --- cppython/builder.py | 84 ++++++++++++++++++++-- cppython/core/resolution.py | 24 +++++-- cppython/core/schema.py | 31 ++++---- cppython/test/pytest/fixtures.py | 3 +- examples/conan_cmake/simple/pyproject.toml | 7 +- examples/vcpkg_cmake/simple/pyproject.toml | 7 +- tests/unit/test_data.py | 19 +++++ 7 files changed, 134 insertions(+), 41 deletions(-) diff --git a/cppython/builder.py b/cppython/builder.py index beded73c..25fd4517 100644 --- a/cppython/builder.py +++ b/cppython/builder.py @@ -34,6 +34,7 @@ from cppython.data import Data, Plugins from cppython.defaults import DefaultSCM from cppython.utility.exception import PluginError +from cppython.utility.utility import TypeName class Resolver: @@ -59,14 +60,14 @@ def generate_plugins( raw_generator_plugins = self.find_generators() generator_plugins = self.filter_plugins( raw_generator_plugins, - cppython_local_configuration.generator_name, + self._get_effective_generator_name(cppython_local_configuration), 'Generator', ) raw_provider_plugins = self.find_providers() provider_plugins = self.filter_plugins( raw_provider_plugins, - cppython_local_configuration.provider_name, + self._get_effective_provider_name(cppython_local_configuration), 'Provider', ) @@ -79,6 +80,74 @@ def generate_plugins( return PluginBuildData(generator_type=generator_type, provider_type=provider_type, scm_type=scm_type) + def _get_effective_generator_name(self, config: CPPythonLocalConfiguration) -> str | None: + """Get the effective generator name from configuration + + Args: + config: The local configuration + + Returns: + The generator name to use, or None for auto-detection + """ + if config.generators: + # For now, pick the first generator (in future, could support selection logic) + return list(config.generators.keys())[0] + + # No generators specified, use auto-detection + return None + + def _get_effective_provider_name(self, config: CPPythonLocalConfiguration) -> str | None: + """Get the effective provider name from configuration + + Args: + config: The local configuration + + Returns: + The provider name to use, or None for auto-detection + """ + if config.providers: + # For now, pick the first provider (in future, could support selection logic) + return list(config.providers.keys())[0] + + # No providers specified, use auto-detection + return None + + def _get_effective_generator_config( + self, config: CPPythonLocalConfiguration, generator_name: str + ) -> dict[str, Any]: + """Get the effective generator configuration + + Args: + config: The local configuration + generator_name: The name of the generator being used + + Returns: + The configuration dict for the generator + """ + generator_type_name = TypeName(generator_name) + if config.generators and generator_type_name in config.generators: + return config.generators[generator_type_name] + + # Return empty config if not found + return {} + + def _get_effective_provider_config(self, config: CPPythonLocalConfiguration, provider_name: str) -> dict[str, Any]: + """Get the effective provider configuration + + Args: + config: The local configuration + provider_name: The name of the provider being used + + Returns: + The configuration dict for the provider + """ + provider_type_name = TypeName(provider_name) + if config.providers and provider_type_name in config.providers: + return config.providers[provider_type_name] + + # Return empty config if not found + return {} + @staticmethod def generate_cppython_plugin_data(plugin_build_data: PluginBuildData) -> PluginCPPythonData: """Generates the CPPython plugin data from the resolved plugins @@ -447,11 +516,18 @@ def build( pep621_data = self._resolver.generate_pep621_data(pep621_configuration, self._project_configuration, scm) # Create the chosen plugins + generator_config = self._resolver._get_effective_generator_config( + cppython_local_configuration, plugin_build_data.generator_type.name() + ) generator = self._resolver.create_generator( - core_data, pep621_data, cppython_local_configuration.generator, plugin_build_data.generator_type + core_data, pep621_data, generator_config, plugin_build_data.generator_type + ) + + provider_config = self._resolver._get_effective_provider_config( + cppython_local_configuration, plugin_build_data.provider_type.name() ) provider = self._resolver.create_provider( - core_data, pep621_data, cppython_local_configuration.provider, plugin_build_data.provider_type + core_data, pep621_data, provider_config, plugin_build_data.provider_type ) plugins = Plugins(generator=generator, provider=provider, scm=scm) diff --git a/cppython/core/resolution.py b/cppython/core/resolution.py index 0c7c09d3..8cb8f6a5 100644 --- a/cppython/core/resolution.py +++ b/cppython/core/resolution.py @@ -139,16 +139,22 @@ def resolve_cppython( if not modified_build_path.is_absolute(): modified_build_path = root_directory / modified_build_path - modified_provider_name = local_configuration.provider_name - modified_generator_name = local_configuration.generator_name + modified_provider_name = plugin_build_data.provider_name + modified_generator_name = plugin_build_data.generator_name - if modified_provider_name is None: - modified_provider_name = plugin_build_data.provider_name + modified_scm_name = plugin_build_data.scm_name - if modified_generator_name is None: - modified_generator_name = plugin_build_data.generator_name + # Extract provider and generator configuration data + provider_type_name = TypeName(modified_provider_name) + generator_type_name = TypeName(modified_generator_name) - modified_scm_name = plugin_build_data.scm_name + provider_data = {} + if local_configuration.providers and provider_type_name in local_configuration.providers: + provider_data = local_configuration.providers[provider_type_name] + + generator_data = {} + if local_configuration.generators and generator_type_name in local_configuration.generators: + generator_data = local_configuration.generators[generator_type_name] # Construct dependencies from the local configuration only dependencies: list[Requirement] = [] @@ -173,6 +179,8 @@ def resolve_cppython( generator_name=modified_generator_name, scm_name=modified_scm_name, dependencies=dependencies, + provider_data=provider_data, + generator_data=generator_data, ) return cppython_data @@ -200,6 +208,8 @@ def resolve_cppython_plugin(cppython_data: CPPythonData, plugin_type: type[Plugi generator_name=cppython_data.generator_name, scm_name=cppython_data.scm_name, dependencies=cppython_data.dependencies, + provider_data=cppython_data.provider_data, + generator_data=cppython_data.generator_data, ) return cast(CPPythonPluginData, plugin_data) diff --git a/cppython/core/schema.py b/cppython/core/schema.py index 0d29be3c..7b43589c 100644 --- a/cppython/core/schema.py +++ b/cppython/core/schema.py @@ -118,6 +118,9 @@ class CPPythonData(CPPythonModel, extra='forbid'): scm_name: TypeName dependencies: list[Requirement] + provider_data: Annotated[dict[str, Any], Field(description='Resolved provider configuration data')] + generator_data: Annotated[dict[str, Any], Field(description='Resolved generator configuration data')] + @field_validator('configuration_path', 'install_path', 'tool_path', 'build_path') # type: ignore @classmethod def validate_absolute_path(cls, value: Path) -> Path: @@ -302,29 +305,21 @@ class CPPythonLocalConfiguration(CPPythonModel, extra='forbid'): ), ] = Path('build') - provider: Annotated[ProviderData, Field(description="Provider plugin data associated with 'provider_name")] = ( - ProviderData({}) - ) - - provider_name: Annotated[ - TypeName | None, + providers: Annotated[ + dict[TypeName, ProviderData], Field( - alias='provider-name', - description='If empty, the provider will be automatically deduced.', + description='Named provider configurations. Key is the provider name, value is the provider configuration.' ), - ] = None - - generator: Annotated[GeneratorData, Field(description="Generator plugin data associated with 'generator_name'")] = ( - GeneratorData({}) - ) + ] = {} - generator_name: Annotated[ - TypeName | None, + generators: Annotated[ + dict[TypeName, GeneratorData], Field( - alias='generator-name', - description='If empty, the generator will be automatically deduced.', + description=( + 'Named generator configurations. Key is the generator name, value is the generator configuration.' + ) ), - ] = None + ] = {} dependencies: Annotated[ list[str] | None, diff --git a/cppython/test/pytest/fixtures.py b/cppython/test/pytest/fixtures.py index 5fa0175b..c76c07a0 100644 --- a/cppython/test/pytest/fixtures.py +++ b/cppython/test/pytest/fixtures.py @@ -27,7 +27,6 @@ PyProject, ToolData, ) -from cppython.utility.utility import TypeName @pytest.fixture( @@ -92,7 +91,7 @@ def fixture_cppython_local_configuration(install_path: Path) -> CPPythonLocalCon Variation of CPPython data """ cppython_local_configuration = CPPythonLocalConfiguration( - install_path=install_path, provider_name=TypeName('mock'), generator_name=TypeName('mock') + install_path=install_path, providers={'mock': {}}, generators={'mock': {}} ) return cppython_local_configuration diff --git a/examples/conan_cmake/simple/pyproject.toml b/examples/conan_cmake/simple/pyproject.toml index 6803860d..b8414d36 100644 --- a/examples/conan_cmake/simple/pyproject.toml +++ b/examples/conan_cmake/simple/pyproject.toml @@ -13,16 +13,13 @@ dependencies = ["cppython[conan, cmake, git]>=0.9.0"] [tool.cppython] -generator-name = "cmake" -provider-name = "conan" - install-path = "install" dependencies = ["fmt>=11.2.0"] -[tool.cppython.generator] +[tool.cppython.generators.cmake] -[tool.cppython.provider] +[tool.cppython.providers.conan] [tool.pdm] distribution = false diff --git a/examples/vcpkg_cmake/simple/pyproject.toml b/examples/vcpkg_cmake/simple/pyproject.toml index 47bae63d..be94ba0a 100644 --- a/examples/vcpkg_cmake/simple/pyproject.toml +++ b/examples/vcpkg_cmake/simple/pyproject.toml @@ -13,16 +13,13 @@ dependencies = ["cppython[vcpkg, cmake, git]>=0.9.0"] [tool.cppython] -generator-name = "cmake" -provider-name = "vcpkg" - install-path = "install" dependencies = ["fmt>=11.0.2"] -[tool.cppython.generator] +[tool.cppython.generators.cmake] -[tool.cppython.provider] +[tool.cppython.providers.vcpkg] [tool.pdm] distribution = false diff --git a/tests/unit/test_data.py b/tests/unit/test_data.py index d62efd22..8b5e8d2f 100644 --- a/tests/unit/test_data.py +++ b/tests/unit/test_data.py @@ -8,13 +8,16 @@ from cppython.core.resolution import PluginBuildData from cppython.core.schema import ( CPPythonLocalConfiguration, + GeneratorData, PEP621Configuration, ProjectConfiguration, + ProviderData, ) from cppython.data import Data from cppython.test.mock.generator import MockGenerator from cppython.test.mock.provider import MockProvider from cppython.test.mock.scm import MockSCM +from cppython.utility.utility import TypeName class TestData: @@ -58,3 +61,19 @@ def test_sync(data: Data) -> None: data: Fixture for the mocked data class """ data.sync() + + @staticmethod + def test_named_plugin_configuration() -> None: + """Test that named plugin configuration is properly validated""" + # Test valid named configuration + config = CPPythonLocalConfiguration( + providers={TypeName('conan'): ProviderData({'some_setting': 'value'})}, + generators={TypeName('cmake'): GeneratorData({'another_setting': True})}, + ) + assert config.providers == {TypeName('conan'): {'some_setting': 'value'}} + assert config.generators == {TypeName('cmake'): {'another_setting': True}} + + # Test empty configuration is valid + config_empty = CPPythonLocalConfiguration() + assert config_empty.providers == {} + assert config_empty.generators == {} From 55e792e266dca5946c210453d2612f7711f24d88 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 04:13:07 -0400 Subject: [PATCH 13/20] CMake Program Path --- cppython/plugins/conan/resolution.py | 76 +++++++++++++++++++-- tests/unit/plugins/conan/test_resolution.py | 46 ++++++++----- 2 files changed, 102 insertions(+), 20 deletions(-) diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index ee8f4735..2f1f663c 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -1,6 +1,8 @@ """Provides functionality to resolve Conan-specific data for the CPPython project.""" +import importlib import logging +from pathlib import Path from typing import Any from conan.api.conan_api import ConanAPI @@ -19,13 +21,44 @@ from cppython.utility.exception import ProviderConfigurationError -def _profile_post_process(profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any) -> None: +def _detect_cmake_program() -> str | None: + """Detect CMake program path from the cmake module if available. + + Returns: + Path to cmake executable, or None if not found + """ + try: + # Try to import cmake module and get its executable path + # Note: cmake is an optional dependency, so we import it conditionally + cmake = importlib.import_module('cmake') + + cmake_bin_dir = Path(cmake.CMAKE_BIN_DIR) + + # Try common cmake executable names (pathlib handles platform differences) + for cmake_name in ['cmake.exe', 'cmake']: + cmake_exe = cmake_bin_dir / cmake_name + if cmake_exe.exists(): + return str(cmake_exe) + + return None + except ImportError: + # cmake module not available + return None + except (AttributeError, Exception): + # If cmake module doesn't have expected attributes + return None + + +def _profile_post_process( + profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any, cmake_program: str | None = None +) -> None: """Apply profile plugin and settings processing to a list of profiles. Args: profiles: List of profiles to process conan_api: The Conan API instance cache_settings: The settings configuration + cmake_program: Optional path to cmake program to configure in profiles """ logger = logging.getLogger('cppython.conan') @@ -46,6 +79,15 @@ def _profile_post_process(profiles: list[Profile], conan_api: ConanAPI, cache_se # Apply the full profile processing pipeline for each profile for profile in profiles: + # Set cmake program configuration if provided + if cmake_program is not None: + try: + # Set the tools.cmake:cmake_program configuration in the profile + profile.conf.update('tools.cmake:cmake_program', cmake_program) + logger.debug('Set tools.cmake:cmake_program=%s in profile', cmake_program) + except (AttributeError, Exception) as cmake_error: + logger.debug('Failed to set cmake program configuration: %s', str(cmake_error)) + # Process settings to initialize processed_settings try: profile.process_settings(cache_settings) @@ -66,8 +108,25 @@ def _profile_post_process(profiles: list[Profile], conan_api: ConanAPI, cache_se logger.debug('Configuration rebase failed for profile: %s', str(rebase_error)) +def _apply_cmake_config_to_profile(profile: Profile, cmake_program: str | None, profile_type: str) -> None: + """Apply cmake program configuration to a profile. + + Args: + profile: The profile to configure + cmake_program: Path to cmake program to configure + profile_type: Type of profile (for logging) + """ + if cmake_program is not None: + logger = logging.getLogger('cppython.conan') + try: + profile.conf.update('tools.cmake:cmake_program', cmake_program) + logger.debug('Set tools.cmake:cmake_program=%s in %s profile', cmake_program, profile_type) + except (AttributeError, Exception) as cmake_error: + logger.debug('Failed to set cmake program in %s profile: %s', profile_type, str(cmake_error)) + + def _resolve_profiles( - host_profile_name: str | None, build_profile_name: str | None, conan_api: ConanAPI + host_profile_name: str | None, build_profile_name: str | None, conan_api: ConanAPI, cmake_program: str | None = None ) -> tuple[Profile, Profile]: """Resolve host and build profiles, with fallback to auto-detection. @@ -75,6 +134,7 @@ def _resolve_profiles( host_profile_name: The host profile name to resolve, or None for auto-detection build_profile_name: The build profile name to resolve, or None for auto-detection conan_api: The Conan API instance + cmake_program: Optional path to cmake program to configure in profiles Returns: A tuple of (host_profile, build_profile) @@ -91,6 +151,7 @@ def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile: logger.debug('Loading %s profile: %s', profile_type, profile_name) profile = conan_api.profiles.get_profile([profile_name]) logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name) + _apply_cmake_config_to_profile(profile, cmake_program, profile_type) return profile except Exception as e: logger.error('Failed to load %s profile %s: %s', profile_type, profile_name, str(e)) @@ -105,6 +166,7 @@ def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile: logger.debug('Loading %s profile: %s', profile_type, profile_name) profile = conan_api.profiles.get_profile([profile_name]) logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name) + _apply_cmake_config_to_profile(profile, cmake_program, profile_type) return profile except Exception as e: logger.debug( @@ -123,6 +185,7 @@ def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile: profile = conan_api.profiles.get_profile([default_profile_path]) logger.debug('Using default %s profile', profile_type) + _apply_cmake_config_to_profile(profile, cmake_program, profile_type) return profile except Exception as e: logger.warning('Default %s profile not available, using auto-detection: %s', profile_type, str(e)) @@ -132,7 +195,7 @@ def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile: cache_settings = conan_api.config.settings_yml # Apply profile plugin processing - _profile_post_process([profile], conan_api, cache_settings) + _profile_post_process([profile], conan_api, cache_settings, cmake_program) logger.debug('Auto-detected %s profile with plugin processing applied', profile_type) return profile @@ -245,8 +308,13 @@ def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> Conan # Initialize Conan API for profile resolution conan_api = ConanAPI() + # Try to detect cmake program path from current virtual environment + cmake_program = _detect_cmake_program() + # Resolve profiles - host_profile, build_profile = _resolve_profiles(parsed_data.host_profile, parsed_data.build_profile, conan_api) + host_profile, build_profile = _resolve_profiles( + parsed_data.host_profile, parsed_data.build_profile, conan_api, cmake_program + ) return ConanData( remotes=parsed_data.remotes, diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py index ab888e42..c9187523 100644 --- a/tests/unit/plugins/conan/test_resolution.py +++ b/tests/unit/plugins/conan/test_resolution.py @@ -280,7 +280,9 @@ def test_resolve_profiles_by_name(self) -> None: mock_build_profile = Mock() mock_conan_api.profiles.get_profile.side_effect = [mock_host_profile, mock_build_profile] - host_result, build_result = _resolve_profiles('host-profile', 'build-profile', mock_conan_api) + host_result, build_result = _resolve_profiles( + 'host-profile', 'build-profile', mock_conan_api, cmake_program=None + ) assert host_result == mock_host_profile assert build_result == mock_build_profile @@ -294,7 +296,7 @@ def test_resolve_profiles_by_name_failure(self) -> None: mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') with pytest.raises(ProviderConfigurationError, match='Failed to load host profile'): - _resolve_profiles('missing-profile', 'other-profile', mock_conan_api) + _resolve_profiles('missing-profile', 'other-profile', mock_conan_api, cmake_program=None) def test_resolve_profiles_auto_detect(self) -> None: """Test auto-detecting profiles.""" @@ -308,7 +310,7 @@ def test_resolve_profiles_auto_detect(self) -> None: mock_conan_api.profiles.get_default_build.return_value = mock_build_default_path mock_conan_api.profiles.get_profile.side_effect = [mock_host_profile, mock_build_profile] - host_result, build_result = _resolve_profiles(None, None, mock_conan_api) + host_result, build_result = _resolve_profiles(None, None, mock_conan_api, cmake_program=None) assert host_result == mock_host_profile assert build_result == mock_build_profile @@ -334,14 +336,14 @@ def test_resolve_profiles_fallback_to_detect(self, mock_post_process: Mock) -> N mock_conan_api.profiles.detect.side_effect = [mock_host_profile, mock_build_profile] mock_conan_api.config.settings_yml = mock_cache_settings - host_result, build_result = _resolve_profiles(None, None, mock_conan_api) + host_result, build_result = _resolve_profiles(None, None, mock_conan_api, cmake_program=None) assert host_result == mock_host_profile assert build_result == mock_build_profile assert mock_conan_api.profiles.detect.call_count == 2 assert mock_post_process.call_count == 2 - mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings) - mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings) + mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings, None) + mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings, None) @patch('cppython.plugins.conan.resolution._profile_post_process') def test_resolve_profiles_default_fallback_to_detect(self, mock_post_process: Mock) -> None: @@ -360,14 +362,14 @@ def test_resolve_profiles_default_fallback_to_detect(self, mock_post_process: Mo mock_conan_api.profiles.detect.side_effect = [mock_host_profile, mock_build_profile] mock_conan_api.config.settings_yml = mock_cache_settings - host_result, build_result = _resolve_profiles('default', 'default', mock_conan_api) + host_result, build_result = _resolve_profiles('default', 'default', mock_conan_api, cmake_program=None) assert host_result == mock_host_profile assert build_result == mock_build_profile assert mock_conan_api.profiles.detect.call_count == 2 assert mock_post_process.call_count == 2 - mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings) - mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings) + mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings, None) + mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings, None) class TestResolveConanData: @@ -375,8 +377,12 @@ class TestResolveConanData: @patch('cppython.plugins.conan.resolution.ConanAPI') @patch('cppython.plugins.conan.resolution._resolve_profiles') - def test_resolve_conan_data_with_profiles(self, mock_resolve_profiles: Mock, mock_conan_api_class: Mock) -> None: + @patch('cppython.plugins.conan.resolution._detect_cmake_program') + def test_resolve_conan_data_with_profiles( + self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock + ) -> None: """Test resolving ConanData with profile configuration.""" + mock_detect_cmake.return_value = None # No cmake detected for test mock_conan_api = Mock() mock_conan_api_class.return_value = mock_conan_api @@ -395,12 +401,16 @@ def test_resolve_conan_data_with_profiles(self, mock_resolve_profiles: Mock, moc assert result.remotes == ['conancenter'] # Verify profile resolution was called correctly - mock_resolve_profiles.assert_called_once_with('linux-x64', 'linux-gcc11', mock_conan_api) + mock_resolve_profiles.assert_called_once_with('linux-x64', 'linux-gcc11', mock_conan_api, None) @patch('cppython.plugins.conan.resolution.ConanAPI') @patch('cppython.plugins.conan.resolution._resolve_profiles') - def test_resolve_conan_data_default_profiles(self, mock_resolve_profiles: Mock, mock_conan_api_class: Mock) -> None: + @patch('cppython.plugins.conan.resolution._detect_cmake_program') + def test_resolve_conan_data_default_profiles( + self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock + ) -> None: """Test resolving ConanData with default profile configuration.""" + mock_detect_cmake.return_value = None # No cmake detected for test mock_conan_api = Mock() mock_conan_api_class.return_value = mock_conan_api @@ -419,12 +429,16 @@ def test_resolve_conan_data_default_profiles(self, mock_resolve_profiles: Mock, assert result.remotes == ['conancenter'] # Default remote # Verify profile resolution was called with default values - mock_resolve_profiles.assert_called_once_with('default', 'default', mock_conan_api) + mock_resolve_profiles.assert_called_once_with('default', 'default', mock_conan_api, None) @patch('cppython.plugins.conan.resolution.ConanAPI') @patch('cppython.plugins.conan.resolution._resolve_profiles') - def test_resolve_conan_data_null_profiles(self, mock_resolve_profiles: Mock, mock_conan_api_class: Mock) -> None: + @patch('cppython.plugins.conan.resolution._detect_cmake_program') + def test_resolve_conan_data_null_profiles( + self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock + ) -> None: """Test resolving ConanData with null profile configuration.""" + mock_detect_cmake.return_value = None # No cmake detected for test mock_conan_api = Mock() mock_conan_api_class.return_value = mock_conan_api @@ -443,7 +457,7 @@ def test_resolve_conan_data_null_profiles(self, mock_resolve_profiles: Mock, moc assert result.remotes == [] # Verify profile resolution was called with None values - mock_resolve_profiles.assert_called_once_with(None, None, mock_conan_api) + mock_resolve_profiles.assert_called_once_with(None, None, mock_conan_api, None) def test_auto_detected_profile_gets_post_processed(self, conan_mock_api: Mock): """Test that auto-detected profiles get proper post-processing. @@ -467,7 +481,7 @@ def test_auto_detected_profile_gets_post_processed(self, conan_mock_api: Mock): conan_mock_api.config.global_conf = Mock() # Call the resolution - this should trigger auto-detection and post-processing - host_profile, build_profile = _resolve_profiles(None, None, conan_mock_api) + host_profile, build_profile = _resolve_profiles(None, None, conan_mock_api, cmake_program=None) # Verify that process_settings was called on both profiles assert mock_profile.process_settings.call_count == EXPECTED_PROFILE_CALL_COUNT From 2c1a6622f3453d298c799a116b33b8b150d77583 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 04:29:43 -0400 Subject: [PATCH 14/20] Rename Some Tests --- tests/unit/plugins/cmake/test_schema.py | 10 +-- tests/unit/plugins/conan/test_install.py | 8 +- tests/unit/plugins/conan/test_publish.py | 14 ++-- tests/unit/plugins/conan/test_resolution.py | 87 +++++++++++---------- tests/unit/plugins/vcpkg/test_resolution.py | 2 +- 5 files changed, 62 insertions(+), 59 deletions(-) diff --git a/tests/unit/plugins/cmake/test_schema.py b/tests/unit/plugins/cmake/test_schema.py index 765c9742..493d12ac 100644 --- a/tests/unit/plugins/cmake/test_schema.py +++ b/tests/unit/plugins/cmake/test_schema.py @@ -7,35 +7,35 @@ class TestCacheVariable: """Tests for the CacheVariable class""" @staticmethod - def test_cache_variable_bool() -> None: + def test_bool() -> None: """Tests the CacheVariable class with a boolean value""" var = CacheVariable(type=VariableType.BOOL, value=True) assert var.type == VariableType.BOOL assert var.value is True @staticmethod - def test_cache_variable_string() -> None: + def test_string() -> None: """Tests the CacheVariable class with a string value""" var = CacheVariable(type=VariableType.STRING, value='SomeValue') assert var.type == VariableType.STRING assert var.value == 'SomeValue' @staticmethod - def test_cache_variable_null_type() -> None: + def test_null_type() -> None: """Tests the CacheVariable class with a null type""" var = CacheVariable(type=None, value='Unset') assert var.type is None assert var.value == 'Unset' @staticmethod - def test_cache_variable_bool_value_as_string() -> None: + def test_bool_value_as_string() -> None: """Tests the CacheVariable class with a boolean value as a string""" # CMake allows bool as "TRUE"/"FALSE" as well var = CacheVariable(type=VariableType.BOOL, value='TRUE') assert var.value == 'TRUE' @staticmethod - def test_cache_variable_type_optional() -> None: + def test_type_optional() -> None: """Tests the CacheVariable class with an optional type""" # type is optional var = CacheVariable(value='SomeValue') diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py index 509059da..c165ebab 100644 --- a/tests/unit/plugins/conan/test_install.py +++ b/tests/unit/plugins/conan/test_install.py @@ -49,7 +49,7 @@ def fixture_plugin_type() -> type[ConanProvider]: """ return ConanProvider - def test_install_with_dependencies( + def test_with_dependencies( self, plugin: ConanProvider, conan_temp_conanfile: Path, @@ -87,7 +87,7 @@ def test_install_with_dependencies( # Verify ConanAPI constructor was called conan_setup_mocks['conan_api_constructor'].assert_called_once() - def test_install_conan_command_failure( + def test_conan_command_failure( self, plugin: ConanProvider, conan_temp_conanfile: Path, @@ -117,7 +117,7 @@ def test_install_conan_command_failure( # Mock resolve_conan_dependency def mock_resolve(requirement: Requirement) -> ConanDependency: - return ConanDependency(name=requirement.name, version_ge=None) + return ConanDependency(name=requirement.name) mocker.patch('cppython.plugins.conan.plugin.resolve_conan_dependency', side_effect=mock_resolve) @@ -136,7 +136,7 @@ def mock_resolve(requirement: Requirement) -> ConanDependency: # Verify Conan API was attempted mock_conan_api_constructor.assert_called_once() - def test_install_with_default_profiles( + def test_with_default_profiles( self, plugin: ConanProvider, conan_temp_conanfile: Path, diff --git a/tests/unit/plugins/conan/test_publish.py b/tests/unit/plugins/conan/test_publish.py index e395a4c3..6e6e60d5 100644 --- a/tests/unit/plugins/conan/test_publish.py +++ b/tests/unit/plugins/conan/test_publish.py @@ -42,7 +42,7 @@ def fixture_plugin_type() -> type[ConanProvider]: """ return ConanProvider - def test_publish_local_only( + def test_local_only( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish with remotes=[] only exports and builds locally @@ -87,7 +87,7 @@ def test_publish_local_only( # Verify upload was NOT called for local mode conan_mock_api_publish.upload.upload_full.assert_not_called() - def test_publish_with_upload( + def test_with_upload( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish with remotes=['conancenter'] exports, builds, and uploads @@ -121,7 +121,7 @@ def test_publish_with_upload( conan_mock_api_publish.list.select.assert_called_once() conan_mock_api_publish.upload.upload_full.assert_called_once() - def test_publish_no_remotes_configured( + def test_no_remotes_configured( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish raises error when no remotes are configured for upload @@ -149,7 +149,7 @@ def test_publish_no_remotes_configured( with pytest.raises(ProviderConfigurationError, match='No configured remotes found'): plugin.publish() - def test_publish_no_packages_found( + def test_no_packages_found( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish raises error when no packages are found to upload @@ -179,7 +179,7 @@ def test_publish_no_packages_found( with pytest.raises(ProviderInstallationError, match='No packages found to upload'): plugin.publish() - def test_publish_with_default_profiles( + def test_with_default_profiles( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish uses pre-resolved profiles from plugin construction @@ -209,7 +209,7 @@ def test_publish_with_default_profiles( assert call_args.kwargs['profile_host'] == plugin.data.host_profile assert call_args.kwargs['profile_build'] == plugin.data.build_profile - def test_publish_upload_parameters( + def test_upload_parameters( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish upload is called with correct parameters @@ -254,7 +254,7 @@ def test_publish_upload_parameters( dry_run=False, ) - def test_publish_list_pattern_creation( + def test_list_pattern_creation( self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture ) -> None: """Test that publish creates correct ListPattern for package selection diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py index c9187523..6021223c 100644 --- a/tests/unit/plugins/conan/test_resolution.py +++ b/tests/unit/plugins/conan/test_resolution.py @@ -32,7 +32,7 @@ class TestResolveDependency: """Test dependency resolution.""" - def test_resolve_dependency_with_version(self) -> None: + def test_with_version(self) -> None: """Test resolving a dependency with a >= version specifier.""" requirement = Requirement('boost>=1.80.0') @@ -43,7 +43,7 @@ def test_resolve_dependency_with_version(self) -> None: assert result.version_range.expression == '>=1.80.0' assert result.version is None - def test_resolve_dependency_with_exact_version(self) -> None: + def test_with_exact_version(self) -> None: """Test resolving a dependency with an exact version specifier.""" requirement = Requirement('abseil==20240116.2') @@ -54,7 +54,7 @@ def test_resolve_dependency_with_exact_version(self) -> None: assert str(result.version) == '20240116.2' assert result.version_range is None - def test_resolve_dependency_without_version(self) -> None: + def test_without_version(self) -> None: """Test resolving a dependency without a version specifier.""" requirement = Requirement('boost') @@ -64,7 +64,7 @@ def test_resolve_dependency_without_version(self) -> None: assert result.version is None assert result.version_range is None - def test_resolve_dependency_compatible_release(self) -> None: + def test_compatible_release(self) -> None: """Test resolving a dependency with ~= (compatible release) operator.""" requirement = Requirement('package~=1.2.3') @@ -75,7 +75,7 @@ def test_resolve_dependency_compatible_release(self) -> None: assert result.version_range.expression == '~1.2' assert result.version is None - def test_resolve_dependency_multiple_specifiers(self) -> None: + def test_multiple_specifiers(self) -> None: """Test resolving a dependency with multiple specifiers.""" requirement = Requirement('boost>=1.80.0,<2.0.0') @@ -86,14 +86,14 @@ def test_resolve_dependency_multiple_specifiers(self) -> None: assert result.version_range.expression == '>=1.80.0 <2.0.0' assert result.version is None - def test_resolve_dependency_unsupported_operator(self) -> None: + def test_unsupported_operator(self) -> None: """Test that unsupported operators raise an error.""" requirement = Requirement('boost===1.80.0') with pytest.raises(ConfigException, match="Unsupported single specifier '==='"): resolve_conan_dependency(requirement) - def test_resolve_dependency_contradictory_exact_versions(self) -> None: + def test_contradictory_exact_versions(self) -> None: """Test that multiple specifiers work correctly for valid ranges.""" # Test our logic with a valid range instead of invalid syntax requirement = Requirement('package>=1.0,<=2.0') # Valid range @@ -103,37 +103,37 @@ def test_resolve_dependency_contradictory_exact_versions(self) -> None: assert result.version_range is not None assert result.version_range.expression == '>=1.0 <=2.0' - def test_conan_dependency_requires_exact_version(self) -> None: + def test_requires_exact_version(self) -> None: """Test that ConanDependency generates correct requires for exact versions.""" dependency = ConanDependency(name='abseil', version=ConanVersion.from_string('20240116.2')) assert dependency.requires() == 'abseil/20240116.2' - def test_conan_dependency_requires_version_range(self) -> None: + def test_requires_version_range(self) -> None: """Test that ConanDependency generates correct requires for version ranges.""" dependency = ConanDependency(name='boost', version_range=ConanVersionRange(expression='>=1.80.0 <2.0')) assert dependency.requires() == 'boost/[>=1.80.0 <2.0]' - def test_conan_dependency_requires_legacy_minimum_version(self) -> None: + def test_requires_legacy_minimum_version(self) -> None: """Test that ConanDependency generates correct requires for legacy minimum versions.""" dependency = ConanDependency(name='boost', version_range=ConanVersionRange(expression='>=1.80.0')) assert dependency.requires() == 'boost/[>=1.80.0]' - def test_conan_dependency_requires_legacy_exact_version(self) -> None: + def test_requires_legacy_exact_version(self) -> None: """Test that ConanDependency generates correct requires for legacy exact versions.""" dependency = ConanDependency(name='abseil', version=ConanVersion.from_string('20240116.2')) assert dependency.requires() == 'abseil/20240116.2' - def test_conan_dependency_requires_no_version(self) -> None: + def test_requires_no_version(self) -> None: """Test that ConanDependency generates correct requires for dependencies without version.""" dependency = ConanDependency(name='somelib') assert dependency.requires() == 'somelib' - def test_conan_dependency_with_user_channel(self) -> None: + def test_with_user_channel(self) -> None: """Test that ConanDependency handles user/channel correctly.""" dependency = ConanDependency( name='mylib', @@ -143,7 +143,7 @@ def test_conan_dependency_with_user_channel(self) -> None: assert dependency.requires() == 'mylib/1.0.0@myuser/stable' - def test_conan_dependency_with_revision(self) -> None: + def test_with_revision(self) -> None: """Test that ConanDependency handles revisions correctly.""" dependency = ConanDependency( name='mylib', version=ConanVersion.from_string('1.0.0'), revision=ConanRevision(revision='abc123') @@ -151,7 +151,7 @@ def test_conan_dependency_with_revision(self) -> None: assert dependency.requires() == 'mylib/1.0.0#abc123' - def test_conan_dependency_full_reference(self) -> None: + def test_full_reference(self) -> None: """Test that ConanDependency handles full references correctly.""" dependency = ConanDependency( name='mylib', @@ -162,7 +162,7 @@ def test_conan_dependency_full_reference(self) -> None: assert dependency.requires() == 'mylib/1.0.0@myuser/stable#abc123' - def test_from_conan_reference_simple(self) -> None: + def test_from_reference_simple(self) -> None: """Test parsing a simple package name.""" dependency = ConanDependency.from_conan_reference('mylib') @@ -171,7 +171,7 @@ def test_from_conan_reference_simple(self) -> None: assert dependency.user_channel is None assert dependency.revision is None - def test_from_conan_reference_with_version(self) -> None: + def test_from_reference_with_version(self) -> None: """Test parsing a package with version.""" dependency = ConanDependency.from_conan_reference('mylib/1.0.0') @@ -181,7 +181,7 @@ def test_from_conan_reference_with_version(self) -> None: assert dependency.user_channel is None assert dependency.revision is None - def test_from_conan_reference_with_version_range(self) -> None: + def test_from_reference_with_version_range(self) -> None: """Test parsing a package with version range.""" dependency = ConanDependency.from_conan_reference('mylib/[>=1.0 <2.0]') @@ -192,7 +192,7 @@ def test_from_conan_reference_with_version_range(self) -> None: assert dependency.user_channel is None assert dependency.revision is None - def test_from_conan_reference_full(self) -> None: + def test_from_reference_full(self) -> None: """Test parsing a full Conan reference.""" dependency = ConanDependency.from_conan_reference('mylib/1.0.0@myuser/stable#abc123') @@ -209,7 +209,7 @@ def test_from_conan_reference_full(self) -> None: class TestProfileProcessing: """Test profile processing functionality.""" - def test_apply_profile_processing_success(self) -> None: + def test_success(self) -> None: """Test successful profile processing.""" mock_conan_api = Mock() mock_profile = Mock() @@ -224,7 +224,7 @@ def test_apply_profile_processing_success(self) -> None: mock_plugin.assert_called_once_with(mock_profile) mock_profile.process_settings.assert_called_once_with(mock_cache_settings) - def test_apply_profile_processing_no_plugin(self) -> None: + def test_no_plugin(self) -> None: """Test profile processing when no plugin is available.""" mock_conan_api = Mock() mock_profile = Mock() @@ -237,7 +237,7 @@ def test_apply_profile_processing_no_plugin(self) -> None: mock_profile.process_settings.assert_called_once_with(mock_cache_settings) - def test_apply_profile_processing_plugin_failure(self, caplog: pytest.LogCaptureFixture) -> None: + def test_plugin_failure(self, caplog: pytest.LogCaptureFixture) -> None: """Test profile processing when plugin fails.""" mock_conan_api = Mock() mock_profile = Mock() @@ -254,7 +254,7 @@ def test_apply_profile_processing_plugin_failure(self, caplog: pytest.LogCapture assert 'Profile plugin failed for profile' in caplog.text mock_profile.process_settings.assert_called_once_with(mock_cache_settings) - def test_apply_profile_processing_settings_failure(self, caplog: pytest.LogCaptureFixture) -> None: + def test_settings_failure(self, caplog: pytest.LogCaptureFixture) -> None: """Test profile processing when settings processing fails.""" mock_conan_api = Mock() mock_profile = Mock() @@ -273,7 +273,7 @@ def test_apply_profile_processing_settings_failure(self, caplog: pytest.LogCaptu class TestResolveProfiles: """Test profile resolution functionality.""" - def test_resolve_profiles_by_name(self) -> None: + def test_by_name(self) -> None: """Test resolving profiles by name.""" mock_conan_api = Mock() mock_host_profile = Mock() @@ -290,7 +290,7 @@ def test_resolve_profiles_by_name(self) -> None: mock_conan_api.profiles.get_profile.assert_any_call(['host-profile']) mock_conan_api.profiles.get_profile.assert_any_call(['build-profile']) - def test_resolve_profiles_by_name_failure(self) -> None: + def test_by_name_failure(self) -> None: """Test resolving profiles by name when host profile fails.""" mock_conan_api = Mock() mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') @@ -298,7 +298,7 @@ def test_resolve_profiles_by_name_failure(self) -> None: with pytest.raises(ProviderConfigurationError, match='Failed to load host profile'): _resolve_profiles('missing-profile', 'other-profile', mock_conan_api, cmake_program=None) - def test_resolve_profiles_auto_detect(self) -> None: + def test_auto_detect(self) -> None: """Test auto-detecting profiles.""" mock_conan_api = Mock() mock_host_profile = Mock() @@ -320,7 +320,7 @@ def test_resolve_profiles_auto_detect(self) -> None: mock_conan_api.profiles.get_profile.assert_any_call([mock_build_default_path]) @patch('cppython.plugins.conan.resolution._profile_post_process') - def test_resolve_profiles_fallback_to_detect(self, mock_post_process: Mock) -> None: + def test_fallback_to_detect(self, mock_post_process: Mock) -> None: """Test falling back to profile detection when defaults fail.""" mock_conan_api = Mock() mock_host_profile = Mock() @@ -346,7 +346,7 @@ def test_resolve_profiles_fallback_to_detect(self, mock_post_process: Mock) -> N mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings, None) @patch('cppython.plugins.conan.resolution._profile_post_process') - def test_resolve_profiles_default_fallback_to_detect(self, mock_post_process: Mock) -> None: + def test_default_fallback_to_detect(self, mock_post_process: Mock) -> None: """Test falling back to profile detection when default profile fails.""" mock_conan_api = Mock() mock_host_profile = Mock() @@ -459,15 +459,21 @@ def test_resolve_conan_data_null_profiles( # Verify profile resolution was called with None values mock_resolve_profiles.assert_called_once_with(None, None, mock_conan_api, None) - def test_auto_detected_profile_gets_post_processed(self, conan_mock_api: Mock): + @patch('cppython.plugins.conan.resolution.ConanAPI') + @patch('cppython.plugins.conan.resolution._profile_post_process') + def test_auto_detected_profile_processing(self, mock_post_process: Mock, mock_conan_api_class: Mock): """Test that auto-detected profiles get proper post-processing. Args: - conan_mock_api: Mock ConanAPI instance from fixture + mock_post_process: Mock for _profile_post_process function + mock_conan_api_class: Mock for ConanAPI class """ + mock_conan_api = Mock() + mock_conan_api_class.return_value = mock_conan_api + # Configure the mock to simulate no default profiles - conan_mock_api.profiles.get_default_host.side_effect = Exception('No default profile') - conan_mock_api.profiles.get_default_build.side_effect = Exception('No default profile') + mock_conan_api.profiles.get_default_host.side_effect = Exception('No default profile') + mock_conan_api.profiles.get_default_build.side_effect = Exception('No default profile') # Create a profile that simulates auto-detection mock_profile = Mock() @@ -477,17 +483,14 @@ def test_auto_detected_profile_gets_post_processed(self, conan_mock_api: Mock): mock_profile.conf.validate = Mock() mock_profile.conf.rebase_conf_definition = Mock() - conan_mock_api.profiles.detect.return_value = mock_profile - conan_mock_api.config.global_conf = Mock() + mock_conan_api.profiles.detect.return_value = mock_profile + mock_conan_api.config.global_conf = Mock() # Call the resolution - this should trigger auto-detection and post-processing - host_profile, build_profile = _resolve_profiles(None, None, conan_mock_api, cmake_program=None) - - # Verify that process_settings was called on both profiles - assert mock_profile.process_settings.call_count == EXPECTED_PROFILE_CALL_COUNT + host_profile, build_profile = _resolve_profiles(None, None, mock_conan_api, cmake_program=None) - # Verify that conf validation was called on both profiles - assert mock_profile.conf.validate.call_count == EXPECTED_PROFILE_CALL_COUNT + # Verify that auto-detection was called for both profiles + assert mock_conan_api.profiles.detect.call_count == EXPECTED_PROFILE_CALL_COUNT - # Verify that conf rebase was called on both profiles - assert mock_profile.conf.rebase_conf_definition.call_count == EXPECTED_PROFILE_CALL_COUNT + # Verify that post-processing was called for both profiles + assert mock_post_process.call_count == EXPECTED_PROFILE_CALL_COUNT diff --git a/tests/unit/plugins/vcpkg/test_resolution.py b/tests/unit/plugins/vcpkg/test_resolution.py index e8df2f17..927eb7f6 100644 --- a/tests/unit/plugins/vcpkg/test_resolution.py +++ b/tests/unit/plugins/vcpkg/test_resolution.py @@ -9,7 +9,7 @@ class TestVcpkgResolution: """Test the resolution of Vcpkg dependencies""" @staticmethod - def test_resolve_vcpkg_dependency() -> None: + def test_dependency_resolution() -> None: """Test resolving a VcpkgDependency from a packaging requirement.""" requirement = Requirement('example-package>=1.2.3') From 5a469207aca03233d19dc6c17adc4bc1d8cf4c51 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 04:30:39 -0400 Subject: [PATCH 15/20] Update test_resolution.py --- tests/unit/plugins/conan/test_resolution.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py index 6021223c..232fb8b5 100644 --- a/tests/unit/plugins/conan/test_resolution.py +++ b/tests/unit/plugins/conan/test_resolution.py @@ -378,7 +378,7 @@ class TestResolveConanData: @patch('cppython.plugins.conan.resolution.ConanAPI') @patch('cppython.plugins.conan.resolution._resolve_profiles') @patch('cppython.plugins.conan.resolution._detect_cmake_program') - def test_resolve_conan_data_with_profiles( + def test_with_profiles( self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock ) -> None: """Test resolving ConanData with profile configuration.""" @@ -406,7 +406,7 @@ def test_resolve_conan_data_with_profiles( @patch('cppython.plugins.conan.resolution.ConanAPI') @patch('cppython.plugins.conan.resolution._resolve_profiles') @patch('cppython.plugins.conan.resolution._detect_cmake_program') - def test_resolve_conan_data_default_profiles( + def test_default_profiles( self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock ) -> None: """Test resolving ConanData with default profile configuration.""" @@ -434,7 +434,7 @@ def test_resolve_conan_data_default_profiles( @patch('cppython.plugins.conan.resolution.ConanAPI') @patch('cppython.plugins.conan.resolution._resolve_profiles') @patch('cppython.plugins.conan.resolution._detect_cmake_program') - def test_resolve_conan_data_null_profiles( + def test_null_profiles( self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock ) -> None: """Test resolving ConanData with null profile configuration.""" From 562666fc67ee19683938cd3960822919de1f3d5f Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 04:37:51 -0400 Subject: [PATCH 16/20] Fix Lint Errors --- cppython/test/pytest/fixtures.py | 7 ++++++- tests/unit/plugins/conan/test_resolution.py | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cppython/test/pytest/fixtures.py b/cppython/test/pytest/fixtures.py index c76c07a0..23fb3ca0 100644 --- a/cppython/test/pytest/fixtures.py +++ b/cppython/test/pytest/fixtures.py @@ -20,13 +20,16 @@ CPPythonData, CPPythonGlobalConfiguration, CPPythonLocalConfiguration, + GeneratorData, PEP621Configuration, PEP621Data, ProjectConfiguration, ProjectData, + ProviderData, PyProject, ToolData, ) +from cppython.utility.utility import TypeName @pytest.fixture( @@ -91,7 +94,9 @@ def fixture_cppython_local_configuration(install_path: Path) -> CPPythonLocalCon Variation of CPPython data """ cppython_local_configuration = CPPythonLocalConfiguration( - install_path=install_path, providers={'mock': {}}, generators={'mock': {}} + install_path=install_path, + providers={TypeName('mock'): ProviderData({})}, + generators={TypeName('mock'): GeneratorData({})}, ) return cppython_local_configuration diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py index 232fb8b5..85511c70 100644 --- a/tests/unit/plugins/conan/test_resolution.py +++ b/tests/unit/plugins/conan/test_resolution.py @@ -286,7 +286,7 @@ def test_by_name(self) -> None: assert host_result == mock_host_profile assert build_result == mock_build_profile - assert mock_conan_api.profiles.get_profile.call_count == 2 + assert mock_conan_api.profiles.get_profile.call_count == EXPECTED_PROFILE_CALL_COUNT mock_conan_api.profiles.get_profile.assert_any_call(['host-profile']) mock_conan_api.profiles.get_profile.assert_any_call(['build-profile']) @@ -340,8 +340,8 @@ def test_fallback_to_detect(self, mock_post_process: Mock) -> None: assert host_result == mock_host_profile assert build_result == mock_build_profile - assert mock_conan_api.profiles.detect.call_count == 2 - assert mock_post_process.call_count == 2 + assert mock_conan_api.profiles.detect.call_count == EXPECTED_PROFILE_CALL_COUNT + assert mock_post_process.call_count == EXPECTED_PROFILE_CALL_COUNT mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings, None) mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings, None) @@ -366,8 +366,8 @@ def test_default_fallback_to_detect(self, mock_post_process: Mock) -> None: assert host_result == mock_host_profile assert build_result == mock_build_profile - assert mock_conan_api.profiles.detect.call_count == 2 - assert mock_post_process.call_count == 2 + assert mock_conan_api.profiles.detect.call_count == EXPECTED_PROFILE_CALL_COUNT + assert mock_post_process.call_count == EXPECTED_PROFILE_CALL_COUNT mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings, None) mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings, None) From 038e14c5c4a15cdc215bf3dd5745d9bab761d904 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 09:52:52 -0400 Subject: [PATCH 17/20] Install Logging --- cppython/plugins/conan/plugin.py | 258 +++++++++++++++++++++---------- 1 file changed, 180 insertions(+), 78 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index aa80fcb3..d2fcd05a 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -96,62 +96,98 @@ def _install_dependencies(self, *, update: bool = False) -> None: # Initialize Conan API conan_api = ConanAPI() + logger.debug('Conan API initialized successfully') # Get project paths project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' + logger.debug('Project root: %s, Conanfile path: %s', project_root, conanfile_path) if not conanfile_path.exists(): raise ProviderInstallationError('conan', 'Generated conanfile.py not found') # Get all remotes - all_remotes = conan_api.remotes.list() - logger.debug('Available remotes: %s', [remote.name for remote in all_remotes]) + try: + all_remotes = conan_api.remotes.list() + logger.debug('Available remotes: %s', [remote.name for remote in all_remotes]) + except Exception as e: + logger.error('Failed to list remotes: %s', e) + raise # Get profiles from resolved data profile_host, profile_build = self.data.host_profile, self.data.build_profile + logger.debug('Using profiles - host: %s, build: %s', profile_host, profile_build) path = str(conanfile_path) remotes = all_remotes update_flag = None if not update else True check_updates_flag = update - deps_graph = conan_api.graph.load_graph_consumer( - path=path, - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=remotes, - update=update_flag, - check_updates=check_updates_flag, - is_build_require=False, - profile_host=profile_host, - profile_build=profile_build, - ) - - logger.debug('Dependency graph loaded with %d nodes', len(deps_graph.nodes)) + logger.debug('Loading dependency graph with parameters: path=%s, remotes=%d, update=%s, check_updates=%s', + path, len(remotes), update_flag, check_updates_flag) + + try: + deps_graph = conan_api.graph.load_graph_consumer( + path=path, + name=None, + version=None, + user=None, + channel=None, + lockfile=None, + remotes=remotes, + update=update_flag, + check_updates=check_updates_flag, + is_build_require=False, + profile_host=profile_host, + profile_build=profile_build, + ) + logger.debug('Dependency graph loaded successfully, type: %s', type(deps_graph)) + if hasattr(deps_graph, 'nodes'): + logger.debug('Graph has %d nodes', len(deps_graph.nodes)) + else: + logger.warning('Dependency graph does not have nodes attribute') + except Exception as e: + logger.error('Failed to load dependency graph: %s', e) + raise # Analyze binaries to determine what needs to be built/downloaded - conan_api.graph.analyze_binaries( - graph=deps_graph, - build_mode=['missing'], # Only build what's missing - remotes=all_remotes, - update=None if not update else True, - lockfile=None, - ) + logger.debug('Starting binary analysis with build_mode=["missing"], update=%s', update) + try: + conan_api.graph.analyze_binaries( + graph=deps_graph, + build_mode=['missing'], # Only build what's missing + remotes=all_remotes, + update=None if not update else True, + lockfile=None, + ) + logger.debug('Binary analysis completed successfully') + except Exception as e: + logger.error('Failed to analyze binaries: %s', e) + raise # Install all dependencies - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + logger.debug('Starting binary installation') + try: + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + logger.debug('Binary installation completed successfully') + except Exception as e: + logger.error('Failed to install binaries: %s', e) + raise # Generate files for the consumer (conandata.yml, conan_toolchain.cmake, etc.) - conan_api.install.install_consumer( - deps_graph=deps_graph, - generators=['CMakeToolchain', 'CMakeDeps'], - source_folder=str(project_root), - output_folder=str(self.core_data.cppython_data.build_path), - ) + logger.debug('Generating consumer files with generators=["CMakeToolchain", "CMakeDeps"]') + logger.debug('Source folder: %s, Output folder: %s', project_root, self.core_data.cppython_data.build_path) + try: + conan_api.install.install_consumer( + deps_graph=deps_graph, + generators=['CMakeToolchain', 'CMakeDeps'], + source_folder=str(project_root), + output_folder=str(self.core_data.cppython_data.build_path), + ) + logger.debug('Consumer file generation completed successfully') + except Exception as e: + logger.error('Failed to install consumer files: %s', e) + raise logger.debug('Successfully installed dependencies using Conan API') @@ -208,22 +244,38 @@ async def download_tooling(cls, directory: Path) -> None: def publish(self) -> None: """Publishes the package using conan create workflow.""" + logger = logging.getLogger('cppython.conan') + logger.debug('Starting package publish workflow') + # Get the project root directory where conanfile.py should be located project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' + logger.debug('Project root: %s, Conanfile path: %s', project_root, conanfile_path) if not conanfile_path.exists(): raise FileNotFoundError(f'conanfile.py not found at {conanfile_path}') # Initialize Conan API - conan_api = ConanAPI() + try: + conan_api = ConanAPI() + logger.debug('Conan API initialized successfully for publish') + except Exception as e: + logger.error('Failed to initialize Conan API for publish: %s', e) + raise # Get configured remotes from Conan API and filter by our configuration # TODO: We want to replace the global conan remotes with the ones configured in CPPython. - all_remotes = conan_api.remotes.list() + try: + all_remotes = conan_api.remotes.list() + logger.debug('Retrieved %d remotes for publish', len(all_remotes)) + except Exception as e: + logger.error('Failed to list remotes for publish: %s', e) + raise + if not self.data.local_only: # Filter remotes to only include those specified in configuration configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes] + logger.debug('Configured remotes: %s', [r.name for r in configured_remotes]) if not configured_remotes: available_remotes = [remote.name for remote in all_remotes] @@ -235,72 +287,122 @@ def publish(self) -> None: ) else: configured_remotes = [] + logger.debug('Local only mode - no remotes configured') # Step 1: Export the recipe to the cache # This is equivalent to the export part of `conan create` - ref, conanfile = conan_api.export.export( - path=str(conanfile_path), - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=all_remotes, # Use all remotes for dependency resolution during export - ) + logger.debug('Starting export step with path: %s', conanfile_path) + try: + ref, conanfile = conan_api.export.export( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + lockfile=None, + remotes=all_remotes, # Use all remotes for dependency resolution during export + ) + logger.debug('Export completed successfully. Ref: %s, Conanfile type: %s', ref, type(conanfile)) + if conanfile is None: + logger.error('Export returned None for conanfile!') + else: + conanfile_attrs = dir(conanfile) if hasattr(conanfile, '__dict__') else 'No attributes' + logger.debug('Conanfile attributes: %s', conanfile_attrs) + except Exception as e: + logger.error('Export failed: %s', e) + raise # Step 2: Get profiles from resolved data profile_host, profile_build = self.data.host_profile, self.data.build_profile + logger.debug('Using profiles for publish - host: %s, build: %s', profile_host, profile_build) # Step 3: Build dependency graph for the package - prepare parameters path = str(conanfile_path) remotes = all_remotes # Use all remotes for dependency resolution + logger.debug('Loading dependency graph for publish with path: %s', path) - deps_graph = conan_api.graph.load_graph_consumer( - path=path, - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=remotes, - update=None, - check_updates=False, - is_build_require=False, - profile_host=profile_host, - profile_build=profile_build, - ) + try: + deps_graph = conan_api.graph.load_graph_consumer( + path=path, + name=None, + version=None, + user=None, + channel=None, + lockfile=None, + remotes=remotes, + update=None, + check_updates=False, + is_build_require=False, + profile_host=profile_host, + profile_build=profile_build, + ) + logger.debug('Dependency graph loaded successfully for publish') + except Exception as e: + logger.error('Failed to load dependency graph for publish: %s', e) + raise # Step 4: Analyze binaries and install/build them if needed - conan_api.graph.analyze_binaries( - graph=deps_graph, - build_mode=['*'], # Build from source (equivalent to the create behavior) - remotes=all_remotes, # Use all remotes for dependency resolution - update=None, - lockfile=None, - ) + logger.debug('Starting binary analysis for publish') + try: + conan_api.graph.analyze_binaries( + graph=deps_graph, + build_mode=['*'], # Build from source (equivalent to the create behavior) + remotes=all_remotes, # Use all remotes for dependency resolution + update=None, + lockfile=None, + ) + logger.debug('Binary analysis completed for publish') + except Exception as e: + logger.error('Failed to analyze binaries for publish: %s', e) + raise # Step 5: Install all dependencies and build the package - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + logger.debug('Starting binary installation for publish') + try: + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + logger.debug('Binary installation completed for publish') + except Exception as e: + logger.error('Failed to install binaries for publish: %s', e) + raise # If not local only, upload the package if not self.data.local_only: + logger.debug('Starting package upload (not local only)') # Get all packages matching the created reference - ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False) - package_list = conan_api.list.select(ref_pattern) + try: + logger.debug('Creating ref pattern with ref.name: %s (ref type: %s)', ref.name, type(ref)) + ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False) + package_list = conan_api.list.select(ref_pattern) + recipe_count = len(package_list.recipes) if package_list.recipes else 0 + logger.debug('Package list retrieved: %s recipes found', recipe_count) + except AttributeError as e: + logger.error('Failed to access ref.name - ref object: %s, error: %s', ref, e) + raise + except Exception as e: + logger.error('Failed to get package list for upload: %s', e) + raise if package_list.recipes: # Use the first configured remote for upload remote = configured_remotes[0] - - # Upload the package to configured remotes - conan_api.upload.upload_full( - package_list=package_list, - remote=remote, - enabled_remotes=configured_remotes, # Only upload to configured remotes - check_integrity=False, - force=False, - metadata=None, - dry_run=False, - ) + logger.debug('Uploading to remote: %s', remote.name) + + try: + # Upload the package to configured remotes + conan_api.upload.upload_full( + package_list=package_list, + remote=remote, + enabled_remotes=configured_remotes, # Only upload to configured remotes + check_integrity=False, + force=False, + metadata=None, + dry_run=False, + ) + logger.debug('Package upload completed successfully') + except Exception as e: + logger.error('Failed to upload package: %s', e) + raise else: raise ProviderInstallationError('conan', 'No packages found to upload') + else: + logger.debug('Local only mode - skipping upload') From 2e711187f90e4c77cf5b75eaaed7b666c7b5c41b Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 10:05:31 -0400 Subject: [PATCH 18/20] Update plugin.py --- cppython/plugins/conan/plugin.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index d2fcd05a..ea7457ff 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -123,8 +123,13 @@ def _install_dependencies(self, *, update: bool = False) -> None: update_flag = None if not update else True check_updates_flag = update - logger.debug('Loading dependency graph with parameters: path=%s, remotes=%d, update=%s, check_updates=%s', - path, len(remotes), update_flag, check_updates_flag) + logger.debug( + 'Loading dependency graph with parameters: path=%s, remotes=%d, update=%s, check_updates=%s', + path, + len(remotes), + update_flag, + check_updates_flag, + ) try: deps_graph = conan_api.graph.load_graph_consumer( @@ -246,7 +251,7 @@ def publish(self) -> None: """Publishes the package using conan create workflow.""" logger = logging.getLogger('cppython.conan') logger.debug('Starting package publish workflow') - + # Get the project root directory where conanfile.py should be located project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' @@ -271,7 +276,7 @@ def publish(self) -> None: except Exception as e: logger.error('Failed to list remotes for publish: %s', e) raise - + if not self.data.local_only: # Filter remotes to only include those specified in configuration configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes] From ccfd437b159c3d3725aec50a0decc68bbaa8a44f Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 10:09:05 -0400 Subject: [PATCH 19/20] Clean Conan Plugin --- cppython/plugins/conan/plugin.py | 363 ++++++++++--------------------- 1 file changed, 112 insertions(+), 251 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index ea7457ff..01ec57e9 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -5,7 +5,6 @@ installation, and synchronization with other tools. """ -import logging from pathlib import Path from typing import Any @@ -78,128 +77,62 @@ def _install_dependencies(self, *, update: bool = False) -> None: If False, use cached versions when available. """ try: - logger = logging.getLogger('cppython.conan') - logger.debug('Starting dependency installation/update (update=%s)', update) - + # Resolve dependencies and generate conanfile.py resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] - logger.debug( - 'Resolved %d dependencies: %s', len(resolved_dependencies), [str(dep) for dep in resolved_dependencies] - ) - - # Generate conanfile.py self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies) - logger.debug('Generated conanfile.py at %s', self.core_data.project_data.project_root) # Ensure build directory exists self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True) - logger.debug('Created build path: %s', self.core_data.cppython_data.build_path) - # Initialize Conan API + # Setup paths and API conan_api = ConanAPI() - logger.debug('Conan API initialized successfully') - - # Get project paths project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' - logger.debug('Project root: %s, Conanfile path: %s', project_root, conanfile_path) if not conanfile_path.exists(): raise ProviderInstallationError('conan', 'Generated conanfile.py not found') - # Get all remotes - try: - all_remotes = conan_api.remotes.list() - logger.debug('Available remotes: %s', [remote.name for remote in all_remotes]) - except Exception as e: - logger.error('Failed to list remotes: %s', e) - raise - - # Get profiles from resolved data + all_remotes = conan_api.remotes.list() profile_host, profile_build = self.data.host_profile, self.data.build_profile - logger.debug('Using profiles - host: %s, build: %s', profile_host, profile_build) - - path = str(conanfile_path) - remotes = all_remotes - update_flag = None if not update else True - check_updates_flag = update - - logger.debug( - 'Loading dependency graph with parameters: path=%s, remotes=%d, update=%s, check_updates=%s', - path, - len(remotes), - update_flag, - check_updates_flag, + + # Load dependency graph + deps_graph = conan_api.graph.load_graph_consumer( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + lockfile=None, + remotes=all_remotes, + update=update or None, + check_updates=update, + is_build_require=False, + profile_host=profile_host, + profile_build=profile_build, ) - try: - deps_graph = conan_api.graph.load_graph_consumer( - path=path, - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=remotes, - update=update_flag, - check_updates=check_updates_flag, - is_build_require=False, - profile_host=profile_host, - profile_build=profile_build, - ) - logger.debug('Dependency graph loaded successfully, type: %s', type(deps_graph)) - if hasattr(deps_graph, 'nodes'): - logger.debug('Graph has %d nodes', len(deps_graph.nodes)) - else: - logger.warning('Dependency graph does not have nodes attribute') - except Exception as e: - logger.error('Failed to load dependency graph: %s', e) - raise - - # Analyze binaries to determine what needs to be built/downloaded - logger.debug('Starting binary analysis with build_mode=["missing"], update=%s', update) - try: - conan_api.graph.analyze_binaries( - graph=deps_graph, - build_mode=['missing'], # Only build what's missing - remotes=all_remotes, - update=None if not update else True, - lockfile=None, - ) - logger.debug('Binary analysis completed successfully') - except Exception as e: - logger.error('Failed to analyze binaries: %s', e) - raise - - # Install all dependencies - logger.debug('Starting binary installation') - try: - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) - logger.debug('Binary installation completed successfully') - except Exception as e: - logger.error('Failed to install binaries: %s', e) - raise - - # Generate files for the consumer (conandata.yml, conan_toolchain.cmake, etc.) - logger.debug('Generating consumer files with generators=["CMakeToolchain", "CMakeDeps"]') - logger.debug('Source folder: %s, Output folder: %s', project_root, self.core_data.cppython_data.build_path) - try: - conan_api.install.install_consumer( - deps_graph=deps_graph, - generators=['CMakeToolchain', 'CMakeDeps'], - source_folder=str(project_root), - output_folder=str(self.core_data.cppython_data.build_path), - ) - logger.debug('Consumer file generation completed successfully') - except Exception as e: - logger.error('Failed to install consumer files: %s', e) - raise + # Analyze and install binaries + conan_api.graph.analyze_binaries( + graph=deps_graph, + build_mode=['missing'], + remotes=all_remotes, + update=update or None, + lockfile=None, + ) + + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) - logger.debug('Successfully installed dependencies using Conan API') + # Generate consumer files + conan_api.install.install_consumer( + deps_graph=deps_graph, + generators=['CMakeToolchain', 'CMakeDeps'], + source_folder=str(project_root), + output_folder=str(self.core_data.cppython_data.build_path), + ) except Exception as e: operation = 'update' if update else 'install' - error_msg = str(e) - raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {error_msg}', e) from e + raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {e}', e) from e def install(self) -> None: """Installs the provider""" @@ -240,7 +173,7 @@ def sync_data(self, consumer: SyncConsumer) -> SyncData: top_level_includes=self.core_data.cppython_data.install_path / 'conan_provider.cmake', ) - raise NotSupportedError('OOF') + raise NotSupportedError(f'Unsupported sync types: {consumer.sync_types()}') @classmethod async def download_tooling(cls, directory: Path) -> None: @@ -249,165 +182,93 @@ async def download_tooling(cls, directory: Path) -> None: def publish(self) -> None: """Publishes the package using conan create workflow.""" - logger = logging.getLogger('cppython.conan') - logger.debug('Starting package publish workflow') - - # Get the project root directory where conanfile.py should be located project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' - logger.debug('Project root: %s, Conanfile path: %s', project_root, conanfile_path) if not conanfile_path.exists(): raise FileNotFoundError(f'conanfile.py not found at {conanfile_path}') - # Initialize Conan API - try: - conan_api = ConanAPI() - logger.debug('Conan API initialized successfully for publish') - except Exception as e: - logger.error('Failed to initialize Conan API for publish: %s', e) - raise + conan_api = ConanAPI() + all_remotes = conan_api.remotes.list() - # Get configured remotes from Conan API and filter by our configuration - # TODO: We want to replace the global conan remotes with the ones configured in CPPython. - try: - all_remotes = conan_api.remotes.list() - logger.debug('Retrieved %d remotes for publish', len(all_remotes)) - except Exception as e: - logger.error('Failed to list remotes for publish: %s', e) - raise + # Configure remotes for upload + configured_remotes = self._get_configured_remotes(all_remotes) - if not self.data.local_only: - # Filter remotes to only include those specified in configuration - configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes] - logger.debug('Configured remotes: %s', [r.name for r in configured_remotes]) - - if not configured_remotes: - available_remotes = [remote.name for remote in all_remotes] - raise ProviderConfigurationError( - 'conan', - f'No configured remotes found. Available remotes: {available_remotes}, ' - f'Configured remotes: {self.data.remotes}', - 'remotes', - ) - else: - configured_remotes = [] - logger.debug('Local only mode - no remotes configured') + # Export the recipe to cache + ref, _ = conan_api.export.export( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + lockfile=None, + remotes=all_remotes, + ) - # Step 1: Export the recipe to the cache - # This is equivalent to the export part of `conan create` - logger.debug('Starting export step with path: %s', conanfile_path) - try: - ref, conanfile = conan_api.export.export( - path=str(conanfile_path), - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=all_remotes, # Use all remotes for dependency resolution during export - ) - logger.debug('Export completed successfully. Ref: %s, Conanfile type: %s', ref, type(conanfile)) - if conanfile is None: - logger.error('Export returned None for conanfile!') - else: - conanfile_attrs = dir(conanfile) if hasattr(conanfile, '__dict__') else 'No attributes' - logger.debug('Conanfile attributes: %s', conanfile_attrs) - except Exception as e: - logger.error('Export failed: %s', e) - raise - - # Step 2: Get profiles from resolved data + # Build dependency graph and install profile_host, profile_build = self.data.host_profile, self.data.build_profile - logger.debug('Using profiles for publish - host: %s, build: %s', profile_host, profile_build) + deps_graph = conan_api.graph.load_graph_consumer( + path=str(conanfile_path), + name=None, + version=None, + user=None, + channel=None, + lockfile=None, + remotes=all_remotes, + update=None, + check_updates=False, + is_build_require=False, + profile_host=profile_host, + profile_build=profile_build, + ) + + # Analyze and build binaries + conan_api.graph.analyze_binaries( + graph=deps_graph, + build_mode=['*'], + remotes=all_remotes, + update=None, + lockfile=None, + ) + + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + + # Upload if not local only + if not self.data.local_only: + self._upload_package(conan_api, ref, configured_remotes) - # Step 3: Build dependency graph for the package - prepare parameters - path = str(conanfile_path) - remotes = all_remotes # Use all remotes for dependency resolution - logger.debug('Loading dependency graph for publish with path: %s', path) + def _get_configured_remotes(self, all_remotes): + """Get and validate configured remotes for upload.""" + if self.data.local_only: + return [] - try: - deps_graph = conan_api.graph.load_graph_consumer( - path=path, - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=remotes, - update=None, - check_updates=False, - is_build_require=False, - profile_host=profile_host, - profile_build=profile_build, - ) - logger.debug('Dependency graph loaded successfully for publish') - except Exception as e: - logger.error('Failed to load dependency graph for publish: %s', e) - raise + configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes] - # Step 4: Analyze binaries and install/build them if needed - logger.debug('Starting binary analysis for publish') - try: - conan_api.graph.analyze_binaries( - graph=deps_graph, - build_mode=['*'], # Build from source (equivalent to the create behavior) - remotes=all_remotes, # Use all remotes for dependency resolution - update=None, - lockfile=None, + if not configured_remotes: + available_remotes = [remote.name for remote in all_remotes] + raise ProviderConfigurationError( + 'conan', + f'No configured remotes found. Available: {available_remotes}, Configured: {self.data.remotes}', + 'remotes', ) - logger.debug('Binary analysis completed for publish') - except Exception as e: - logger.error('Failed to analyze binaries for publish: %s', e) - raise - - # Step 5: Install all dependencies and build the package - logger.debug('Starting binary installation for publish') - try: - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) - logger.debug('Binary installation completed for publish') - except Exception as e: - logger.error('Failed to install binaries for publish: %s', e) - raise - # If not local only, upload the package - if not self.data.local_only: - logger.debug('Starting package upload (not local only)') - # Get all packages matching the created reference - try: - logger.debug('Creating ref pattern with ref.name: %s (ref type: %s)', ref.name, type(ref)) - ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False) - package_list = conan_api.list.select(ref_pattern) - recipe_count = len(package_list.recipes) if package_list.recipes else 0 - logger.debug('Package list retrieved: %s recipes found', recipe_count) - except AttributeError as e: - logger.error('Failed to access ref.name - ref object: %s, error: %s', ref, e) - raise - except Exception as e: - logger.error('Failed to get package list for upload: %s', e) - raise - - if package_list.recipes: - # Use the first configured remote for upload - remote = configured_remotes[0] - logger.debug('Uploading to remote: %s', remote.name) - - try: - # Upload the package to configured remotes - conan_api.upload.upload_full( - package_list=package_list, - remote=remote, - enabled_remotes=configured_remotes, # Only upload to configured remotes - check_integrity=False, - force=False, - metadata=None, - dry_run=False, - ) - logger.debug('Package upload completed successfully') - except Exception as e: - logger.error('Failed to upload package: %s', e) - raise - else: - raise ProviderInstallationError('conan', 'No packages found to upload') - else: - logger.debug('Local only mode - skipping upload') + return configured_remotes + + def _upload_package(self, conan_api, ref, configured_remotes): + """Upload the package to configured remotes.""" + ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False) + package_list = conan_api.list.select(ref_pattern) + + if not package_list.recipes: + raise ProviderInstallationError('conan', 'No packages found to upload') + + remote = configured_remotes[0] + conan_api.upload.upload_full( + package_list=package_list, + remote=remote, + enabled_remotes=configured_remotes, + check_integrity=False, + force=False, + metadata=None, + dry_run=False, + ) From af2be898c5308e6e0a7ad2ae46e5614d7bc5ed43 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sat, 19 Jul 2025 17:43:56 -0400 Subject: [PATCH 20/20] Update plugin.py --- cppython/plugins/conan/plugin.py | 89 +++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 01ec57e9..9e794d72 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -76,6 +76,32 @@ def _install_dependencies(self, *, update: bool = False) -> None: update: If True, check remotes for newer versions/revisions and install those. If False, use cached versions when available. """ + try: + # Setup environment and generate conanfile + conan_api, conanfile_path = self._prepare_installation() + + # Load dependency graph + deps_graph = self._load_dependency_graph(conan_api, conanfile_path, update) + + # Install dependencies + self._install_binaries(conan_api, deps_graph, update) + + # Generate consumer files + self._generate_consumer_files(conan_api, deps_graph) + + except Exception as e: + operation = 'update' if update else 'install' + raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {e}', e) from e + + def _prepare_installation(self) -> tuple[ConanAPI, Path]: + """Prepare the installation environment and generate conanfile. + + Returns: + Tuple of (ConanAPI instance, conanfile path) + + Raises: + ProviderInstallationError: If conanfile generation or setup fails + """ try: # Resolve dependencies and generate conanfile.py resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] @@ -92,11 +118,30 @@ def _install_dependencies(self, *, update: bool = False) -> None: if not conanfile_path.exists(): raise ProviderInstallationError('conan', 'Generated conanfile.py not found') + return conan_api, conanfile_path + + except Exception as e: + raise ProviderInstallationError('conan', f'Failed to prepare installation environment: {e}', e) from e + + def _load_dependency_graph(self, conan_api: ConanAPI, conanfile_path: Path, update: bool): + """Load and build the dependency graph. + + Args: + conan_api: The Conan API instance + conanfile_path: Path to the conanfile.py + update: Whether to check for updates + + Returns: + The loaded dependency graph + + Raises: + ProviderInstallationError: If dependency graph loading fails + """ + try: all_remotes = conan_api.remotes.list() profile_host, profile_build = self.data.host_profile, self.data.build_profile - # Load dependency graph - deps_graph = conan_api.graph.load_graph_consumer( + return conan_api.graph.load_graph_consumer( path=str(conanfile_path), name=None, version=None, @@ -111,7 +156,24 @@ def _install_dependencies(self, *, update: bool = False) -> None: profile_build=profile_build, ) - # Analyze and install binaries + except Exception as e: + raise ProviderInstallationError('conan', f'Failed to load dependency graph: {e}', e) from e + + def _install_binaries(self, conan_api: ConanAPI, deps_graph, update: bool) -> None: + """Analyze and install binary dependencies. + + Args: + conan_api: The Conan API instance + deps_graph: The dependency graph + update: Whether to check for updates + + Raises: + ProviderInstallationError: If binary analysis or installation fails + """ + try: + all_remotes = conan_api.remotes.list() + + # Analyze binaries to determine what needs to be built/downloaded conan_api.graph.analyze_binaries( graph=deps_graph, build_mode=['missing'], @@ -120,9 +182,25 @@ def _install_dependencies(self, *, update: bool = False) -> None: lockfile=None, ) + # Install all dependencies conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) - # Generate consumer files + except Exception as e: + raise ProviderInstallationError('conan', f'Failed to install binary dependencies: {e}', e) from e + + def _generate_consumer_files(self, conan_api: ConanAPI, deps_graph) -> None: + """Generate consumer files (CMake toolchain, deps, etc.). + + Args: + conan_api: The Conan API instance + deps_graph: The dependency graph + + Raises: + ProviderInstallationError: If consumer file generation fails + """ + try: + project_root = self.core_data.project_data.project_root + conan_api.install.install_consumer( deps_graph=deps_graph, generators=['CMakeToolchain', 'CMakeDeps'], @@ -131,8 +209,7 @@ def _install_dependencies(self, *, update: bool = False) -> None: ) except Exception as e: - operation = 'update' if update else 'install' - raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {e}', e) from e + raise ProviderInstallationError('conan', f'Failed to generate consumer files: {e}', e) from e def install(self) -> None: """Installs the provider"""