diff --git a/pyproject.toml b/pyproject.toml index 50bb169..e088713 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ select = [ "UP", # pyupgrade "W", # pycodestyle ] -ignore = ["D203", "D107"] +ignore = ["D203", "D107", "D212"] src = ["src", "tests"] [tool.ruff.per-file-ignores] diff --git a/src/pybuild_deps/compile_build_dependencies.py b/src/pybuild_deps/compile_build_dependencies.py index 1170f1f..6828c68 100644 --- a/src/pybuild_deps/compile_build_dependencies.py +++ b/src/pybuild_deps/compile_build_dependencies.py @@ -1,3 +1,11 @@ +""" +compile build dependencies module. + +Heavily rely on pip-tools BacktrackingResolver and our own find_build_deps +to recursively find all build dependencies and generate a pinned list +of build dependencies. +""" + from __future__ import annotations from typing import Iterable diff --git a/src/pybuild_deps/constants.py b/src/pybuild_deps/constants.py index 5e404f3..d55c365 100644 --- a/src/pybuild_deps/constants.py +++ b/src/pybuild_deps/constants.py @@ -1,3 +1,5 @@ +"""constants for pybuild deps.""" + from pip._internal.utils.appdirs import user_cache_dir from xdg import xdg_cache_home diff --git a/src/pybuild_deps/exceptions.py b/src/pybuild_deps/exceptions.py index 0dea5ab..77743cb 100644 --- a/src/pybuild_deps/exceptions.py +++ b/src/pybuild_deps/exceptions.py @@ -1,2 +1,5 @@ +"""custom exceptions for pybuild-deps.""" + + class PyBuildDepsError(Exception): - ... + """Custom exception for pybuild-deps.""" diff --git a/src/pybuild_deps/logger.py b/src/pybuild_deps/logger.py index 171647c..2af6dac 100644 --- a/src/pybuild_deps/logger.py +++ b/src/pybuild_deps/logger.py @@ -1,3 +1,6 @@ +"""custom logger for pybuild-deps.""" +# ruff: noqa: D102 + from __future__ import annotations import logging @@ -10,7 +13,12 @@ class Logger: - """Custom logger for pybuild-deps.""" + """ + Custom logger for pybuild-deps. + + When invoked as a CLI, will use click to log messages. Otherwise + will use default python logger. + """ def __init__(self, verbosity: int = 0): self._verbosity = verbosity diff --git a/src/pybuild_deps/parsers/requirements.py b/src/pybuild_deps/parsers/requirements.py index 4b6885a..1615ad2 100644 --- a/src/pybuild_deps/parsers/requirements.py +++ b/src/pybuild_deps/parsers/requirements.py @@ -1,3 +1,5 @@ +"""parser for requirement.txt files.""" + from __future__ import annotations import optparse @@ -25,6 +27,7 @@ def parse_requirements( constraint: bool = False, isolated: bool = False, ) -> Generator[InstallRequirement]: + """Thin wrapper around pip's `parse_requirements`.""" for parsed_req in _parse_requirements( filename, session, finder=finder, options=options, constraint=constraint ): diff --git a/src/pybuild_deps/scripts/compile.py b/src/pybuild_deps/scripts/compile.py index b29ba7f..6eb4348 100644 --- a/src/pybuild_deps/scripts/compile.py +++ b/src/pybuild_deps/scripts/compile.py @@ -1,3 +1,5 @@ +"""compile script.""" + from __future__ import annotations import os @@ -78,7 +80,7 @@ def get_compile_command(click_ctx): default=False, help="Generate pip 8 style hashes in the resulting requirements file.", ) -@click.argument("src_files", nargs=-1, type=click.Path(exists=True, allow_dash=True)) +@click.argument("src_files", nargs=-1, type=click.Path(exists=True, allow_dash=False)) @click.option( "--cache-dir", help="Store the cache data in DIRECTORY.", @@ -104,34 +106,16 @@ def cli( """Compiles build_requirements.txt from requirements.txt.""" log.verbosity = verbose - quiet if len(src_files) == 0: - if Path(REQUIREMENTS_TXT).exists(): - src_files = (REQUIREMENTS_TXT,) - else: - raise click.BadParameter( - f"Couldn't find a '{REQUIREMENTS_TXT}'. " - "You must specify at least one input file." - ) + src_files = _handle_src_files() if not output_file and not dry_run: log.warning("No output file (-o) specified. Defaulting to 'dry run' mode.") dry_run = True - pip_args = [] - repository = PyPIRepository(pip_args, cache_dir=cache_dir) + repository = PyPIRepository([], cache_dir=cache_dir) dependencies: list[InstallRequirement] = [] for src_file in src_files: - try: - dependencies.extend( - parse_requirements( - src_file, - finder=repository.finder, - session=repository.session, - options=repository.options, - ) - ) - except PyBuildDepsError as err: - log.error(str(err)) - sys.exit(2) + dependencies.extend(_parse_requirements(repository, src_file)) compiler = BuildDependencyCompiler(repository) try: @@ -178,3 +162,30 @@ def cli( if dry_run: log.info("Dry-run, so no file created/updated.") + + +def _parse_requirements(repository, src_file): + try: + return list( + parse_requirements( + src_file, + finder=repository.finder, + session=repository.session, + options=repository.options, + ) + ) + except PyBuildDepsError as err: + log.error(str(err)) + sys.exit(2) + + +def _handle_src_files(): + if Path(REQUIREMENTS_TXT).exists(): + src_files = (REQUIREMENTS_TXT,) + else: + raise click.BadParameter( + f"Couldn't find a '{REQUIREMENTS_TXT}'. " + "You must specify at least one input file." + ) + + return src_files diff --git a/tests/test_compile_build_dependencies.py b/tests/test_compile_build_dependencies.py index 4cc212e..7350d04 100644 --- a/tests/test_compile_build_dependencies.py +++ b/tests/test_compile_build_dependencies.py @@ -1,3 +1,5 @@ +"""test compile_build_dependencies module.""" + import pytest from pip._internal.req.constructors import install_req_from_req_string from piptools.repositories import PyPIRepository @@ -9,11 +11,13 @@ @pytest.fixture def compiler() -> BuildDependencyCompiler: + """BuildDependencyCompiler instance.""" repo = PyPIRepository([], cache_dir=PIP_CACHE_DIR) return BuildDependencyCompiler(repo) def test_compile_greenpath(compiler): + """Test compiling build dependencies happy path.""" ireq = install_req_from_req_string("cryptography==40.0.0") dependencies = compiler.resolve([ireq]) deps_per_name = {req.name: req for req in dependencies} @@ -23,6 +27,7 @@ def test_compile_greenpath(compiler): def test_unpinned_dependency(compiler): + """Ensure unpinned dependencies will raise the appropriate error.""" ireq = install_req_from_req_string("cryptography<40") with pytest.raises(PyBuildDepsError): compiler.resolve([ireq]) diff --git a/tests/test_main.py b/tests/test_main.py index 34883c3..0079745 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -16,6 +16,7 @@ @pytest.fixture def pypi_repo(): + """PyPIRepository instance for testing.""" return PyPIRepository([], cache_dir=PIP_CACHE_DIR) @@ -65,6 +66,7 @@ def test_find_build_deps( @pytest.mark.e2e def test_compile_greenpath(runner: CliRunner, tmp_path: Path, pypi_repo): + """Test happy path for compile command.""" output = tmp_path / "requirements-build.txt" requirements_path: Path = tmp_path / "requirements.txt" requirements_path.write_text("cryptography==39.0.0") @@ -78,6 +80,7 @@ def test_compile_greenpath(runner: CliRunner, tmp_path: Path, pypi_repo): def test_compile_missing_requirements_txt(runner: CliRunner, tmp_path: Path): + """Test compile without a requirements.txt.""" chdir(tmp_path) result = runner.invoke(main.cli, args=["compile"]) assert result.exit_code != 0 @@ -97,6 +100,7 @@ def test_compile_implicit_requirements_txt_and_non_default_options( cache: Path, args, ): + """Exercise some options to ensure they are working.""" chdir(tmp_path) requirements_path: Path = tmp_path / "requirements.txt" requirements_path.write_text("setuptools-rust==1.6.0") @@ -106,6 +110,7 @@ def test_compile_implicit_requirements_txt_and_non_default_options( def test_compile_not_pinned_requirements_txt(runner: CliRunner, tmp_path: Path): + """Ensure the appropriate error is thrown for non pinned requirements.""" chdir(tmp_path) requirements_path: Path = tmp_path / "requirements.txt" requirements_path.write_text("setuptools-rust<1") @@ -119,6 +124,7 @@ def test_compile_not_pinned_requirements_txt(runner: CliRunner, tmp_path: Path): def test_compile_unsolvable_dependencies(runner: CliRunner, tmp_path: Path, mocker): + """Test error handling for unsolvable dependencies.""" mocker.patch.object( BuildDependencyCompiler, "resolve", side_effect=PipToolsError("SOME ERROR") ) diff --git a/tests/test_parsers/test_requirements.py b/tests/test_parsers/test_requirements.py index 97d78f6..e470811 100644 --- a/tests/test_parsers/test_requirements.py +++ b/tests/test_parsers/test_requirements.py @@ -1,3 +1,5 @@ +"""test requirements parser.""" + from pathlib import Path import pytest @@ -7,6 +9,7 @@ def test_pinned_requirements(tmp_path, mocker): + """Test parsing requirements with pinned dependencies.""" requirements_path: Path = tmp_path / "requirements.txt" requirements_path.write_text("cryptography==40.0.0") requirements_list = list(parse_requirements(str(requirements_path), mocker.Mock())) @@ -14,6 +17,7 @@ def test_pinned_requirements(tmp_path, mocker): def test_unpinned_requirements(tmp_path, mocker): + """Test parsing requirements with unpinned dependencies.""" requirements_path: Path = tmp_path / "requirements.txt" requirements_path.write_text("cryptography>40") with pytest.raises(PyBuildDepsError):