Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
6 changes: 2 additions & 4 deletions src/hatch_odoo/build_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,21 @@ 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__"
if editable_path.is_dir():
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,
Expand All @@ -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.
Expand Down
23 changes: 16 additions & 7 deletions src/hatch_odoo/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
7 changes: 3 additions & 4 deletions src/hatch_odoo/metadata_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,15 @@ 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(
hatch_odoo_config,
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:
Expand All @@ -56,7 +55,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(
Expand Down
5 changes: 5 additions & 0 deletions tests/data/project7/README.md
Original file line number Diff line number Diff line change
@@ -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.
Empty file added tests/data/project7/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions tests/data/project7/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "a",
"depends": ["mis-builder"],
"external_dependencies": {
"python": ["wrapt"],
},
}
20 changes: 20 additions & 0 deletions tests/data/project7/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = "."
20 changes: 12 additions & 8 deletions tests/test_build.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
# SPDX-FileCopyrightText: 2022-present Stéphane Bidoul <stephane.bidoul@acsone.eu>
# SPDX-FileCopyrightText: 2022-present ACSONE <https://acsone.eu>
# SPDX-FileCopyrightText: 2025-present XCG SAS <https://orbeet.io>
#
# SPDX-License-Identifier: MIT

import subprocess
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,
Expand All @@ -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
18 changes: 10 additions & 8 deletions tests/test_editable.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-FileCopyrightText: 2022-present Stéphane Bidoul <stephane.bidoul@acsone.eu>
# SPDX-FileCopyrightText: 2022-present ACSONE <https://acsone.eu>
# SPDX-FileCopyrightText: 2025-present XCG SAS <https://orbeet.io>
#
# SPDX-License-Identifier: MIT

Expand All @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading