Skip to content

Commit ce02b98

Browse files
Merge branch 'master' into chore/1954/Add_performance_regressions
2 parents 5b23a24 + be5a61b commit ce02b98

25 files changed

+932
-794
lines changed

CONTRIBUTORS.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,5 @@ contributors:
379379
* Matthew Beckers (mattlbeck): contributor
380380

381381
* Yang Yang: contributor
382+
383+
* Andrew J. Simmons (anjsimmo): contributor

ChangeLog

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ What's New in Pylint 2.5.0?
77

88
Release date: TBA
99

10+
* Fix a false negative for ``undefined-variable`` when using class attribute in comprehension.
11+
12+
Close #3494
13+
14+
* Fix a false positive for ``undefined-variable`` when using class attribute in decorator or as type hint.
15+
16+
Close #511
17+
Close #1976
18+
1019
* Remove HTML quoting of messages in JSON output.
1120

1221
Close #2769

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ $(PKG_SDIST):
5050

5151
lint: $(PIP)
5252
$(PIP) install .
53-
$(PYVE)/bin/pylint lint.py || true # for now ignore errors
53+
$(PYVE)/bin/pylint lint/__init__.py || true # for now ignore errors
5454

5555
clean:
5656
rm -rf $(PYVE)

pylint/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616

1717
def run_pylint():
18-
"""run pylint"""
1918
from pylint.lint import Run as PylintRun
2019

2120
try:
@@ -25,7 +24,6 @@ def run_pylint():
2524

2625

2726
def run_epylint():
28-
"""run pylint"""
2927
from pylint.epylint import Run as EpylintRun
3028

3129
EpylintRun()

pylint/__main__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,4 @@
1515
if sys.path[0] == "" or sys.path[0] == os.getcwd():
1616
sys.path.pop(0)
1717

18-
1918
pylint.run_pylint()

pylint/checkers/format.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,7 @@ def is_line_length_check_activated(pylint_pattern_match_object) -> bool:
12911291
if pragma.action == "disable" and "line-too-long" in pragma.messages:
12921292
return False
12931293
except PragmaParserError:
1294-
# Printing usefull informations dealing with this error is done in lint.py
1294+
# Printing useful information dealing with this error is done in the lint package
12951295
pass
12961296
return True
12971297

pylint/checkers/misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def process_tokens(self, tokens):
162162
):
163163
values.extend(pragma_repr.messages)
164164
except PragmaParserError:
165-
# Printing usefull informations dealing with this error is done in lint.py
165+
# Printing useful information dealing with this error is done in the lint package
166166
pass
167167
values = [_val.upper() for _val in values]
168168
if set(values) & set(self.config.notes):

pylint/checkers/variables.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -973,13 +973,11 @@ def visit_name(self, node):
973973
# if the current scope is a class scope but it's not the inner
974974
# scope, ignore it. This prevents to access this scope instead of
975975
# the globals one in function members when there are some common
976-
# names. The only exception is when the starting scope is a
977-
# comprehension and its direct outer scope is a class
978-
if (
979-
current_consumer.scope_type == "class"
980-
and i != start_index
981-
and not (base_scope_type == "comprehension" and i == start_index - 1)
982-
):
976+
# names.
977+
if current_consumer.scope_type == "class" and i != start_index:
978+
# The only exceptions are: when the variable forms an iter within a
979+
# comprehension scope; and/or when used as a default, decorator,
980+
# or annotation within a function.
983981
if self._ignore_class_scope(node):
984982
continue
985983

@@ -1249,16 +1247,53 @@ def _allow_global_unused_variables(self):
12491247

12501248
@staticmethod
12511249
def _defined_in_function_definition(node, frame):
1252-
in_annotation_or_default = False
1250+
in_annotation_or_default_or_decorator = False
12531251
if isinstance(frame, astroid.FunctionDef) and node.statement() is frame:
1254-
in_annotation_or_default = (
1255-
node in frame.args.annotations
1256-
or node in frame.args.posonlyargs_annotations
1257-
or node in frame.args.kwonlyargs_annotations
1258-
or node is frame.args.varargannotation
1259-
or node is frame.args.kwargannotation
1260-
) or frame.args.parent_of(node)
1261-
return in_annotation_or_default
1252+
in_annotation_or_default_or_decorator = (
1253+
(
1254+
node in frame.args.annotations
1255+
or node in frame.args.posonlyargs_annotations
1256+
or node in frame.args.kwonlyargs_annotations
1257+
or node is frame.args.varargannotation
1258+
or node is frame.args.kwargannotation
1259+
)
1260+
or frame.args.parent_of(node)
1261+
or (frame.decorators and frame.decorators.parent_of(node))
1262+
or (
1263+
frame.returns
1264+
and (node is frame.returns or frame.returns.parent_of(node))
1265+
)
1266+
)
1267+
return in_annotation_or_default_or_decorator
1268+
1269+
@staticmethod
1270+
def _in_lambda_or_comprehension_body(
1271+
node: astroid.node_classes.NodeNG, frame: astroid.node_classes.NodeNG
1272+
) -> bool:
1273+
"""return True if node within a lambda/comprehension body (or similar) and thus should not have access to class attributes in frame"""
1274+
child = node
1275+
parent = node.parent
1276+
while parent is not None:
1277+
if parent is frame:
1278+
return False
1279+
if isinstance(parent, astroid.Lambda) and not child is parent.args:
1280+
# Body of lambda should not have access to class attributes.
1281+
return True
1282+
if (
1283+
isinstance(parent, astroid.node_classes.Comprehension)
1284+
and not child is parent.iter
1285+
):
1286+
# Only iter of list/set/dict/generator comprehension should have access.
1287+
return True
1288+
if isinstance(parent, astroid.scoped_nodes.ComprehensionScope) and not (
1289+
parent.generators and child is parent.generators[0]
1290+
):
1291+
# Body of list/set/dict/generator comprehension should not have access to class attributes.
1292+
# Furthermore, only the first generator (if multiple) in comprehension should have access.
1293+
return True
1294+
child = parent
1295+
parent = parent.parent
1296+
return False
12621297

