Skip to content

Commit 5d82d7b

Browse files
authored
Added escaping of vertical bar character in annotation labels (#8610)
1 parent e2d6c4d commit 5d82d7b

18 files changed

+56
-12
lines changed

doc/whatsnew/fragments/8603.bugfix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
``pyreverse``: added escaping of vertical bar character in annotation labels produced by DOT printer to ensure it is not treated as field separator of record-based nodes.
2+
3+
Closes #8603

pylint/pyreverse/dot_printer.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,19 @@ def _build_label_for_node(self, properties: NodeProperties) -> str:
119119
)
120120
label += rf"{method_name}({', '.join(args)})"
121121
if func.returns:
122-
label += ": " + get_annotation_label(func.returns)
122+
annotation_label = get_annotation_label(func.returns)
123+
label += ": " + self._escape_annotation_label(annotation_label)
123124
label += rf"{HTMLLabels.LINEBREAK_LEFT.value}"
124125
label += "}"
125126
return label
126127

128+
def _escape_annotation_label(self, annotation_label: str) -> str:
129+
# Escape vertical bar characters to make them appear as a literal characters
130+
# otherwise it gets treated as field separator of record-based nodes
131+
annotation_label = annotation_label.replace("|", r"\|")
132+
133+
return annotation_label
134+
127135
def emit_edge(
128136
self,
129137
from_node: str,

tests/data/nullable_pattern.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
""" docstring for file nullable_pattern.py """
2+
from typing import Optional
3+
4+
class NullablePatterns:
5+
def return_nullable_1(self) -> int | None:
6+
""" Nullable return type using the | operator as mentioned in PEP 604, see https://peps.python.org/pep-0604 """
7+
pass
8+
9+
def return_nullable_2(self) -> Optional[int]:
10+
pass

tests/pyreverse/data/classes_No_Name.dot

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ charset="utf-8"
77
"data.suppliermodule_test.DoNothing2" [color="black", fontcolor="black", label=<{DoNothing2|<br ALIGN="LEFT"/>|}>, shape="record", style="solid"];
88
"data.suppliermodule_test.DoSomething" [color="black", fontcolor="black", label=<{DoSomething|my_int : Optional[int]<br ALIGN="LEFT"/>my_int_2 : Optional[int]<br ALIGN="LEFT"/>my_string : str<br ALIGN="LEFT"/>|do_it(new_int: int): int<br ALIGN="LEFT"/>}>, shape="record", style="solid"];
99
"data.suppliermodule_test.Interface" [color="black", fontcolor="black", label=<{Interface|<br ALIGN="LEFT"/>|<I>get_value</I>()<br ALIGN="LEFT"/><I>set_value</I>(value)<br ALIGN="LEFT"/>}>, shape="record", style="solid"];
10+
"data.nullable_pattern.NullablePatterns" [color="black", fontcolor="black", label=<{NullablePatterns|<br ALIGN="LEFT"/>|<I>return_nullable_1</I>(): int \| None<br ALIGN="LEFT"/><I>return_nullable_2</I>(): Optional[int]<br ALIGN="LEFT"/>}>, shape="record", style="solid"];
1011
"data.property_pattern.PropertyPatterns" [color="black", fontcolor="black", label=<{PropertyPatterns|prop1<br ALIGN="LEFT"/>prop2<br ALIGN="LEFT"/>|}>, shape="record", style="solid"];
1112
"data.clientmodule_test.Specialization" [color="black", fontcolor="black", label=<{Specialization|TYPE : str<br ALIGN="LEFT"/>relation<br ALIGN="LEFT"/>relation2<br ALIGN="LEFT"/>top : str<br ALIGN="LEFT"/>|from_value(value: int)<br ALIGN="LEFT"/>increment_value(): None<br ALIGN="LEFT"/>transform_value(value: int): int<br ALIGN="LEFT"/>}>, shape="record", style="solid"];
1213
"data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"];

