You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

cppython

Package Overview
Dependencies
Maintainers
1
Versions
121
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cppython - pypi Package Compare versions

Comparing version
0.9.6
to
0.9.7.dev1
+1
tests/fixtures/vcpkg.py
"""Shared fixtures for VCPkg plugin tests"""
+29
-2
"""Defines the data and routines for building a CPPython project type"""
import logging
import os
from importlib.metadata import entry_points
from inspect import getmodule
from logging import Logger
from pprint import pformat
from typing import Any, cast
from rich.console import Console
from rich.logging import RichHandler
from cppython.core.plugin_schema.generator import Generator

@@ -474,4 +479,24 @@ from cppython.core.plugin_schema.provider import Provider

# Add default output stream
self._logger.addHandler(logging.StreamHandler())
# Informal standard to check for color
force_color = os.getenv('FORCE_COLOR', '1') != '0'
console = Console(
force_terminal=force_color,
color_system='auto',
width=120,
legacy_windows=False,
no_color=False,
)
rich_handler = RichHandler(
console=console,
rich_tracebacks=True,
show_time=False,
show_path=False,
markup=True,
show_level=False,
enable_link_path=False,
)
self._logger.addHandler(rich_handler)
self._logger.setLevel(Builder.levels[project_configuration.verbosity])

@@ -536,2 +561,4 @@

self._logger.debug('Project data:\n%s', pformat(dict(core_data)))
return Data(core_data, plugins, self._logger)
+2
-2

@@ -47,3 +47,3 @@ """Data types for CPPython that encapsulate the requirements between the plugins and the core library"""

@field_validator('verbosity') # type: ignore
@field_validator('verbosity')
@classmethod

@@ -125,3 +125,3 @@ def min_max(cls, value: int) -> int:

@field_validator('configuration_path', 'install_path', 'tool_path', 'build_path') # type: ignore
@field_validator('configuration_path', 'install_path', 'tool_path', 'build_path')
@classmethod

@@ -128,0 +128,0 @@ def validate_absolute_path(cls, value: Path) -> Path:

@@ -5,3 +5,9 @@ """Plugin builder"""

from cppython.plugins.cmake.schema import CMakeData, CMakePresets, CMakeSyncData, ConfigurePreset
from cppython.plugins.cmake.schema import (
BuildPreset,
CMakeData,
CMakePresets,
CMakeSyncData,
ConfigurePreset,
)

@@ -16,46 +22,4 @@

@staticmethod
def generate_provider_preset(provider_data: CMakeSyncData) -> CMakePresets:
"""Generates a provider preset from input sync data
Args:
provider_directory: The base directory to place the preset files
provider_data: The providers synchronization data
"""
generated_configure_preset = ConfigurePreset(name=provider_data.provider_name, hidden=True)
# Toss in that sync data from the provider
generated_configure_preset.cacheVariables = {
'CMAKE_PROJECT_TOP_LEVEL_INCLUDES': str(provider_data.top_level_includes.as_posix()),
}
return CMakePresets(configurePresets=[generated_configure_preset])
@staticmethod
def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData) -> None:
"""Writes a provider preset from input sync data
Args:
provider_directory: The base directory to place the preset files
provider_data: The providers synchronization data
"""
generated_preset = Builder.generate_provider_preset(provider_data)
provider_preset_file = provider_directory / f'{provider_data.provider_name}.json'
initial_preset = None
# If the file already exists, we need to compare it
if provider_preset_file.exists():
with open(provider_preset_file, encoding='utf-8') as file:
initial_json = file.read()
initial_preset = CMakePresets.model_validate_json(initial_json)
if generated_preset != initial_preset:
serialized = generated_preset.model_dump_json(exclude_none=True, by_alias=False, indent=4)
with open(provider_preset_file, 'w', encoding='utf8') as file:
file.write(serialized)
@staticmethod
def generate_cppython_preset(
cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData
cppython_preset_directory: Path, provider_preset_file: Path, provider_data: CMakeSyncData
) -> CMakePresets:

@@ -66,3 +30,3 @@ """Generates the cppython preset which inherits from the provider presets

cppython_preset_directory: The tool directory
provider_directory: The base directory containing provider presets
provider_preset_file: Path to the provider's preset file
provider_data: The provider's synchronization data

@@ -73,11 +37,22 @@

"""
generated_configure_preset = ConfigurePreset(name='cppython', inherits=provider_data.provider_name, hidden=True)
generated_preset = CMakePresets(configurePresets=[generated_configure_preset])
configure_presets = []
# Get the relative path to the provider preset file
provider_preset_file = provider_directory / f'{provider_data.provider_name}.json'
relative_preset = provider_preset_file.relative_to(cppython_preset_directory, walk_up=True).as_posix()
preset_name = 'cppython'
# Set the data
generated_preset.include = [relative_preset]
# Create a default preset that inherits from provider's default preset
default_configure = ConfigurePreset(
name=preset_name,
hidden=True,
description='Injected configuration preset for CPPython',
)
if provider_data.toolchain_file:
default_configure.toolchainFile = provider_data.toolchain_file.as_posix()
configure_presets.append(default_configure)
generated_preset = CMakePresets(
configurePresets=configure_presets,
)
return generated_preset

@@ -87,3 +62,3 @@

def write_cppython_preset(
cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData
cppython_preset_directory: Path, provider_preset_file: Path, provider_data: CMakeSyncData
) -> Path:

@@ -94,3 +69,3 @@ """Write the cppython presets which inherit from the provider presets

cppython_preset_directory: The tool directory
provider_directory: The base directory containing provider presets
provider_preset_file: Path to the provider's preset file
provider_data: The provider's synchronization data

@@ -102,3 +77,3 @@

generated_preset = Builder.generate_cppython_preset(
cppython_preset_directory, provider_directory, provider_data
cppython_preset_directory, provider_preset_file, provider_data
)

@@ -124,10 +99,8 @@ cppython_preset_file = cppython_preset_directory / 'cppython.json'

@staticmethod
def generate_root_preset(
preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData, build_directory: Path
) -> CMakePresets:
"""Generates the top level root preset with the include reference.
def _create_presets(
cmake_data: CMakeData, build_directory: Path
) -> tuple[list[ConfigurePreset], list[BuildPreset]]:
"""Create the default configure and build presets for the user.
Args:
preset_file: Preset file to modify
cppython_preset_file: Path to the cppython preset file to include
cmake_data: The CMake data to use

@@ -137,38 +110,140 @@ build_directory: The build directory to use

Returns:
A CMakePresets object
A tuple containing the configure preset and list of build presets
"""
default_configure_preset = ConfigurePreset(
name=cmake_data.configuration_name,
inherits='cppython',
binaryDir=build_directory.as_posix(),
cacheVariables={
'CMAKE_BUILD_TYPE': 'Release' # Ensure compatibility for single-config and multi-config generators
},
user_configure_presets: list[ConfigurePreset] = []
user_build_presets: list[BuildPreset] = []
name = cmake_data.configuration_name
release_name = name + '-release'
debug_name = name + '-debug'
user_configure_presets.append(
ConfigurePreset(
name=name,
description='All multi-configuration generators should inherit from this preset',
inherits='cppython',
binaryDir='${sourceDir}/' + build_directory.as_posix(),
cacheVariables={'CMAKE_CONFIGURATION_TYPES': 'Debug;Release'},
)
)
if preset_file.exists():
with open(preset_file, encoding='utf-8') as file:
initial_json = file.read()
root_preset = CMakePresets.model_validate_json(initial_json)
user_configure_presets.append(
ConfigurePreset(
name=release_name,
description='All single-configuration generators should inherit from this preset',
inherits=name,
cacheVariables={'CMAKE_BUILD_TYPE': 'Release'},
)
)
if root_preset.configurePresets is None:
root_preset.configurePresets = [default_configure_preset]
user_configure_presets.append(
ConfigurePreset(
name=debug_name,
description='All single-configuration generators should inherit from this preset',
inherits=name,
cacheVariables={'CMAKE_BUILD_TYPE': 'Debug'},
)
)
# Set defaults
preset = next((p for p in root_preset.configurePresets if p.name == default_configure_preset.name), None)
if preset:
# If the name matches, we need to verify it inherits from cppython
if preset.inherits is None:
preset.inherits = 'cppython'
elif isinstance(preset.inherits, str) and preset.inherits != 'cppython':
preset.inherits = [preset.inherits, 'cppython']
elif isinstance(preset.inherits, list) and 'cppython' not in preset.inherits:
preset.inherits.append('cppython')
else:
root_preset.configurePresets.append(default_configure_preset)
user_build_presets.append(
BuildPreset(
name=release_name,
description='An example build preset for release',
configurePreset=release_name,
)
)
user_build_presets.append(
BuildPreset(
name=debug_name,
description='An example build preset for debug',
configurePreset=debug_name,
)
)
return user_configure_presets, user_build_presets
@staticmethod
def _load_existing_preset(preset_file: Path) -> CMakePresets | None:
"""Load existing preset file if it exists.
Args:
preset_file: Path to the preset file
Returns:
CMakePresets object if file exists, None otherwise
"""
if not preset_file.exists():
return None
with open(preset_file, encoding='utf-8') as file:
initial_json = file.read()
return CMakePresets.model_validate_json(initial_json)
@staticmethod
def _update_configure_preset(existing_preset: ConfigurePreset, build_directory: Path) -> None:
"""Update an existing configure preset to ensure proper inheritance and binary directory.
Args:
existing_preset: The preset to update
build_directory: The build directory to use
"""
# Update existing preset to ensure it inherits from 'cppython'
if existing_preset.inherits is None:
existing_preset.inherits = 'cppython' # type: ignore[misc]
elif isinstance(existing_preset.inherits, str) and existing_preset.inherits != 'cppython':
existing_preset.inherits = ['cppython', existing_preset.inherits] # type: ignore[misc]
elif isinstance(existing_preset.inherits, list) and 'cppython' not in existing_preset.inherits:
existing_preset.inherits.insert(0, 'cppython')
# Update binary directory if not set
if not existing_preset.binaryDir:
existing_preset.binaryDir = '${sourceDir}/' + build_directory.as_posix() # type: ignore[misc]
@staticmethod
def _modify_presets(
root_preset: CMakePresets,
user_configure_presets: list[ConfigurePreset],
user_build_presets: list[BuildPreset],
build_directory: Path,
) -> None:
"""Handle presets in the root preset.
Args:
root_preset: The root preset to modify
user_configure_presets: The user's configure presets
user_build_presets: The user's build presets
build_directory: The build directory to use
"""
if root_preset.configurePresets is None:
root_preset.configurePresets = user_configure_presets.copy() # type: ignore[misc]
else:
# If the file doesn't exist, we need to default it for the user
root_preset = CMakePresets(configurePresets=[default_configure_preset])
# Update or add the user's configure preset
for user_configure_preset in user_configure_presets:
existing_preset = next(
(p for p in root_preset.configurePresets if p.name == user_configure_preset.name), None
)
if existing_preset:
Builder._update_configure_preset(existing_preset, build_directory)
else:
root_preset.configurePresets.append(user_configure_preset)
if root_preset.buildPresets is None:
root_preset.buildPresets = user_build_presets.copy() # type: ignore[misc]
else:
# Add build presets if they don't exist
for build_preset in user_build_presets:
existing = next((p for p in root_preset.buildPresets if p.name == build_preset.name), None)
if not existing:
root_preset.buildPresets.append(build_preset)
@staticmethod
def _modify_includes(root_preset: CMakePresets, preset_file: Path, cppython_preset_file: Path) -> None:
"""Handle include paths in the root preset.
Args:
root_preset: The root preset to modify
preset_file: Path to the preset file
cppython_preset_file: Path to the cppython preset file to include
"""
# Get the relative path to the cppython preset file

@@ -178,10 +253,39 @@ preset_directory = preset_file.parent.absolute()

# If the include key doesn't exist, we know we will write to disk afterwards
# Handle includes
if not root_preset.include:
root_preset.include = []
root_preset.include = [] # type: ignore[misc]
# Only the included preset file if it doesn't exist. Implied by the above check
if str(relative_preset) not in root_preset.include:
root_preset.include.append(str(relative_preset))
@staticmethod
def generate_root_preset(
preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData, build_directory: Path
) -> CMakePresets:
"""Generates the top level root preset with the include reference.
Args:
preset_file: Preset file to modify
cppython_preset_file: Path to the cppython preset file to include
cmake_data: The CMake data to use
build_directory: The build directory to use
Returns:
A CMakePresets object
"""
# Create user presets
user_configure_presets, user_build_presets = Builder._create_presets(cmake_data, build_directory)
# Load existing preset or create new one
root_preset = Builder._load_existing_preset(preset_file)
if root_preset is None:
root_preset = CMakePresets(
configurePresets=user_configure_presets,
buildPresets=user_build_presets,
)
else:
Builder._modify_presets(root_preset, user_configure_presets, user_build_presets, build_directory)
Builder._modify_includes(root_preset, preset_file, cppython_preset_file)
return root_preset

@@ -213,2 +317,5 @@

# Ensure that the build_directory is relative to the preset_file, allowing upward traversal
build_directory = build_directory.relative_to(preset_file.parent, walk_up=True)
root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data, build_directory)

@@ -215,0 +322,0 @@

@@ -66,8 +66,7 @@ """The CMake generator implementation"""

self._cppython_preset_directory.mkdir(parents=True, exist_ok=True)
self._provider_directory.mkdir(parents=True, exist_ok=True)
self.builder.write_provider_preset(self._provider_directory, sync_data)
cppython_preset_file = self._cppython_preset_directory / 'CPPython.json'
cppython_preset_file = self.builder.write_cppython_preset(
self._cppython_preset_directory, self._provider_directory, sync_data
self._cppython_preset_directory, cppython_preset_file, sync_data
)

@@ -74,0 +73,0 @@

@@ -13,3 +13,2 @@ """CMake plugin schema

from pydantic import Field
from pydantic.types import FilePath

@@ -51,2 +50,4 @@ from cppython.core.schema import CPPythonModel, SyncData

name: str
description: Annotated[str | None, Field(description='A human-readable description of the preset.')] = None
hidden: Annotated[bool | None, Field(description='If true, the preset is hidden and cannot be used directly.')] = (

@@ -63,11 +64,35 @@ None

] = None
toolchainFile: Annotated[
str | Path | None,
Field(description='Path to the toolchain file.'),
] = None
cacheVariables: dict[str, None | bool | str | CacheVariable] | None = None
class BuildPreset(CPPythonModel, extra='allow'):
"""Partial Build Preset specification for CMake build presets"""
name: str
description: Annotated[str | None, Field(description='A human-readable description of the preset.')] = None
hidden: Annotated[bool | None, Field(description='If true, the preset is hidden and cannot be used directly.')] = (
None
)
inherits: Annotated[
str | list[str] | None, Field(description='The inherits field allows inheriting from other presets.')
] = None
configurePreset: Annotated[
str | None,
Field(description='The name of a configure preset to associate with this build preset.'),
] = None
configuration: Annotated[
str | None,
Field(description='Build configuration. Equivalent to --config on the command line.'),
] = None
class CMakePresets(CPPythonModel, extra='allow'):
"""The schema for the CMakePresets and CMakeUserPresets files.
"""The schema for the CMakePresets and CMakeUserPresets files."""
The only information needed is the configure preset list for cache variable injection
"""
version: Annotated[int, Field(description='The version of the JSON schema.')] = 9

@@ -78,2 +103,3 @@ include: Annotated[

configurePresets: Annotated[list[ConfigurePreset] | None, Field(description='The list of configure presets')] = None
buildPresets: Annotated[list[BuildPreset] | None, Field(description='The list of build presets')] = None

@@ -84,3 +110,3 @@

top_level_includes: FilePath
toolchain_file: Path | None = None

@@ -106,3 +132,8 @@

configuration_name: Annotated[
str, Field(description='The CMake configuration preset to look for and override inside the given `preset_file`')
str,
Field(
description='The CMake configuration preset to look for and override inside the given `preset_file`. '
'Additional configurations will be added using this option as the base. For example, given "default", '
'"default-release" will also be written'
),
] = 'default'

@@ -124,14 +124,14 @@ """Construction of Conan data"""

@staticmethod
def _create_conanfile(conan_file: Path, dependencies: list[ConanDependency]) -> None:
def _create_conanfile(conan_file: Path, dependencies: list[ConanDependency], name: str, version: str) -> None:
"""Creates a conanfile.py file with the necessary content."""
template_string = """
from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
from conan.tools.files import copy
class MyProject(ConanFile):
name = "myproject"
version = "1.0"
class AutoPackage(ConanFile):
name = "${name}"
version = "${version}"
settings = "os", "compiler", "build_type", "arch"
requires = ${dependencies}
generators = "CMakeDeps"