12631298
@staticmethod
12641299
def _is_variable_violation(
@@ -1419,13 +1454,19 @@ def _ignore_class_scope(self, node):
14191454

14201455
name = node.name
14211456
frame = node.statement().scope()
1422-
in_annotation_or_default = self._defined_in_function_definition(node, frame)
1423-
if in_annotation_or_default:
1457+
in_annotation_or_default_or_decorator = self._defined_in_function_definition(
1458+
node, frame
1459+
)
1460+
if in_annotation_or_default_or_decorator:
14241461
frame_locals = frame.parent.scope().locals
14251462
else:
14261463
frame_locals = frame.locals
14271464
return not (
1428-
(isinstance(frame, astroid.ClassDef) or in_annotation_or_default)
1465+
(
1466+
isinstance(frame, astroid.ClassDef)
1467+
or in_annotation_or_default_or_decorator
1468+
)
1469+
and not self._in_lambda_or_comprehension_body(node, frame)
14291470
and name in frame_locals
14301471
)
14311472

pylint/lint/__init__.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <[email protected]>
2+
# Copyright (c) 2008 Fabrice Douchant <[email protected]>
3+
# Copyright (c) 2009 Vincent
4+
# Copyright (c) 2009 Mads Kiilerich <[email protected]>
5+
# Copyright (c) 2011-2014 Google, Inc.
6+
# Copyright (c) 2012 David Pursehouse <[email protected]>
7+
# Copyright (c) 2012 Kevin Jing Qiu <[email protected]>
8+
# Copyright (c) 2012 FELD Boris <[email protected]>
9+
# Copyright (c) 2012 JT Olds <[email protected]>
10+
# Copyright (c) 2014-2018 Claudiu Popa <[email protected]>
11+
# Copyright (c) 2014-2015 Michal Nowikowski <[email protected]>
12+
# Copyright (c) 2014 Brett Cannon <[email protected]>
13+
# Copyright (c) 2014 Alexandru Coman <[email protected]>
14+
# Copyright (c) 2014 Daniel Harding <[email protected]>
15+
# Copyright (c) 2014 Arun Persaud <[email protected]>
16+
# Copyright (c) 2014 Dan Goldsmith <[email protected]>
17+
# Copyright (c) 2015-2016 Florian Bruhin <[email protected]>
18+
# Copyright (c) 2015 Aru Sahni <[email protected]>
19+
# Copyright (c) 2015 Steven Myint <[email protected]>
20+
# Copyright (c) 2015 Simu Toni <[email protected]>
21+
# Copyright (c) 2015 Mihai Balint <[email protected]>
22+
# Copyright (c) 2015 Ionel Cristian Maries <[email protected]>
23+
# Copyright (c) 2016-2017 Łukasz Rogalski <[email protected]>
24+
# Copyright (c) 2016 Glenn Matthews <[email protected]>
25+
# Copyright (c) 2016 Alan Evangelista <[email protected]>
26+
# Copyright (c) 2017-2018 Ville Skyttä <[email protected]>
27+
# Copyright (c) 2017-2018 hippo91 <[email protected]>
28+
# Copyright (c) 2017 Daniel Miller <[email protected]>
29+
# Copyright (c) 2017 Roman Ivanov <[email protected]>
30+
# Copyright (c) 2017 Ned Batchelder <[email protected]>
31+
# Copyright (c) 2018 Randall Leeds <[email protected]>
32+
# Copyright (c) 2018 Mike Frysinger <[email protected]>
33+
# Copyright (c) 2018 ssolanki <[email protected]>
34+
# Copyright (c) 2018 Ville Skyttä <[email protected]>
35+
# Copyright (c) 2018 Sushobhit <[email protected]>
36+
# Copyright (c) 2018 Anthony Sottile <[email protected]>
37+
# Copyright (c) 2018 Jason Owen <[email protected]>
38+
# Copyright (c) 2018 Gary Tyler McLeod <[email protected]>
39+
# Copyright (c) 2018 Yuval Langer <[email protected]>
40+
# Copyright (c) 2018 Nick Drozd <[email protected]>
41+
# Copyright (c) 2018 kapsh <[email protected]>
42+
43+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
44+
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
45+
46+
""" pylint [options] modules_or_packages
47+
48+
Check that module(s) satisfy a coding standard (and more !).
49+
50+
pylint --help
51+
52+
Display this help message and exit.
53+
54+
pylint --help-msg <msg-id>[,<msg-id>]
55+
56+
Display help messages about given message identifiers and exit.
57+
"""
58+
import sys
59+
60+
from pylint.lint.check_parallel import check_parallel
61+
from pylint.lint.pylinter import PyLinter
62+
from pylint.lint.report_functions import (
63+
report_messages_by_module_stats,
64+
report_messages_stats,
65+
report_total_messages_stats,
66+
)
67+
from pylint.lint.run import Run
68+
from pylint.lint.utils import (
69+
ArgumentPreprocessingError,
70+
_patch_sys_path,
71+
fix_import_path,
72+
preprocess_options,
73+
)
74+
75+
if __name__ == "__main__":
76+
Run(sys.argv[1:])

pylint/lint/check_parallel.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import collections
2+
import functools
3+
4+
from pylint import reporters
5+
from pylint.lint.utils import _patch_sys_path
6+
from pylint.message import Message
7+
8+
try:
9+
import multiprocessing
10+
except ImportError:
11+
multiprocessing = None # type: ignore
12+
13+
# PyLinter object used by worker processes when checking files using multiprocessing
14+
# should only be used by the worker processes
15+
_worker_linter = None
16+
17+
18+
def _get_new_args(message):
19+
location = (
20+
message.abspath,
21+
message.path,
22+
message.module,
23+
message.obj,
24+
message.line,
25+
message.column,
26+
)
27+
return (message.msg_id, message.symbol, location, message.msg, message.confidence)
28+
29+
30+
def _merge_stats(stats):
31+
merged = {}
32+
by_msg = collections.Counter()
33+
for stat in stats:
34+
message_stats = stat.pop("by_msg", {})
35+
by_msg.update(message_stats)
36+
37+
for key, item in stat.items():
38+
if key not in merged:
39+
merged[key] = item
40+
elif isinstance(item, dict):
41+
merged[key].update(item)
42+
else:
43+
merged[key] = merged[key] + item
44+
45+
merged["by_msg"] = by_msg
46+
return merged
47+
48+
49+
def _worker_initialize(linter, arguments=None):
50+
global _worker_linter # pylint: disable=global-statement
51+
_worker_linter = linter
52+
53+
# On the worker process side the messages are just collected and passed back to
54+
# parent process as _worker_check_file function's return value
55+
_worker_linter.set_reporter(reporters.CollectingReporter())
56+
_worker_linter.open()
57+
58+
# Patch sys.path so that each argument is importable just like in single job mode
59+
_patch_sys_path(arguments or ())
60+
61+
62+
def _worker_check_single_file(file_item):
63+
name, filepath, modname = file_item
64+
65+
_worker_linter.open()
66+
_worker_linter.check_single_file(name, filepath, modname)
67+
68+
msgs = [_get_new_args(m) for m in _worker_linter.reporter.messages]
69+
return (
70+
_worker_linter.current_name,
71+
msgs,
72+
_worker_linter.stats,
73+
_worker_linter.msg_status,
74+
)
75+
76+
77+
def check_parallel(linter, jobs, files, arguments=None):
78+
"""Use the given linter to lint the files with given amount of workers (jobs)"""
79+
# The reporter does not need to be passed to worker processess, i.e. the reporter does
80+
# not need to be pickleable
81+
original_reporter = linter.reporter
82+
linter.reporter = None
83+
84+
# The linter is inherited by all the pool's workers, i.e. the linter
85+
# is identical to the linter object here. This is requred so that
86+
# a custom PyLinter object can be used.
87+
initializer = functools.partial(_worker_initialize, arguments=arguments)
88+
with multiprocessing.Pool(jobs, initializer=initializer, initargs=[linter]) as pool:
89+
# ..and now when the workers have inherited the linter, the actual reporter
90+
# can be set back here on the parent process so that results get stored into
91+
# correct reporter
92+
linter.set_reporter(original_reporter)
93+
linter.open()
94+
95+
all_stats = []
96+
97+
for module, messages, stats, msg_status in pool.imap_unordered(
98+
_worker_check_single_file, files
99+
):
100+
linter.set_current_module(module)
101+
for msg in messages:
102+
msg = Message(*msg)
103+
linter.reporter.handle_message(msg)
104+
105+
all_stats.append(stats)
106+
linter.msg_status |= msg_status
107+
108+
linter.stats = _merge_stats(all_stats)
109+
110+
# Insert stats data to local checkers.
111+
for checker in linter.get_checkers():
112+
if checker is not linter:
113+
checker.stats = linter.stats

0 commit comments

Comments
 (0)