Skip to content
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ pinning_overrides:
- false
python_impl:
- cpython
# Remove a pin inherited from conda-forge-pinning.
docker_image: null
```

Render the build-tool input with:
Expand All @@ -80,6 +82,9 @@ vinca-pinning-render
The renderer downloads that exact `conda-forge-pinning` package and starts with its
`conda_build_config.yaml`. It sorts the named migrations by `migrator_ts`, just as
`conda-smithy` does, and combines each one using the [CFEP-9 variant algebra](https://github.com/conda-forge/cfep/blob/main/cfep-09.md).
An override set to `null` removes that key from the generated config, including
pins added by migrations. To remove a key in a `zip_keys` group, set every member
of that group to `null`; the group is removed as well.

For a full rebuild, update the base and eligible migrations with:

Expand Down
21 changes: 20 additions & 1 deletion vinca/pinning.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,22 @@ def _migration_name(name: str) -> str:


def _overlay(target: Any, source: Any) -> None:
removed_keys = {key for key, value in source.items() if value is None}
for key, value in source.items():
if key == "migrator_ts" or str(key).startswith("__"):
continue
target[key] = value
if value is None:
target.pop(key, None)
else:
target[key] = value
if removed_keys and "zip_keys" in target:
groups = [
group for group in target["zip_keys"] if not set(group) <= removed_keys
]
if groups:
target["zip_keys"] = groups
else:
target.pop("zip_keys")


def _migration_timestamp(payload: bytes) -> float:
Expand All @@ -244,6 +256,13 @@ def _validate_zipped_overrides(rendered: Any, overrides: Any) -> None:
f"{', '.join(sorted(touched))} also requires "
f"{', '.join(sorted(missing))}"
)
removed = {key for key in group if key in overrides and overrides[key] is None}
if removed and removed != group:
raise PinningError(
"Pinning overrides must remove every member of a zip_keys group; "
f"{', '.join(sorted(removed))} also requires "
f"{', '.join(sorted(group - removed))}: null"
)


def _format_rendered_yaml(payload: str) -> str:
Expand Down
71 changes: 71 additions & 0 deletions vinca/test_pinning.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,77 @@ def test_render_applies_base_migration_then_override(tmp_path):
assert not any(line.endswith(" ") for line in output_path.read_text().splitlines())


def test_render_null_override_removes_base_and_migrated_pins(tmp_path):
config_path = tmp_path / "vinca_pinning.yaml"
config_path.write_text(
"""\
conda_forge_pinning_version: '1'
migrations:
- libboost190
pinning_overrides:
python: null
libboost_devel: null
absent: null
"""
)
output_path = tmp_path / "conda_build_config.yaml"

rendered = render_pinning(
config_path,
output_path,
package=(BASE_CONFIG, {"libboost190": BOOST_MIGRATION}),
)

assert dict(rendered) == {"keep": ["base"]}
assert dict(ruamel.yaml.YAML(typ="safe").load(output_path.read_text())) == {
"keep": ["base"]
}


def test_render_null_override_removes_entire_zip_group(tmp_path):
config_path = tmp_path / "vinca_pinning.yaml"
config_path.write_text(
"""\
conda_forge_pinning_version: '1'
pinning_overrides:
python: null
is_python_min: null
"""
)
base = b"""\
python: ['3.13']
is_python_min: [true]
zip_keys:
- [python, is_python_min]
keep: [base]
"""

rendered = render_pinning(config_path, tmp_path / "output.yaml", package=(base, {}))

assert dict(rendered) == {"keep": ["base"]}


def test_render_rejects_mixed_removal_and_override_in_zip_group(tmp_path):
config_path = tmp_path / "vinca_pinning.yaml"
config_path.write_text(
"""\
conda_forge_pinning_version: '1'
pinning_overrides:
python: null
is_python_min: [false]
"""
)
base = b"""\
python: ['3.13']
is_python_min: [true]
zip_keys:
- [python, is_python_min]
"""

with pytest.raises(PinningError, match="remove every member"):
render_pinning(config_path, tmp_path / "output.yaml", package=(base, {}))


def test_render_keeps_selector_on_empty_sequence_item(tmp_path):
config_path = tmp_path / "vinca_pinning.yaml"
config_path.write_text(
Expand Down
Loading