@@ -141,10 +141,33 @@ def layout(self):

def generate(self):
deps = CMakeDeps(self)
deps.generate()
tc = CMakeToolchain(self)
tc.user_presets_path = None
tc.generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()"""
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()
def package_info(self):
self.cpp_info.libs = ["${name}"]
def export_sources(self):
copy(self, "CMakeLists.txt", src=self.recipe_folder, dst=self.export_sources_folder)
copy(self, "include/*", src=self.recipe_folder, dst=self.export_sources_folder)
copy(self, "src/*", src=self.recipe_folder, dst=self.export_sources_folder)
copy(self, "cmake/*", src=self.recipe_folder, dst=self.export_sources_folder)
"""
template = Template(dedent(template_string))
values = {
'name': name,
'version': version,
'dependencies': [dependency.requires() for dependency in dependencies],

@@ -158,3 +181,5 @@ }

def generate_conanfile(self, directory: DirectoryPath, dependencies: list[ConanDependency]) -> None:
def generate_conanfile(
self, directory: DirectoryPath, dependencies: list[ConanDependency], name: str, version: str
) -> None:
"""Generate a conanfile.py file for the project."""

@@ -173,2 +198,2 @@ conan_file = directory / self._filename

directory.mkdir(parents=True, exist_ok=True)
self._create_conanfile(conan_file, dependencies)
self._create_conanfile(conan_file, dependencies, name, version)

@@ -8,8 +8,9 @@ """Conan Provider Plugin

import os
from logging import Logger, getLogger
from pathlib import Path
from typing import Any
import requests
from conan.api.conan_api import ConanAPI
from conan.api.model import ListPattern
from conan.cli.cli import Cli

@@ -24,3 +25,3 @@ from cppython.core.plugin_schema.generator import SyncConsumer

from cppython.plugins.conan.schema import ConanData
from cppython.utility.exception import NotSupportedError, ProviderConfigurationError, ProviderInstallationError
from cppython.utility.exception import NotSupportedError, ProviderInstallationError
from cppython.utility.utility import TypeName

@@ -32,4 +33,2 @@

_provider_url = 'https://raw.githubusercontent.com/conan-io/cmake-conan/refs/heads/develop2/conan_provider.cmake'
def __init__(

@@ -44,12 +43,10 @@ self, group_data: ProviderPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any]

self.builder = Builder()
# Initialize ConanAPI once and reuse it
self._conan_api = ConanAPI()
# Initialize CLI for command API to work properly
self._cli = Cli(self._conan_api)
self._cli.add_commands()
@staticmethod
def _download_file(url: str, file: Path) -> None:
"""Replaces the given file with the contents of the url"""
file.parent.mkdir(parents=True, exist_ok=True)
self._ensure_default_profiles()
with open(file, 'wb') as out_file:
content = requests.get(url, stream=True).content
out_file.write(content)
@staticmethod

@@ -77,3 +74,3 @@ def features(directory: Path) -> SupportedFeatures:

def _install_dependencies(self, *, update: bool = False) -> None:
"""Install/update dependencies using Conan API.
"""Install/update dependencies using Conan CLI.

@@ -85,6 +82,7 @@ Args:

operation = 'update' if update else 'install'
logger = getLogger('cppython.conan')
try:
# Setup environment and generate conanfile
conan_api, conanfile_path = self._prepare_installation()
conanfile_path = self._prepare_installation()
except Exception as e:

@@ -94,28 +92,23 @@ raise ProviderInstallationError('conan', f'Failed to prepare {operation} environment: {e}', e) from e

try:
# Load dependency graph
deps_graph = self._load_dependency_graph(conan_api, conanfile_path, update)
build_types = ['Release', 'Debug']
for build_type in build_types:
logger.info('Installing dependencies for build type: %s', build_type)
self._run_conan_install(conanfile_path, update, build_type, logger)
except Exception as e:
raise ProviderInstallationError('conan', f'Failed to load dependency graph: {e}', e) from e
raise ProviderInstallationError('conan', f'Failed to install dependencies: {e}', e) from e
try:
# Install dependencies
self._install_binaries(conan_api, deps_graph, update)
except Exception as e:
raise ProviderInstallationError('conan', f'Failed to install binary dependencies: {e}', e) from e
try:
# Generate consumer files
self._generate_consumer_files(conan_api, deps_graph)
except Exception as e:
raise ProviderInstallationError('conan', f'Failed to generate consumer files: {e}', e) from e
def _prepare_installation(self) -> tuple[ConanAPI, Path]:
def _prepare_installation(self) -> Path:
"""Prepare the installation environment and generate conanfile.
Returns:
Tuple of (ConanAPI instance, conanfile path)
Path to conanfile.py
"""
# Resolve dependencies and generate conanfile.py
resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies]
self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies)
self.builder.generate_conanfile(
self.core_data.project_data.project_root,
resolved_dependencies,
self.core_data.pep621_data.name,
self.core_data.pep621_data.version,
)

@@ -125,4 +118,3 @@ # Ensure build directory exists

# Setup paths and API
conan_api = ConanAPI()
# Setup paths
project_root = self.core_data.project_data.project_root

@@ -134,71 +126,53 @@ conanfile_path = project_root / 'conanfile.py'

return conan_api, conanfile_path
return conanfile_path
def _load_dependency_graph(self, conan_api: ConanAPI, conanfile_path: Path, update: bool):
"""Load and build the dependency graph.
def _ensure_default_profiles(self) -> None:
"""Ensure default Conan profiles exist, creating them if necessary."""
try:
self._conan_api.profiles.get_default_host()
self._conan_api.profiles.get_default_build()
except Exception:
# If profiles don't exist, create them using profile detect
self._conan_api.command.run(['profile', 'detect'])
def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str, logger: Logger) -> None:
"""Run conan install command using Conan API with optional build type.
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
build_type: Build type (Release, Debug, etc.) or None for default
logger: Logger instance
"""
all_remotes = conan_api.remotes.list()
profile_host, profile_build = self.data.host_profile, self.data.build_profile
# Build conan install command arguments
command_args = ['install', str(conanfile_path)]
return 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,
)
# Add build missing flag
command_args.extend(['--build', 'missing'])
def _install_binaries(self, conan_api: ConanAPI, deps_graph, update: bool) -> None:
"""Analyze and install binary dependencies.
# Add update flag if needed
if update:
command_args.append('--update')
Args:
conan_api: The Conan API instance
deps_graph: The dependency graph
update: Whether to check for updates
"""
all_remotes = conan_api.remotes.list()
# Add build type setting if specified
if build_type:
command_args.extend(['-s', f'build_type={build_type}'])
# Analyze binaries to determine what needs to be built/downloaded
conan_api.graph.analyze_binaries(
graph=deps_graph,
build_mode=['missing'],
remotes=all_remotes,
update=update or None,
lockfile=None,
)
# Log the command being executed
logger.info('Executing conan command: conan %s', ' '.join(command_args))
# Install all dependencies
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes)
try:
# Use reusable Conan API instance instead of subprocess
# Change to project directory since Conan API might not handle cwd like subprocess
original_cwd = os.getcwd()
try:
os.chdir(str(self.core_data.project_data.project_root))
self._conan_api.command.run(command_args)
finally:
os.chdir(original_cwd)
except Exception as e:
error_msg = str(e)
logger.error('Conan install failed: %s', error_msg, exc_info=True)
raise ProviderInstallationError('conan', error_msg, 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
"""
project_root = self.core_data.project_data.project_root
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),
)
def install(self) -> None:

@@ -238,14 +212,29 @@ """Installs the provider"""

if sync_type == CMakeSyncData:
return CMakeSyncData(
provider_name=TypeName('conan'),
top_level_includes=self.core_data.cppython_data.install_path / 'conan_provider.cmake',
)
return self._create_cmake_sync_data()
raise NotSupportedError(f'Unsupported sync types: {consumer.sync_types()}')
def _create_cmake_sync_data(self) -> CMakeSyncData:
"""Creates CMake synchronization data with Conan toolchain configuration.
Returns:
CMakeSyncData configured for Conan integration
"""
conan_toolchain_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake'
return CMakeSyncData(
provider_name=TypeName('conan'),
toolchain_file=conan_toolchain_path,
)
@classmethod
async def download_tooling(cls, directory: Path) -> None:
"""Downloads the conan provider file"""
cls._download_file(cls._provider_url, directory / 'conan_provider.cmake')
"""Download external tooling required by the Conan provider.
Since we're using CMakeToolchain generator instead of cmake-conan provider,
no external tooling needs to be downloaded.
"""
# No external tooling required when using CMakeToolchain
pass
def publish(self) -> None:

@@ -255,2 +244,3 @@ """Publishes the package using conan create workflow."""

conanfile_path = project_root / 'conanfile.py'
logger = getLogger('cppython.conan')

@@ -260,95 +250,60 @@ if not conanfile_path.exists():

conan_api = ConanAPI()
try:
# Build conan create command arguments
command_args = ['create', str(conanfile_path)]
all_remotes = conan_api.remotes.list()
# Add build mode (build everything for publishing)
command_args.extend(['--build', 'missing'])
# Configure remotes for upload
configured_remotes = self._get_configured_remotes(all_remotes)
# Log the command being executed
logger.info('Executing conan create command: conan %s', ' '.join(command_args))
# 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,
)
# Run conan create using reusable Conan API instance
# Change to project directory since Conan API might not handle cwd like subprocess
original_cwd = os.getcwd()
try:
os.chdir(str(project_root))
self._conan_api.command.run(command_args)
finally:
os.chdir(original_cwd)
# Build dependency graph and install
profile_host, profile_build = self.data.host_profile, self.data.build_profile
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, # Use all remotes for dependency resolution
update=None,
check_updates=False,
is_build_require=False,
profile_host=profile_host,
profile_build=profile_build,
)
# Upload if not skipped
if not self.data.skip_upload:
self._upload_package(logger)
# Analyze and build binaries
conan_api.graph.analyze_binaries(
graph=deps_graph,
build_mode=['*'],
remotes=all_remotes, # Use all remotes for dependency resolution
update=None,
lockfile=None,
)
except Exception as e:
error_msg = str(e)
logger.error('Conan create failed: %s', error_msg, exc_info=True)
raise ProviderInstallationError('conan', error_msg, e) from e
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes)
if not self.data.skip_upload:
self._upload_package(conan_api, ref, configured_remotes)
def _get_configured_remotes(self, all_remotes):
"""Get and validate configured remotes for upload.
Note: This only affects upload behavior. For dependency resolution,
we always use all available system remotes regardless of this config.
"""
# If skip_upload is True, don't upload anywhere
if self.data.skip_upload:
return []
# If no remotes specified, upload to all available remotes
def _upload_package(self, logger) -> None:
"""Upload the package to configured remotes using Conan API."""
# If no remotes configured, upload to all remotes
if not self.data.remotes:
return all_remotes
# Upload to all available remotes
command_args = ['upload', '*', '--all', '--confirm']
else:
# Upload only to specified remotes
for remote in self.data.remotes:
command_args = ['upload', '*', '--remote', remote, '--all', '--confirm']
# Otherwise, upload only to specified remotes
configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes]
# Log the command being executed
logger.info('Executing conan upload command: conan %s', ' '.join(command_args))
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',
)
try:
self._conan_api.command.run(command_args)
except Exception as e:
error_msg = str(e)
logger.error('Conan upload failed for remote %s: %s', remote, error_msg, exc_info=True)
raise ProviderInstallationError('conan', f'Upload to {remote} failed: {error_msg}', e) from e
return
return configured_remotes
# Log the command for uploading to all remotes
logger.info('Executing conan upload command: conan %s', ' '.join(command_args))
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,
)
try:
self._conan_api.command.run(command_args)
except Exception as e:
error_msg = str(e)
logger.error('Conan upload failed: %s', error_msg, exc_info=True)
raise ProviderInstallationError('conan', error_msg, e) from e
"""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
from conan.internal.model.profile import Profile
from packaging.requirements import Requirement

@@ -21,191 +17,4 @@

)
from cppython.utility.exception import ProviderConfigurationError
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')
# Get global configuration
global_conf = conan_api.config.global_conf
# 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')
# 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)
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 _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, cmake_program: str | None = None
) -> 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
cmake_program: Optional path to cmake program to configure in profiles
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)
_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))
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)
_apply_cmake_config_to_profile(profile, cmake_program, profile_type)
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)
_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))
# 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, cmake_program)
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 _handle_single_specifier(name: str, specifier) -> ConanDependency:

