Skip to content

Test Cleanup and CMake Inherits Fixes #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 87 additions & 22 deletions cppython/plugins/cmake/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pathlib import Path

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


class Builder:
Expand All @@ -12,8 +12,8 @@ def __init__(self) -> None:
"""Initialize the builder"""

@staticmethod
def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData) -> None:
"""Writes a provider preset from input sync data
def generate_provider_preset(provider_data: CMakeSyncData) -> CMakePresets:
"""Generates a provider preset from input sync data

Args:
provider_directory: The base directory to place the preset files
Expand All @@ -26,7 +26,17 @@ def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData
'CMAKE_PROJECT_TOP_LEVEL_INCLUDES': str(provider_data.top_level_includes.as_posix()),
}

generated_preset = CMakePresets(configurePresets=[generated_configure_preset])
return CMakePresets(configurePresets=[generated_configure_preset])

@staticmethod
def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData) -> None:
"""Writes a provider preset from input sync data

Args:
provider_directory: The base directory to place the preset files
provider_data: The providers synchronization data
"""
generated_preset = Builder.generate_provider_preset(provider_data)

provider_preset_file = provider_directory / f'{provider_data.provider_name}.json'

Expand All @@ -44,18 +54,18 @@ def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData
file.write(serialized)

@staticmethod
def write_cppython_preset(
def generate_cppython_preset(
cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData
) -> Path:
"""Write the cppython presets which inherit from the provider presets
) -> CMakePresets:
"""Generates the cppython preset which inherits from the provider presets

Args:
cppython_preset_directory: The tool directory
provider_directory: The base directory containing provider presets
provider_data: The provider's synchronization data

Returns:
A file path to the written data
A CMakePresets object
"""
generated_configure_preset = ConfigurePreset(name='cppython', inherits=provider_data.provider_name)
generated_preset = CMakePresets(configurePresets=[generated_configure_preset])
Expand All @@ -66,7 +76,25 @@ def write_cppython_preset(

# Set the data
generated_preset.include = [relative_preset]
return generated_preset

@staticmethod
def write_cppython_preset(
cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData
) -> Path:
"""Write the cppython presets which inherit from the provider presets

Args:
cppython_preset_directory: The tool directory
provider_directory: The base directory containing provider presets
provider_data: The provider's synchronization data

Returns:
A file path to the written data
"""
generated_preset = Builder.generate_cppython_preset(
cppython_preset_directory, provider_directory, provider_data
)
cppython_preset_file = cppython_preset_directory / 'cppython.json'

initial_preset = None
Expand All @@ -86,31 +114,44 @@ def write_cppython_preset(
return cppython_preset_file

@staticmethod
def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None:
"""Read the top level json file and insert the include reference.

Receives a relative path to the tool cmake json file

Raises:
ConfigError: If key files do not exists
def generate_root_preset(preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData) -> CMakePresets:
"""Generates the top level root preset with the include reference.

Args:
preset_file: Preset file to modify
cppython_preset_file: Path to the cppython preset file to include

Returns:
A CMakePresets object
"""
initial_root_preset = None
default_configure_preset = ConfigurePreset(
name=cmake_data.configuration_name,
inherits='cppython',
)

# If the file already exists, we need to compare it
if preset_file.exists():
with open(preset_file, encoding='utf-8') as file:
initial_json = file.read()
initial_root_preset = CMakePresets.model_validate_json(initial_json)
root_preset = initial_root_preset.model_copy(deep=True)
root_preset = CMakePresets.model_validate_json(initial_json)

if root_preset.configurePresets is None:
root_preset.configurePresets = [default_configure_preset]

# Set defaults
preset = next((p for p in root_preset.configurePresets if p.name == default_configure_preset.name), None)
if preset:
# If the name matches, we need to verify it inherits from cppython
if preset.inherits is None:
preset.inherits = 'cppython'
elif isinstance(preset.inherits, str) and preset.inherits != 'cppython':
preset.inherits = [preset.inherits, 'cppython']
elif isinstance(preset.inherits, list) and 'cppython' not in preset.inherits:
preset.inherits.append('cppython')
else:
root_preset.configurePresets.append(default_configure_preset)

else:
# If the file doesn't exist, we need to default it for the user

# Forward the tool's build directory
default_configure_preset = ConfigurePreset(name='default', inherits='cppython', binaryDir='build')
root_preset = CMakePresets(configurePresets=[default_configure_preset])

# Get the relative path to the cppython preset file
Expand All @@ -125,6 +166,30 @@ def write_root_presets(preset_file: Path, cppython_preset_file: Path) -> None:
if str(relative_preset) not in root_preset.include:
root_preset.include.append(str(relative_preset))

return root_preset

@staticmethod
def write_root_presets(preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData) -> None:
"""Read the top level json file and insert the include reference.

Receives a relative path to the tool cmake json file

Raises:
ConfigError: If key files do not exists

Args:
preset_file: Preset file to modify
cppython_preset_file: Path to the cppython preset file to include
"""
initial_root_preset = None

if preset_file.exists():
with open(preset_file, encoding='utf-8') as file:
initial_json = file.read()
initial_root_preset = CMakePresets.model_validate_json(initial_json)

root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data)

