Skip to content

Commit 5fed063

Browse files
Merge branch 'master' into issue-4412-python-360
2 parents 75914fb + 9b268ec commit 5fed063

30 files changed

+398
-98
lines changed

.github/dependabot.yml

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ updates:
77
labels:
88
- "dependency"
99
open-pull-requests-limit: 10
10+
rebase-strategy: "disabled"
1011

1112
- package-ecosystem: "github-actions"
1213
directory: "/"
@@ -15,3 +16,4 @@ updates:
1516
labels:
1617
- "dependency"
1718
open-pull-requests-limit: 10
19+
rebase-strategy: "disabled"

.github/workflows/ci.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ jobs:
147147
runs-on: ubuntu-latest
148148
strategy:
149149
matrix:
150-
python-version: [3.6, 3.7, 3.8, 3.9]
150+
python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev]
151151
outputs:
152152
python-key: ${{ steps.generate-python-key.outputs.key }}
153153
steps:
@@ -189,7 +189,7 @@ jobs:
189189
strategy:
190190
fail-fast: false
191191
matrix:
192-
python-version: [3.6, 3.7, 3.8, 3.9]
192+
python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev]
193193
steps:
194194
- name: Check out code from GitHub
195195
uses: actions/[email protected]
@@ -321,7 +321,7 @@ jobs:
321321
runs-on: windows-latest
322322
strategy:
323323
matrix:
324-
python-version: [3.6, 3.7, 3.8, 3.9]
324+
python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev]
325325
outputs:
326326
python-key: ${{ steps.generate-python-key.outputs.key }}
327327
steps:
@@ -363,7 +363,7 @@ jobs:
363363
strategy:
364364
fail-fast: false
365365
matrix:
366-
python-version: [3.6, 3.7, 3.8, 3.9]
366+
python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev]
367367
steps:
368368
- name: Set temp directory
369369
run: echo "TEMP=$env:USERPROFILE\AppData\Local\Temp" >> $env:GITHUB_ENV

.pre-commit-config.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ repos:
2121
- --remove-duplicate-keys
2222
- --remove-unused-variables
2323
- repo: https://github.com/asottile/pyupgrade
24-
rev: v2.14.0
24+
rev: v2.15.0
2525
hooks:
2626
- id: pyupgrade
2727
args: [--py36-plus]
@@ -31,7 +31,7 @@ repos:
3131
hooks:
3232
- id: isort
3333
- repo: https://github.com/psf/black
34-
rev: 21.4b2
34+
rev: 21.5b1
3535
hooks:
3636
- id: black
3737
args: [--safe, --quiet]
@@ -41,7 +41,7 @@ repos:
4141
hooks:
4242
- id: black-disable-checker
4343
- repo: https://github.com/PyCQA/flake8
44-
rev: 3.9.1
44+
rev: 3.9.2
4545
hooks:
4646
- id: flake8
4747
exclude: *fixtures
@@ -80,7 +80,7 @@ repos:
8080
additional_dependencies: []
8181
exclude: tests/functional/|tests/input|tests/extensions/data|tests/regrtest_data/|tests/data/|doc/|bin/
8282
- repo: https://github.com/pre-commit/mirrors-prettier
83-
rev: v2.2.1
83+
rev: v2.3.0
8484
hooks:
8585
- id: prettier
8686
args: [--prose-wrap=always, --print-width=88]

CONTRIBUTORS.txt

+4
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,7 @@ contributors:
482482
* das-intensity: contributor
483483

484484
* Jiajunsu (victor): contributor
485+
486+
* Andrew Haigh (nelfin): contributor
487+
488+
* Pang Yu Shao (yushao2): contributor

ChangeLog

+17-7
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
Pylint's ChangeLog
33
------------------
44

5-
6-
7-
What's New in Pylint 2.8.3?
5+
What's New in Pylint 2.9.0?
86
===========================
9-
Release date: 2021-04-26
7+
Release date: TBA
108

119
..
1210
Put new features and bugfixes here and also in 'doc/whatsnew/2.9.rst'
1311

12+
* The warning for ``arguments-differ`` now signals explicitly the difference it detected
13+
by naming the argument or arguments that changed and the type of change that occurred.
14+
1415
* Suppress ``consider-using-with`` inside context managers.
1516

1617
Closes #4430
@@ -31,6 +32,18 @@ Release date: 2021-04-26
3132

3233
Closes #4412
3334