@@ -261,3 +70,3 @@ """Handle a single version specifier."""

# Handle multiple specifiers - convert to Conan range syntax
range_parts = []
range_parts: list[str] = []

@@ -310,18 +119,11 @@ # Define order for operators to ensure consistent output

# Initialize Conan API for profile resolution
conan_api = ConanAPI()
profile_dir = Path(parsed_data.profile_dir)
# Try to detect cmake program path from current virtual environment
cmake_program = _detect_cmake_program()
if not profile_dir.is_absolute():
profile_dir = core_data.cppython_data.tool_path / profile_dir
# Resolve profiles
host_profile, build_profile = _resolve_profiles(
parsed_data.host_profile, parsed_data.build_profile, conan_api, cmake_program
)
return ConanData(
remotes=parsed_data.remotes,
skip_upload=parsed_data.skip_upload,
host_profile=host_profile,
build_profile=build_profile,
profile_dir=profile_dir,
)

@@ -9,5 +9,5 @@ """Conan plugin schema

import re
from pathlib import Path
from typing import Annotated
from conan.internal.model.profile import Profile
from pydantic import Field, field_validator

@@ -298,4 +298,3 @@

skip_upload: bool
host_profile: Profile
build_profile: Profile
profile_dir: Path

@@ -312,17 +311,11 @@

bool,
Field(description='If true, skip uploading packages during publish (local-only mode).'),
Field(description='If true, skip uploading packages to a remote during publishing.'),
] = False
host_profile: Annotated[
str | None,
profile_dir: Annotated[
str,
Field(
description='Conan host profile defining the target platform where the built software will run. '
'Used for cross-compilation scenarios.'
description='Directory containing Conan profiles. Profiles will be looked up relative to this directory. '
'If profiles do not exist in this directory, Conan will fall back to default profiles.'
"If a relative path is provided, it will be resolved relative to the tool's working directory."
),
] = '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'
] = 'profiles'

@@ -36,2 +36,35 @@ """The vcpkg provider implementation"""

@staticmethod
def _handle_subprocess_error(
logger_instance, operation: str, error: subprocess.CalledProcessError, exception_class: type
) -> None:
"""Handles subprocess errors with comprehensive error message formatting.
Args:
logger_instance: The logger instance to use for error logging
operation: Description of the operation that failed (e.g., 'install', 'clone')
error: The CalledProcessError exception
exception_class: The exception class to raise
Raises:
The specified exception_class with the formatted error message
"""
# Capture both stdout and stderr for better error reporting
stdout_msg = error.stdout.strip() if error.stdout else ''
stderr_msg = error.stderr.strip() if error.stderr else ''
# Combine both outputs for comprehensive error message
error_parts = []
if stderr_msg:
error_parts.append(f'stderr: {stderr_msg}')
if stdout_msg:
error_parts.append(f'stdout: {stdout_msg}')
if not error_parts:
error_parts.append(f'Command failed with exit code {error.returncode}')
error_msg = ' | '.join(error_parts)
logger_instance.error('Unable to %s: %s', operation, error_msg, exc_info=True)
raise exception_class('vcpkg', operation, error_msg, error) from error
@staticmethod
def features(directory: Path) -> SupportedFeatures:

@@ -86,2 +119,3 @@ """Queries vcpkg support

