diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index a1878c52..3a272532 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -5,3 +5,4 @@ language: python files: "dependencies.yaml" pass_filenames: false + args: [--warn-all, --strict] diff --git a/src/rapids_dependency_file_generator/__init__.py b/src/rapids_dependency_file_generator/__init__.py index 30d6cdca..87f49388 100644 --- a/src/rapids_dependency_file_generator/__init__.py +++ b/src/rapids_dependency_file_generator/__init__.py @@ -5,13 +5,15 @@ a Conda environment from ``dependencies.yaml``. """ -from . import _config, _rapids_dependency_file_generator +from . import _config, _rapids_dependency_file_generator, _warnings from ._config import * # noqa: F401,F403 from ._rapids_dependency_file_generator import * # noqa: F401,F403 from ._version import __version__ +from ._warnings import * # noqa: F401,F403 __all__ = [ "__version__", *_config.__all__, *_rapids_dependency_file_generator.__all__, + *_warnings.__all__, ] diff --git a/src/rapids_dependency_file_generator/_cli.py b/src/rapids_dependency_file_generator/_cli.py index f4cad785..72af8fa6 100644 --- a/src/rapids_dependency_file_generator/_cli.py +++ b/src/rapids_dependency_file_generator/_cli.py @@ -1,12 +1,15 @@ import argparse import os +import warnings +from . import DependencyFileGeneratorWarning from ._config import Output, load_config_from_file from ._constants import cli_name, default_dependency_file_path from ._rapids_dependency_file_generator import ( delete_existing_files, make_dependency_files, ) +from ._rapids_dependency_file_validator import UnusedDependencySetWarning from ._version import __version__ as version @@ -80,6 +83,27 @@ def validate_args(argv): help="Show the version and exit.", ) + parser.add_argument( + "--warn-all", + default=False, + action="store_true", + help="Activate all warnings.", + ) + + parser.add_argument( + "--warn-unused-dependencies", + default=False, + action="store_true", + help="Warn if there are unused dependency sets.", + ) + + parser.add_argument( + "--strict", + default=False, + action="store_true", + help="Treat warnings as errors.", + ) + args = parser.parse_args(argv) dependent_arg_keys = ["file_key", "output", "matrix"] @@ -118,6 +142,11 @@ def main(argv=None) -> None: print(f"{cli_name}, version {version}") return + if args.strict: + warnings.simplefilter("error", category=DependencyFileGeneratorWarning) + if not args.warn_unused_dependencies and not args.warn_all: + warnings.simplefilter("ignore", category=UnusedDependencySetWarning) + parsed_config = load_config_from_file(args.config) matrix = generate_matrix(args.matrix) diff --git a/src/rapids_dependency_file_generator/_rapids_dependency_file_validator.py b/src/rapids_dependency_file_generator/_rapids_dependency_file_validator.py index 6822a039..1cd0a96b 100644 --- a/src/rapids_dependency_file_generator/_rapids_dependency_file_validator.py +++ b/src/rapids_dependency_file_generator/_rapids_dependency_file_validator.py @@ -5,10 +5,13 @@ import sys import textwrap import typing +import warnings import jsonschema from jsonschema.exceptions import best_match +from ._warnings import UnusedDependencySetWarning + SCHEMA = json.loads(importlib.resources.files(__package__).joinpath("schema.json").read_bytes()) @@ -32,3 +35,10 @@ def validate_dependencies(dependencies: dict[str, typing.Any]) -> None: best_matching_error = best_match(errors) print("\n", textwrap.indent(str(best_matching_error), "\t"), "\n", file=sys.stderr) raise RuntimeError("The provided dependencies data is invalid.") + + unused_dependency_sets = set(dependencies["dependencies"].keys()) + unused_dependency_sets.difference_update( + i for file_config in dependencies["files"].values() for i in file_config["includes"] + ) + for dep in sorted(unused_dependency_sets): + warnings.warn(f'Dependency set "{dep}" is not referred to anywhere in "files:"', UnusedDependencySetWarning) diff --git a/src/rapids_dependency_file_generator/_warnings.py b/src/rapids_dependency_file_generator/_warnings.py new file mode 100644 index 00000000..b8943645 --- /dev/null +++ b/src/rapids_dependency_file_generator/_warnings.py @@ -0,0 +1,11 @@ +__all__ = [ + "DependencyFileGeneratorWarning", +] + + +class DependencyFileGeneratorWarning(UserWarning): + pass + + +class UnusedDependencySetWarning(DependencyFileGeneratorWarning): + pass diff --git a/tests/test_cli.py b/tests/test_cli.py index 9b5feb72..df2d291e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,11 @@ +import contextlib +import os.path +from textwrap import dedent + import pytest -from rapids_dependency_file_generator._cli import generate_matrix, validate_args +from rapids_dependency_file_generator._cli import generate_matrix, main, validate_args +from rapids_dependency_file_generator._rapids_dependency_file_validator import UnusedDependencySetWarning def test_generate_matrix(): @@ -181,3 +186,52 @@ def test_validate_args(): args = validate_args(["--version"]) assert args.version + + +@pytest.mark.parametrize( + ["extra_args", "context"], + [ + ( + [], + contextlib.nullcontext(), + ), + ( + ["--strict"], + contextlib.nullcontext(), + ), + ( + ["--warn-unused-dependencies"], + pytest.warns(UnusedDependencySetWarning), + ), + ( + ["--warn-unused-dependencies", "--strict"], + pytest.raises(UnusedDependencySetWarning), + ), + ( + ["--warn-all"], + pytest.warns(UnusedDependencySetWarning), + ), + ( + ["--warn-all", "--strict"], + pytest.raises(UnusedDependencySetWarning), + ), + ], +) +def test_warnings(tmp_path, extra_args, context): + config_file = os.path.join(tmp_path, "dependencies.yaml") + with open(config_file, "w") as f: + f.write(dedent(""" + files: + all: + output: conda + includes: [a] + channels: [] + dependencies: + a: + common: [] + b: + common: [] + """)) + + with context: + main(["--config", config_file, *extra_args]) diff --git a/tests/test_rapids_dependency_file_validator.py b/tests/test_rapids_dependency_file_validator.py new file mode 100644 index 00000000..81ac8702 --- /dev/null +++ b/tests/test_rapids_dependency_file_validator.py @@ -0,0 +1,33 @@ +import pytest +from rapids_dependency_file_generator._rapids_dependency_file_validator import UnusedDependencySetWarning, validate_dependencies + + +def test_validate_dependencies_warn_on_unused_deps(): + with pytest.warns(UnusedDependencySetWarning) as warnings: + validate_dependencies({ + "files": { + "all": { + "output": "conda", + "includes": ["a", "b"], + } + }, + "channels": [], + "dependencies": { + "a": { + "common": [], + }, + "b": { + "common": [], + }, + "d": { + "common": [], + }, + "c": { + "common": [], + }, + }, + }) + + assert len(warnings) == 2 + assert warnings[0].message.args[0] == 'Dependency set "c" is not referred to anywhere in "files:"' + assert warnings[1].message.args[0] == 'Dependency set "d" is not referred to anywhere in "files:"'