diff --git a/dep_checker/__init__.py b/dep_checker/__init__.py index 84b21fe..6fb87c7 100644 --- a/dep_checker/__init__.py +++ b/dep_checker/__init__.py @@ -39,7 +39,7 @@ from consolekit.terminal_colours import Fore, resolve_color_default from domdf_python_tools.paths import PathPlus, in_directory from domdf_python_tools.typing import PathLike -from shippinglabel.requirements import read_requirements +from packaging.requirements import Requirement # this package from dep_checker.config import AllowedUnused, ConfigReader, NameMapping, NamespacePackages @@ -315,18 +315,18 @@ def iter_files_to_check(basepath: PathLike, pkg_name: str) -> Iterator[PathPlus] def check_imports( pkg_name: str, - req_file: PathLike = "requirements.txt", + *requirements: Requirement, allowed_unused: Optional[List[str]] = None, colour: Optional[bool] = None, name_mapping: Optional[Dict[str, str]] = None, namespace_packages: Optional[List[str]] = None, work_dir: PathLike = '.', ) -> int: - """ + r""" Check imports for the given package, against the given requirements file. :param pkg_name: - :param req_file: + :param \*requirements: :param allowed_unused: List of requirements which are allowed to be unused in the source code. :default allowed_unused: ``[]`` :param colour: Whether to use coloured output. @@ -346,6 +346,10 @@ def check_imports( * Added the ``name_mapping`` option. * Added the ``work_dir`` option. + + .. versionchanged:: 1.0.0 Replaced the ``req_file`` argument with the ``*requirements`` argument. + Use :func:`shippinglabel.requirements.read_requirements(req_file)[0] ` + to get the original bevhaviour. """ ret = 0 @@ -362,17 +366,11 @@ def check_imports( namespace_packages = NamespacePackages.get(config) work_dir = PathPlus(work_dir) - req_file = PathPlus(req_file) - - if not req_file.is_absolute(): - req_file = work_dir / req_file - - req_file = req_file.abspath() work_dir = work_dir.abspath() checker = DepChecker( pkg_name, - requirements=map(attrgetter("name"), read_requirements(req_file)[0]), + requirements=map(attrgetter("name"), requirements), allowed_unused=allowed_unused, name_mapping=name_mapping, namespace_packages=namespace_packages, diff --git a/dep_checker/__main__.py b/dep_checker/__main__.py index 7c91ad7..80ea330 100644 --- a/dep_checker/__main__.py +++ b/dep_checker/__main__.py @@ -28,12 +28,13 @@ # stdlib import sys -from typing import List, Optional +from typing import List, Optional, Set # 3rd party import click +import dom_toml from consolekit import click_command -from consolekit.options import colour_option +from consolekit.options import colour_option, flag_option from consolekit.utils import abort # this package @@ -61,8 +62,13 @@ "--req-file", type=click.STRING, metavar="FILENAME", - default="requirements.txt", - help="The requirements file.", + help="Parse the requirements from the given requirements file (or pyproject.toml file with --pyproject).", + ) +@flag_option( + "-p", + "--pyproject", + help="Parse the requirements from 'pyproject.toml'.", + default=False, ) @click.argument( "pkg-name", @@ -71,25 +77,56 @@ @click_command() def main( pkg_name: str, - req_file: str, allowed_unused: Optional[List[str]], - colour: Optional[bool], + colour: Optional[bool] = None, + req_file: Optional[str] = None, work_dir: str = '.', + pyproject: bool = False ) -> None: """ Tool to check all requirements are actually required. """ + # 3rd party + from domdf_python_tools.paths import PathLike, PathPlus + from shippinglabel.requirements import ComparableRequirement, parse_pyproject_dependencies, read_requirements + if allowed_unused == (): allowed_unused = None + work_dir_p = PathPlus(work_dir) + + def read_req_file(req_file: PathLike) -> Set[ComparableRequirement]: + req_file = PathPlus(req_file) + + if not req_file.is_absolute(): + req_file = work_dir_p / req_file + + return read_requirements(req_file)[0] + + if pyproject: + if req_file is None: + req_file = "pyproject.toml" + dynamic = dom_toml.load(req_file)["project"].get("dynamic", ()) + + if "requirements" in dynamic: + requirements = read_requirements("requirements.txt")[0] + else: + requirements = parse_pyproject_dependencies(req_file, flavour="pep621") + + else: + if req_file is None: + req_file = "requirements.txt" + + requirements = read_req_file(req_file) + try: ret = check_imports( pkg_name, - req_file=req_file, + *requirements, allowed_unused=allowed_unused, colour=colour, - work_dir=work_dir, + work_dir=work_dir_p, ) sys.exit(ret) except FileNotFoundError as e: diff --git a/doc-source/usage.rst b/doc-source/usage.rst index c4e7453..0b486a4 100644 --- a/doc-source/usage.rst +++ b/doc-source/usage.rst @@ -109,6 +109,12 @@ Command Line :prog: dep-checker +The :option:`-P / --pyproject <-P>` option takes precedence over the :option:`--req-file` option. + +.. versionchanged:: 1.0.0 + + Added the :option:`-P / --pyproject <-P>` option. + As a ``pre-commit`` hook ---------------------------- diff --git a/requirements.txt b/requirements.txt index a1ad829..54a2f9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ configconfig>=0.4.0 consolekit>=0.4.0 dom-toml>=0.2.0 domdf-python-tools>=3.2.0 +packaging>=20.9 shippinglabel>=0.1.0 diff --git a/tests/test_dep_checker.py b/tests/test_dep_checker.py index 9eab2b8..06a55ca 100644 --- a/tests/test_dep_checker.py +++ b/tests/test_dep_checker.py @@ -1,12 +1,13 @@ # stdlib -from typing import Any, Dict +from typing import Any, Dict, List # 3rd party import pytest from coincidence import AdvancedDataRegressionFixture +from coincidence.regressions import AdvancedFileRegressionFixture from consolekit.testing import CliRunner, Result from domdf_python_tools.paths import PathPlus, in_directory -from pytest_regressions.file_regression import FileRegressionFixture +from packaging.requirements import Requirement # this package from dep_checker import ( @@ -22,46 +23,52 @@ def test_check_imports( single_file_project: PathPlus, capsys, + requirements: List[str], advanced_data_regression: AdvancedDataRegressionFixture, ): - assert check_imports("my_project", work_dir=single_file_project, colour=False) == 1 + assert check_imports( + "my_project", *map(Requirement, requirements), work_dir=single_file_project, colour=False + ) == 1 advanced_data_regression.check(capsys.readouterr()) def test_check_imports_package( package_project: PathPlus, capsys, + requirements: List[str], advanced_data_regression: AdvancedDataRegressionFixture, ): - assert check_imports("my_project", work_dir=package_project, colour=False) == 1 + assert check_imports( + "my_project", *map(Requirement, requirements), work_dir=package_project, colour=False + ) == 1 advanced_data_regression.check(capsys.readouterr()) def test_cli( single_file_project: PathPlus, - file_regression: FileRegressionFixture, + advanced_file_regression: AdvancedFileRegressionFixture, ): with in_directory(single_file_project): runner = CliRunner() result: Result = runner.invoke(main, args=["my_project", "--no-colour"]) - result.check_stdout(file_regression) + result.check_stdout(advanced_file_regression) assert result.exit_code == 1 -def test_cli_package(package_project: PathPlus, file_regression: FileRegressionFixture): +def test_cli_package(package_project: PathPlus, advanced_file_regression: AdvancedFileRegressionFixture): with in_directory(package_project): runner = CliRunner() result: Result = runner.invoke(main, args=["my_project", "--no-colour"]) - result.check_stdout(file_regression) + result.check_stdout(advanced_file_regression) assert result.exit_code == 1 -def test_cli_package_srcdir(package_project: PathPlus, file_regression: FileRegressionFixture): +def test_cli_package_srcdir(package_project: PathPlus, advanced_file_regression: AdvancedFileRegressionFixture): (package_project / "my_project").move(package_project / "src" / "my_project") with in_directory(package_project): @@ -71,7 +78,7 @@ def test_cli_package_srcdir(package_project: PathPlus, file_regression: FileRegr args=["my_project", "--no-colour", "--work-dir", "src", "--req-file", "../requirements.txt"], ) - result.check_stdout(file_regression) + result.check_stdout(advanced_file_regression) assert result.exit_code == 1 @@ -96,9 +103,11 @@ def test_with_config( capsys, advanced_data_regression: AdvancedDataRegressionFixture, config: Dict[str, Any], + requirements: List[str], ): assert check_imports( "my_project", + *map(Requirement, requirements), work_dir=single_file_project, colour=False, **config, diff --git a/tests/test_dep_checker_pyproject.py b/tests/test_dep_checker_pyproject.py new file mode 100644 index 0000000..c5a97bd --- /dev/null +++ b/tests/test_dep_checker_pyproject.py @@ -0,0 +1,120 @@ +# stdlib +from typing import List + +# 3rd party +import dom_toml +from coincidence.regressions import AdvancedFileRegressionFixture +from consolekit.testing import CliRunner, Result +from domdf_python_tools.paths import PathPlus, in_directory + +# this package +from dep_checker.__main__ import main + + +def test_cli( + single_file_project: PathPlus, + requirements: List[str], + advanced_file_regression: AdvancedFileRegressionFixture, + ): + + with in_directory(single_file_project): + dom_toml.dump( + {"project": {"name": "foo", "requirements": requirements}}, + single_file_project / "pyproject.toml", + ) + runner = CliRunner() + result: Result = runner.invoke(main, args=["my_project", "--no-colour", "-p"]) + + result.check_stdout(advanced_file_regression) + assert result.exit_code == 1 + + +def test_cli_package( + package_project: PathPlus, + requirements: List[str], + advanced_file_regression: AdvancedFileRegressionFixture + ): + + with in_directory(package_project): + dom_toml.dump( + {"project": {"name": "foo", "requirements": requirements}}, + package_project / "pyproject.toml", + ) + runner = CliRunner() + result: Result = runner.invoke(main, args=["my_project", "--no-colour", "-p"]) + + result.check_stdout(advanced_file_regression) + assert result.exit_code == 1 + + +def test_cli_package_srcdir( + package_project: PathPlus, + requirements: List[str], + advanced_file_regression: AdvancedFileRegressionFixture + ): + (package_project / "my_project").move(package_project / "src" / "my_project") + + with in_directory(package_project): + dom_toml.dump( + {"project": {"name": "foo", "requirements": requirements}}, + package_project / "pyproject.toml", + ) + runner = CliRunner() + result: Result = runner.invoke( + main, + args=["my_project", "--no-colour", "--work-dir", "src", "-p"], + ) + + result.check_stdout(advanced_file_regression) + assert result.exit_code == 1 + + +def test_cli_dynamic( + single_file_project: PathPlus, + advanced_file_regression: AdvancedFileRegressionFixture, + ): + + with in_directory(single_file_project): + dom_toml.dump( + {"project": {"name": "foo", "dynamic": ["requirements"]}}, + single_file_project / "pyproject.toml", + ) + runner = CliRunner() + result: Result = runner.invoke(main, args=["my_project", "--no-colour", "-p"]) + + result.check_stdout(advanced_file_regression) + assert result.exit_code == 1 + + +def test_cli_package_dynamic(package_project: PathPlus, advanced_file_regression: AdvancedFileRegressionFixture): + + with in_directory(package_project): + dom_toml.dump( + {"project": {"name": "foo", "dynamic": ["requirements"]}}, + package_project / "pyproject.toml", + ) + runner = CliRunner() + result: Result = runner.invoke(main, args=["my_project", "--no-colour", "-p"]) + + result.check_stdout(advanced_file_regression) + assert result.exit_code == 1 + + +def test_cli_package_srcdir_dynamic( + package_project: PathPlus, advanced_file_regression: AdvancedFileRegressionFixture + ): + (package_project / "my_project").move(package_project / "src" / "my_project") + + with in_directory(package_project): + dom_toml.dump( + {"project": {"name": "foo", "dynamic": ["requirements"]}}, + package_project / "pyproject.toml", + ) + runner = CliRunner() + result: Result = runner.invoke( + main, + args=["my_project", "--no-colour", "--work-dir", "src", "-p"], + ) + + result.check_stdout(advanced_file_regression) + assert result.exit_code == 1 diff --git a/tests/test_dep_checker_pyproject_/test_cli.txt b/tests/test_dep_checker_pyproject_/test_cli.txt new file mode 100644 index 0000000..442e94e --- /dev/null +++ b/tests/test_dep_checker_pyproject_/test_cli.txt @@ -0,0 +1,12 @@ +✘ pytest imported at my_project.py:5 but not listed as a requirement +✘ chemistry_tools imported at my_project.py:6 but not listed as a requirement +✘ pathlib2 imported at my_project.py:7 but not listed as a requirement +✘ typing_extensions imported at my_project.py:8 but not listed as a requirement +✘ domdf_python_tools imported at my_project.py:9 but not listed as a requirement +✘ consolekit imported at my_project.py:10 but not listed as a requirement +✘ click imported at my_project.py:11 but not listed as a requirement +✘ pandas imported at my_project.py:12 but not listed as a requirement +✘ ruamel imported at my_project.py:13 but not listed as a requirement +✘ Bio imported at my_project.py:15 but not listed as a requirement +✘ configconfig imported at my_project.py:31 but not listed as a requirement +✘ setuptools imported at my_project.py:35 but not listed as a requirement diff --git a/tests/test_dep_checker_pyproject_/test_cli_dynamic.txt b/tests/test_dep_checker_pyproject_/test_cli_dynamic.txt new file mode 100644 index 0000000..0e4e81d --- /dev/null +++ b/tests/test_dep_checker_pyproject_/test_cli_dynamic.txt @@ -0,0 +1,15 @@ +✘ pytest imported at my_project.py:5 but not listed as a requirement +✘ chemistry_tools imported at my_project.py:6 but not listed as a requirement +✘ pathlib2 imported at my_project.py:7 but not listed as a requirement +✘ typing_extensions imported at my_project.py:8 but not listed as a requirement +✘ domdf_python_tools imported at my_project.py:9 but not listed as a requirement +✘ click imported at my_project.py:11 but not listed as a requirement +✘ ruamel imported at my_project.py:13 but not listed as a requirement +✘ Bio imported at my_project.py:15 but not listed as a requirement +✘ configconfig imported at my_project.py:31 but not listed as a requirement +✘ setuptools imported at my_project.py:35 but not listed as a requirement +✘ biopython never imported +✘ coincidence never imported +✔ consolekit imported at my_project.py:10 +✘ numpy never imported +✔ pandas imported at my_project.py:12 diff --git a/tests/test_dep_checker_pyproject_/test_cli_package.txt b/tests/test_dep_checker_pyproject_/test_cli_package.txt new file mode 100644 index 0000000..cb74eac --- /dev/null +++ b/tests/test_dep_checker_pyproject_/test_cli_package.txt @@ -0,0 +1,12 @@ +✘ pytest imported at my_project/__init__.py:5 but not listed as a requirement +✘ chemistry_tools imported at my_project/__init__.py:6 but not listed as a requirement +✘ pathlib2 imported at my_project/__init__.py:7 but not listed as a requirement +✘ typing_extensions imported at my_project/__init__.py:8 but not listed as a requirement +✘ domdf_python_tools imported at my_project/__init__.py:9 but not listed as a requirement +✘ consolekit imported at my_project/__init__.py:10 but not listed as a requirement +✘ click imported at my_project/__init__.py:11 but not listed as a requirement +✘ pandas imported at my_project/__init__.py:12 but not listed as a requirement +✘ ruamel imported at my_project/__init__.py:13 but not listed as a requirement +✘ Bio imported at my_project/__init__.py:15 but not listed as a requirement +✘ configconfig imported at my_project/__init__.py:31 but not listed as a requirement +✘ setuptools imported at my_project/__init__.py:35 but not listed as a requirement diff --git a/tests/test_dep_checker_pyproject_/test_cli_package_dynamic.txt b/tests/test_dep_checker_pyproject_/test_cli_package_dynamic.txt new file mode 100644 index 0000000..f46e57d --- /dev/null +++ b/tests/test_dep_checker_pyproject_/test_cli_package_dynamic.txt @@ -0,0 +1,15 @@ +✘ pytest imported at my_project/__init__.py:5 but not listed as a requirement +✘ chemistry_tools imported at my_project/__init__.py:6 but not listed as a requirement +✘ pathlib2 imported at my_project/__init__.py:7 but not listed as a requirement +✘ typing_extensions imported at my_project/__init__.py:8 but not listed as a requirement +✘ domdf_python_tools imported at my_project/__init__.py:9 but not listed as a requirement +✘ click imported at my_project/__init__.py:11 but not listed as a requirement +✘ ruamel imported at my_project/__init__.py:13 but not listed as a requirement +✘ Bio imported at my_project/__init__.py:15 but not listed as a requirement +✘ configconfig imported at my_project/__init__.py:31 but not listed as a requirement +✘ setuptools imported at my_project/__init__.py:35 but not listed as a requirement +✘ biopython never imported +✘ coincidence never imported +✔ consolekit imported at my_project/__init__.py:10 +✘ numpy never imported +✔ pandas imported at my_project/__init__.py:12 diff --git a/tests/test_dep_checker_pyproject_/test_cli_package_srcdir.txt b/tests/test_dep_checker_pyproject_/test_cli_package_srcdir.txt new file mode 100644 index 0000000..cb74eac --- /dev/null +++ b/tests/test_dep_checker_pyproject_/test_cli_package_srcdir.txt @@ -0,0 +1,12 @@ +✘ pytest imported at my_project/__init__.py:5 but not listed as a requirement +✘ chemistry_tools imported at my_project/__init__.py:6 but not listed as a requirement +✘ pathlib2 imported at my_project/__init__.py:7 but not listed as a requirement +✘ typing_extensions imported at my_project/__init__.py:8 but not listed as a requirement +✘ domdf_python_tools imported at my_project/__init__.py:9 but not listed as a requirement +✘ consolekit imported at my_project/__init__.py:10 but not listed as a requirement +✘ click imported at my_project/__init__.py:11 but not listed as a requirement +✘ pandas imported at my_project/__init__.py:12 but not listed as a requirement +✘ ruamel imported at my_project/__init__.py:13 but not listed as a requirement +✘ Bio imported at my_project/__init__.py:15 but not listed as a requirement +✘ configconfig imported at my_project/__init__.py:31 but not listed as a requirement +✘ setuptools imported at my_project/__init__.py:35 but not listed as a requirement diff --git a/tests/test_dep_checker_pyproject_/test_cli_package_srcdir_dynamic.txt b/tests/test_dep_checker_pyproject_/test_cli_package_srcdir_dynamic.txt new file mode 100644 index 0000000..f46e57d --- /dev/null +++ b/tests/test_dep_checker_pyproject_/test_cli_package_srcdir_dynamic.txt @@ -0,0 +1,15 @@ +✘ pytest imported at my_project/__init__.py:5 but not listed as a requirement +✘ chemistry_tools imported at my_project/__init__.py:6 but not listed as a requirement +✘ pathlib2 imported at my_project/__init__.py:7 but not listed as a requirement +✘ typing_extensions imported at my_project/__init__.py:8 but not listed as a requirement +✘ domdf_python_tools imported at my_project/__init__.py:9 but not listed as a requirement +✘ click imported at my_project/__init__.py:11 but not listed as a requirement +✘ ruamel imported at my_project/__init__.py:13 but not listed as a requirement +✘ Bio imported at my_project/__init__.py:15 but not listed as a requirement +✘ configconfig imported at my_project/__init__.py:31 but not listed as a requirement +✘ setuptools imported at my_project/__init__.py:35 but not listed as a requirement +✘ biopython never imported +✘ coincidence never imported +✔ consolekit imported at my_project/__init__.py:10 +✘ numpy never imported +✔ pandas imported at my_project/__init__.py:12