35+
* Stdlib deprecated modules check is moved to stdlib checker. New deprecated
36+
modules are added.
37+
38+
* Fix raising false-positive ``no-member`` on abstract properties
39+
40+
* New checker ``consider-using-dict-items``. Emitted when iterating over dictionary keys and then
41+
indexing the same dictionary with the key within loop body.
42+
43+
Closes #3389
44+
45+
* Don't emit ``import-error`` if import guarded behind ``if sys.version_info >= (x, x)``
46+
3447

3548
What's New in Pylint 2.8.2?
3649
===========================
@@ -59,9 +72,6 @@ Release date: 2021-04-25
5972

6073
Closes #4399
6174

62-
* The warning for ``arguments-differ`` now signals explicitly the difference it detected
63-
by naming the argument or arguments that changed and the type of change that occured.
64-
6575

6676
What's New in Pylint 2.8.0?
6777
===========================

doc/requirements.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Sphinx==3.5.4
2-
python-docs-theme==2020.12
1+
Sphinx==4.0.0
2+
python-docs-theme==2021.5
33
-e .

doc/whatsnew/2.9.rst

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Summary -- Release highlights
1212
New checkers
1313
============
1414

15+
* ``consider-using-dict-items``: Emitted when iterating over dictionary keys and then
16+
indexing the same dictionary with the key within loop body.
17+
1518
Other Changes
1619
=============
1720

pylint/checkers/classes.py

