Skip to content

Commit

Permalink
Add oca-gen-metapackage
Browse files Browse the repository at this point in the history
  • Loading branch information
sbidoul committed Oct 22, 2023
1 parent 969238e commit ab8aba8
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@
pass_filenames: false
language: python
files: (__manifest__\.py|__openerp__\.py|__terp__\.py)$

- id: oca-gen-metapackage
name: Generate setup/_metapackage project with dependencies on all addons in repo
always_run: true
entry: oca-gen-metapackage
language: python
pass_filenames: false
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
"docutils",
"pypandoc", # for oca-gen-addon-readme to work with markdown fragments
"ERPpeek",
"freezegun",
"github3.py>=1",
"jinja2",
"manifestoo-core>=1.1",
"PyYAML",
"polib",
"pygments",
"requests",
"toml>=0.10.0", # for oca-towncrier
"tomli ; python_version < '3.11'", # from 3.11 tomllib is in stdlib
"towncrier>=21.3", # for oca-towncrier
"selenium",
"twine",
Expand Down Expand Up @@ -64,6 +67,7 @@
"oca-publish-modules = tools.publish_modules:main",
"oca-gen-addon-readme = tools.gen_addon_readme:gen_addon_readme",
"oca-gen-addon-icon = tools.gen_addon_icon:gen_addon_icon",
"oca-gen-metapackage = tools.gen_metapackage:main",
"oca-towncrier = tools.oca_towncrier:oca_towncrier",
"oca-create-migration-issue = tools.create_migration_issue:main",
"oca-update-pre-commit-excluded-addons = "
Expand Down
Empty file added tests/__init__.py
Empty file.
87 changes: 87 additions & 0 deletions tests/test_gen_metapackage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import textwrap

from freezegun import freeze_time

from tools.gen_metapackage import main as gen_metapackage

from .utils import dir_changer


def _make_addon(addons_dir, addon_name, installable=True):
addon_dir = addons_dir / addon_name
addon_dir.mkdir()
manifest = {
"name": addon_name,
"version": "16.0.1.0.0",
"installable": installable,
}
addon_dir.joinpath("__manifest__.py").write_text(repr(manifest))
addon_dir.joinpath("__init__.py").touch()


@freeze_time("2023-05-01")
def test_gen_metapackage(tmp_path):
_make_addon(tmp_path, "addon1")
_make_addon(tmp_path, "addon2")
with dir_changer(tmp_path):
gen_metapackage(["odoo-addons-oca-test-repo"])
assert (
tmp_path / "setup" / "_metapackage" / "pyproject.toml"
).read_text() == textwrap.dedent(
"""\
[project]
name = "odoo-addons-oca-test-repo"
version = "16.0.20230501.0"
dependencies = [
"odoo-addon-addon1>=16.0dev,<16.1dev",
"odoo-addon-addon2>=16.0dev,<16.1dev",
]
classifiers=[
"Programming Language :: Python",
"Framework :: Odoo",
"Framework :: Odoo :: 16.0",
]
"""
)
# regenerate with no change
gen_metapackage(["odoo-addons-oca-test-repo"])
assert (
tmp_path / "setup" / "_metapackage" / "pyproject.toml"
).read_text() == textwrap.dedent(
"""\
[project]
name = "odoo-addons-oca-test-repo"
version = "16.0.20230501.0"
dependencies = [
"odoo-addon-addon1>=16.0dev,<16.1dev",
"odoo-addon-addon2>=16.0dev,<16.1dev",
]
classifiers=[
"Programming Language :: Python",
"Framework :: Odoo",
"Framework :: Odoo :: 16.0",
]
"""
)
# regenerate with one more addon, test version was incremented
_make_addon(tmp_path, "addon3")
gen_metapackage(["odoo-addons-oca-test-repo"])
assert (
tmp_path / "setup" / "_metapackage" / "pyproject.toml"
).read_text() == textwrap.dedent(
"""\
[project]
name = "odoo-addons-oca-test-repo"
version = "16.0.20230501.1"
dependencies = [
"odoo-addon-addon1>=16.0dev,<16.1dev",
"odoo-addon-addon2>=16.0dev,<16.1dev",
"odoo-addon-addon3>=16.0dev,<16.1dev",
]
classifiers=[
"Programming Language :: Python",
"Framework :: Odoo",
"Framework :: Odoo :: 16.0",
]
"""
)
15 changes: 15 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator


@contextmanager
def dir_changer(path: Path) -> Iterator[None]:
"""A context manager that changes the current working directory"""
old_cwd = Path.cwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_cwd)
10 changes: 10 additions & 0 deletions tools/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import sys

__all__ = [
"tomllib",
]

if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
136 changes: 136 additions & 0 deletions tools/gen_metapackage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Generate setup/_metapackage with dependencies on all installable addons in repo."""

import datetime
import re
import sys
from pathlib import Path
from typing import Any, List, Optional


from manifestoo_core.odoo_series import (
OdooSeries,
detect_from_addon_version as detect_odoo_series_from_addon_version,
)
from manifestoo_core.addon import is_addon_dir, Addon
from manifestoo_core.metadata import addon_name_to_requirement

from .compat import tomllib

METAPACKAGE_PATH = Path("setup") / "_metapackage"

PYPROJECT_TOML_METAPACKAGE = """\
[project]
name = "{name}"
version = "{version}"
dependencies = {dependencies}
classifiers=[
"Programming Language :: Python",
"Framework :: Odoo",
"Framework :: Odoo :: {odoo_series}",
]
"""


def _gen_metapackage(addons_dir: Path, name: str):
meta_install_requires = []
odoo_series_detected = set()
metapackage_path = addons_dir / METAPACKAGE_PATH
pyproject_toml_path = metapackage_path / "pyproject.toml"

for addon_dir in addons_dir.iterdir():
if not is_addon_dir(addon_dir):
continue
addon = Addon.from_addon_dir(addon_dir)
odoo_series = detect_odoo_series_from_addon_version(addon.manifest.version)
if not odoo_series:
sys.stderr.write(
f"Could not detect Odoo series from addon version in {addon_dir}\n"
)
return
meta_install_requires.append(
addon_name_to_requirement(addon_dir.name, odoo_series)
)
odoo_series_detected.add(odoo_series)

if len(meta_install_requires) == 0:
sys.stderr.write("No installable addon found, not generating metapackage.\n")
return
if len(odoo_series_detected) > 1:
raise RuntimeError(
f"Not all addon are for the same Odoo version: {odoo_series_detected}"
)

odoo_series = next(iter(odoo_series_detected))

dependencies = "[\n{}]".format(
"".join(
[
" " * 4 + '"' + install_require + '",\n'
for install_require in sorted(meta_install_requires)
]
),
)

version = _get_current_version(pyproject_toml_path)
if set(meta_install_requires) != set(
_get_current_dependencies(pyproject_toml_path)
):
version = _get_next_version(odoo_series, version)

if not metapackage_path.exists():
metapackage_path.mkdir(parents=True)
pyproject_toml = PYPROJECT_TOML_METAPACKAGE.format(
name=name,
version=version,
odoo_series=odoo_series.value,
dependencies=dependencies,
)
pyproject_toml_path.write_text(pyproject_toml)

_cleanup(metapackage_path)


def _get_current_dependencies(pyproject_toml_path: Path) -> Any:
if not pyproject_toml_path.exists():
return []
pyproject_toml = tomllib.loads(pyproject_toml_path.read_text())
return pyproject_toml.get("project", {}).get("dependencies", [])


def _get_current_version(pyproject_toml_path: Path) -> Optional[str]:
if not pyproject_toml_path.exists():
return None
pyproject_toml = tomllib.loads(pyproject_toml_path.read_text())
return pyproject_toml.get("project", {}).get("version", None)


def _get_next_version(odoo_series: OdooSeries, current_version: str):
version_date = datetime.date.today().strftime("%Y%m%d")
if current_version:
version_re = r"^[0-9]{1,2}\.0.(?P<date>[0-9]{8})\.(?P<index>[0-9]+)$"
mo = re.match(version_re, current_version)
if not mo:
raise RuntimeError(f"Could not parse version {current_version}")
if mo.group("date") == version_date:
index = int(mo.group("index")) + 1
else:
index = 0
else:
index = 0
return f"{odoo_series.value}.{version_date}.{index}"


def _cleanup(metapackage_path: Path):
for name in ("setup.py", "setup.cfg", "VERSION.txt"):
path = metapackage_path / name
if path.exists():
path.unlink()


def main(argv: Optional[List[str]] = None) -> int:
_gen_metapackage(Path.cwd(), argv[0] if argv else sys.argv[1])


if __name__ == "__main__":
sys.exit(main())

0 comments on commit ab8aba8

Please sign in to comment.