Skip to content

Added escaping of vertical bar character in annotation labels #8610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/8603.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.

Closes #8603
10 changes: 9 additions & 1 deletion pylint/pyreverse/dot_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,19 @@ def _build_label_for_node(self, properties: NodeProperties) -> str:
)
label += rf"{method_name}({', '.join(args)})"
if func.returns:
label += ": " + get_annotation_label(func.returns)
annotation_label = get_annotation_label(func.returns)
label += ": " + self._escape_annotation_label(annotation_label)
label += rf"{HTMLLabels.LINEBREAK_LEFT.value}"
label += "}"
return label

def _escape_annotation_label(self, annotation_label: str) -> str:
# Escape vertical bar characters to make them appear as a literal characters
# otherwise it gets treated as field separator of record-based nodes
annotation_label = annotation_label.replace("|", r"\|")

return annotation_label

def emit_edge(
self,
from_node: str,
Expand Down
10 changes: 10 additions & 0 deletions tests/data/nullable_pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
""" docstring for file nullable_pattern.py """
from typing import Optional

class NullablePatterns:
def return_nullable_1(self) -> int | None:
""" Nullable return type using the | operator as mentioned in PEP 604, see https://peps.python.org/pep-0604 """
pass

def return_nullable_2(self) -> Optional[int]:
pass
1 change: 1 addition & 0 deletions tests/pyreverse/data/classes_No_Name.dot
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ charset="utf-8"
"data.suppliermodule_test.DoNothing2" [color="black", fontcolor="black", label=<{DoNothing2|<br ALIGN="LEFT"/>|}>, shape="record", style="solid"];
"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"];
"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"];
"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"];
"data.property_pattern.PropertyPatterns" [color="black", fontcolor="black", label=<{PropertyPatterns|prop1<br ALIGN="LEFT"/>prop2<br ALIGN="LEFT"/>|}>, shape="record", style="solid"];
"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"];
"data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"];
Expand Down
6 changes: 5 additions & 1 deletion tests/pyreverse/data/classes_No_Name.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
get_value()*
set_value(value)*
}
class NullablePatterns {
return_nullable_1()* int | None
return_nullable_2()* Optional[int]
}
class PropertyPatterns {
prop1
prop2
Expand All @@ -43,7 +47,7 @@
DoNothing --* Ancestor : cls_member
DoNothing --* Specialization : relation
DoNothing2 --o Specialization : relation2

</div>
</body>
</html>
4 changes: 4 additions & 0 deletions tests/pyreverse/data/classes_No_Name.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ classDiagram
get_value()*
set_value(value)*
}
class NullablePatterns {
return_nullable_1()* int | None
return_nullable_2()* Optional[int]
}
class PropertyPatterns {
prop1
prop2
Expand Down
4 changes: 4 additions & 0 deletions tests/pyreverse/data/classes_No_Name.puml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class "Interface" as data.suppliermodule_test.Interface {
{abstract}get_value()
{abstract}set_value(value)
}
class "NullablePatterns" as data.nullable_pattern.NullablePatterns {
{abstract}return_nullable_1() -> int | None
{abstract}return_nullable_2() -> Optional[int]
}
class "PropertyPatterns" as data.property_pattern.PropertyPatterns {
prop1
prop2
Expand Down
1 change: 1 addition & 0 deletions tests/pyreverse/data/classes_colorized.dot
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ charset="utf-8"
"data.suppliermodule_test.DoNothing2" [color="#77AADD", fontcolor="black", label=<{DoNothing2|<br ALIGN="LEFT"/>|}>, shape="record", style="filled"];
"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"];
"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"];
"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"];
"data.property_pattern.PropertyPatterns" [color="#77AADD", fontcolor="black", label=<{PropertyPatterns|prop1<br ALIGN="LEFT"/>prop2<br ALIGN="LEFT"/>|}>, shape="record", style="filled"];
"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"];
"data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"];
Expand Down
4 changes: 4 additions & 0 deletions tests/pyreverse/data/classes_colorized.puml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class "Interface" as data.suppliermodule_test.Interface #77AADD {
{abstract}get_value()
{abstract}set_value(value)
}
class "NullablePatterns" as data.nullable_pattern.NullablePatterns #77AADD {
{abstract}return_nullable_1() -> int | None
{abstract}return_nullable_2() -> Optional[int]
}
class "PropertyPatterns" as data.property_pattern.PropertyPatterns #77AADD {
prop1
prop2
Expand Down
1 change: 1 addition & 0 deletions tests/pyreverse/data/packages_No_Name.dot
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ rankdir=BT
charset="utf-8"
"data" [color="black", label=<data>, shape="box", style="solid"];
"data.clientmodule_test" [color="black", label=<data.clientmodule_test>, shape="box", style="solid"];
"data.nullable_pattern" [color="black", label=<data.nullable_pattern>, shape="box", style="solid"];
"data.property_pattern" [color="black", label=<data.property_pattern>, shape="box", style="solid"];
"data.suppliermodule_test" [color="black", label=<data.suppliermodule_test>, shape="box", style="solid"];
"data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"];
Expand Down
4 changes: 3 additions & 1 deletion tests/pyreverse/data/packages_No_Name.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
}
class clientmodule_test {
}
class nullable_pattern {
}
class property_pattern {
}
class suppliermodule_test {
}
clientmodule_test --> suppliermodule_test

