Skip to content

Commit 8866ef0

Browse files
authored
Fix workspace option overrides (#2104)
* Add support for option overrides for workspaces. * Clean up errors
1 parent 4d8b5d5 commit 8866ef0

File tree

3 files changed

+252
-2
lines changed

3 files changed

+252
-2
lines changed

src/hatch/project/env.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"scripts": dict,
2828
"skip-install": bool,
2929
"type": str,
30+
"workspace": dict,
3031
}
3132

3233

@@ -43,7 +44,11 @@ def apply_overrides(env_name, source, condition, condition_value, options, new_c
4344
continue
4445

4546
override_type = option_types.get(option)
46-
if override_type in TYPE_OVERRIDES:
47+
if option == "workspace":
48+
_apply_override_to_workspace(
49+
env_name, option, data, source, condition, condition_value, new_config, overwrite
50+
)
51+
elif override_type in TYPE_OVERRIDES:
4752
TYPE_OVERRIDES[override_type](
4853
env_name, option, data, source, condition, condition_value, new_config, overwrite
4954
)
@@ -300,6 +305,31 @@ def _apply_override_to_boolean(
300305
raise TypeError(message)
301306

302307

308+
def _apply_override_to_workspace(env_name, option, data, source, condition, condition_value, new_config, overwrite):
309+
"""Handle workspace dict with nested members/exclude/parallel."""
310+
if not isinstance(data, dict):
311+
message = f"Field `tool.hatch.envs.{env_name}.overrides.{source}.{condition}.{option}` must be a table"
312+
raise TypeError(message)
313+
314+
# Get or create workspace dict
315+
workspace = {} if overwrite else new_config.setdefault(option, {})
316+
317+
for key, value in data.items():
318+
if key in {"members", "exclude"}:
319+
# Delegate to array handler - pass workspace dict
320+
_apply_override_to_array(env_name, key, value, source, condition, condition_value, workspace, overwrite)
321+
elif key == "parallel":
322+
# Delegate to boolean handler - pass workspace dict
323+
_apply_override_to_boolean(env_name, key, value, source, condition, condition_value, workspace, overwrite)
324+
else:
325+
message = f"Unknown workspace option: {key}"
326+
raise ValueError(message)
327+
328+
# Update new_config with the workspace dict
329+
if overwrite or workspace:
330+
new_config[option] = workspace
331+
332+
303333
def _resolve_condition(env_name, option, source, condition, condition_value, condition_config, condition_index=None):
304334
location = "field" if condition_index is None else f"entry #{condition_index} in field"
305335

tests/project/test_config.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
ARRAY_OPTIONS = [o for o, t in RESERVED_OPTIONS.items() if t is list]
1313
BOOLEAN_OPTIONS = [o for o, t in RESERVED_OPTIONS.items() if t is bool]
14-
MAPPING_OPTIONS = [o for o, t in RESERVED_OPTIONS.items() if t is dict]
14+
MAPPING_OPTIONS = [o for o, t in RESERVED_OPTIONS.items() if t is dict and o != "workspace"]
1515
STRING_OPTIONS = [o for o, t in RESERVED_OPTIONS.items() if t is str and o != "matrix-name-format"]
16+
WORKSPACE_OPTIONS = ["workspace"] # Workspace has nested structure, tested separately
1617

1718

1819
def construct_matrix_data(env_name, config, overrides=None):
@@ -2570,6 +2571,99 @@ def finalize_environments(self, config):
25702571
assert project_config.envs == expected_envs
25712572
assert project_config.matrices["foo"] == construct_matrix_data("foo", env_config)
25722573

2574+
@pytest.mark.parametrize("option", WORKSPACE_OPTIONS)
2575+
def test_overrides_matrix_workspace_invalid_type(self, isolation, option):
2576+
with pytest.raises(
2577+
TypeError,
2578+
match=f"Field `tool.hatch.envs.foo.overrides.matrix.version.{option}` must be a table",
2579+
):
2580+
_ = ProjectConfig(
2581+
isolation,
2582+
{
2583+
"envs": {
2584+
"foo": {"matrix": [{"version": ["9000"]}], "overrides": {"matrix": {"version": {option: 9000}}}}
2585+
}
2586+
},
2587+
PluginManager(),
2588+
).envs
2589+
2590+
@pytest.mark.parametrize("option", WORKSPACE_OPTIONS)
2591+
def test_overrides_matrix_workspace_members_append(self, isolation, option):
2592+
env_config = {
2593+
"foo": {
2594+
option: {"members": ["packages/core"]},
2595+
"matrix": [{"version": ["9000"]}, {"feature": ["bar"]}],
2596+
"overrides": {"matrix": {"version": {option: {"members": ["packages/extra"]}}}},
2597+
}
2598+
}
2599+
project_config = ProjectConfig(isolation, {"envs": env_config}, PluginManager())
2600+
2601+
expected_envs = {
2602+
"default": {"type": "virtual"},
2603+
"foo.9000": {"type": "virtual", option: {"members": ["packages/core", "packages/extra"]}},
2604+
"foo.bar": {"type": "virtual", option: {"members": ["packages/core"]}},
2605+
}
2606+
2607+
assert project_config.envs == expected_envs
2608+
2609+
@pytest.mark.parametrize("option", WORKSPACE_OPTIONS)
2610+
def test_overrides_matrix_workspace_members_conditional(self, isolation, option):
2611+
env_config = {
2612+
"foo": {
2613+
option: {"members": ["packages/core"]},
2614+
"matrix": [{"version": ["9000", "42"]}],
2615+
"overrides": {
2616+
"matrix": {"version": {option: {"members": [{"value": "packages/special", "if": ["42"]}]}}}
2617+
},
2618+
}
2619+
}
2620+
project_config = ProjectConfig(isolation, {"envs": env_config}, PluginManager())
2621+
2622+
expected_envs = {
2623+
"default": {"type": "virtual"},
2624+
"foo.9000": {"type": "virtual", option: {"members": ["packages/core"]}},
2625+
"foo.42": {"type": "virtual", option: {"members": ["packages/core", "packages/special"]}},
2626+
}
2627+
2628+
assert project_config.envs == expected_envs
2629+
2630+
@pytest.mark.parametrize("option", WORKSPACE_OPTIONS)
2631+
def test_overrides_matrix_workspace_parallel(self, isolation, option):
2632+
env_config = {
2633+
"foo": {
2634+
option: {"members": ["packages/*"], "parallel": True},
2635+
"matrix": [{"version": ["9000", "42"]}],
2636+
"overrides": {"matrix": {"version": {option: {"parallel": {"value": False, "if": ["42"]}}}}},
2637+
}
2638+
}
2639+
project_config = ProjectConfig(isolation, {"envs": env_config}, PluginManager())
2640+
2641+
expected_envs = {
2642+
"default": {"type": "virtual"},
2643+
"foo.9000": {"type": "virtual", option: {"members": ["packages/*"], "parallel": True}},
2644+
"foo.42": {"type": "virtual", option: {"members": ["packages/*"], "parallel": False}},
2645+
}
2646+
2647+
assert project_config.envs == expected_envs
2648+
2649+
@pytest.mark.parametrize("option", WORKSPACE_OPTIONS)
2650+
def test_overrides_matrix_workspace_overwrite(self, isolation, option):
2651+
env_config = {
2652+
"foo": {
2653+
option: {"members": ["packages/core"], "parallel": True},
2654+
"matrix": [{"version": ["9000"]}],
2655+
"overrides": {"matrix": {"version": {f"set-{option}": {"members": ["packages/new"]}}}},
2656+
}
2657+
}
2658+
project_config = ProjectConfig(isolation, {"envs": env_config}, PluginManager())
2659+
2660+
expected_envs = {
2661+
"default": {"type": "virtual"},
2662+
"foo.9000": {"type": "virtual", option: {"members": ["packages/new"]}},
2663+
}
2664+
2665+
assert project_config.envs == expected_envs
2666+
25732667

25742668
class TestPublish:
25752669
def test_not_table(self, isolation):

tests/workspaces/test_config.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,3 +698,129 @@ def test_workspace_development_workflow(self, temp_dir, hatch, monkeypatch):
698698
monkeypatch.setenv("FEATURE_PACKAGE", "utils")
699699
result = hatch("env", "create", "feature")
700700
assert result.exit_code == 0
701+
702+
result = hatch("env", "create", "release")
703+
assert result.exit_code == 0
704+
705+
def test_workspace_overrides_matrix_conditional_members(self, temp_dir, hatch):
706+
"""Test workspace members added conditionally via matrix overrides."""
707+
workspace_root = temp_dir / "workspace"
708+
workspace_root.mkdir()
709+
710+
workspace_config = workspace_root / "pyproject.toml"
711+
workspace_config.write_text("""
712+
[project]
713+
name = "workspace-root"
714+
version = "0.1.0"
715+
716+
[[tool.hatch.envs.test.matrix]]
717+
python = ["3.9", "3.11"]
718+
719+
[tool.hatch.envs.test]
720+
workspace.members = ["packages/core"]
721+
722+
[tool.hatch.envs.test.overrides]
723+
matrix.python.workspace.members = [
724+
{ value = "packages/py311-only", if = ["3.11"] }
725+
]
726+
""")
727+
728+
packages_dir = workspace_root / "packages"
729+
packages_dir.mkdir()
730+
731+
# Core package (always included)
732+
core_dir = packages_dir / "core"
733+
core_dir.mkdir()
734+
(core_dir / "pyproject.toml").write_text("""
735+
[project]
736+
name = "core"
737+
version = "0.1.0"
738+
""")
739+
740+
# Python 3.11+ only package
741+
py311_dir = packages_dir / "py311-only"
742+
py311_dir.mkdir()
743+
(py311_dir / "pyproject.toml").write_text("""
744+
[project]
745+
name = "py311-only"
746+
version = "0.1.0"
747+
""")
748+
749+
with workspace_root.as_cwd():
750+
# Both environments should be created
751+
result = hatch("env", "create", "test")
752+
assert result.exit_code == 0
753+
754+
def test_workspace_overrides_platform_conditional_members(self, temp_dir, hatch):
755+
"""Test workspace members added conditionally via platform overrides."""
756+
workspace_root = temp_dir / "workspace"
757+
workspace_root.mkdir()
758+
759+
workspace_config = workspace_root / "pyproject.toml"
760+
workspace_config.write_text("""
761+
[project]
762+
name = "workspace-root"
763+
version = "0.1.0"
764+
765+
[tool.hatch.envs.default]
766+
workspace.members = ["packages/core"]
767+
768+
[tool.hatch.envs.default.overrides]
769+
platform.linux.workspace.members = ["packages/linux-specific"]
770+
platform.windows.workspace.members = ["packages/windows-specific"]
771+
""")
772+
773+
packages_dir = workspace_root / "packages"
774+
packages_dir.mkdir()
775+
776+
for pkg in ["core", "linux-specific", "windows-specific"]:
777+
pkg_dir = packages_dir / pkg
778+
pkg_dir.mkdir()
779+
(pkg_dir / "pyproject.toml").write_text(f"""
780+
[project]
781+
name = "{pkg}"
782+
version = "0.1.0"
783+
""")
784+
785+
with workspace_root.as_cwd():
786+
result = hatch("env", "create")
787+
assert result.exit_code == 0
788+
789+
def test_workspace_overrides_combined_conditions(self, temp_dir, hatch):
790+
"""Test workspace members with combined matrix and platform conditions."""
791+
workspace_root = temp_dir / "workspace"
792+
workspace_root.mkdir()
793+
794+
workspace_config = workspace_root / "pyproject.toml"
795+
workspace_config.write_text("""
796+
[project]
797+
name = "workspace-root"
798+
version = "0.1.0"
799+
800+
[[tool.hatch.envs.test.matrix]]
801+
python = ["3.9", "3.11"]
802+
803+
[tool.hatch.envs.test]
804+
workspace.members = ["packages/core"]
805+
806+
[tool.hatch.envs.test.overrides]
807+
matrix.python.workspace.members = [
808+
{ value = "packages/linux-py311", if = ["3.11"], platform = ["linux"] }
809+
]
810+
""")
811+
812+
packages_dir = workspace_root / "packages"
813+
packages_dir.mkdir()
814+
815+
for pkg in ["core", "linux-py311"]:
816+
pkg_dir = packages_dir / pkg
817+
pkg_dir.mkdir()
818+
(pkg_dir / "pyproject.toml").write_text(f"""
819+
[project]
820+
name = "{pkg}"
821+
version = "0.1.0"
822+
""")
823+
824+
with workspace_root.as_cwd():
825+
result = hatch("env", "create", "test")
826+
assert result.exit_code == 0

0 commit comments

Comments
 (0)