From efb88b9bf41bd27d5314f5f7b0d0de23ee3e3f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 23 Sep 2023 12:39:58 +0200 Subject: [PATCH] Add pre-commit hook to generate external dependencies --- .pre-commit-hooks.yaml | 7 +++ setup.py | 4 ++ tests/__init__.py | 0 tests/test_gen_external_dependencies.py | 65 ++++++++++++++++++++++++ tests/utils.py | 15 ++++++ tools/gen_external_dependencies.py | 66 +++++++++++++++++++++++++ 6 files changed, 157 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_gen_external_dependencies.py create mode 100644 tests/utils.py create mode 100644 tools/gen_external_dependencies.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 8aa5c4ecf..1a89f9ece 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -32,3 +32,10 @@ pass_filenames: false language: python files: (__manifest__\.py|__openerp__\.py|__terp__\.py)$ + +- id: oca-gen-external-dependencies + name: Generate requirements.txt for an addons directory + entry: oca-gen-external-dependencies + language: python + pass_filenames: false + files: (__manifest__\.py|__openerp__\.py|__terp__\.py|setup\.py|pyproject\.toml)$ diff --git a/setup.py b/setup.py index 46f9215b5..27d20ee30 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,9 @@ "selenium", "twine", "wheel", + "pyproject_dependencies ; python_version>='3.7'", + "setuptools-odoo", # for oca-gen-external-dependencies + "whool", # for oca-gen-external-dependencies ], python_requires=">=3.6", classifiers=[ @@ -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-external-dependencies = tools.gen_external_dependencies:main", "oca-towncrier = tools.oca_towncrier:oca_towncrier", "oca-create-migration-issue = tools.create_migration_issue:main", "oca-update-pre-commit-excluded-addons = " diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_gen_external_dependencies.py b/tests/test_gen_external_dependencies.py new file mode 100644 index 000000000..591bb8ec3 --- /dev/null +++ b/tests/test_gen_external_dependencies.py @@ -0,0 +1,65 @@ +import subprocess +import sys +import textwrap + +import pytest + +from tools.gen_external_dependencies import main as gen_external_dependencies + +from .utils import dir_changer + + +def _make_addon( + addons_dir, addon_name, depends, external_dependencies, installable=True +): + addon_dir = addons_dir / addon_name + addon_dir.mkdir() + manifest = { + "name": addon_name, + "version": "16.0.1.0.0", + "depends": depends, + "external_dependencies": external_dependencies, + "installable": installable, + } + addon_dir.joinpath("__manifest__.py").write_text(repr(manifest)) + addon_dir.joinpath("__init__.py").touch() + + +@pytest.mark.skipif("sys.version_info < (3,7)") +def test_gen_external_dependencies(tmp_path): + ... + _make_addon( + tmp_path, + addon_name="addon1", + depends=["mis_builder"], + external_dependencies={"python": ["requests", "xlrd"]}, + ) + _make_addon( + tmp_path, + addon_name="addon2", + depends=[], + external_dependencies={"python": ["requests", "pydantic>=2"]}, + ) + _make_addon( + tmp_path, + addon_name="addon3", + depends=[], + external_dependencies={}, + installable=False, + ) + with dir_changer(tmp_path): + assert gen_external_dependencies() != 0 # no pyproject.toml + subprocess.run([sys.executable, "-m", "whool", "init"], check=True) + assert tmp_path.joinpath("addon1").joinpath("pyproject.toml").is_file() + assert tmp_path.joinpath("addon2").joinpath("pyproject.toml").is_file() + assert gen_external_dependencies() == 0 + requirements_txt_path = tmp_path.joinpath("requirements.txt") + assert requirements_txt_path.is_file() + assert requirements_txt_path.read_text() == textwrap.dedent( + """\ + # generated from manifests external_dependencies + pydantic>=2 + requests + xlrd + """ + ) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..de526f621 --- /dev/null +++ b/tests/utils.py @@ -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) diff --git a/tools/gen_external_dependencies.py b/tools/gen_external_dependencies.py new file mode 100644 index 000000000..7de81d0d7 --- /dev/null +++ b/tools/gen_external_dependencies.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Generate requirements.txt with external dependencies of Odoo addons.""" + +import os +import subprocess +import sys +from pathlib import Path + + +def main() -> int: + if sys.version_info < (3, 7): + raise SystemExit("Python 3.7+ is required.") + + projects = [ + *Path.glob(Path.cwd(), "*/pyproject.toml"), + *Path.glob(Path.cwd(), "setup/*/setup.py"), + ] + + if not projects: + return 1 + + env = os.environ.copy() + env.update( + { + # for better performance, since we are not interested in precise versions + "WHOOL_POST_VERSION_STRATEGY_OVERRIDE": "none", + "SETUPTOOLS_ODOO_POST_VERSION_STRATEGY_OVERRIDE": "none", + } + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "pyproject_dependencies", + "--no-isolation", # whool and setuptools Odoo must be preinstalled + "--ignore-build-errors", # ignore uninstallable addons + "--name-filter", + r"^(odoo$|odoo\d*-addon-)", # filter out odoo and odoo addons + *projects, + ], + env=env, + check=False, + stdout=subprocess.PIPE, + text=True, + ) + + if result.returncode != 0: + return result.returncode + + requirements = result.stdout + + requirements_path = Path("requirements.txt") + if requirements: + with requirements_path.open("w") as f: + f.write("# generated from manifests external_dependencies\n") + f.write(requirements) + else: + if requirements_path.exists(): + requirements_path.unlink() + + return 0 + + +if __name__ == "__main__": + sys.exit(main())