Skip to content
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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# 2.17 (unreleased)

* Colour output and add `-c`/`--color {yes,no,auto}` option (#415, Hugo van Kemenade).
* Add support for Python 3.15 and drop 3.9 (Hugo van Kemenade, #416).

# 2.16 (2026-03-25)
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ tool for higher code quality.
* tested: tests itself and has complete test coverage
* complements pyflakes and has the same output syntax
* sorts unused classes and functions by size with `--sort-by-size`
* colour output, with confidence shown via a temperature gradient

## Installation

Expand Down Expand Up @@ -164,6 +165,16 @@ def foo(arg: Sequence):
```


## Colour output

Output is in colour by default when stdout is a terminal. The
confidence percentage uses a temperature gradient: 60% dark grey, 90%
yellow, 100% red.

Use `-c`/`--color {yes,no,auto}` to force colour on or off, or set the
[`NO_COLOR`](https://no-color.org/) or
[`FORCE_COLOR`](https://force-color.org/) environment variables.

## Configuration

You can also store command line arguments in `pyproject.toml` under the
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ classifiers = [
"Topic :: Software Development :: Quality Assurance",
]
dependencies = [
"termcolor >= 2.3.0",
"tomli >= 1.1.0; python_version < '3.11'",
]

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
termcolor >= 2.3.0
tomli >= 2.4.1; python_version < '3.11'
1 change: 1 addition & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def test_config_merging():
exclude=["cli_exclude"],
ignore_decorators=["cli_deco"],
ignore_names=["cli_name"],
color="auto",
config="pyproject.toml",
make_whitelist=True,
min_confidence=20,
Expand Down
9 changes: 8 additions & 1 deletion tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_report(code, expected, make_whitelist=False):
filename = "foo.py"
v.scan(code, filename=filename)
capsys.readouterr()
ret = v.report(make_whitelist=make_whitelist)
ret = v.report(make_whitelist=make_whitelist, color="no")
assert ret
assert capsys.readouterr().out == expected.format(filename=filename)

Expand Down Expand Up @@ -61,6 +61,13 @@ def test_item_report(check_report):
check_report(mock_code, expected)


def test_color(v):
v.scan("import os\n")
[item] = v.get_unused_code()
assert "\x1b[" in item.get_report(color="yes")
assert "\x1b[" not in item.get_report(color="no")


def test_make_whitelist(check_report):
expected = """\
foo # unused import ({filename}:1)
Expand Down
14 changes: 14 additions & 0 deletions vulture/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#: Possible configuration options and their respective defaults
DEFAULTS = {
"color": "auto",
"config": "pyproject.toml",
"min_confidence": 0,
"paths": [],
Expand All @@ -26,6 +27,8 @@
"verbose": False,
}

COLOR_CHOICES = ("yes", "no", "auto")


class InputError(Exception):
def __init__(self, message):
Expand Down Expand Up @@ -56,6 +59,10 @@ def _check_output_config(config):
"""
if not config["paths"]:
raise InputError("Please pass at least one file or directory")
if config["color"] not in COLOR_CHOICES:
raise InputError(
f"color must be one of {COLOR_CHOICES}, got {config['color']!r}"
)


def _parse_toml(infile):
Expand Down Expand Up @@ -160,6 +167,13 @@ def csv(exclude):
default=missing,
help="Sort unused functions and classes by their lines of code.",
)
parser.add_argument(
"-c",
"--color",
choices=COLOR_CHOICES,
default=missing,
help="Colour the output (default: auto).",
)
parser.add_argument(
"--config",
type=str,
Expand Down
48 changes: 42 additions & 6 deletions vulture/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from functools import partial
from pathlib import Path

from termcolor import colored

from vulture import lines, noqa, utils
from vulture.config import InputError, make_config
from vulture.reachability import Reachability
Expand Down Expand Up @@ -151,16 +153,45 @@ def size(self):
assert self.last_lineno >= self.first_lineno
return self.last_lineno - self.first_lineno + 1

def get_report(self, add_size=False):
def get_report(self, add_size=False, *, color="auto"):
if add_size:
line_format = "line" if self.size == 1 else "lines"
size_report = f", {self.size:d} {line_format}"
else:
size_report = ""
return (
f"{utils.format_path(self.filename)}:{self.first_lineno:d}: "
f"{self.message} ({self.confidence}% confidence{size_report})"

no_color = color == "no"
force_color = color == "yes"

def c(text, *args, **kwargs):
return colored(
text,
*args,
no_color=no_color,
force_color=force_color,
**kwargs,
)

path = c(utils.format_path(self.filename), attrs=["bold"])
colon = c(":", "cyan")
lineno = f"{self.first_lineno:d}"
if self.confidence >= 100:
confidence_color = "red"
elif self.confidence >= 90:
confidence_color = "yellow"
else:
confidence_color = "dark_grey"
confidence = c(
f"({self.confidence}% confidence{size_report})", confidence_color
)
if self.typ == "unreachable_code":
message = c(self.message, "red", attrs=["bold"])
else:
message = (
f"unused {c(self.typ, 'red', attrs=['bold'])} "
f"{c(repr(self.name), attrs=['bold'])}"
)
return f"{path}{colon}{lineno}{colon} {message} {confidence}"

def get_whitelist_string(self):
filename = utils.format_path(self.filename)
Expand Down Expand Up @@ -347,7 +378,11 @@ def by_size(item):
)

def report(
self, min_confidence=0, sort_by_size=False, make_whitelist=False
self,
min_confidence=0,
sort_by_size=False,
make_whitelist=False,
color="auto",
):
"""
Print ordered list of Item objects to stdout.
Expand All @@ -358,7 +393,7 @@ def report(
self._log(
item.get_whitelist_string()
if make_whitelist
else item.get_report(add_size=sort_by_size),
else item.get_report(add_size=sort_by_size, color=color),
force=True,
)
self.exit_code = ExitCode.DeadCode
Expand Down Expand Up @@ -679,5 +714,6 @@ def main():
min_confidence=config["min_confidence"],
sort_by_size=config["sort_by_size"],
make_whitelist=config["make_whitelist"],
color=config["color"],
)
)
Loading