diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c04cecd..5e73bfe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +3.4.0 (UNRELEASED) +------------------ + +* Previously the ``name``, ``channels``, and ``platforms`` keys would only be overwritten by the root ``devenv.yml`` file (the one use to invoke ``conda devenv`` with). + Now any downstream ``devenv.yml`` file will override these keys, with the most downstream ``devenv.yml`` file "winning", which was always the intended behavior. + 3.3.0 (2024-02-21) ------------------ diff --git a/src/conda_devenv/devenv.py b/src/conda_devenv/devenv.py index bf21a41..0185df3 100644 --- a/src/conda_devenv/devenv.py +++ b/src/conda_devenv/devenv.py @@ -499,10 +499,12 @@ def load_yaml_dict( merge_dependencies_version_specifications(merged_dict, key_to_merge="dependencies") - # Force these keys to always be set by the starting/root devenv file, when defined. - force_root_keys = ("name", "channels", "platforms") - forced_keys = {k: root_yaml[k] for k in force_root_keys if k in root_yaml} - merged_dict.update(forced_keys) + # Force these keys to always be set by the most downstream devenv file. + for forced_key in ("name", "channels", "platforms"): + for yaml_dict in all_yaml_dicts.values(): + if forced_key in yaml_dict: + merged_dict[forced_key] = yaml_dict[forced_key] + break if "environment" not in merged_dict: merged_dict["environment"] = {} diff --git a/tests/test_load_yaml_dict.py b/tests/test_load_yaml_dict.py index fc5bbe6..3f72639 100644 --- a/tests/test_load_yaml_dict.py +++ b/tests/test_load_yaml_dict.py @@ -136,6 +136,8 @@ def test_downstream_overrides_channels(tmp_path) -> None: ) ) + # This is the most downstream file which defines 'channels', so it overwrites any + # upstream definition. b_fn = tmp_path / "b.devenv.yml" b_fn.write_text( textwrap.dedent( @@ -150,13 +152,114 @@ def test_downstream_overrides_channels(tmp_path) -> None: ) ) - assert load_yaml_dict(b_fn) == { - "name": "b", + c_fn = tmp_path / "c.devenv.yml" + c_fn.write_text( + textwrap.dedent( + """ + name: c + includes: + - {{ root }}/b.devenv.yml + """ + ) + ) + + assert load_yaml_dict(c_fn) == { + "name": "c", "channels": ["b1_channel", "b2_channel"], "environment": {}, } +def test_no_downstream_overrides_channels(tmp_path) -> None: + # The 'channels' key is defined only by one upstream file. + a_fn = tmp_path / "a.devenv.yml" + a_fn.write_text( + textwrap.dedent( + """ + name: a + channels: + - z_channel + - a_channel + """ + ) + ) + + b_fn = tmp_path / "b.devenv.yml" + b_fn.write_text( + textwrap.dedent( + """ + name: b + includes: + - {{ root }}/a.devenv.yml + """ + ) + ) + + c_fn = tmp_path / "c.devenv.yml" + c_fn.write_text( + textwrap.dedent( + """ + name: c + includes: + - {{ root }}/b.devenv.yml + """ + ) + ) + + assert load_yaml_dict(c_fn) == { + "name": "c", + "channels": ["z_channel", "a_channel"], + "environment": {}, + } + + +def test_root_overrides_channels(tmp_path) -> None: + a_fn = tmp_path / "a.devenv.yml" + a_fn.write_text( + textwrap.dedent( + """ + name: a + channels: + - z_channel + - a_channel + """ + ) + ) + + b_fn = tmp_path / "b.devenv.yml" + b_fn.write_text( + textwrap.dedent( + """ + name: b + includes: + - {{ root }}/a.devenv.yml + """ + ) + ) + + # This is the root file, so it overwrites the 'channels' completely. + c_fn = tmp_path / "c.devenv.yml" + c_fn.write_text( + textwrap.dedent( + """ + name: c + includes: + - {{ root }}/b.devenv.yml + channels: + - a_channel + - z_channel + - b_channel + """ + ) + ) + + assert load_yaml_dict(c_fn) == { + "name": "c", + "channels": ["a_channel", "z_channel", "b_channel"], + "environment": {}, + } + + def test_downstream_overrides_platforms(tmp_path) -> None: a_fn = tmp_path / "a.devenv.yml" a_fn.write_text( @@ -170,6 +273,96 @@ def test_downstream_overrides_platforms(tmp_path) -> None: ) ) + # This is the most downstream file which defines 'platforms', so it overwrites any + # upstream definition. + b_fn = tmp_path / "b.devenv.yml" + b_fn.write_text( + textwrap.dedent( + """ + name: b + includes: + - {{ root }}/a.devenv.yml + platforms: + - win-64 + - osx-64 + """ + ) + ) + + c_fn = tmp_path / "c.devenv.yml" + c_fn.write_text( + textwrap.dedent( + """ + name: c + includes: + - {{ root }}/b.devenv.yml + """ + ) + ) + + assert load_yaml_dict(c_fn) == { + "name": "c", + "platforms": ["win-64", "osx-64"], + "environment": {}, + } + + +def test_no_downstream_overrides_platforms(tmp_path) -> None: + # The 'platforms' key is defined only by one upstream file. + a_fn = tmp_path / "a.devenv.yml" + a_fn.write_text( + textwrap.dedent( + """ + name: a + platforms: + - win-64 + - linux-64 + """ + ) + ) + + b_fn = tmp_path / "b.devenv.yml" + b_fn.write_text( + textwrap.dedent( + """ + name: b + includes: + - {{ root }}/a.devenv.yml + """ + ) + ) + + c_fn = tmp_path / "c.devenv.yml" + c_fn.write_text( + textwrap.dedent( + """ + name: c + includes: + - {{ root }}/b.devenv.yml + """ + ) + ) + + assert load_yaml_dict(c_fn) == { + "name": "c", + "platforms": ["win-64", "linux-64"], + "environment": {}, + } + + +def test_root_overrides_platforms(tmp_path) -> None: + a_fn = tmp_path / "a.devenv.yml" + a_fn.write_text( + textwrap.dedent( + """ + name: a + platforms: + - win-64 + - linux-64 + """ + ) + ) + b_fn = tmp_path / "b.devenv.yml" b_fn.write_text( textwrap.dedent( @@ -177,6 +370,18 @@ def test_downstream_overrides_platforms(tmp_path) -> None: name: b includes: - {{ root }}/a.devenv.yml + """ + ) + ) + + # This is the root file, so it overwrites the 'platforms' completely. + c_fn = tmp_path / "c.devenv.yml" + c_fn.write_text( + textwrap.dedent( + """ + name: c + includes: + - {{ root }}/b.devenv.yml platforms: - win-64 - osx-64 @@ -184,8 +389,8 @@ def test_downstream_overrides_platforms(tmp_path) -> None: ) ) - assert load_yaml_dict(b_fn) == { - "name": "b", + assert load_yaml_dict(c_fn) == { + "name": "c", "platforms": ["win-64", "osx-64"], "environment": {}, }