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

Issue #227, Feature/pathformat, adds --absolute-paths option #230

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ vulture.egg-info/
.pytest_cache/
.tox/
.venv/
.idea/
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# unreleased

* added --absolute-paths option to have vulture output absolute paths instead of relative paths (mcooperman, #227)
changed git signing key to correct verification

* Only parse format strings when being used with `locals()` (jingw, #225).
* Don't override paths in pyproject.toml with empty CLI paths (bcbnz, #228).
* Run continuous integration tests for Python 3.9 (ju-sh, #232).
Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ ignore_names = ["visit_*", "do_*"]
make_whitelist = true
min_confidence = 80
sort_by_size = true
absolute_paths = false
verbose = true
paths = ["myscript.py", "mydir"]
```
Expand All @@ -179,6 +180,54 @@ When using the `--sort-by-size` option, Vulture sorts unused code by its
number of lines. This helps developers prioritize where to look for dead
code first.

## Path Formatting

The `—path-format` option allows control of how file paths are emitted in the output of vulture.

This can be useful when using vulture as a plugin tool for IDEs like PyCharm. Using absolute paths enables ‘jump-to-code’ from the output error messages when vulture is used in PyCharm.

Currently supported formats are:

* `relative` (default) this is the original behavior of vulture before this feature was added
* `absolute` absolute file path

additional formats my be added in the future

### vulture inside PyCharm

Reference test platform: *macOS 10.14 (Mojave), anaconda python distribution, PyCharm Community 2019.3*

Assumes:

* vulture installed in your (virtual) python environment
* the same (virtual) environment configured into your PyCharm project settings

Navigate from **PyCharm** menu -> **Preferences** -> **Tools** -> **External Tools**

**Add a new tool** using the PLUS (+) icon

Suggested Settings:

* Name: `vulture`

* Group: `External Tools`

* Description: `dead code identification`

* Tool Settings / Program: `$PyInterpreterDirectory$/python`

* Tool Settings / Arguments: `-m vulture --path-format absolute $FilePath$`

* Tool Settings / Working directory: `$ProjectFileDir$`

* Select all checkboxes under Advanced Options

* Add these Output Filters:
* `$FILE_PATH$\:$LINE$\:$COLUMN$\:.*`
* `$FILE_PATH$\:$LINE$\:.*`

Save the settings

## Examples

Consider the following Python script (`dead_code.py`):
Expand Down Expand Up @@ -214,6 +263,24 @@ Vulture correctly reports "os" and "message" as unused, but it fails to
detect that "greet" is actually used. The recommended method to deal
with false positives like this is to create a whitelist Python file.

**Absolute Paths**

Calling:

```
$ vulture --path-format absolute dead_code.py
```

results in output similar to the following, depending on your exact path:

```
/Users/<user>/PycharmProjects/vulture/dead_code.py:1: unused import 'os' (90% confidence)
/Users/<user>/PycharmProjects/vulture/dead_code.py:4: unused function 'greet' (60% confidence)
/Users/<user>/PycharmProjects/vulture/dead_code.py:8: unused variable 'message' (60% confidence)
```



**Preparing whitelists**

In a whitelist we simulate the usage of variables, attributes, etc. For
Expand Down
5 changes: 5 additions & 0 deletions deadcode/deadcode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def deadcode():
"""
don't call this function from anywhere
intentional dead code for testing purposes
"""
37 changes: 37 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pathlib
import subprocess
from subprocess import PIPE, STDOUT
import sys

import pytest
Expand All @@ -11,13 +12,49 @@
str(path) for path in (REPO / "vulture" / "whitelists").glob("*.py")
]

CALL_TIMEOUT_SEC = 60


def call_vulture(args, **kwargs):
return subprocess.call(
[sys.executable, "-m", "vulture"] + args, cwd=REPO, **kwargs
)


def run_vulture(args_list, **kwargs):
"""
Run vulture using subprocess.run(...)

Returns a subprocess.CompletedProcess object
in order that a caller (i.e. tests) can examine
the output in more detail than with call_vulture(...)

added to enable testing of absolute path generation
"""
check = kwargs.get("check", False)
if "check" in kwargs:
del kwargs["check"]
# WARNING - setting check = True may raise an Error/Exception
# on process failure
result = subprocess.run(
[sys.executable, "-m", "vulture"] + args_list,
stdin=None,
input=None,
stdout=PIPE,
stderr=STDOUT,
shell=False,
cwd=REPO,
timeout=CALL_TIMEOUT_SEC,
check=check,
encoding=None,
errors=None,
env=None,
universal_newlines=True,
**kwargs
)
return result


def check(items_or_names, expected_names):
"""items_or_names must be a collection of Items or a set of strings."""
try:
Expand Down
7 changes: 7 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def test_cli_args():
ignore_names=["name1", "name2"],
make_whitelist=True,
min_confidence=10,
format="relative",
sort_by_size=True,
verbose=True,
)
Expand All @@ -39,6 +40,7 @@ def test_cli_args():
"--min-confidence=10",
"--sort-by-size",
"--verbose",
"--format=relative",
"path1",
"path2",
]
Expand All @@ -60,6 +62,7 @@ def test_toml_config():
min_confidence=10,
sort_by_size=True,
verbose=True,
format="relative",
)
data = StringIO(
dedent(
Expand All @@ -73,6 +76,7 @@ def test_toml_config():
sort_by_size = true
verbose = true
paths = ["path1", "path2"]
format = "relative"
"""
)
)
Expand All @@ -97,6 +101,7 @@ def test_config_merging():
min_confidence = 10
sort_by_size = false
verbose = false
format = "relative"
paths = ["toml_path"]
"""
)
Expand All @@ -108,6 +113,7 @@ def test_config_merging():
"--make-whitelist",
"--min-confidence=20",
"--sort-by-size",
"--format=relative",
"--verbose",
"cli_path",
]
Expand All @@ -120,6 +126,7 @@ def test_config_merging():
make_whitelist=True,
min_confidence=20,
sort_by_size=True,
format="relative",
verbose=True,
)
assert result == expected
Expand Down
44 changes: 42 additions & 2 deletions tests/test_script.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import glob
import os.path
import os
import subprocess
import sys
from pathlib import Path

from . import call_vulture, REPO, WHITELISTS
from . import call_vulture, run_vulture, REPO, WHITELISTS


def test_module_with_explicit_whitelists():
Expand Down Expand Up @@ -73,3 +74,42 @@ def test_make_whitelist():
== 1
)
assert call_vulture(["vulture/", "--make-whitelist"]) == 0


def test_absolute_paths():
try:
completed_process = run_vulture(
["--format", "absolute", "deadcode/"], check=False
)
output_lines = completed_process.stdout.strip().split(os.linesep)
for line in output_lines:
if line:
lineparts = line.split(":")
# platform independent logic
# Windows differs from other root paths, uses ':' in volumes
# unix-like platforms should have 3 parts on an output line
# windows (absolute paths) have > 3 (4) including drive spec
partcount = len(lineparts)
filename = lineparts[0]
if partcount >= 3:
for i in range(1, partcount - 2):
filename += f":{lineparts[i]}"
# make sure the file resolves to an actual file
# and it's an absolute path
path = Path(filename)
assert path.exists()
path = path.resolve()
assert path.is_absolute()
except subprocess.TimeoutExpired as time_err:
raise AssertionError from time_err
except subprocess.SubprocessError as sub_err:
raise AssertionError from sub_err


def test_path_format_config():
"""
Verify any unrecognized format generates an error.
By definition, implemented format names will be registered,
so no sense testing them.
"""
assert call_vulture(["--format", "unimplemented", "tests/"]) == 1
22 changes: 22 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import ast
from pathlib import PurePath

from vulture import utils
from vulture.utils import format_path
from vulture.config import ABSOLUTE_PATH_FORMAT, RELATIVE_PATH_FORMAT


def check_paths(filename, format_name="relative"):
assert format_name in (ABSOLUTE_PATH_FORMAT, RELATIVE_PATH_FORMAT)
pathstr = format_path(filename, None, format_id=format_name)
pure_path = PurePath(pathstr)
check = pure_path.is_absolute()
if format_name == "absolute":
assert check
# even if absolute == True, the path might have been specified absolute
# so can't conclude negatively


def test_absolute_path():
check_paths(__file__, format_name="absolute")


def test_relative_path():
check_paths(__file__, format_name="relative")


def check_decorator_names(code, expected_names):
Expand Down
6 changes: 3 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ whitelist_externals =
commands =
black --check --diff .
# C408: unnecessary dict call
flake8 --extend-ignore=C408 setup.py tests/ vulture/
bash -c "pyupgrade --py36-plus `find dev/ tests/ vulture/ -name '*.py'` setup.py"
flake8 --extend-ignore=C408 setup.py tests/ deadcode/ vulture/
bash -c "pyupgrade --py36-plus `find dev/ tests/ deadcode/ vulture/ -name '*.py'` setup.py"

[testenv:fix-style]
basepython = python3
Expand All @@ -43,4 +43,4 @@ whitelist_externals =
bash
commands =
black .
bash -c "pyupgrade --py36-plus --exit-zero `find dev/ tests/ vulture/ -name '*.py'` setup.py"
bash -c "pyupgrade --py36-plus --exit-zero `find dev/ tests/ deadcode/ vulture/ -name '*.py'` setup.py"
13 changes: 13 additions & 0 deletions vulture/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

from .version import __version__

RELATIVE_PATH_FORMAT = "relative"
ABSOLUTE_PATH_FORMAT = "absolute"

#: Possible configuration options and their respective defaults
DEFAULTS = {
"min_confidence": 0,
Expand All @@ -20,6 +23,7 @@
"make_whitelist": False,
"sort_by_size": False,
"verbose": False,
"format": RELATIVE_PATH_FORMAT,
}


Expand Down Expand Up @@ -69,6 +73,7 @@ def _parse_toml(infile):
make_whitelist = true
min_confidence = 10
sort_by_size = true
format = relative
verbose = true
paths = ["path1", "path2"]
"""
Expand Down Expand Up @@ -150,6 +155,14 @@ def csv(exclude):
default=missing,
help="Sort unused functions and classes by their lines of code.",
)
parser.add_argument(
"--format",
type=str,
action="store",
default=RELATIVE_PATH_FORMAT,
required=False,
help="Specify path format.",
)
parser.add_argument(
"-v", "--verbose", action="store_true", default=missing
)
Expand Down
Loading