Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read exclude patterns from .gitignore in absence of user-provided patterns (#344) #345

Merged
merged 10 commits into from
Dec 23, 2023
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
whosayn marked this conversation as resolved.
Show resolved Hide resolved
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
whosayn marked this conversation as resolved.
Show resolved Hide resolved
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
whosayn marked this conversation as resolved.
Show resolved Hide resolved
# a toml file, use .gitignore patterns to inform exclusion
whosayn marked this conversation as resolved.
Show resolved Hide resolved
return (
_match(path, exclude, case=False)
if exclude
else gitignore.match_file(path)
)

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

Expand Down
Loading