From 375847a7270398f62c197d18f6cf45468fd2de4b Mon Sep 17 00:00:00 2001 From: Silvio Traversaro Date: Sun, 20 Sep 2026 15:41:40 +0200 Subject: [PATCH] Add support in vinca-pinning-render to completly delete pinning key from conda-forge-pinnings --- README.md | 5 +++ vinca/pinning.py | 21 ++++++++++++- vinca/test_pinning.py | 71 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16cf6e7..14164d4 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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: diff --git a/vinca/pinning.py b/vinca/pinning.py index 19313f4..b3a40d4 100644 --- a/vinca/pinning.py +++ b/vinca/pinning.py @@ -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: @@ -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: diff --git a/vinca/test_pinning.py b/vinca/test_pinning.py index 02daf04..aad52f6 100644 --- a/vinca/test_pinning.py +++ b/vinca/test_pinning.py @@ -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(