From 9c2de8859b9888f8f6cdad59443b76fcf9cd460b Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:21:15 -0400 Subject: [PATCH] Allow primary to be union of extras --- hatch_multi/plugin.py | 5 +- hatch_multi/structs.py | 6 +- .../tests/test_project_basic/pyproject.toml | 2 +- .../project/__init__.py | 0 .../pyproject.toml | 26 ++++ hatch_multi/tests/test_projects.py | 115 ------------------ hatch_multi/tests/test_projects_basic.py | 107 ++++++++++++++++ .../tests/test_projects_multiple_primary.py | 108 ++++++++++++++++ 8 files changed, 249 insertions(+), 120 deletions(-) create mode 100644 hatch_multi/tests/test_project_multiple_primary/project/__init__.py create mode 100644 hatch_multi/tests/test_project_multiple_primary/pyproject.toml delete mode 100644 hatch_multi/tests/test_projects.py create mode 100644 hatch_multi/tests/test_projects_basic.py create mode 100644 hatch_multi/tests/test_projects_multiple_primary.py diff --git a/hatch_multi/plugin.py b/hatch_multi/plugin.py index 6190832..e472011 100644 --- a/hatch_multi/plugin.py +++ b/hatch_multi/plugin.py @@ -36,7 +36,10 @@ def update(self, metadata: dict) -> None: metadata["name"] = config.name if config.primary: self._logger.info(f"Setting metadata for primary dependency set '{config.primary}' in hatch-multi") - metadata["dependencies"] = metadata["optional-dependencies"].get(config.primary, []) + if isinstance(config.primary, list): + metadata["dependencies"] = [dep for extra in config.primary for dep in metadata["optional-dependencies"].get(extra, [])] + else: + metadata["dependencies"] = metadata["optional-dependencies"].get(config.primary, []) else: self._logger.info("Setting metadata for default dependency set in hatch-multi") # If no primary is set, use the first extra as default diff --git a/hatch_multi/structs.py b/hatch_multi/structs.py index 6f6a744..ff9d239 100644 --- a/hatch_multi/structs.py +++ b/hatch_multi/structs.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional +from typing import List, Optional, Union from pydantic import AliasChoices, BaseModel, Field @@ -9,8 +9,8 @@ class HatchMultiConfig(BaseModel, validate_assignment=True): name: str = Field(description="Name of the package") - primary: Optional[str] = Field( + primary: Optional[Union[str, List[str]]] = Field( default=None, - description="Optional dependecy set to use as primary", + description="Optional dependency set/s to use as primary", alias=AliasChoices("primary", "default"), ) diff --git a/hatch_multi/tests/test_project_basic/pyproject.toml b/hatch_multi/tests/test_project_basic/pyproject.toml index 6a3c73c..d4a24c9 100644 --- a/hatch_multi/tests/test_project_basic/pyproject.toml +++ b/hatch_multi/tests/test_project_basic/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "hatch-cpp-test-project-basic" description = "Basic test project for hatch-cpp" version = "0.1.0" -requires-python = ">=3.9" +requires-python = ">=3.11" dynamic = ["dependencies"] [project.optional-dependencies] diff --git a/hatch_multi/tests/test_project_multiple_primary/project/__init__.py b/hatch_multi/tests/test_project_multiple_primary/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hatch_multi/tests/test_project_multiple_primary/pyproject.toml b/hatch_multi/tests/test_project_multiple_primary/pyproject.toml new file mode 100644 index 0000000..9616649 --- /dev/null +++ b/hatch_multi/tests/test_project_multiple_primary/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["hatchling", "hatch-multi"] +build-backend = "hatchling.build" + +[project] +name = "hatch-cpp-test-project-basic" +description = "Basic test project for hatch-cpp" +version = "0.1.0" +requires-python = ">=3.11" +dynamic = ["dependencies"] + +[project.optional-dependencies] +main = ["superstore"] +other = ["organizeit2"] + +[tool.hatch.build.sources] +src = "/" + +[tool.hatch.build.targets.sdist] +packages = ["project"] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.hatch.metadata.hooks.hatch-multi] +default = ["main", "other"] diff --git a/hatch_multi/tests/test_projects.py b/hatch_multi/tests/test_projects.py deleted file mode 100644 index 6877415..0000000 --- a/hatch_multi/tests/test_projects.py +++ /dev/null @@ -1,115 +0,0 @@ -from os import listdir -from pathlib import Path -from shutil import rmtree -from subprocess import check_call -from sys import executable -from zipfile import ZipFile - -import pytest - - -class TestProject: - @pytest.mark.parametrize( - "project", - [ - "test_project_basic", - ], - ) - def test_basic(self, project): - try: - rmtree(f"hatch_multi/tests/{project}/dist") - except FileNotFoundError: - pass - - check_call( - [ - executable, - "-m", - "build", - "-n", - "-w", - ], - cwd=f"hatch_multi/tests/{project}", - env={"SKIP_HATCH_MULTI": "1"}, - ) - - assert Path(f"hatch_multi/tests/{project}/dist").exists() - assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl"] - with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl", "r") as zip_ref: - zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") - assert ( - Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic-0.1.0.dist-info/METADATA").read_text() - == """Metadata-Version: 2.4 -Name: hatch-cpp-test-project-basic -Version: 0.1.0 -Dynamic: Requires-Dist -Summary: Basic test project for hatch-cpp -Requires-Python: >=3.9 -Provides-Extra: main -Requires-Dist: superstore; extra == 'main' -Provides-Extra: other -Requires-Dist: organizeit2; extra == 'other' -""" - ) - rmtree(f"hatch_multi/tests/{project}/dist") - - check_call( - [ - executable, - "-m", - "build", - "-n", - "-w", - ], - cwd=f"hatch_multi/tests/{project}", - ) - - assert Path(f"hatch_multi/tests/{project}/dist").exists() - assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl"] - with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl", "r") as zip_ref: - zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") - assert ( - Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic-0.1.0.dist-info/METADATA").read_text() - == """Metadata-Version: 2.4 -Name: hatch-cpp-test-project-basic -Version: 0.1.0 -Summary: Basic test project for hatch-cpp -Requires-Python: >=3.9 -Requires-Dist: superstore -Provides-Extra: main -Requires-Dist: superstore; extra == 'main' -Provides-Extra: other -Requires-Dist: organizeit2; extra == 'other' -""" - ) - rmtree(f"hatch_multi/tests/{project}/dist") - - check_call( - [ - executable, - "-m", - "build", - "-n", - "-w", - ], - cwd=f"hatch_multi/tests/{project}", - env={"HATCH_MULTI_BUILD": "other"}, - ) - - assert Path(f"hatch_multi/tests/{project}/dist").exists() - assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic_other-0.1.0-py3-none-any.whl"] - with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic_other-0.1.0-py3-none-any.whl", "r") as zip_ref: - zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") - assert ( - Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic_other-0.1.0.dist-info/METADATA").read_text() - == """Metadata-Version: 2.4 -Name: hatch-cpp-test-project-basic-other -Version: 0.1.0 -Summary: Basic test project for hatch-cpp -Requires-Python: >=3.9 -Requires-Dist: organizeit2 -Provides-Extra: main -Requires-Dist: superstore; extra == 'main' -""" - ) - rmtree(f"hatch_multi/tests/{project}/dist") diff --git a/hatch_multi/tests/test_projects_basic.py b/hatch_multi/tests/test_projects_basic.py new file mode 100644 index 0000000..b0844b9 --- /dev/null +++ b/hatch_multi/tests/test_projects_basic.py @@ -0,0 +1,107 @@ +from os import listdir +from pathlib import Path +from shutil import rmtree +from subprocess import check_call +from sys import executable +from zipfile import ZipFile + + +def test_basic(): + project = "test_project_basic" + try: + rmtree(f"hatch_multi/tests/{project}/dist") + except FileNotFoundError: + pass + + check_call( + [ + executable, + "-m", + "build", + "-n", + "-w", + ], + cwd=f"hatch_multi/tests/{project}", + env={"SKIP_HATCH_MULTI": "1"}, + ) + + assert Path(f"hatch_multi/tests/{project}/dist").exists() + assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl"] + with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl", "r") as zip_ref: + zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") + assert ( + Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic-0.1.0.dist-info/METADATA").read_text() + == """Metadata-Version: 2.4 +Name: hatch-cpp-test-project-basic +Version: 0.1.0 +Dynamic: Requires-Dist +Summary: Basic test project for hatch-cpp +Requires-Python: >=3.11 +Provides-Extra: main +Requires-Dist: superstore; extra == 'main' +Provides-Extra: other +Requires-Dist: organizeit2; extra == 'other' +""" + ) + rmtree(f"hatch_multi/tests/{project}/dist") + + check_call( + [ + executable, + "-m", + "build", + "-n", + "-w", + ], + cwd=f"hatch_multi/tests/{project}", + ) + + assert Path(f"hatch_multi/tests/{project}/dist").exists() + assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl"] + with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl", "r") as zip_ref: + zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") + assert ( + Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic-0.1.0.dist-info/METADATA").read_text() + == """Metadata-Version: 2.4 +Name: hatch-cpp-test-project-basic +Version: 0.1.0 +Summary: Basic test project for hatch-cpp +Requires-Python: >=3.11 +Requires-Dist: superstore +Provides-Extra: main +Requires-Dist: superstore; extra == 'main' +Provides-Extra: other +Requires-Dist: organizeit2; extra == 'other' +""" + ) + rmtree(f"hatch_multi/tests/{project}/dist") + + check_call( + [ + executable, + "-m", + "build", + "-n", + "-w", + ], + cwd=f"hatch_multi/tests/{project}", + env={"HATCH_MULTI_BUILD": "other"}, + ) + + assert Path(f"hatch_multi/tests/{project}/dist").exists() + assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic_other-0.1.0-py3-none-any.whl"] + with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic_other-0.1.0-py3-none-any.whl", "r") as zip_ref: + zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") + assert ( + Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic_other-0.1.0.dist-info/METADATA").read_text() + == """Metadata-Version: 2.4 +Name: hatch-cpp-test-project-basic-other +Version: 0.1.0 +Summary: Basic test project for hatch-cpp +Requires-Python: >=3.11 +Requires-Dist: organizeit2 +Provides-Extra: main +Requires-Dist: superstore; extra == 'main' +""" + ) + rmtree(f"hatch_multi/tests/{project}/dist") diff --git a/hatch_multi/tests/test_projects_multiple_primary.py b/hatch_multi/tests/test_projects_multiple_primary.py new file mode 100644 index 0000000..99a1f0d --- /dev/null +++ b/hatch_multi/tests/test_projects_multiple_primary.py @@ -0,0 +1,108 @@ +from os import listdir +from pathlib import Path +from shutil import rmtree +from subprocess import check_call +from sys import executable +from zipfile import ZipFile + + +def test_project_basic_multiple_primary(): + project = "test_project_multiple_primary" + try: + rmtree(f"hatch_multi/tests/{project}/dist") + except FileNotFoundError: + pass + + check_call( + [ + executable, + "-m", + "build", + "-n", + "-w", + ], + cwd=f"hatch_multi/tests/{project}", + env={"SKIP_HATCH_MULTI": "1"}, + ) + + assert Path(f"hatch_multi/tests/{project}/dist").exists() + assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl"] + with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl", "r") as zip_ref: + zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") + assert ( + Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic-0.1.0.dist-info/METADATA").read_text() + == """Metadata-Version: 2.4 +Name: hatch-cpp-test-project-basic +Version: 0.1.0 +Dynamic: Requires-Dist +Summary: Basic test project for hatch-cpp +Requires-Python: >=3.11 +Provides-Extra: main +Requires-Dist: superstore; extra == 'main' +Provides-Extra: other +Requires-Dist: organizeit2; extra == 'other' +""" + ) + rmtree(f"hatch_multi/tests/{project}/dist") + + check_call( + [ + executable, + "-m", + "build", + "-n", + "-w", + ], + cwd=f"hatch_multi/tests/{project}", + ) + + assert Path(f"hatch_multi/tests/{project}/dist").exists() + assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl"] + with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic-0.1.0-py3-none-any.whl", "r") as zip_ref: + zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") + assert ( + Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic-0.1.0.dist-info/METADATA").read_text() + == """Metadata-Version: 2.4 +Name: hatch-cpp-test-project-basic +Version: 0.1.0 +Summary: Basic test project for hatch-cpp +Requires-Python: >=3.11 +Requires-Dist: organizeit2 +Requires-Dist: superstore +Provides-Extra: main +Requires-Dist: superstore; extra == 'main' +Provides-Extra: other +Requires-Dist: organizeit2; extra == 'other' +""" + ) + rmtree(f"hatch_multi/tests/{project}/dist") + + check_call( + [ + executable, + "-m", + "build", + "-n", + "-w", + ], + cwd=f"hatch_multi/tests/{project}", + env={"HATCH_MULTI_BUILD": "other"}, + ) + + assert Path(f"hatch_multi/tests/{project}/dist").exists() + assert listdir(f"hatch_multi/tests/{project}/dist") == ["hatch_cpp_test_project_basic_other-0.1.0-py3-none-any.whl"] + with ZipFile(f"hatch_multi/tests/{project}/dist/hatch_cpp_test_project_basic_other-0.1.0-py3-none-any.whl", "r") as zip_ref: + zip_ref.extractall(f"hatch_multi/tests/{project}/dist/extracted") + assert ( + Path(f"hatch_multi/tests/{project}/dist/extracted").joinpath("hatch_cpp_test_project_basic_other-0.1.0.dist-info/METADATA").read_text() + == """Metadata-Version: 2.4 +Name: hatch-cpp-test-project-basic-other +Version: 0.1.0 +Summary: Basic test project for hatch-cpp +Requires-Python: >=3.11 +Requires-Dist: organizeit2 +Provides-Extra: main +Requires-Dist: superstore; extra == 'main' +""" + ) + rmtree(f"hatch_multi/tests/{project}/dist")