+4-24
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,6 @@ def _has_different_parameters(
271271
original: List[astroid.AssignName],
272272
overridden: List[astroid.AssignName],
273273
dummy_parameter_regex: Pattern,
274-
counter: int,
275274
) -> List[str]:
276275
result = []
277276
zipped = zip_longest(original, overridden)
@@ -280,25 +279,14 @@ def _has_different_parameters(
280279
if not all(params):
281280
return ["Number of parameters "]
282281

283-
# check for the arguments' type
284-
original_type = original_param.parent.annotations[counter]
285-
if original_type is not None:
286-
overridden_type = overridden_param.parent.annotations[counter]
287-
if overridden_type is not None:
288-
if original_type.name != overridden_type.name:
289-
result.append(
290-
f"Parameter '{original_param.name}' was of type '{original_type.name}' and is now"
291-
+ f" of type '{overridden_type.name}' in"
292-
)
293-
counter += 1
294-
295282
# check for the arguments' name
296283
names = [param.name for param in params]
297284
if any(dummy_parameter_regex.match(name) for name in names):
298285
continue
299286
if original_param.name != overridden_param.name:
300287
result.append(
301-
f"Parameter '{original_param.name}' has been renamed to '{overridden_param.name}' in"
288+
f"Parameter '{original_param.name}' has been renamed "
289+
f"to '{overridden_param.name}' in"
302290
)
303291

304292
return result
@@ -343,19 +331,11 @@ def _different_parameters(
343331
v for v in original.args.kwonlyargs if v.name in overidden_names
344332
]
345333

346-
arguments = list(original.args.args)
347-
# variable 'count' helps to check if the type of an argument has changed
348-
# at the _has_different_parameters method
349-
if any(arg.name == "self" for arg in arguments) and len(arguments) > 1:
350-
count = 1
351-
else:
352-
count = 0
353-
354334
different_positional = _has_different_parameters(
355-
original_parameters, overridden_parameters, dummy_parameter_regex, count
335+
original_parameters, overridden_parameters, dummy_parameter_regex
356336
)
357337
different_kwonly = _has_different_parameters(
358-
original_kwonlyargs, overridden.args.kwonlyargs, dummy_parameter_regex, count
338+
original_kwonlyargs, overridden.args.kwonlyargs, dummy_parameter_regex
359339
)
360340
if different_kwonly and different_positional:
361341
if "Number " in different_positional[0] and "Number " in different_kwonly[0]:

pylint/checkers/imports.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ def _ignore_import_failure(node, modname, ignored_modules):
135135
if submodule in ignored_modules:
136136
return True
137137

138+
# ignore import failure if guarded by `sys.version_info` test
139+
if isinstance(node.parent, astroid.If) and isinstance(
140+
node.parent.test, astroid.Compare
141+
):
142+
value = node.parent.test.left
143+
if isinstance(value, astroid.Subscript):
144+
value = value.value
145+
if (
146+
isinstance(value, astroid.Attribute)
147+
and value.as_string() == "sys.version_info"
148+
):
149+
return True
150+
138151
return node_ignores_exception(node, ImportError)
139152

140153

@@ -309,7 +322,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
309322
name = "imports"
310323
msgs = MSGS
311324
priority = -2
312-
default_deprecated_modules = ("optparse", "tkinter.tix")
325+
default_deprecated_modules = ()
313326

314327
options = (
315328
(

pylint/checkers/refactoring/recommendation_checker.py

+93-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
22
# For details: https://github.com/PyCQA/pylint/blob/master/LICENSE
3+
from typing import cast
34

45
import astroid
56

@@ -26,6 +27,14 @@ class RecommendationChecker(checkers.BaseChecker):
2627
"method. It is enough to just iterate through the dictionary itself, as "
2728
'in "for key in dictionary".',
2829
),
30+
"C0206": (
31+
"Consider iterating with .items()",
32+
"consider-using-dict-items",
33+
"Emitted when iterating over the keys of a dictionary and accessing the "
34+
"value by index lookup."
35+
"Both the key and value can be accessed by iterating using the .items() "
36+
"method of the dictionary instead.",
37+
),
2938
}
3039

3140
@staticmethod
@@ -53,8 +62,12 @@ def visit_call(self, node):
5362
if isinstance(node.parent, (astroid.For, astroid.Comprehension)):
5463
self.add_message("consider-iterating-dictionary", node=node)
5564

56-
@utils.check_messages("consider-using-enumerate")
57-
def visit_for(self, node):
65+
@utils.check_messages("consider-using-enumerate", "consider-using-dict-items")
66+
def visit_for(self, node: astroid.For) -> None:
67+
self._check_consider_using_enumerate(node)
68+
self._check_consider_using_dict_items(node)
69+
70+
def _check_consider_using_enumerate(self, node: astroid.For) -> None:
5871
"""Emit a convention whenever range and len are used for indexing."""
5972
# Verify that we have a `range([start], len(...), [stop])` call and
6073
# that the object which is iterated is used as a subscript in the
@@ -119,3 +132,81 @@ def visit_for(self, node):
119132
continue
120133
self.add_message("consider-using-enumerate", node=node)
121134
return
135+
136+
def _check_consider_using_dict_items(self, node: astroid.For) -> None:
137+
"""Add message when accessing dict values by index lookup."""
138+
# Verify that we have a .keys() call and
139+
# that the object which is iterated is used as a subscript in the
140+
# body of the for.
141+
142+
iterating_object_name = utils.get_iterating_dictionary_name(node)
143+
if iterating_object_name is None:
144+
return
145+
146+
# Verify that the body of the for loop uses a subscript
147+
# with the object that was iterated. This uses some heuristics
148+
# in order to make sure that the same object is used in the
149+
# for body.
150+
for child in node.body:
151+
for subscript in child.nodes_of_class(astroid.Subscript):
152+
subscript = cast(astroid.Subscript, subscript)
153+
154+
if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)):
155+
continue
156+
157+
value = subscript.slice
158+
if isinstance(value, astroid.Index):
159+
value = value.value
160+
if (
161+
not isinstance(value, astroid.Name)
162+
or value.name != node.target.name
163+
or iterating_object_name != subscript.value.as_string()
164+
):
165+
continue
166+
last_definition_lineno = value.lookup(value.name)[1][-1].lineno
167+
if last_definition_lineno > node.lineno:
168+
# Ignore this subscript if it has been redefined after
169+
# the for loop. This checks for the line number using .lookup()
170+
# to get the line number where the iterating object was last
171+
# defined and compare that to the for loop's line number
172+
continue
173+
if (
174+
isinstance(subscript.parent, astroid.Assign)
175+
and subscript in subscript.parent.targets
176+
or isinstance(subscript.parent, astroid.AugAssign)
177+
and subscript == subscript.parent.target
178+
):
179+
# Ignore this subscript if it is the target of an assignment
180+
continue
181+
182+
self.add_message("consider-using-dict-items", node=node)
183+
return
184+
185+
@utils.check_messages("consider-using-dict-items")
186+
def visit_comprehension(self, node: astroid.Comprehension) -> None:
187+
iterating_object_name = utils.get_iterating_dictionary_name(node)
188+
if iterating_object_name is None:
189+
return
190+
191+
children = list(node.parent.get_children())
192+
if node.ifs:
193+
children.extend(node.ifs)
194+
for child in children:
195+
for subscript in child.nodes_of_class(astroid.Subscript):
196+
subscript = cast(astroid.Subscript, subscript)
197+
198+
if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)):
199+
continue
200+
201+
value = subscript.slice
202+
if isinstance(value, astroid.Index):
203+
value = value.value
204+
if (
205+
not isinstance(value, astroid.Name)
206+
or value.name != node.target.name
207+
or iterating_object_name != subscript.value.as_string()
208+
):
209+
continue
210+
211+
self.add_message("consider-using-dict-items", node=node)
212+
return

0 commit comments

Comments
 (0)