Skip to content

Commit 2259aaf

Browse files
Merge branch 'main' into f-strings-variable
2 parents f7f4f89 + 85b69c9 commit 2259aaf

26 files changed

+726
-221
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ repos:
2121
- --remove-duplicate-keys
2222
- --remove-unused-variables
2323
- repo: https://github.com/asottile/pyupgrade
24-
rev: v2.23.0
24+
rev: v2.23.1
2525
hooks:
2626
- id: pyupgrade
2727
args: [--py36-plus]
2828
exclude: *fixtures
2929
- repo: https://github.com/PyCQA/isort
30-
rev: 5.9.2
30+
rev: 5.9.3
3131
hooks:
3232
- id: isort
3333
- repo: https://github.com/psf/black

ChangeLog

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ Release date: TBA
7272

7373
Closes #2507
7474

75+
* Fix false positives for ``superfluous-parens`` with walrus operator, ternary operator and inside list comprehension.
76+
77+
Closes #2818
78+
Closes #3249
79+
Closes #3608
80+
Closes #4346
81+
7582

7683
What's New in Pylint 2.9.6?
7784
===========================

doc/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Sphinx==4.1.1
1+
Sphinx==4.1.2
22
python-docs-theme==2021.5
33
-e .

doc/whatsnew/2.10.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,10 @@ Other Changes
5858
Ref #1162
5959
Closes #1990
6060
Closes #4168
61+
62+
* Fix false positives for ``superfluous-parens`` with walrus operator, ternary operator and inside list comprehension.
63+
64+
Closes #2818
65+
Closes #3249
66+
Closes #3608
67+
Closes #4346

pylint/checkers/format.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
# Copyright (c) 2019 Hugo van Kemenade <[email protected]>
3838
# Copyright (c) 2020 Raphael Gaschignard <[email protected]>
3939
# Copyright (c) 2021 Marc Mueller <[email protected]>
40+
# Copyright (c) 2021 Daniel van Noord <[email protected]>
4041

