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 .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade build coveralls setuptools tox wheel
python -m pip install -r requirements.txt

- name: Build Vulture wheel
run: python -m build
Expand Down
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 (whosayn, #344, #345).

# 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.

Vulture 2.11+ parses the `.gitignore` file in the current working directory 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
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pathspec >= 0.12.1
tomli >= 1.1.0; python_version < '3.11'
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ def find_version(*parts):
with open("README.md") as f1, open("CHANGELOG.md") as f2:
long_description = f1.read() + "\n\n" + f2.read()

with open("requirements.txt") as f:
install_requires = f.read().splitlines()

setuptools.setup(
name="vulture",
version=find_version("vulture", "version.py"),
Expand Down Expand Up @@ -47,7 +50,7 @@ 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=install_requires,
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
Expand Up @@ -6,6 +6,7 @@
import string
import sys

from pathspec import PathSpec
from vulture import lines
from vulture import noqa
from vulture import utils
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 via the CLI 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