capture_output=True,
text=True,
)

@@ -95,7 +129,6 @@ elif system_name == 'posix':

capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
logger.error('Unable to bootstrap the vcpkg repository: %s', error_msg, exc_info=True)
raise ProviderToolingError('vcpkg', 'bootstrap', error_msg, e) from e
cls._handle_subprocess_error(logger, 'bootstrap the vcpkg repository', e, ProviderToolingError)

@@ -116,9 +149,20 @@ def sync_data(self, consumer: SyncConsumer) -> SyncData:

if sync_type == CMakeSyncData:
return CMakeSyncData(
provider_name=TypeName('vcpkg'),
top_level_includes=self.core_data.cppython_data.install_path / 'scripts/buildsystems/vcpkg.cmake',
)
return self._create_cmake_sync_data()
raise NotSupportedError('OOF')
def _create_cmake_sync_data(self) -> CMakeSyncData:
"""Creates CMake synchronization data with vcpkg configuration.
Returns:
CMakeSyncData configured for vcpkg integration
"""
# Create CMakeSyncData with vcpkg configuration
vcpkg_cmake_path = self.core_data.cppython_data.install_path / 'scripts/buildsystems/vcpkg.cmake'
return CMakeSyncData(
provider_name=TypeName('vcpkg'),
toolchain_file=vcpkg_cmake_path,
)
@classmethod

@@ -165,2 +209,3 @@ def tooling_downloaded(cls, path: Path) -> bool:

capture_output=True,
text=True,
)

@@ -172,7 +217,6 @@ subprocess.run(

capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
logger.error('Unable to update the vcpkg repository: %s', error_msg, exc_info=True)
raise ProviderToolingError('vcpkg', 'update', error_msg, e) from e
cls._handle_subprocess_error(logger, 'update the vcpkg repository', e, ProviderToolingError)
else:

@@ -188,8 +232,7 @@ try:

capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
logger.error('Unable to clone the vcpkg repository: %s', error_msg, exc_info=True)
raise ProviderToolingError('vcpkg', 'clone', error_msg, e) from e
cls._handle_subprocess_error(logger, 'clone the vcpkg repository', e, ProviderToolingError)

@@ -219,7 +262,6 @@ cls._update_provider(directory)

capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
logger.error('Unable to install project dependencies: %s', error_msg, exc_info=True)
raise ProviderInstallationError('vcpkg', error_msg, e) from e
self._handle_subprocess_error(logger, 'install project dependencies', e, ProviderInstallationError)

@@ -248,7 +290,6 @@ def update(self) -> None:

capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
logger.error('Unable to update project dependencies: %s', error_msg, exc_info=True)
raise ProviderInstallationError('vcpkg', error_msg, e) from e
self._handle_subprocess_error(logger, 'update project dependencies', e, ProviderInstallationError)

@@ -255,0 +296,0 @@ def publish(self) -> None:

@@ -101,3 +101,12 @@ """Manages data flow to and from plugins"""

"""
if not self._enabled:
self.logger.info('Skipping publish because the project is not enabled')
return
self.logger.info('Publishing project')
# Ensure sync is performed before publishing to generate necessary files
self._data.sync()
# Let provider handle its own exceptions for better error context
self._data.plugins.provider.publish()
Metadata-Version: 2.1
Name: cppython
Version: 0.9.6
Version: 0.9.7.dev1
Summary: A Python management solution for C++ dependencies

@@ -14,3 +14,3 @@ Author-Email: Synodic Software <contact@synodic.software>

Requires-Dist: requests>=2.32.4
Requires-Dist: types-requests>=2.32.4.20250611
Requires-Dist: types-requests>=2.32.4.20250809
Provides-Extra: pytest

@@ -20,9 +20,9 @@ Requires-Dist: pytest>=8.4.1; extra == "pytest"

Provides-Extra: git
Requires-Dist: dulwich>=0.23.2; extra == "git"
Requires-Dist: dulwich>=0.24.1; extra == "git"
Provides-Extra: pdm
Requires-Dist: pdm>=2.25.4; extra == "pdm"
Requires-Dist: pdm>=2.25.6; extra == "pdm"
Provides-Extra: cmake
Requires-Dist: cmake>=4.0.3; extra == "cmake"
Requires-Dist: cmake>=4.1.0; extra == "cmake"
Provides-Extra: conan
Requires-Dist: conan>=2.18.1; extra == "conan"
Requires-Dist: conan>=2.19.1; extra == "conan"
Requires-Dist: libcst>=1.8.2; extra == "conan"

@@ -29,0 +29,0 @@ Description-Content-Type: text/markdown

@@ -15,5 +15,5 @@ [project]

"requests>=2.32.4",
"types-requests>=2.32.4.20250611",
"types-requests>=2.32.4.20250809",
]
version = "0.9.6"
version = "0.9.7.dev1"

@@ -29,12 +29,12 @@ [project.license]

git = [
"dulwich>=0.23.2",
"dulwich>=0.24.1",
]
pdm = [
"pdm>=2.25.4",
"pdm>=2.25.6",
]
cmake = [
"cmake>=4.0.3",
"cmake>=4.1.0",
]
conan = [
"conan>=2.18.1",
"conan>=2.19.1",
"libcst>=1.8.2",

@@ -68,4 +68,4 @@ ]

lint = [
"ruff>=0.12.4",
"pyrefly>=0.24.2",
"ruff>=0.12.9",
"pyrefly>=0.28.1",
]

@@ -122,2 +122,7 @@ test = [

[tool.pyrefly]
project-excludes = [
"examples",
]
[tool.pdm]

@@ -124,0 +129,0 @@ plugins = [

@@ -169,3 +169,2 @@ """Shared fixtures for Conan plugin tests"""

plugin: ConanProvider,
conan_mock_api: Mock,
mocker: MockerFixture,

@@ -177,3 +176,2 @@ ) -> dict[str, Mock]:

plugin: The plugin instance
conan_mock_api: Mock ConanAPI instance
mocker: Pytest mocker fixture

@@ -190,4 +188,5 @@

# Mock ConanAPI constructor
mock_conan_api_constructor = mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api)
# Mock subprocess.run to simulate successful command execution
mock_subprocess_run = mocker.patch('cppython.plugins.conan.plugin.subprocess.run')
mock_subprocess_run.return_value = mocker.Mock(returncode=0)

@@ -202,6 +201,11 @@ # Mock resolve_conan_dependency

# Mock getLogger to avoid logging setup issues
mock_logger = mocker.Mock()
mocker.patch('cppython.plugins.conan.plugin.getLogger', return_value=mock_logger)
return {
'builder': mock_builder,
'conan_api_constructor': mock_conan_api_constructor,
'subprocess_run': mock_subprocess_run,
'resolve_conan_dependency': mock_resolve_conan_dependency,
'logger': mock_logger,
}

@@ -24,39 +24,81 @@ """Integration tests for the conan and CMake project variation.

@staticmethod
def test_simple(example_runner: CliRunner) -> None:
"""Simple project"""
# Create project configuration
def _create_project(skip_upload: bool = True) -> Project:
"""Create a project instance with common configuration."""
project_root = Path.cwd()
project_configuration = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True)
# Create console interface
config = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True)
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)
if skip_upload:
TestConanCMake._ensure_conan_config(pyproject_data)
pyproject_data['tool']['cppython']['providers']['conan']['skip_upload'] = True
# Call install directly to get structured results
project.install()
return Project(config, interface, pyproject_data)
# Run the CMake configuration command
@staticmethod
def _run_cmake_configure() -> None:
"""Run CMake configuration and assert success."""
result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False)
assert result.returncode == 0, f'CMake configuration failed: {result.stderr}'
assert result.returncode == 0, f'Cmake failed: {result.stderr}'
@staticmethod
def _run_cmake_build() -> None:
"""Run CMake build and assert success."""
result = subprocess.run(['cmake', '--build', 'build'], capture_output=True, text=True, check=False)
assert result.returncode == 0, f'CMake build failed: {result.stderr}'
path = Path('build').absolute()
@staticmethod
def _verify_build_artifacts() -> Path:
"""Verify basic build artifacts exist and return build path."""
build_path = Path('build').absolute()
assert (build_path / 'CMakeCache.txt').exists(), f'CMakeCache.txt not found in {build_path}'
return build_path
# Verify that the build directory contains the expected files
assert (path / 'CMakeCache.txt').exists(), f'{path / "CMakeCache.txt"} not found'
@staticmethod
def _ensure_conan_config(pyproject_data: dict) -> None:
"""Helper method to ensure Conan configuration exists in pyproject data"""
if 'tool' not in pyproject_data:
pyproject_data['tool'] = {}
if 'cppython' not in pyproject_data['tool']:
pyproject_data['tool']['cppython'] = {}
if 'providers' not in pyproject_data['tool']['cppython']:
pyproject_data['tool']['cppython']['providers'] = {}
if 'conan' not in pyproject_data['tool']['cppython']['providers']:
pyproject_data['tool']['cppython']['providers']['conan'] = {}
# --- Setup for Publish with modified config ---
# Modify the in-memory representation of the pyproject data
pyproject_data['tool']['cppython']['providers']['conan']['skip_upload'] = True
@staticmethod
def test_simple(example_runner: CliRunner) -> None:
"""Simple project"""
# Create project and install dependencies
project = TestConanCMake._create_project(skip_upload=False)
project.install()
# Create a new project instance with the modified configuration for the 'publish' step
publish_project = Project(project_configuration, interface, pyproject_data)
# Configure and verify build
TestConanCMake._run_cmake_configure()
TestConanCMake._verify_build_artifacts()
# Publish the project to the local cache
# Test publishing with skip_upload enabled
publish_project = TestConanCMake._create_project(skip_upload=True)
publish_project.publish()
@staticmethod
def test_library(example_runner: CliRunner) -> None:
"""Test library creation and packaging workflow"""
# Create project and install dependencies
project = TestConanCMake._create_project(skip_upload=False)
project.install()
# Configure, build, and verify
TestConanCMake._run_cmake_configure()
TestConanCMake._run_cmake_build()
build_path = TestConanCMake._verify_build_artifacts()
# Verify library files exist (platform-specific)
lib_files = list(build_path.glob('**/libmathutils.*')) + list(build_path.glob('**/mathutils.lib'))
assert len(lib_files) > 0, f'No library files found in {build_path}'
# Package the library to local cache
publish_project = TestConanCMake._create_project(skip_upload=True)
publish_project.publish()

