From c41daf449f01a06ff7174fea72d550470e2eb023 Mon Sep 17 00:00:00 2001 From: Vincent Hatakeyama Date: Wed, 17 Dec 2025 09:55:41 +0100 Subject: [PATCH 1/2] Fix exception messages The messages mention tools.hatch-odoo instead of tool.hatch-odoo. --- src/hatch_odoo/config.py | 2 +- src/hatch_odoo/metadata_hook.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hatch_odoo/config.py b/src/hatch_odoo/config.py index 130696f..cd7f7ad 100644 --- a/src/hatch_odoo/config.py +++ b/src/hatch_odoo/config.py @@ -29,7 +29,7 @@ def load_hatch_odoo_config(root: str) -> dict: def iter_addons_dirs(root: str, config: dict) -> Iterator[Path]: addons_dirs = config.get("addons_dirs") if not addons_dirs: - raise RuntimeError("missing tools.hatch-odoo.addons_dir in pyproject.toml") + raise RuntimeError("missing tool.hatch-odoo.addons_dir in pyproject.toml") for addons_dir in [Path(root) / d for d in addons_dirs]: if not addons_dir.is_dir(): continue diff --git a/src/hatch_odoo/metadata_hook.py b/src/hatch_odoo/metadata_hook.py index e8edfa0..6efc0e0 100644 --- a/src/hatch_odoo/metadata_hook.py +++ b/src/hatch_odoo/metadata_hook.py @@ -56,7 +56,7 @@ def update(self, metadata: dict) -> None: "'dependencies' may not be listed in the 'project' table when using " "hatch-odoo to populate dependencies from Odoo addons manifests. " "If you need to add dependencies that are not in Odoo addons " - "manifests, please use the 'tools.hatch-odoo.dependencies' key." + "manifests, please use the 'tool.hatch-odoo.dependencies' key." ) if "dependencies" not in metadata.get("dynamic", []): raise ValueError( From bcef70fccaa214450b2b08be9a31dc5727b539b6 Mon Sep 17 00:00:00 2001 From: Vincent Hatakeyama Date: Wed, 17 Dec 2025 12:01:27 +0100 Subject: [PATCH 2/2] Add the ability to package addons by name and path --- README.md | 8 ++++++++ src/hatch_odoo/build_hook.py | 6 ++---- src/hatch_odoo/config.py | 21 +++++++++++++++------ src/hatch_odoo/metadata_hook.py | 5 ++--- tests/data/project7/README.md | 5 +++++ tests/data/project7/__init__.py | 0 tests/data/project7/__manifest__.py | 7 +++++++ tests/data/project7/pyproject.toml | 20 ++++++++++++++++++++ tests/test_build.py | 20 ++++++++++++-------- tests/test_editable.py | 18 ++++++++++-------- tests/test_metadata.py | 10 +++++++++- 11 files changed, 90 insertions(+), 30 deletions(-) create mode 100644 tests/data/project7/README.md create mode 100644 tests/data/project7/__init__.py create mode 100644 tests/data/project7/__manifest__.py create mode 100644 tests/data/project7/pyproject.toml diff --git a/README.md b/README.md index e843f01..e2acc59 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,14 @@ dependencies = ["click-odoo-contrib"] addons_dirs = ["."] ``` +Instead of setting `tool.hatch-odoo.addons_dirs`, a table can be set up to indicate addon +name and the relative path. + +```toml +[tool.hatch-odoo.addon_dirs] +my_module = "." +``` + You can then install it in editable mode, together with its dependencies in a virtual environment with a procedure like this: diff --git a/src/hatch_odoo/build_hook.py b/src/hatch_odoo/build_hook.py index e7dcd53..15301f1 100644 --- a/src/hatch_odoo/build_hook.py +++ b/src/hatch_odoo/build_hook.py @@ -26,14 +26,13 @@ def initialize(self, version: str, build_data: Dict[str, Any]) -> None: hatch_odoo_config = load_hatch_odoo_config(self.root) if version == "standard": force_include = build_data["force_include"] - for addon_dir in iter_addon_dirs( + for addon_dir, addon_name in iter_addon_dirs( self.root, hatch_odoo_config, # We force-include addons that are installable False to avoid that the # default hatch behaviour adds them at the wrong place in the wheel. allow_not_installable=True, ): - addon_name = addon_dir.name force_include[addon_dir] = f"odoo/addons/{addon_name}" elif version == "editable" and self.config.get("editable_symlinks", True): editable_path = Path(self.root) / "build" / "__editable_odoo_addons__" @@ -41,7 +40,7 @@ def initialize(self, version: str, build_data: Dict[str, Any]) -> None: shutil.rmtree(editable_path) editable_odoo_addons_path = editable_path / "odoo" / "addons" has_editable_symlinks = False - for addon_dir in iter_addon_dirs( + for addon_dir, addon_name in iter_addon_dirs( self.root, hatch_odoo_config, allow_not_installable=False, @@ -50,7 +49,6 @@ def initialize(self, version: str, build_data: Dict[str, Any]) -> None: if not has_editable_symlinks: editable_odoo_addons_path.mkdir(parents=True) has_editable_symlinks = True - addon_name = addon_dir.name editable_addon_path = editable_odoo_addons_path / addon_name editable_addon_path.symlink_to(addon_dir) # Add .pth to build/__editable_odoo_addons__ in wheel. diff --git a/src/hatch_odoo/config.py b/src/hatch_odoo/config.py index cd7f7ad..fea2ad3 100644 --- a/src/hatch_odoo/config.py +++ b/src/hatch_odoo/config.py @@ -4,7 +4,7 @@ import sys from pathlib import Path -from typing import Iterator +from typing import Iterator, Tuple if sys.version_info < (3, 11): import tomli as tomllib @@ -40,8 +40,17 @@ def iter_addon_dirs( root: str, config: dict, allow_not_installable: bool, -) -> Iterator[Path]: - for addons_dir in iter_addons_dirs(root, config): - for addon_dir in addons_dir.iterdir(): - if is_addon_dir(addon_dir, allow_not_installable=allow_not_installable): - yield addon_dir +) -> Iterator[Tuple[Path, str]]: + addon_dirs = config.get("addon_dirs") + if addon_dirs: + for addon_name in addon_dirs: + addon_dir_path = Path(root, addon_dirs[addon_name]) + if is_addon_dir( + addon_dir_path, allow_not_installable=allow_not_installable + ): + yield addon_dir_path, addon_name + else: + for addons_dir in iter_addons_dirs(root, config): + for addon_dir in addons_dir.iterdir(): + if is_addon_dir(addon_dir, allow_not_installable=allow_not_installable): + yield addon_dir, addon_dir.name diff --git a/src/hatch_odoo/metadata_hook.py b/src/hatch_odoo/metadata_hook.py index 6efc0e0..4311a08 100644 --- a/src/hatch_odoo/metadata_hook.py +++ b/src/hatch_odoo/metadata_hook.py @@ -29,8 +29,7 @@ def _get_odooo_addons_dependencies(self) -> List[str]: ) ) depends_override = hatch_odoo_config.get("depends_override", {}) - for addon_dir in addon_dirs: - addon_name = addon_dir.name + for addon_dir, addon_name in addon_dirs: # Do not add dependencies on addons that are in the project. depends_override[addon_name] = None options = dict( @@ -38,7 +37,7 @@ def _get_odooo_addons_dependencies(self) -> List[str]: depends_override=depends_override, post_version_strategy_override=POST_VERSION_STRATEGY_NONE, ) - for addon_dir in addon_dirs: + for addon_dir, _ in addon_dirs: try: addon_metadata = metadata_from_addon_dir(addon_dir, options) except Exception as e: diff --git a/tests/data/project7/README.md b/tests/data/project7/README.md new file mode 100644 index 0000000..4a8386c --- /dev/null +++ b/tests/data/project7/README.md @@ -0,0 +1,5 @@ +# project7 + +A basic project where the root directory is an odoo addon. + +This layout can be used when using whool is not possible. diff --git a/tests/data/project7/__init__.py b/tests/data/project7/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/project7/__manifest__.py b/tests/data/project7/__manifest__.py new file mode 100644 index 0000000..93ab4ba --- /dev/null +++ b/tests/data/project7/__manifest__.py @@ -0,0 +1,7 @@ +{ + "name": "a", + "depends": ["mis-builder"], + "external_dependencies": { + "python": ["wrapt"], + }, +} diff --git a/tests/data/project7/pyproject.toml b/tests/data/project7/pyproject.toml new file mode 100644 index 0000000..8f769eb --- /dev/null +++ b/tests/data/project7/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["hatchling", "hatch-odoo"] +build-backend = "hatchling.build" + +[project] +name = "project7" +version = "1.0" +readme = "README.md" +dynamic = ["dependencies"] + +[tool.hatch.metadata.hooks.odoo-addons-dependencies] + +[tool.hatch.build.hooks.odoo-addons-dirs] + +[tool.hatch-odoo] +odoo_version_override = "15.0" +dependencies = ["click-odoo-contrib"] + +[tool.hatch-odoo.addon_dirs] +project7 = "." diff --git a/tests/test_build.py b/tests/test_build.py index 666f493..cb942f3 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2022-present Stéphane Bidoul # SPDX-FileCopyrightText: 2022-present ACSONE +# SPDX-FileCopyrightText: 2025-present XCG SAS # # SPDX-License-Identifier: MIT @@ -7,25 +8,28 @@ import sys import zipfile from pathlib import Path +from typing import Tuple import pytest @pytest.mark.parametrize( - "project, addons_only", + "project, addons_only, addon_names", [ - ("project1", False), - ("project2", False), - ("project3", False), - ("project4", False), - ("project5", False), - ("project6", True), + ("project1", False, ("addona", "addonb", "addon_uninstallable")), + ("project2", False, ("addona", "addonb", "addon_uninstallable")), + ("project3", False, ("addona", "addonb", "addon_uninstallable")), + ("project4", False, ("addona", "addonb", "addon_uninstallable")), + ("project5", False, ("addona", "addonb", "addon_uninstallable")), + ("project6", True, ("addona", "addonb", "addon_uninstallable")), + ("project7", True, ("project7",)), ], ) @pytest.mark.parametrize("build_via_sdist", [True, False]) def test_build( project: str, addons_only: bool, + addon_names: Tuple[str], build_via_sdist: bool, data_path: Path, tmp_path: Path, @@ -48,7 +52,7 @@ def test_build( wheel_file = next(tmp_path.glob(f"{project}-*.whl")) with zipfile.ZipFile(wheel_file) as zip_file: files = set(zip_file.namelist()) - for addon_name in ("addona", "addonb", "addon_uninstallable"): + for addon_name in addon_names: assert f"odoo/addons/{addon_name}/__init__.py" in files assert f"odoo/addons/{addon_name}/__manifest__.py" in files assert addons_only or f"{project}/__init__.py" in files diff --git a/tests/test_editable.py b/tests/test_editable.py index 210830b..e786502 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2022-present Stéphane Bidoul # SPDX-FileCopyrightText: 2022-present ACSONE +# SPDX-FileCopyrightText: 2025-present XCG SAS # # SPDX-License-Identifier: MIT @@ -19,19 +20,21 @@ @pytest.mark.parametrize( - "project_name,expected_editable_pth_lines", + "project_name,expected_editable_pth_lines,expected_editable_addon_names", [ - ("project1", ["src"]), - ("project2", ["src", "build/__editable_odoo_addons__"]), - ("project3", [""]), - ("project4", ["src", "addons_group1", "addons_group2"]), - ("project5", ["", "build/__editable_odoo_addons__"]), - ("project6", ["build/__editable_odoo_addons__"]), + ("project1", ["src"], ["addona", "addonb"]), + ("project2", ["src", "build/__editable_odoo_addons__"], ["addona", "addonb"]), + ("project3", [""], ["addona", "addonb"]), + ("project4", ["src", "addons_group1", "addons_group2"], ["addona", "addonb"]), + ("project5", ["", "build/__editable_odoo_addons__"], ["addona", "addonb"]), + ("project6", ["build/__editable_odoo_addons__"], ["addona", "addonb"]), + ("project7", ["build/__editable_odoo_addons__"], ["project7"]), ], ) def test_odoo_addons_dependencies( project_name: str, expected_editable_pth_lines: List[str], + expected_editable_addon_names: List[str], data_path: Path, tmp_path: Path, ) -> None: @@ -64,7 +67,6 @@ def test_odoo_addons_dependencies( str(data_path / project_name / line) for line in expected_editable_pth_lines } # Check all addons are in the editable paths. - expected_editable_addon_names = ["addona", "addonb"] editable_addon_names = [] for pth_line in pth_lines: addons_dir = Path(pth_line) / "odoo" / "addons" diff --git a/tests/test_metadata.py b/tests/test_metadata.py index bbbe326..45fc851 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -12,7 +12,15 @@ @pytest.mark.parametrize( "project_name", - ["project1", "project2", "project3", "project4", "project5", "project6"], + [ + "project1", + "project2", + "project3", + "project4", + "project5", + "project6", + "project7", + ], ) def test_odoo_addons_dependencies( project_name: str, data_path: Path, tmp_path: Path