Skip to content

Commit a547566

Browse files
authored
Merge branch 'main' into issue-626
2 parents 161a271 + 85b69c9 commit a547566

23 files changed

+722
-217
lines changed

ChangeLog

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

6969
Closes #4681
7070

71+
* Fix false positives for ``superfluous-parens`` with walrus operator, ternary operator and inside list comprehension.
72+
73+
Closes #2818
74+
Closes #3249
75+
Closes #3608
76+
Closes #4346
77+
7178
* Fix false negative for ``used-before-assignment`` when the variable is assigned
7279
in an exception handler, but used outside of the handler.
7380

doc/whatsnew/2.10.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ Other Changes
5555
Closes #1990
5656
Closes #4168
5757

58+
* Fix false positives for ``superfluous-parens`` with walrus operator, ternary operator and inside list comprehension.
59+
60+
Closes #2818
61+
Closes #3249
62+
Closes #3608
63+
Closes #4346
64+
5865
* Fix false negative for ``used-before-assignment`` when the variable is assigned
5966
in an exception handler, but used outside of the handler.
6067

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

pylint/pyreverse/printer.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
Base class defining the interface for a printer.
8+
"""
9+
from abc import ABC, abstractmethod
10+
from enum import Enum
11+
from typing import List, NamedTuple, Optional
12+
13+
14+
class NodeType(Enum):
15+
CLASS = "class"
16+
INTERFACE = "interface"
17+
PACKAGE = "package"
18+
19+
20+
class EdgeType(Enum):
21+
INHERITS = "inherits"
22+
IMPLEMENTS = "implements"
23+
ASSOCIATION = "association"
24+
USES = "uses"
25+
26+
27+
class Layout(Enum):
28+
LEFT_TO_RIGHT = "LR"
29+
RIGHT_TO_LEFT = "RL"
30+
TOP_TO_BOTTOM = "TB"
31+
BOTTOM_TO_TOP = "BT"
32+
33+
34+
class NodeProperties(NamedTuple):
35+
label: str
36+
color: Optional[str] = None
37+
fontcolor: Optional[str] = None
38+
body: Optional[str] = None
39+
40+
41+
class Printer(ABC):
42+
"""Base class defining the interface for a printer"""
43+
44+
def __init__(
45+
self,
46+
title: str,
47+
layout: Optional[Layout] = None,
48+
use_automatic_namespace: Optional[bool] = None,
49+
):
50+
self.title: str = title
51+
self.layout = layout
52+
self.use_automatic_namespace = use_automatic_namespace
53+
self.lines: List[str] = []
54+
self._open_graph()
55+
56+
@abstractmethod
57+
def _open_graph(self) -> None:
58+
"""Emit the header lines, i.e. all boilerplate code that defines things like layout etc."""
59+
60+
def emit(self, line: str, force_newline: Optional[bool] = True) -> None:
61+
if force_newline and not line.endswith("\n"):
62+
line += "\n"
63+
self.lines.append(line)
64+
65+
@abstractmethod
66+
def emit_node(
67+
self,
68+
name: str,
69+
type_: NodeType,
70+
properties: Optional[NodeProperties] = None,
71+
) -> None:
72+
"""Create a new node. Nodes can be classes, packages, participants etc."""
73+
74+
@abstractmethod
75+
def emit_edge(
76+
self,
77+
from_node: str,
78+
to_node: str,
79+
type_: EdgeType,
80+
label: Optional[str] = None,
81+
) -> None:
82+
"""Create an edge from one node to another to display relationships."""
83+
84+
def generate(self, outputfile: str) -> None:
85+
"""Generate and save the final outputfile."""
86+
self._close_graph()
87+
with open(outputfile, "w", encoding="utf-8") as outfile:
88+
outfile.writelines(self.lines)
89+
90+
@abstractmethod
91+
def _close_graph(self) -> None:
92+
"""Emit the lines needed to properly close the graph."""

0 commit comments

Comments
 (0)