@@ -9,2 +9,3 @@ """Integration tests for the vcpkg and CMake project variation.

from pathlib import Path
from tomllib import loads

@@ -14,5 +15,10 @@ import pytest

pytest_plugins = ['tests.fixtures.example']
from cppython.console.schema import ConsoleInterface
from cppython.core.schema import ProjectConfiguration
from cppython.project import Project
pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.vcpkg']
@pytest.mark.skip(reason='Address file locks.')
class TestVcpkgCMake:

@@ -22,10 +28,36 @@ """Test project variation of vcpkg and CMake"""

@staticmethod
@pytest.mark.skip(reason='TODO')
def _create_project(skip_upload: bool = True) -> Project:
"""Create a project instance with common configuration."""
project_root = Path.cwd()
config = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True)
interface = ConsoleInterface()
pyproject_path = project_root / 'pyproject.toml'
pyproject_data = loads(pyproject_path.read_text(encoding='utf-8'))
if skip_upload:
TestVcpkgCMake._ensure_vcpkg_config(pyproject_data)
pyproject_data['tool']['cppython']['providers']['vcpkg']['skip_upload'] = True
return Project(config, interface, pyproject_data)
@staticmethod
def _ensure_vcpkg_config(pyproject_data: dict) -> None:
"""Helper method to ensure Vcpkg configuration exists in pyproject data"""
if 'tool' not in pyproject_data:
pyproject_data['tool'] = {}
if 'cppython' not in pyproject_data['tool']:
pyproject_data['tool']['cppython'] = {}
if 'providers' not in pyproject_data['tool']['cppython']:
pyproject_data['tool']['cppython']['providers'] = {}
if 'vcpkg' not in pyproject_data['tool']['cppython']['providers']:
pyproject_data['tool']['cppython']['providers']['vcpkg'] = {}
@staticmethod
def test_simple(example_runner: CliRunner) -> None:
"""Simple project"""
# By nature of running the test, we require PDM to develop the project and so it will be installed
result = subprocess.run(['pdm', 'install'], capture_output=True, text=True, check=False)
# Create project and install dependencies
project = TestVcpkgCMake._create_project(skip_upload=False)
project.install()
assert result.returncode == 0, f'PDM install failed: {result.stderr}'
# Run the CMake configuration command

@@ -32,0 +64,0 @@ result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False)

"""Tests for CMakePresets"""
from pathlib import Path
import json

@@ -57,39 +57,2 @@ from cppython.core.schema import ProjectData

@staticmethod
def test_provider_write(tmp_path: Path) -> None:
"""Verifies that the provider preset writing works as intended
Args:
tmp_path: The input path the use
"""
builder = Builder()
includes_file = tmp_path / 'includes.cmake'
with includes_file.open('w', encoding='utf-8') as file:
file.write('example contents')
data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file)
builder.write_provider_preset(tmp_path, data)
@staticmethod
def test_cppython_write(tmp_path: Path) -> None:
"""Verifies that the cppython preset writing works as intended
Args:
tmp_path: The input path the use
"""
builder = Builder()
provider_directory = tmp_path / 'providers'
provider_directory.mkdir(parents=True, exist_ok=True)
includes_file = provider_directory / 'includes.cmake'
with includes_file.open('w', encoding='utf-8') as file:
file.write('example contents')
data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file)
builder.write_provider_preset(provider_directory, data)
builder.write_cppython_preset(tmp_path, provider_directory, data)
@staticmethod
def test_root_write(project_data: ProjectData) -> None:

@@ -120,7 +83,12 @@ """Verifies that the root preset writing works as intended

data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file)
builder.write_provider_preset(provider_directory, data)
# Create a mock provider preset file
provider_preset_file = provider_directory / 'CMakePresets.json'
provider_preset_data = {'version': 3, 'configurePresets': [{'name': 'test-provider-base', 'hidden': True}]}
with provider_preset_file.open('w') as f:
json.dump(provider_preset_data, f)
cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data)
data = CMakeSyncData(provider_name=TypeName('test-provider'))
cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_preset_file, data)
build_directory = project_data.project_root / 'build'

@@ -162,7 +130,12 @@ builder.write_root_presets(

data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file)
builder.write_provider_preset(provider_directory, data)
# Create a mock provider preset file
provider_preset_file = provider_directory / 'CMakePresets.json'
provider_preset_data = {'version': 3, 'configurePresets': [{'name': 'test-provider-base', 'hidden': True}]}
with provider_preset_file.open('w') as f:
json.dump(provider_preset_data, f)
cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data)
data = CMakeSyncData(provider_name=TypeName('test-provider'))
cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_preset_file, data)
build_directory = project_data.project_root / 'build'

