diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index bb77ed2e..4d43b0e9 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -23,3 +23,9 @@ types_or: ["text"] files: \.(po|pot)$ require_serial: false +- id: oca-check-unused-python-file + name: Check unused python files + entry: oca-check-unused-python-file + language: python + types: [python] + files: (controllers|models|report|wizard)\.py$|tests\/test_.*\.py$ diff --git a/setup.py b/setup.py index e1e29c6a..9880796b 100755 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ def generage_long_description(): "console_scripts": [ "oca-checks-odoo-module = oca_pre_commit_hooks.cli:main", "oca-checks-po = oca_pre_commit_hooks.cli_po:main", + "oca-check-unused-python-file = oca_pre_commit_hooks.check_unused_python_file:main", ] }, ) diff --git a/src/oca_pre_commit_hooks/check_unused_python_file.py b/src/oca_pre_commit_hooks/check_unused_python_file.py new file mode 100644 index 00000000..dd785ca3 --- /dev/null +++ b/src/oca_pre_commit_hooks/check_unused_python_file.py @@ -0,0 +1,39 @@ +import argparse +import ast +import functools +import os +from typing import Set, Union + + +@functools.lru_cache() +def get_imported_files(dirname: str) -> Set[str]: + imported_files = set() + init_file = os.path.join(dirname, "__init__.py") + if os.path.exists(init_file): + with open(init_file, encoding="utf-8") as init_fd: + init_ast = ast.parse(init_fd.read()) + + if isinstance(init_ast.body, list): + for module in filter(lambda elem: isinstance(elem, ast.ImportFrom), init_ast.body): + for name in module.names: + imported_files.add(name.name) + + return imported_files + + +def check_unused_python_file(filenames: list[str]) -> int: + status = 0 + for filename in filenames: + if os.path.basename(filename)[:-3] not in get_imported_files(os.path.dirname(filename)): + print(f"{filename}: not imported") + status = -1 + + return status + + +def main(argv: Union[list[str], None] = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("filenames", nargs="*") + args = parser.parse_args(argv) + + return check_unused_python_file(args.filenames) diff --git a/tests/test_check_unused_python_file.py b/tests/test_check_unused_python_file.py new file mode 100644 index 00000000..03096131 --- /dev/null +++ b/tests/test_check_unused_python_file.py @@ -0,0 +1,59 @@ +import os +from pathlib import Path + +from oca_pre_commit_hooks.check_unused_python_file import main + +FILE_NAMES = ["res_partner", "project_task", "helpdesk_ticket"] + + +def _gen_filepaths(filenames: list[str], basedir) -> list[str]: + return [f"{os.path.join(basedir, filename)}.py" for filename in filenames] + + +def test_all_used_files(tmpdir): + filepaths = _gen_filepaths(FILE_NAMES, tmpdir) + init_file = os.path.join(tmpdir, "__init__.py") + with open(init_file, "w", encoding="utf-8") as init_fd: + init_fd.writelines([f"from . import {filename}\n" for filename in FILE_NAMES]) + + for filepath in filepaths: + Path(filepath).touch() + + assert main(filepaths) == 0 + + Path(init_file).unlink() + with open(init_file, "w", encoding="utf-8") as init_fd: + init_fd.write(f"from . import {','.join(FILE_NAMES)}") + + assert main(filepaths) == 0 + + +def test_all_unused_files(tmpdir): + filepaths = _gen_filepaths(FILE_NAMES, tmpdir) + init_file = os.path.join(tmpdir, "__init__.py") + + Path(init_file).touch() + for filepath in filepaths: + Path(filepath).touch() + + assert main(filepaths) == -1 + + +def test_complex_init(tmpdir): + filepaths = _gen_filepaths(FILE_NAMES, tmpdir) + init_file = os.path.join(tmpdir, "__init__.py") + with open(init_file, "w", encoding="utf-8") as init_fd: + init_fd.writelines( + ["def hello(cr):\n", "\treturn cr.commit()\n"] + [f"from . import {filename}\n" for filename in FILE_NAMES] + ) + + for filepath in filepaths: + Path(filepath).touch() + + assert main(filepaths) == 0 + + extra_file = os.path.join(tmpdir, "extrafile.py") + Path(extra_file).touch() + filepaths.append(extra_file) + + assert main(filepaths) == -1