Skip to content

Commit fc23f33

Browse files
authored
Simplify decorators that are too hard to parse (#284)
1 parent c264fb8 commit fc23f33

File tree

4 files changed

+50
-6
lines changed

4 files changed

+50
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 2.9 (unreleased)
22
* Use exit code 3 when dead code is found (whosayn, #319).
3+
* Simplify decorator names that are too hard to parse to "@" (Llandy3d and Jendrik Seipp, #284).
34

45
# 2.8 (2023-08-10)
56

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ starting with `foo` and the names `bar` and `baz`. Additionally, the
111111
`--ignore-decorators` option can be used to ignore functions decorated
112112
with the given decorator. This is helpful for example in Flask projects,
113113
where you can use `--ignore-decorators "@app.route"` to ignore all
114-
functions with the `@app.route` decorator.
114+
functions with the `@app.route` decorator. Note that Vulture simplifies
115+
decorators it cannot parse: `@foo.bar(x, y)` becomes "@foo.bar" and
116+
`@foo.bar(x, y).baz` becomes "@" internally.
115117

116118
We recommend using whitelists instead of `--ignore-names` or
117119
`--ignore-decorators` whenever possible, since whitelists are

tests/test_utils.py

+38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ast
22
import os
33
import pathlib
4+
import sys
45

56
import pytest
67

@@ -119,3 +120,40 @@ class Foo:
119120
pass
120121
"""
121122
check_decorator_names(code, ["@foo", "@bar.yz"])
123+
124+
125+
def test_get_decorator_name_end_function_call():
126+
code = """\
127+
@foo.bar(x, y, z)
128+
def bar():
129+
pass
130+
"""
131+
check_decorator_names(code, ["@foo.bar"])
132+
133+
134+
@pytest.mark.skipif(
135+
sys.version_info < (3, 9), reason="requires Python 3.9 or higher"
136+
)
137+
@pytest.mark.parametrize(
138+
"decorated",
139+
[
140+
("def foo():"),
141+
("async def foo():"),
142+
("class Foo:"),
143+
],
144+
)
145+
def test_get_decorator_name_multiple_callables(decorated):
146+
decorated = f"{decorated}\n pass"
147+
code = f"""\
148+
@foo
149+
@bar.prop
150+
@z.func("hi").bar().k.foo
151+
@k("hello").doo("world").x
152+
@k.hello("world")
153+
@foo[2]
154+
{decorated}
155+
"""
156+
check_decorator_names(
157+
code,
158+
["@foo", "@bar.prop", "@", "@", "@k.hello", "@"],
159+
)

vulture/utils.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,14 @@ def format_path(path):
6464
def get_decorator_name(decorator):
6565
if isinstance(decorator, ast.Call):
6666
decorator = decorator.func
67-
parts = []
68-
while isinstance(decorator, ast.Attribute):
69-
parts.append(decorator.attr)
70-
decorator = decorator.value
71-
parts.append(decorator.id)
67+
try:
68+
parts = []
69+
while isinstance(decorator, ast.Attribute):
70+
parts.append(decorator.attr)
71+
decorator = decorator.value
72+
parts.append(decorator.id)
73+
except AttributeError:
74+
parts = []
7275
return "@" + ".".join(reversed(parts))
7376

7477

0 commit comments

Comments
 (0)