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

Ignore results annotated with '# noqa' #195

Merged
merged 22 commits into from
Mar 31, 2020
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Unreleased

* Report issue codes in output (e.g., `code.py:1: V104 unused import ...`)
(RJ722, #195)
* Support `# noqa` comments to suppress results on that line. (RJ722, #195).
RJ722 marked this conversation as resolved.
Show resolved Hide resolved

# 1.4 (2020-03-30)

* Ignore unused import statements in `__init__.py` (RJ722, #192).
Expand Down
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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`
* respects `# noqa` comments
* supports Python 2.7 and Python \>= 3.5

## Installation
Expand Down Expand Up @@ -58,6 +59,17 @@ We collect whitelists for common Python modules and packages in
a whole file or directory, use the `--exclude` parameter (e.g.,
`--exclude *settings.py,docs/`).

Another way of ignoring errors is to annotate the line causing the false
positive with `# noqa: <ERROR_CODE>` in a trailing comment (e.g.,
`# noqa: V103`).

The `ERROR_CODE` specifies what kind of dead code to ignore (see the table
below for the list of error codes). In case no error code is specified,
Vulture ignores all results for the line.

Note that the line number for any decorated object is the same as the line
number of the first decorator.

**Ignoring names**

You can use `--ignore-names foo*,ba[rz]` to let Vulture ignore all names
Expand Down Expand Up @@ -130,9 +142,9 @@ Calling :

results in the following output:

dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:4: unused function 'greet' (60% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
dead_code.py:1: V104 unused import 'os' (90% confidence)
dead_code.py:4: V103 unused function 'greet' (60% confidence)
dead_code.py:8: V106 unused variable 'message' (60% confidence)

Vulture correctly reports "os" and "message" as unused, but it fails to
detect that "greet" is actually used. The recommended method to deal
Expand All @@ -158,8 +170,30 @@ Passing both the original program and the whitelist to Vulture

makes Vulture ignore the `greet` method:

dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
dead_code.py:1: V104 unused import 'os' (90% confidence)
dead_code.py:8: V106 unused variable 'message' (60% confidence)

**Using "# noqa"**

```python
import os # noqa

class Greeter: # noqa: 102
def greet(self): # noqa: V103
print("Hi")
```

## Error codes

| Error code | Description |
| ---------- | ----------------- |
| V101 | Unused attribute |
| V102 | Unused class |
| V103 | Unused function |
| V104 | Unused import |
| V105 | Unused property |
| V106 | Unused variable |
| V201 | Unreachable code |

## Exit codes

Expand Down
292 changes: 292 additions & 0 deletions tests/test_noqa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import pytest

from vulture.noqa import NOQA_REGEXP, _parse_error_codes
from . import check, v

assert v # Silence pyflakes.


@pytest.mark.parametrize(
"line, codes",
[
("# noqa", ["all"]),
("## noqa", ["all"]),
("# noqa Hi, go on.", ["all"]),
("# noqa: V101", ["V101"]),
("# noqa: V101, V106", ["V101", "V106"]),
("# NoQA: V101, V103, \t V104", ["V101", "V103", "V104"]),
],
)
def test_noqa_regex_present(line, codes):
match = NOQA_REGEXP.search(line)
parsed = _parse_error_codes(match)
assert parsed == codes


@pytest.mark.parametrize(
"line",
[
("# noqa: 123V"),
("# noqa explanation: V012"),
("# noqa: ,V101"),
("# noqa: #noqa: V102"),
("# noqa: # noqa: V102"),
],
)
def test_noqa_regex_no_groups(line):
assert NOQA_REGEXP.search(line).groupdict()["codes"] is None


@pytest.mark.parametrize(
"line",
[("#noqa"), ("##noqa"), ("# n o q a"), ("#NOQA"), ("# Hello, noqa")],
)
def test_noqa_regex_not_present(line):
assert not NOQA_REGEXP.search(line)


def test_noqa_without_codes(v):
v.scan(
"""\
import this # noqa

@underground # noqa
class Cellar:
@property # noqa
def wine(self):
grapes = True # noqa

@without_ice # noqa
def serve(self, quantity=50):
self.quantity_served = quantity # noqa
return
self.pour() # noqa
"""
)
check(v.unused_attrs, [])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_props, [])
check(v.unreachable_code, [])
check(v.unused_vars, [])
RJ722 marked this conversation as resolved.
Show resolved Hide resolved


def test_noqa_specific_issue_codes(v):
v.scan(
"""\
import this # noqa: V104

@underground # noqa: V102
class Cellar:
@property # noqa: V105
def wine(self):
grapes = True # noqa: V106

@without_ice # noqa: V103
def serve(self, quantity=50):
self.quantity_served = quantity # noqa: V101
return
self.pour() # noqa: V201
"""
)
check(v.unused_attrs, [])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_props, [])
check(v.unreachable_code, [])
check(v.unused_vars, [])


def test_noqa_attributes(v):
v.scan(
"""\
something.x = 'x' # noqa: V101
something.z = 'z' # noqa: V106 (code for unused variable)
something.u = 'u' # noqa
"""
)
check(v.unused_attrs, ["z"])


def test_noqa_classes(v):
v.scan(
"""\
class QtWidget: # noqa: V102
pass

class ABC(QtWidget):
pass # noqa: V102 (should not ignore)

class DEF: # noqa
pass
"""
)
check(v.unused_classes, ["ABC"])


def test_noqa_functions(v):
v.scan(
"""\
def play(tune, instrument='bongs', _hz='50'): # noqa: V103
pass


# noqa
def problems(): # noqa: V104
pass # noqa: V103

def hello(name): # noqa
print("Hello")
"""
)
check(v.unused_funcs, ["problems"])
check(v.unused_vars, ["instrument", "tune"])


def test_noqa_imports(v):
v.scan(
"""\
import foo
import this # noqa: V104
import zoo
from koo import boo # noqa
from me import *
import dis # noqa: V101 (code for unused attr)
"""
)
check(v.unused_imports, ["foo", "zoo", "dis"])


def test_noqa_properties(v):
v.scan(
"""\
class Zoo:
@property
def no_of_koalas(self): # noqa
pass

@property
def area(self, width, depth): # noqa: V105
pass

@property # noqa
def entry_gates(self):
pass

@property # noqa: V103 (code for unused function)
def tickets(self):
pass
"""
)
check(v.unused_props, ["no_of_koalas", "area", "tickets"])
check(v.unused_classes, ["Zoo"])
check(v.unused_vars, ["width", "depth"])


def test_noqa_multiple_decorators(v):
v.scan(
"""\
@bar # noqa: V102
class Foo:
@property # noqa: V105
@make_it_cool
@log
def something(self):
pass

@coolify
@property
def something_else(self): # noqa: V105
pass

@a
@property
@b # noqa
def abcd(self):
pass
"""
)
check(v.unused_props, ["something_else", "abcd"])
check(v.unused_classes, [])


def test_noqa_unreacahble_code(v):
v.scan(
"""\
def shave_sheep(sheep):
for a_sheep in sheep:
if a_sheep.is_bald:
continue
a_sheep.grow_hair() # noqa: V201
a_sheep.shave()
return
for a_sheep in sheep: # noqa: V201
if a_sheep.still_has_hair:
a_sheep.shave_again()
"""
)
check(v.unreachable_code, [])
check(v.unused_funcs, ["shave_sheep"])
RJ722 marked this conversation as resolved.
Show resolved Hide resolved


def test_noqa_variables(v):
v.scan(
"""\
mitsi = "Mother" # noqa: V106
harry = "Father" # noqa
shero = "doggy" # noqa: V101, V104 (code for unused import, attr)
shinchan.friend = ['masao'] # noqa: V106 (code for unused variable)
"""
)
check(v.unused_vars, ["shero"])
check(v.unused_attrs, ["friend"])


def test_noqa_with_multiple_issue_codes(v):
v.scan(
"""\
def world(axis): # noqa: V103, V201
pass


for _ in range(3):
continue
xyz = hello(something, else): # noqa: V201, V106
"""
)
check(v.get_unused_code(), [])


def test_noqa_on_empty_line(v):
v.scan(
"""\
# noqa
import this
# noqa
"""
)
check(v.unused_imports, ["this"])


def test_noqa_with_invalid_codes(v):
v.scan(
"""\
import this # V098, A123, F876
"""
)
check(v.unused_imports, ["this"])


@pytest.mark.parametrize(
"first_file, second_file",
[
("foo = None", "bar = None # noqa"),
("bar = None # noqa", "foo = None"),
],
)
def test_noqa_multiple_files(first_file, second_file, v):
v.scan(first_file, filename="first_file.py")
v.scan(second_file, filename="second_file.py")
check(v.unused_vars, ["foo"])
14 changes: 7 additions & 7 deletions tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ def test_report(code, expected, make_whitelist=False):

def test_item_report(check_report):
expected = """\
{filename}:1: unused import 'foo' (90% confidence)
{filename}:3: unused class 'Foo' (60% confidence)
{filename}:7: unused function 'bar' (60% confidence)
{filename}:8: unused attribute 'foobar' (60% confidence)
{filename}:9: unused variable 'foobar' (60% confidence)
{filename}:11: unreachable code after 'return' (100% confidence)
{filename}:13: unused property 'myprop' (60% confidence)
{filename}:1: V104 unused import 'foo' (90% confidence)
{filename}:3: V102 unused class 'Foo' (60% confidence)
{filename}:7: V103 unused function 'bar' (60% confidence)
{filename}:8: V101 unused attribute 'foobar' (60% confidence)
{filename}:9: V106 unused variable 'foobar' (60% confidence)
{filename}:11: V201 unreachable code after 'return' (100% confidence)
{filename}:13: V105 unused property 'myprop' (60% confidence)
RJ722 marked this conversation as resolved.
Show resolved Hide resolved
"""
check_report(mock_code, expected)

Expand Down
Loading