@@ -169,0 +142,0 @@ builder.write_root_presets(

"""Unit tests for the conan plugin install functionality"""
from pathlib import Path
from typing import Any
from unittest.mock import Mock
import pytest
from packaging.requirements import Requirement
from pytest_mock import MockerFixture
from cppython.plugins.conan.plugin import ConanProvider
from cppython.plugins.conan.schema import ConanDependency
from cppython.test.pytest.mixins import ProviderPluginTestMixin
from cppython.utility.exception import ProviderInstallationError
# Constants for test assertions
EXPECTED_PROFILE_CALLS = 2
EXPECTED_GET_PROFILE_CALLS = 2
# Use shared fixtures

@@ -49,125 +39,1 @@ pytest_plugins = ['tests.fixtures.conan']

return ConanProvider
def test_with_dependencies(
self,
plugin: ConanProvider,
conan_temp_conanfile: Path,
conan_mock_dependencies: list[Requirement],
conan_setup_mocks: dict[str, Mock],
) -> None:
"""Test install method with dependencies and existing conanfile
Args:
plugin: The plugin instance
conan_temp_conanfile: Path to temporary conanfile.py
conan_mock_dependencies: List of mock dependencies
conan_setup_mocks: Dictionary containing all mocks
"""
# Setup dependencies
plugin.core_data.cppython_data.dependencies = conan_mock_dependencies
# Execute
plugin.install()
# Verify builder was called
conan_setup_mocks['builder'].generate_conanfile.assert_called_once()
assert (
conan_setup_mocks['builder'].generate_conanfile.call_args[0][0]
== plugin.core_data.project_data.project_root
)
assert len(conan_setup_mocks['builder'].generate_conanfile.call_args[0][1]) == EXPECTED_DEPENDENCY_COUNT
# Verify dependency resolution was called
assert conan_setup_mocks['resolve_conan_dependency'].call_count == EXPECTED_DEPENDENCY_COUNT
# Verify build path was created
assert plugin.core_data.cppython_data.build_path.exists()
# Verify ConanAPI constructor was called
conan_setup_mocks['conan_api_constructor'].assert_called_once()
def test_conan_command_failure(
self,
plugin: ConanProvider,
conan_temp_conanfile: Path,
conan_mock_dependencies: list[Requirement],
conan_mock_api: Mock,
mocker: MockerFixture,
) -> None:
"""Test install method when conan API operations fail
Args:
plugin: The plugin instance
conan_temp_conanfile: Path to temporary conanfile.py
conan_mock_dependencies: List of mock dependencies
conan_mock_api: Mock ConanAPI instance
mocker: Pytest mocker fixture
"""
# Mock builder
mock_builder = mocker.Mock()
mock_builder.generate_conanfile = mocker.Mock()
plugin.builder = mock_builder # type: ignore[attr-defined]
# Configure the API mock to fail on graph loading
conan_mock_api.graph.load_graph_consumer.side_effect = Exception('Conan API error: package not found')
# Mock ConanAPI constructor to return our configured mock
mock_conan_api_constructor = mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api)
# Mock resolve_conan_dependency
def mock_resolve(requirement: Requirement) -> ConanDependency:
return ConanDependency(name=requirement.name)
mocker.patch('cppython.plugins.conan.plugin.resolve_conan_dependency', side_effect=mock_resolve)
# Add a dependency
plugin.core_data.cppython_data.dependencies = [conan_mock_dependencies[0]]
# Execute and verify exception is raised
with pytest.raises(
ProviderInstallationError,
match='Failed to load dependency graph: Conan API error: package not found',
):
plugin.install()
# Verify builder was still called
mock_builder.generate_conanfile.assert_called_once()
# Verify Conan API was attempted
mock_conan_api_constructor.assert_called_once()
def test_with_default_profiles(
self,
plugin: ConanProvider,
conan_temp_conanfile: Path,
conan_mock_dependencies: list[Requirement],
conan_setup_mocks: dict[str, Mock],
conan_mock_api: Mock,
) -> None:
"""Test install method uses pre-resolved profiles from plugin construction
Args:
plugin: The plugin instance
conan_temp_conanfile: Path to temporary conanfile.py
conan_mock_dependencies: List of mock dependencies
conan_setup_mocks: Dictionary containing all mocks
conan_mock_api: Mock ConanAPI instance
"""
# Setup dependencies
plugin.core_data.cppython_data.dependencies = conan_mock_dependencies
# Execute - should use the profiles resolved during plugin construction
plugin.install()
# Verify that the API was used for installation
conan_setup_mocks['conan_api_constructor'].assert_called_once()
# 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
"""Unit tests for the conan plugin publish functionality"""
from typing import Any
from unittest.mock import MagicMock, Mock
import pytest
from pytest_mock import MockerFixture
from cppython.plugins.conan.plugin import ConanProvider
from cppython.test.pytest.mixins import ProviderPluginTestMixin
from cppython.utility.exception import ProviderConfigurationError, ProviderInstallationError

@@ -16,6 +13,3 @@ # Use shared fixtures

# Constants for test assertions
EXPECTED_PROFILE_CALLS = 2
class TestConanPublish(ProviderPluginTestMixin[ConanProvider]):

@@ -45,246 +39,1 @@ """Tests for the Conan provider publish functionality"""