</div>
</body>
</html>
2 changes: 2 additions & 0 deletions tests/pyreverse/data/packages_No_Name.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ classDiagram
}
class clientmodule_test {
}
class nullable_pattern {
}
class property_pattern {
}
class suppliermodule_test {
Expand Down
6 changes: 2 additions & 4 deletions tests/pyreverse/data/packages_No_Name.puml
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
@startuml packages_No_Name
set namespaceSeparator none
package "data" as data {

}
package "data.clientmodule_test" as data.clientmodule_test {

}
package "data.nullable_pattern" as data.nullable_pattern {
}
package "data.property_pattern" as data.property_pattern {

}
package "data.suppliermodule_test" as data.suppliermodule_test {

}
data.clientmodule_test --> data.suppliermodule_test
@enduml
1 change: 1 addition & 0 deletions tests/pyreverse/data/packages_colorized.dot
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ rankdir=BT
charset="utf-8"
"data" [color="#77AADD", label=<data>, shape="box", style="filled"];
"data.clientmodule_test" [color="#77AADD", label=<data.clientmodule_test>, shape="box", style="filled"];
"data.nullable_pattern" [color="#77AADD", label=<data.nullable_pattern>, shape="box", style="filled"];
"data.property_pattern" [color="#77AADD", label=<data.property_pattern>, shape="box", style="filled"];
"data.suppliermodule_test" [color="#77AADD", label=<data.suppliermodule_test>, shape="box", style="filled"];
"data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"];
Expand Down
6 changes: 2 additions & 4 deletions tests/pyreverse/data/packages_colorized.puml
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
@startuml packages_colorized
set namespaceSeparator none
package "data" as data #77AADD {

}
package "data.clientmodule_test" as data.clientmodule_test #77AADD {

}
package "data.nullable_pattern" as data.nullable_pattern #77AADD {
}
package "data.property_pattern" as data.property_pattern #77AADD {

}
package "data.suppliermodule_test" as data.suppliermodule_test #77AADD {

}
data.clientmodule_test --> data.suppliermodule_test
@enduml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ charset="utf-8"
"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"];
"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"];
"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"];
"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"];
"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"];
"pylint.checkers.exceptions.ExceptionsChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker1", style="solid"];
"pylint.checkers.stdlib.StdlibChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker3", style="solid"];
"pylint.extensions.check_elif.ElseifUsedChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker2", style="solid"];
Expand Down
2 changes: 2 additions & 0 deletions tests/pyreverse/test_diadefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ def test_known_values1(HANDLER: DiadefsHandler, PROJECT: Project) -> None:
assert modules == [
(True, "data"),
(True, "data.clientmodule_test"),
(True, "data.nullable_pattern"),
(True, "data.property_pattern"),
(True, "data.suppliermodule_test"),
]
Expand All @@ -196,6 +197,7 @@ def test_known_values1(HANDLER: DiadefsHandler, PROJECT: Project) -> None:
(True, "DoNothing2"),
(True, "DoSomething"),
(True, "Interface"),
(True, "NullablePatterns"),
(True, "PropertyPatterns"),
(True, "Specialization"),
]
Expand Down
1 change: 1 addition & 0 deletions tests/pyreverse/test_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def test_project_node(project: Project) -> None:
expected = [
"data",
"data.clientmodule_test",
"data.nullable_pattern",
"data.property_pattern",
"data.suppliermodule_test",
]
Expand Down