Skip to content

Commit

Permalink
Read exclude patterns from .gitignore in absence of user-provided
Browse files Browse the repository at this point in the history
patterns (jendrikseipp#344)

Use .gitignore to exclude files if --exclude is missing from both
pyproject.toml and the command line. Vulture now requires the
pathspec library to run.
  • Loading branch information
whosayn committed Dec 22, 2023
1 parent 3d0ad1a commit 731ace6
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Bump flake8, flake8-comprehensions and flake8-bugbear (Sebastian Csar, #341).
* Switch to tomllib/tomli to support heterogeneous arrays (Sebastian Csar, #340).
* Provide whitelist parity for `MagicMock` and `Mock` (maxrake).
* Use .gitignore to exclude files if --exclude is missing from both pyproject.toml and the command line.

# 2.10 (2023-10-06)

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ If you want to ignore a whole file or directory, use the `--exclude` parameter
(e.g., `--exclude "*settings.py,*/docs/*.py,*/test_*.py,*/.venv/*.py"`). The
exclude patterns are matched against absolute paths.

Note that releases after 2.10 will parse the project's .gitignore files for
exclude patterns if the `--exclude` parameter is unused and if there are no
exclude patterns in the pyproject.toml file.

#### Flake8 noqa comments

<!-- Hide noqa docs until we decide whether we want to support it.
Expand Down
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ def find_version(*parts):
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Quality Assurance",
],
install_requires=["tomli >= 1.1.0; python_version < '3.11'"],
install_requires=[
"tomli >= 1.1.0; python_version < '3.11'",
"pathspec >= 0.12.1",
],
entry_points={"console_scripts": ["vulture = vulture.core:main"]},
python_requires=">=3.8",
packages=setuptools.find_packages(exclude=["tests"]),
Expand Down
56 changes: 56 additions & 0 deletions tests/test_gitignore_patterns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import pathlib
import pytest
import shutil

from . import call_vulture
from vulture.utils import ExitCode


class TempGitignore:
def __init__(self, patterns):
self.patterns = patterns
root = pathlib.Path(".").resolve()
self.file = root / ".gitignore"
self.tmpfile = root / ".tmp_gitignore"

def __enter__(self):
shutil.move(self.file, self.tmpfile)
with open(self.file, "w") as f:
f.write("\n".join(self.patterns))

return self.file

def __exit__(self, *args):
os.remove(self.file)
shutil.move(self.tmpfile, self.file)


@pytest.fixture(scope="function")
def gitignore(request):
with TempGitignore(request.param) as fpath:
yield fpath


@pytest.mark.parametrize(
"exclude_patterns,gitignore,exit_code",
(
([], [], ExitCode.NoDeadCode),
([""], [], ExitCode.NoDeadCode),
([], [""], ExitCode.NoDeadCode),
([""], ["core.py", "utils.py"], ExitCode.NoDeadCode),
(["core.py", "utils.py"], [""], ExitCode.DeadCode),
([], ["core.py", "utils.py"], ExitCode.DeadCode),
),
indirect=("gitignore",),
)
def test_gitignore(exclude_patterns, gitignore, exit_code):
def get_csv(paths):
return ",".join(os.path.join("vulture", path) for path in paths)

cli_args = ["vulture/"]
if exclude_patterns:
cli_args.extend(["--exclude", get_csv(exclude_patterns)])

assert gitignore.is_file()
assert call_vulture(cli_args) == exit_code
17 changes: 16 additions & 1 deletion vulture/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ast
from fnmatch import fnmatch, fnmatchcase
from pathlib import Path
from pathspec import PathSpec
import pkgutil
import re
import string
Expand Down Expand Up @@ -114,6 +115,13 @@ def _ignore_variable(filename, varname):
)


def _get_gitignore_pathspec():
if (gitignore := Path(".gitignore").resolve()).is_file:
with gitignore.open() as fh:
return PathSpec.from_lines("gitwildmatch", fh)
return PathSpec.from_lines("gitwildmatch", [])


class Item:
"""
Hold the name, type and location of defined code.
Expand Down Expand Up @@ -263,9 +271,16 @@ def prepare_pattern(pattern):
return pattern

exclude = [prepare_pattern(pattern) for pattern in (exclude or [])]
gitignore = _get_gitignore_pathspec()

def exclude_path(path):
return _match(path, exclude, case=False)
# if no exclude patterns are provided through the cli arguments or
# a toml file, use .gitignore patterns to inform exclusion
return (
_match(path, exclude, case=False)
if exclude
else gitignore.match_file(path)
)

paths = [Path(path) for path in paths]

Expand Down

0 comments on commit 731ace6

Please sign in to comment.