return ConanProvider
def test_skip_upload(
self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture
) -> None:
"""Test that publish with skip_upload=True only exports and builds locally
Args:
plugin: The plugin instance
conan_mock_api_publish: Mock ConanAPI for publish operations
conan_temp_conanfile: Fixture to create conanfile.py
mocker: Pytest mocker fixture
"""
# Set plugin to skip upload mode
plugin.data.skip_upload = True
# Mock the necessary imports and API creation
mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish)
# Mock the dependencies graph
mock_graph = mocker.Mock()
conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph
# Execute publish
plugin.publish()
# Verify export was called
conan_mock_api_publish.export.export.assert_called_once()
# Verify graph loading and analysis
conan_mock_api_publish.graph.load_graph_consumer.assert_called_once()
conan_mock_api_publish.graph.analyze_binaries.assert_called_once_with(
graph=mock_graph,
build_mode=['*'],
remotes=conan_mock_api_publish.remotes.list(),
update=None,
lockfile=None,
)
# Verify install was called
conan_mock_api_publish.install.install_binaries.assert_called_once_with(
deps_graph=mock_graph, remotes=conan_mock_api_publish.remotes.list()
)
# Verify upload was NOT called for local mode
conan_mock_api_publish.upload.upload_full.assert_not_called()
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
Args:
plugin: The plugin instance
conan_mock_api_publish: Mock ConanAPI for publish operations
conan_temp_conanfile: Fixture to create conanfile.py
mocker: Pytest mocker fixture
"""
# Set plugin to upload mode
plugin.data.remotes = ['conancenter']
# Mock the necessary imports and API creation
mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish)
# Mock the dependencies graph
mock_graph = mocker.Mock()
conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph
# Execute publish
plugin.publish()
# Verify all steps were called
conan_mock_api_publish.export.export.assert_called_once()
conan_mock_api_publish.graph.load_graph_consumer.assert_called_once()
conan_mock_api_publish.graph.analyze_binaries.assert_called_once()
conan_mock_api_publish.install.install_binaries.assert_called_once()
# Verify upload was called
conan_mock_api_publish.list.select.assert_called_once()
conan_mock_api_publish.upload.upload_full.assert_called_once()
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
Args:
plugin: The plugin instance
conan_mock_api_publish: Mock ConanAPI for publish operations
conan_temp_conanfile: Fixture to create conanfile.py
mocker: Pytest mocker fixture
"""
# Set plugin to upload mode
plugin.data.remotes = ['conancenter']
# Mock the necessary imports and API creation
mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish)
# Mock the dependencies graph
mock_graph = mocker.Mock()
conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph
# Mock no remotes configured
conan_mock_api_publish.remotes.list.return_value = []
# Execute publish and expect ProviderConfigurationError
with pytest.raises(ProviderConfigurationError, match='No configured remotes found'):
plugin.publish()
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
Args:
plugin: The plugin instance
conan_mock_api_publish: Mock ConanAPI for publish operations
conan_temp_conanfile: Fixture to create conanfile.py
mocker: Pytest mocker fixture
"""
# Set plugin to upload mode
plugin.data.remotes = ['conancenter']
# Mock the necessary imports and API creation
mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish)
# Mock the dependencies graph
mock_graph = mocker.Mock()
conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph
# Mock empty package list
mock_select_result = mocker.Mock()
mock_select_result.recipes = []
conan_mock_api_publish.list.select.return_value = mock_select_result
# Execute publish and expect ProviderInstallationError
with pytest.raises(ProviderInstallationError, match='No packages found to upload'):
plugin.publish()
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
Args:
plugin: The plugin instance
conan_mock_api_publish: Mock ConanAPI for publish operations
conan_temp_conanfile: Fixture to create conanfile.py
mocker: Pytest mocker fixture
"""
# Set plugin to skip upload mode
plugin.data.skip_upload = True
# Mock the necessary imports and API creation
mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish)
# Mock the dependencies graph
mock_graph = mocker.Mock()
conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph
# Execute publish
plugin.publish()
# 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_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
Args:
plugin: The plugin instance
conan_mock_api_publish: Mock ConanAPI for publish operations
conan_temp_conanfile: Fixture to create conanfile.py
mocker: Pytest mocker fixture
"""
# Set plugin to upload mode
plugin.data.remotes = ['conancenter']
# Mock the necessary imports and API creation
mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish)
# Mock the dependencies graph
mock_graph = mocker.Mock()
conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph
# Mock remotes and package list
mock_remote = MagicMock()
mock_remote.name = 'conancenter'
remotes = [mock_remote]
conan_mock_api_publish.remotes.list.return_value = remotes
mock_package_list = MagicMock()
mock_package_list.recipes = ['test_package/1.0@user/channel']
conan_mock_api_publish.list.select.return_value = mock_package_list
# Execute publish
plugin.publish()
# Verify upload_full was called with correct parameters
conan_mock_api_publish.upload.upload_full.assert_called_once_with(
package_list=mock_package_list,
remote=mock_remote,
enabled_remotes=remotes,
check_integrity=False,
force=False,
metadata=None,
dry_run=False,
)
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
Args:
plugin: The plugin instance
conan_mock_api_publish: Mock ConanAPI for publish operations
conan_temp_conanfile: Fixture to create conanfile.py
mocker: Pytest mocker fixture
"""
# Set plugin to upload mode
plugin.data.remotes = ['conancenter']
# Mock the necessary imports and API creation
mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish)
mock_list_pattern = mocker.patch('cppython.plugins.conan.plugin.ListPattern')
# Mock the dependencies graph
mock_graph = mocker.Mock()
conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph
# Execute publish
plugin.publish()
# Get the ref from the export call to verify ListPattern creation
# The export call returns (ref, conanfile) - we need the ref.name
export_return = conan_mock_api_publish.export.export.return_value
ref = export_return[0] # First element of the tuple
# Verify ListPattern was created with correct reference pattern
mock_list_pattern.assert_called_once_with(f'{ref.name}/*', package_id='*', only_recipe=False)
"""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,
ConanDependency,

@@ -26,3 +17,2 @@ ConanRevision,

)
from cppython.utility.exception import ProviderConfigurationError

@@ -140,3 +130,3 @@ # Constants for test validation

dependency = ConanDependency(
name='mylib',
name='example',
version=ConanVersion.from_string('1.0.0'),

@@ -146,3 +136,3 @@ user_channel=ConanUserChannel(user='myuser', channel='stable'),

assert dependency.requires() == 'mylib/1.0.0@myuser/stable'
assert dependency.requires() == 'example/1.0.0@myuser/stable'

@@ -152,6 +142,6 @@ def test_with_revision(self) -> None:

dependency = ConanDependency(
name='mylib', version=ConanVersion.from_string('1.0.0'), revision=ConanRevision(revision='abc123')
name='example', version=ConanVersion.from_string('1.0.0'), revision=ConanRevision(revision='abc123')
)
assert dependency.requires() == 'mylib/1.0.0#abc123'
assert dependency.requires() == 'example/1.0.0#abc123'

@@ -161,3 +151,3 @@ def test_full_reference(self) -> None:

dependency = ConanDependency(
name='mylib',
name='example',
version=ConanVersion.from_string('1.0.0'),

@@ -168,9 +158,9 @@ user_channel=ConanUserChannel(user='myuser', channel='stable'),

assert dependency.requires() == 'mylib/1.0.0@myuser/stable#abc123'
assert dependency.requires() == 'example/1.0.0@myuser/stable#abc123'
def test_from_reference_simple(self) -> None:
"""Test parsing a simple package name."""
dependency = ConanDependency.from_conan_reference('mylib')
dependency = ConanDependency.from_conan_reference('example')
assert dependency.name == 'mylib'
assert dependency.name == 'example'
assert dependency.version is None

@@ -182,5 +172,5 @@ assert dependency.user_channel is None

"""Test parsing a package with version."""
dependency = ConanDependency.from_conan_reference('mylib/1.0.0')
dependency = ConanDependency.from_conan_reference('example/1.0.0')
assert dependency.name == 'mylib'
assert dependency.name == 'example'
assert dependency.version is not None

@@ -193,5 +183,5 @@ assert str(dependency.version) == '1.0.0'

"""Test parsing a package with version range."""
dependency = ConanDependency.from_conan_reference('mylib/[>=1.0 <2.0]')
dependency = ConanDependency.from_conan_reference('example/[>=1.0 <2.0]')
assert dependency.name == 'mylib'
assert dependency.name == 'example'
assert dependency.version is None

@@ -205,5 +195,5 @@ assert dependency.version_range is not None

"""Test parsing a full Conan reference."""
dependency = ConanDependency.from_conan_reference('mylib/1.0.0@myuser/stable#abc123')
dependency = ConanDependency.from_conan_reference('example/1.0.0@myuser/stable#abc123')
assert dependency.name == 'mylib'
assert dependency.name == 'example'
assert dependency.version is not None

@@ -218,290 +208,7 @@ assert str(dependency.version) == '1.0.0'

class TestProfileProcessing:
"""Test profile processing functionality."""
def test_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_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_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_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_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, cmake_program=None
)
assert host_result == mock_host_profile
assert build_result == mock_build_profile
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'])
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')
with pytest.raises(ProviderConfigurationError, match='Failed to load host profile'):
_resolve_profiles('missing-profile', 'other-profile', mock_conan_api, cmake_program=None)
def test_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, cmake_program=None)
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_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, cmake_program=None)
assert host_result == mock_host_profile
assert build_result == mock_build_profile
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)
@patch('cppython.plugins.conan.resolution._profile_post_process')
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()
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, cmake_program=None)
assert host_result == mock_host_profile
assert build_result == mock_build_profile
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)
class TestResolveConanData:
"""Test Conan data resolution."""
@patch('cppython.plugins.conan.resolution.ConanAPI')
@patch('cppython.plugins.conan.resolution._resolve_profiles')
@patch('cppython.plugins.conan.resolution._detect_cmake_program')
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."""
mock_detect_cmake.return_value = None # No cmake detected for test
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, None)
@patch('cppython.plugins.conan.resolution.ConanAPI')
@patch('cppython.plugins.conan.resolution._resolve_profiles')
@patch('cppython.plugins.conan.resolution._detect_cmake_program')
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."""
mock_detect_cmake.return_value = None # No cmake detected for test
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, None)
@patch('cppython.plugins.conan.resolution.ConanAPI')
@patch('cppython.plugins.conan.resolution._resolve_profiles')
@patch('cppython.plugins.conan.resolution._detect_cmake_program')
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."""
mock_detect_cmake.return_value = None # No cmake detected for test
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': [], 'skip_upload': False}
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 == []
assert result.skip_upload is False
# Verify profile resolution was called with None values
mock_resolve_profiles.assert_called_once_with(None, None, mock_conan_api, None)
@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:
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
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()
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()
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, mock_conan_api, cmake_program=None)
# Verify that auto-detection was called for both profiles
assert mock_conan_api.profiles.detect.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