diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 8aa5c4ec..1a89f9ec 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 46f9215b..27d20ee3 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 00000000..e69de29b diff --git a/tests/test_gen_external_dependencies.py b/tests/test_gen_external_dependencies.py new file mode 100644 index 00000000..591bb8ec --- /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 00000000..de526f62 --- /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 00000000..6daa5551 --- /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", + "--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())