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

This change updated vulture to use the exclude patterns from the
.gitignore file at the root of the input project in the absence of
user-provided inputs as a solution to jendrikseipp#344. Vulture now requires the
pathspec library to run.
  • Loading branch information
whosayn committed Dec 22, 2023
1 parent 3d0ad1a commit e256e8c
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 2 deletions.
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 e256e8c

Please sign in to comment.