tests/pyreverse/data/classes_No_Name.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
get_value()*
2727
set_value(value)*
2828
}
29+
class NullablePatterns {
30+
return_nullable_1()* int | None
31+
return_nullable_2()* Optional[int]
32+
}
2933
class PropertyPatterns {
3034
prop1
3135
prop2
@@ -43,7 +47,7 @@
4347
DoNothing --* Ancestor : cls_member
4448
DoNothing --* Specialization : relation
4549
DoNothing2 --o Specialization : relation2
46-
50+
4751
</div>
4852
</body>
4953
</html>

tests/pyreverse/data/classes_No_Name.mmd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ classDiagram
2121
get_value()*
2222
set_value(value)*
2323
}
24+
class NullablePatterns {
25+
return_nullable_1()* int | None
26+
return_nullable_2()* Optional[int]
27+
}
2428
class PropertyPatterns {
2529
prop1
2630
prop2

tests/pyreverse/data/classes_No_Name.puml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class "Interface" as data.suppliermodule_test.Interface {
2222
{abstract}get_value()
2323
{abstract}set_value(value)
2424
}
25+
class "NullablePatterns" as data.nullable_pattern.NullablePatterns {
26+
{abstract}return_nullable_1() -> int | None
27+
{abstract}return_nullable_2() -> Optional[int]
28+
}
2529
class "PropertyPatterns" as data.property_pattern.PropertyPatterns {
2630
prop1
2731
prop2

tests/pyreverse/data/classes_colorized.dot

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ charset="utf-8"
77
"data.suppliermodule_test.DoNothing2" [color="#77AADD", fontcolor="black", label=<{DoNothing2|<br ALIGN="LEFT"/>|}>, shape="record", style="filled"];
88
"data.suppliermodule_test.DoSomething" [color="#77AADD", fontcolor="black", label=<{DoSomething|my_int : Optional[int]<br ALIGN="LEFT"/>my_int_2 : Optional[int]<br ALIGN="LEFT"/>my_string : str<br ALIGN="LEFT"/>|do_it(new_int: int): int<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
99
"data.suppliermodule_test.Interface" [color="#77AADD", fontcolor="black", label=<{Interface|<br ALIGN="LEFT"/>|<I>get_value</I>()<br ALIGN="LEFT"/><I>set_value</I>(value)<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
10+
"data.nullable_pattern.NullablePatterns" [color="#77AADD", fontcolor="black", label=<{NullablePatterns|<br ALIGN="LEFT"/>|<I>return_nullable_1</I>(): int \| None<br ALIGN="LEFT"/><I>return_nullable_2</I>(): Optional[int]<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
1011
"data.property_pattern.PropertyPatterns" [color="#77AADD", fontcolor="black", label=<{PropertyPatterns|prop1<br ALIGN="LEFT"/>prop2<br ALIGN="LEFT"/>|}>, shape="record", style="filled"];
1112
"data.clientmodule_test.Specialization" [color="#77AADD", fontcolor="black", label=<{Specialization|TYPE : str<br ALIGN="LEFT"/>relation<br ALIGN="LEFT"/>relation2<br ALIGN="LEFT"/>top : str<br ALIGN="LEFT"/>|from_value(value: int)<br ALIGN="LEFT"/>increment_value(): None<br ALIGN="LEFT"/>transform_value(value: int): int<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
1213
"data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"];

tests/pyreverse/data/classes_colorized.puml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class "Interface" as data.suppliermodule_test.Interface #77AADD {
2222
{abstract}get_value()
2323
{abstract}set_value(value)
2424
}
25+
class "NullablePatterns" as data.nullable_pattern.NullablePatterns #77AADD {
26+
{abstract}return_nullable_1() -> int | None
27+
{abstract}return_nullable_2() -> Optional[int]
28+
}
2529
class "PropertyPatterns" as data.property_pattern.PropertyPatterns #77AADD {
2630
prop1
2731
prop2

tests/pyreverse/data/packages_No_Name.dot

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ rankdir=BT
33
charset="utf-8"
44
"data" [color="black", label=<data>, shape="box", style="solid"];
55
"data.clientmodule_test" [color="black", label=<data.clientmodule_test>, shape="box", style="solid"];
6+
"data.nullable_pattern" [color="black", label=<data.nullable_pattern>, shape="box", style="solid"];
67
"data.property_pattern" [color="black", label=<data.property_pattern>, shape="box", style="solid"];
78
"data.suppliermodule_test" [color="black", label=<data.suppliermodule_test>, shape="box", style="solid"];
89
"data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"];

tests/pyreverse/data/packages_No_Name.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
}
99
class clientmodule_test {
1010
}
11+
class nullable_pattern {
12+
}
1113
class property_pattern {
1214
}
1315
class suppliermodule_test {
1416
}
1517
clientmodule_test --> suppliermodule_test
16-
18+
1719
</div>
1820
</body>
1921
</html>

tests/pyreverse/data/packages_No_Name.mmd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ classDiagram
33
}
44
class clientmodule_test {
55
}
6+
class nullable_pattern {
7+
}
68
class property_pattern {
79
}
810
class suppliermodule_test {
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
@startuml packages_No_Name
22
set namespaceSeparator none
33
package "data" as data {
4-
54
}
65
package "data.clientmodule_test" as data.clientmodule_test {
7-
6+
}
7+
package "data.nullable_pattern" as data.nullable_pattern {
88
}
99
package "data.property_pattern" as data.property_pattern {
10-
1110
}
1211
package "data.suppliermodule_test" as data.suppliermodule_test {
13-
1412
}
1513
data.clientmodule_test --> data.suppliermodule_test
1614
@enduml

tests/pyreverse/data/packages_colorized.dot

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ rankdir=BT
33
charset="utf-8"
44
"data" [color="#77AADD", label=<data>, shape="box", style="filled"];
55
"data.clientmodule_test" [color="#77AADD", label=<data.clientmodule_test>, shape="box", style="filled"];
6+
"data.nullable_pattern" [color="#77AADD", label=<data.nullable_pattern>, shape="box", style="filled"];
67
"data.property_pattern" [color="#77AADD", label=<data.property_pattern>, shape="box", style="filled"];
78
"data.suppliermodule_test" [color="#77AADD", label=<data.suppliermodule_test>, shape="box", style="filled"];
89
"data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"];
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
@startuml packages_colorized
22
set namespaceSeparator none
33
package "data" as data #77AADD {
4-
54
}
65
package "data.clientmodule_test" as data.clientmodule_test #77AADD {
7-
6+
}
7+
package "data.nullable_pattern" as data.nullable_pattern #77AADD {
88
}
99
package "data.property_pattern" as data.property_pattern #77AADD {
10-
1110
}
1211
package "data.suppliermodule_test" as data.suppliermodule_test #77AADD {
13-
1412
}
1513
data.clientmodule_test --> data.suppliermodule_test
1614
@enduml

tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.dot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ charset="utf-8"
44
"custom_colors.CheckerCollector" [color="red", fontcolor="black", label=<{CheckerCollector|checker1<br ALIGN="LEFT"/>checker2<br ALIGN="LEFT"/>checker3<br ALIGN="LEFT"/>|}>, shape="record", style="filled"];
55
"pylint.extensions.check_elif.ElseifUsedChecker" [color="#44BB88", fontcolor="black", label=<{ElseifUsedChecker|msgs : dict<br ALIGN="LEFT"/>name : str<br ALIGN="LEFT"/>|leave_module(_: nodes.Module): None<br ALIGN="LEFT"/>process_tokens(tokens: list[TokenInfo]): None<br ALIGN="LEFT"/>visit_if(node: nodes.If): None<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
66
"pylint.checkers.exceptions.ExceptionsChecker" [color="yellow", fontcolor="black", label=<{ExceptionsChecker|msgs : dict<br ALIGN="LEFT"/>name : str<br ALIGN="LEFT"/>options : tuple<br ALIGN="LEFT"/>|open(): None<br ALIGN="LEFT"/>visit_binop(node: nodes.BinOp): None<br ALIGN="LEFT"/>visit_compare(node: nodes.Compare): None<br ALIGN="LEFT"/>visit_raise(node: nodes.Raise): None<br ALIGN="LEFT"/>visit_tryexcept(node: nodes.TryExcept): None<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
7-
"pylint.checkers.stdlib.StdlibChecker" [color="yellow", fontcolor="black", label=<{StdlibChecker|msgs : dict[str, MessageDefinitionTuple]<br ALIGN="LEFT"/>name : str<br ALIGN="LEFT"/>|deprecated_arguments(method: str): tuple[tuple[int | None, str], ...]<br ALIGN="LEFT"/>deprecated_classes(module: str): Iterable[str]<br ALIGN="LEFT"/>deprecated_decorators(): Iterable[str]<br ALIGN="LEFT"/>deprecated_methods(): set[str]<br ALIGN="LEFT"/>visit_boolop(node: nodes.BoolOp): None<br ALIGN="LEFT"/>visit_call(node: nodes.Call): None<br ALIGN="LEFT"/>visit_functiondef(node: nodes.FunctionDef): None<br ALIGN="LEFT"/>visit_if(node: nodes.If): None<br ALIGN="LEFT"/>visit_ifexp(node: nodes.IfExp): None<br ALIGN="LEFT"/>visit_unaryop(node: nodes.UnaryOp): None<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
7+
"pylint.checkers.stdlib.StdlibChecker" [color="yellow", fontcolor="black", label=<{StdlibChecker|msgs : dict[str, MessageDefinitionTuple]<br ALIGN="LEFT"/>name : str<br ALIGN="LEFT"/>|deprecated_arguments(method: str): tuple[tuple[int \| None, str], ...]<br ALIGN="LEFT"/>deprecated_classes(module: str): Iterable[str]<br ALIGN="LEFT"/>deprecated_decorators(): Iterable[str]<br ALIGN="LEFT"/>deprecated_methods(): set[str]<br ALIGN="LEFT"/>visit_boolop(node: nodes.BoolOp): None<br ALIGN="LEFT"/>visit_call(node: nodes.Call): None<br ALIGN="LEFT"/>visit_functiondef(node: nodes.FunctionDef): None<br ALIGN="LEFT"/>visit_if(node: nodes.If): None<br ALIGN="LEFT"/>visit_ifexp(node: nodes.IfExp): None<br ALIGN="LEFT"/>visit_unaryop(node: nodes.UnaryOp): None<br ALIGN="LEFT"/>}>, shape="record", style="filled"];
88
"pylint.checkers.exceptions.ExceptionsChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker1", style="solid"];
99
"pylint.checkers.stdlib.StdlibChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker3", style="solid"];
1010
"pylint.extensions.check_elif.ElseifUsedChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker2", style="solid"];

tests/pyreverse/test_diadefs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def test_known_values1(HANDLER: DiadefsHandler, PROJECT: Project) -> None:
183183
assert modules == [
184184
(True, "data"),
185185
(True, "data.clientmodule_test"),
186+
(True, "data.nullable_pattern"),
186187
(True, "data.property_pattern"),
187188
(True, "data.suppliermodule_test"),
188189
]
@@ -196,6 +197,7 @@ def test_known_values1(HANDLER: DiadefsHandler, PROJECT: Project) -> None:
196197
(True, "DoNothing2"),
197198
(True, "DoSomething"),
198199
(True, "Interface"),
200+
(True, "NullablePatterns"),
199201
(True, "PropertyPatterns"),
200202
(True, "Specialization"),
201203
]

tests/pyreverse/test_inspector.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def test_project_node(project: Project) -> None:
7373
expected = [
7474
"data",
7575
"data.clientmodule_test",
76+
"data.nullable_pattern",
7677
"data.property_pattern",
7778
"data.suppliermodule_test",
7879
]

0 commit comments

Comments
 (0)