# Only write the file if the data has changed
if root_preset != initial_root_preset:
with open(preset_file, 'w', encoding='utf-8') as file:
Expand Down
2 changes: 1 addition & 1 deletion cppython/plugins/cmake/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ def sync(self, sync_data: SyncData) -> None:
self._cppython_preset_directory, self._provider_directory, sync_data
)

self.builder.write_root_presets(self.data.preset_file, cppython_preset_file)
self.builder.write_root_presets(self.data.preset_file, cppython_preset_file, self.data)
case _:
raise ValueError('Unsupported sync data type')
10 changes: 6 additions & 4 deletions cppython/plugins/cmake/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMake

root_directory = core_data.project_data.project_root.absolute()

modified_preset_dir = parsed_data.preset_file
if not modified_preset_dir.is_absolute():
modified_preset_dir = root_directory / modified_preset_dir
modified_preset_file = parsed_data.preset_file
if not modified_preset_file.is_absolute():
modified_preset_file = root_directory / modified_preset_file

return CMakeData(preset_file=modified_preset_dir, configuration_name=parsed_data.configuration_name)


return CMakeData(preset_file=modified_preset_file, configuration_name=parsed_data.configuration_name)
3 changes: 1 addition & 2 deletions cppython/plugins/cmake/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ class ConfigurePreset(CPPythonModel, extra='allow'):
inherits: Annotated[
str | list[str] | None, Field(description='The inherits field allows inheriting from other presets.')
] = None
binaryDir: Annotated[str | None, Field(description='The binary directory for the build output.')] = None
cacheVariables: dict[str, None | bool | str | CacheVariable] | None = None


Expand Down Expand Up @@ -92,4 +91,4 @@ class CMakeConfiguration(CPPythonModel):
] = Path('CMakePresets.json')
configuration_name: Annotated[
str, Field(description='The CMake configuration preset to look for and override inside the given `preset_file`')
] = 'cppython'
] = 'default'
55 changes: 26 additions & 29 deletions cppython/plugins/conan/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ def __init__(self, dependencies: list[ConanDependency]) -> None:
def _create_requires_assignment(self) -> cst.Assign:
"""Create a `requires` assignment statement."""
return cst.Assign(
targets=[cst.AssignTarget(cst.Name('requires'))],
value=cst.List([
cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies
]),
targets=[cst.AssignTarget(cst.Name(value='requires'))],
value=cst.List(
[cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies]
),
)

def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.BaseStatement:
Expand Down Expand Up @@ -56,24 +56,23 @@ def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef:
for body_statement_line in updated_node.body.body:
if not isinstance(body_statement_line, cst.SimpleStatementLine):
continue

assignment_statement = body_statement_line.body[0]
if not isinstance(assignment_statement, cst.Assign):
continue

for target in assignment_statement.targets:
if not isinstance(target.target, cst.Name) or target.target.value != 'requires':
for assignment_statement in body_statement_line.body:
if not isinstance(assignment_statement, cst.Assign):
continue

return self._replace_requires(updated_node, body_statement_line, assignment_statement)
for target in assignment_statement.targets:
if not isinstance(target.target, cst.Name) or target.target.value != 'requires':
continue
# Replace only the assignment within the SimpleStatementLine
return self._replace_requires(updated_node, body_statement_line, assignment_statement)

# Find the last attribute assignment before methods
last_attribute = None
for body_statement_line in updated_node.body.body:
if not isinstance(body_statement_line, cst.SimpleStatementLine):
break
assignment_statement = body_statement_line.body[0]
if not isinstance(assignment_statement, cst.Assign):
if not body_statement_line.body:
break
if not isinstance(body_statement_line.body[0], cst.Assign):
break
last_attribute = body_statement_line

Expand All @@ -89,29 +88,27 @@ def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef:
new_body.insert(index + 1, new_statement)
else:
new_body = [new_statement] + list(updated_node.body.body)

return updated_node.with_changes(body=updated_node.body.with_changes(body=new_body))

def _replace_requires(
self, updated_node: cst.ClassDef, body_statement_line: cst.SimpleStatementLine, assignment_statement: cst.Assign
) -> cst.ClassDef:
"""Replace the existing 'requires' assignment with a new one.
"""Replace the existing 'requires' assignment with a new one, preserving other statements on the same line."""
new_value = cst.List(
[cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies]
)
new_assignment = assignment_statement.with_changes(value=new_value)

Args:
updated_node (cst.ClassDef): The class definition to update.
body_statement_line (cst.SimpleStatementLine): The body item containing the assignment.
assignment_statement (cst.Assign): The existing assignment statement.
# Replace only the relevant assignment in the SimpleStatementLine
new_body = [
new_assignment if statement is assignment_statement else statement for statement in body_statement_line.body
]
new_statement_line = body_statement_line.with_changes(body=new_body)

Returns:
cst.ClassDef: The updated class definition.
"""
new_value = cst.List([
cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies
])
new_assignment = assignment_statement.with_changes(value=new_value)
# Replace the statement line in the class body
return updated_node.with_changes(
body=updated_node.body.with_changes(
body=[new_assignment if item is body_statement_line else item for item in updated_node.body.body]
body=[new_statement_line if item is body_statement_line else item for item in updated_node.body.body]
)
)

Expand Down
Loading
Loading