From dd7b6e23dd57551b5005984ec9465675dc5bc341 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Mon, 30 Oct 2023 17:07:08 +0100 Subject: [PATCH] Add --exclude-from option to CLI Include a few tests demonstrating the successful use of --exclude-from. Rely on unit tests in previous commits for verifying the underlying exclude-from functionality. --- README.md | 23 +++++++++----- fawltydeps/cli_parser.py | 13 ++++++++ tests/test_cmdline.py | 66 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1642c88d..33480c46 100644 --- a/README.md +++ b/README.md @@ -311,9 +311,6 @@ fawltydeps --exclude tests/ The format of the exclude patterns is the same as used by `.gitignore` files, [see here for a full description](https://git-scm.com/docs/gitignore#_pattern_format). -(Note that some patterns that must be interpreted relative to the location of a -`.gitignore` file are not (yet) supported by FawltyDeps. Search for "relative" -in the linked page to see which pattern types are not yet supported.) When the `--exclude` option is not specified, its default value is `".*"`, which matches all paths that start with a dot (`.`), aka. "hidden" paths. In the above @@ -324,8 +321,21 @@ example, if you want to exclude both hidden paths, and everything under fawltydeps --exclude tests/ ".*" ``` -The `--exclude` patterns have lower priority than any paths you pass directly -on the command line, e.g. in this command: +(The extra quotes here are needed to prevent the shell from interpreting and +replacing the `*` wildcard.) + +You can also point to exclude patterns stored in a file, with the +`--exclude-from` option. E.g. to read exclude patterns from `./my_excludes.txt`: + +```sh +fawltydeps --exclude my_excludes.txt +``` + +When not given, the `--exclude-from` option will default to reading exclude +patterns from `./.ignore`, `./.gitignore`, and `./.git/info/exclude`. + +Exclude patterns have lower priority than any paths you pass directly on the +command line, e.g. in this command: ```sh fawltydeps --code my_file.py --exclude my_file.py @@ -340,9 +350,6 @@ notebooks: fawltydeps --code my_dir --exclude "*.ipynb" ``` -(The extra quotes here are needed to prevent the shell from interpreting and -replacing the `*` wildcard.) - ### Ignoring irrelevant results There may be `import` statements in your code that should not be considered an diff --git a/fawltydeps/cli_parser.py b/fawltydeps/cli_parser.py index 8236685f..46dd3722 100644 --- a/fawltydeps/cli_parser.py +++ b/fawltydeps/cli_parser.py @@ -191,6 +191,19 @@ def populate_parser_paths_options(parser: argparse._ActionsContainer) -> None: " Defaults to '.*', meaning that hidden/dot paths are excluded." ), ) + parser.add_argument( + "--exclude-from", + nargs="+", + action="union", + type=Path, + metavar="PATH", + help=( + "Files containing exclude patterns to apply when looking for code" + " (imports), dependency declarations and/or Python environments." + " Defaults to '.ignore', `.gitignore`, and `.git/info/exclude`" + " under the current directory." + ), + ) parser.add_argument( "--install-deps", dest="install_deps", diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index dec93f7c..057d28b2 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -588,6 +588,46 @@ def test_list_sources_detailed__from_both_python_file_and_stdin(fake_project): assert returncode == 0 +def test_list_sources__with_exclude_from(fake_project): + tmp_path = fake_project( + files_with_imports={ + "code.py": ["foo"], + str(Path("subdir", "notebook.ipynb")): ["foo"], + str(Path("subdir", "other.py")): ["foo"], + }, + files_with_declared_deps={ + "pyproject.toml": ["foo"], + "requirements.txt": ["foo"], + "setup.cfg": ["foo"], + "setup.py": ["foo"], + }, + fake_venvs={"venvs/my_venv": {}}, + extra_file_contents={ + "my_ignore": dedent( + """ + subdir/*.py + /setup.* + *envs + """ + ) + }, + ) + output, returncode = run_fawltydeps_function( + "--list-sources", f"{tmp_path}", "--exclude-from", f"{tmp_path / 'my_ignore'}" + ) + expect = [ + str(tmp_path / filename) + for filename in [ + "code.py", + str(Path("subdir", "notebook.ipynb")), + "requirements.txt", + "pyproject.toml", + ] + ] + assert_unordered_equivalence(output.splitlines()[:-2], expect) + assert returncode == 0 + + @dataclass class ProjectTestVector: """Test vectors for FawltyDeps Settings configuration.""" @@ -1279,6 +1319,32 @@ def test_cmdline_on_ignored_undeclared_option( ).splitlines(), id="generate_toml_config_with_install_deps", ), + pytest.param( + {"exclude": ["/foo/bar", "baz/*"]}, + ["--list-sources", "--exclude-from", "my_ignore", "--generate-toml-config"], + dedent( + f"""\ + # Copy this TOML section into your pyproject.toml to configure FawltyDeps + # (default values are commented) + [tool.fawltydeps] + actions = ['list_sources'] + # output_format = 'human_summary' + # code = ['.'] + # deps = ['.'] + # pyenvs = ['.'] + # ignore_undeclared = [] + # ignore_unused = {sorted(DEFAULT_IGNORE_UNUSED)} + # deps_parser_choice = ... + # install_deps = false + exclude = ['/foo/bar', 'baz/*'] + exclude_from = ['my_ignore'] + # verbosity = 0 + # custom_mapping_file = [] + # [tool.fawltydeps.custom_mapping] + """ + ).splitlines(), + id="generate_toml_config_with_list_sources_exclude_and_exclude_from", + ), ], ) def test_cmdline_args_in_combination_with_config_file(