4142
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
4243
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
@@ -374,6 +375,7 @@ def _check_keyword_parentheses(
374375
found_and_or = False
375376
contains_walrus_operator = False
376377
walrus_operator_depth = 0
378+
contains_double_parens = 0
377379
depth = 0
378380
keyword_token = str(tokens[start].string)
379381
line_num = tokens[start].start[0]
@@ -393,19 +395,25 @@ def _check_keyword_parentheses(
393395
walrus_operator_depth = depth
394396
if token.string == "(":
395397
depth += 1
398+
if tokens[i + 1].string == "(":
399+
contains_double_parens = 1
396400
elif token.string == ")":
397401
depth -= 1
398402
if depth:
403+
if contains_double_parens and tokens[i + 1].string == ")":
404+
self.add_message(
405+
"superfluous-parens", line=line_num, args=keyword_token
406+
)
407+
return
408+
contains_double_parens = 0
399409
continue
400410
# ')' can't happen after if (foo), since it would be a syntax error.
401411
if tokens[i + 1].string in (":", ")", "]", "}", "in") or tokens[
402412
i + 1
403413
].type in (tokenize.NEWLINE, tokenize.ENDMARKER, tokenize.COMMENT):
404-
# The empty tuple () is always accepted.
405414
if contains_walrus_operator and walrus_operator_depth - 1 == depth:
406-
# Reset variable for possible following expressions
407-
contains_walrus_operator = False
408-
continue
415+
return
416+
# The empty tuple () is always accepted.
409417
if i == start + 2:
410418
return
411419
if keyword_token == "not":
@@ -417,7 +425,7 @@ def _check_keyword_parentheses(
417425
self.add_message(
418426
"superfluous-parens", line=line_num, args=keyword_token
419427
)
420-
elif not found_and_or:
428+
elif not found_and_or and keyword_token != "in":
421429
self.add_message(
422430
"superfluous-parens", line=line_num, args=keyword_token
423431
)
@@ -440,6 +448,13 @@ def _check_keyword_parentheses(
440448
# without an error.
441449
elif token[1] == "for":
442450
return
451+
# A generator expression can have a 'else' token in it.
452+
# We check the rest of the tokens to see if any problems incure after
453+
# the 'else'.
454+
elif token[1] == "else":
455+
if "(" in (i.string for i in tokens[i:]):
456+
self._check_keyword_parentheses(tokens[i:], 0)
457+
return
443458

444459
def _prepare_token_dispatcher(self):
445460
dispatch = {}

pylint/pyreverse/diagrams.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ def __init__(self, title="No name", node=None):
4545
self.node = node
4646

4747

48+
class PackageEntity(DiagramEntity):
49+
"""A diagram object representing a package"""
50+
51+
52+
class ClassEntity(DiagramEntity):
53+
"""A diagram object representing a class"""
54+
55+
def __init__(self, title, node):
56+
super().__init__(title=title, node=node)
57+
self.attrs = None
58+
self.methods = None
59+
60+
4861
class ClassDiagram(Figure, FilterMixIn):
4962
"""main class diagram handling"""
5063

pylint/pyreverse/dot_printer.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright (c) 2021 Andreas Finkler <[email protected]>
2+
3+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
4+
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
5+
6+
"""
7+
Class to generate files in dot format and image formats supported by Graphviz.
8+
"""
9+
import os
10+
import subprocess
11+
import sys
12+
import tempfile
13+
from pathlib import Path
14+
from typing import Dict, FrozenSet, Optional
15+
16+
from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer
17+
from pylint.pyreverse.utils import check_graphviz_availability
18+
19+
ALLOWED_CHARSETS: FrozenSet[str] = frozenset(("utf-8", "iso-8859-1", "latin1"))
20+
SHAPES: Dict[NodeType, str] = {
21+
NodeType.PACKAGE: "box",
22+
NodeType.INTERFACE: "record",
23+
NodeType.CLASS: "record",
24+
}
25+
ARROWS: Dict[EdgeType, Dict] = {
26+
EdgeType.INHERITS: dict(arrowtail="none", arrowhead="empty"),
27+
EdgeType.IMPLEMENTS: dict(arrowtail="node", arrowhead="empty", style="dashed"),
28+
EdgeType.ASSOCIATION: dict(
29+
fontcolor="green", arrowtail="none", arrowhead="diamond", style="solid"
30+
),
31+
EdgeType.USES: dict(arrowtail="none", arrowhead="open"),
32+
}
33+
34+
35+
class DotPrinter(Printer):
36+
def __init__(
37+
self,
38+
title: str,
39+
layout: Optional[Layout] = None,
40+
use_automatic_namespace: Optional[bool] = None,
41+
):
42+
self.charset = "utf-8"
43+
self.node_style = "solid"
44+
super().__init__(title, layout, use_automatic_namespace)
45+
46+
def _open_graph(self) -> None:
47+
"""Emit the header lines"""
48+
self.emit(f'digraph "{self.title}" {{')
49+
if self.layout:
50+
self.emit(f"rankdir={self.layout.value}")
51+
if self.charset:
52+
assert (
53+
self.charset.lower() in ALLOWED_CHARSETS
54+
), f"unsupported charset {self.charset}"
55+
self.emit(f'charset="{self.charset}"')
56+
57+
def emit_node(
58+
self,
59+
name: str,
60+
type_: NodeType,
61+
properties: Optional[NodeProperties] = None,
62+
) -> None:
63+
"""Create a new node. Nodes can be classes, packages, participants etc."""
64+
if properties is None:
65+
properties = NodeProperties(label=name)
66+
shape = SHAPES[type_]
67+
color = properties.color if properties.color is not None else "black"
68+
label = properties.label
69+
if label:
70+
if type_ is NodeType.INTERFACE:
71+
label = "<<interface>>\\n" + label
72+
label_part = f', label="{label}"'
73+
else:
74+
label_part = ""
75+
fontcolor_part = (
76+
f', fontcolor="{properties.fontcolor}"' if properties.fontcolor else ""
77+
)
78+
self.emit(
79+
f'"{name}" [color="{color}"{fontcolor_part}{label_part}, shape="{shape}", style="{self.node_style}"];'
80+
)
81+
82+
def emit_edge(
83+
self,
84+
from_node: str,
85+
to_node: str,
86+
type_: EdgeType,
87+
label: Optional[str] = None,
88+
) -> None:
89+
"""Create an edge from one node to another to display relationships."""
90+
arrowstyle = ARROWS[type_]
91+
attrs = [f'{prop}="{value}"' for prop, value in arrowstyle.items()]
92+
if label:
93+
attrs.append(f'label="{label}"')
94+
self.emit(f'"{from_node}" -> "{to_node}" [{", ".join(sorted(attrs))}];')
95+
96+
def generate(self, outputfile: str) -> None:
97+
self._close_graph()
98+
graphviz_extensions = ("dot", "gv")
99+
name = self.title
100+
if outputfile is None:
101+
target = "png"
102+
pdot, dot_sourcepath = tempfile.mkstemp(".gv", name)
103+
ppng, outputfile = tempfile.mkstemp(".png", name)
104+
os.close(pdot)
105+
os.close(ppng)
106+
else:
107+
target = Path(outputfile).suffix.lstrip(".")
108+
if not target:
109+
target = "png"
110+
outputfile = outputfile + "." + target
111+
if target not in graphviz_extensions:
112+
pdot, dot_sourcepath = tempfile.mkstemp(".gv", name)
113+
os.close(pdot)
114+
else:
115+
dot_sourcepath = outputfile
116+
with open(dot_sourcepath, "w", encoding="utf8") as outfile:
117+
outfile.writelines(self.lines)
118+
if target not in graphviz_extensions:
119+
check_graphviz_availability()
120+
use_shell = sys.platform == "win32"
121+
subprocess.call(
122+
["dot", "-T", target, dot_sourcepath, "-o", outputfile],
123+
shell=use_shell,
124+
)
125+
os.unlink(dot_sourcepath)
126+
127+
def _close_graph(self) -> None:
128+
"""Emit the lines needed to properly close the graph."""
129+
self.emit("}\n")

pylint/pyreverse/main.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@
2121
create UML diagrams for classes and modules in <packages>
2222
"""
2323
import os
24-
import subprocess
2524
import sys
2625
from typing import Iterable
2726

2827
from pylint.config import ConfigurationMixIn
2928
from pylint.pyreverse import writer
3029
from pylint.pyreverse.diadefslib import DiadefsHandler
3130
from pylint.pyreverse.inspector import Linker, project_from_files
32-
from pylint.pyreverse.utils import insert_default_options
31+
from pylint.pyreverse.utils import check_graphviz_availability, insert_default_options
3332

3433
OPTIONS = (
3534
(
@@ -175,19 +174,6 @@
175174
)
176175

177176

178-
def _check_graphviz_available(output_format):
179-
"""check if we need graphviz for different output format"""
180-
try:
181-
subprocess.call(["dot", "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
182-
except OSError:
183-
print(
184-
"The output format '%s' is currently not available.\n"
185-
"Please install 'Graphviz' to have other output formats "
186-
"than 'dot' or 'vcg'." % output_format
187-
)
188-
sys.exit(32)
189-
190-
191177
class Run(ConfigurationMixIn):
192178
"""base class providing common behaviour for pyreverse commands"""
193179

@@ -198,7 +184,7 @@ def __init__(self, args: Iterable[str]):
198184
insert_default_options()
199185
args = self.load_command_line_configuration(args)
200186
if self.config.output_format not in ("dot", "vcg"):
201-
_check_graphviz_available(self.config.output_format)
187+
check_graphviz_availability()
202188

203189
sys.exit(self.run(args))
204190

0 commit comments

Comments
 (0)