Skip to content
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

Pyreverse autocolor #4521

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Release date: TBA
..
Put new features and bugfixes here and also in 'doc/whatsnew/2.9.rst'

* Added ``--colorized`` option to ``pyreverse`` to visualize modules/packages of the same parent package.

Closes #4488

* Added ``deprecated-decorator``: Emitted when deprecated decorator is used.

Closes #4429
Expand Down
2 changes: 2 additions & 0 deletions doc/whatsnew/2.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ Other Changes

* ``ignore-paths`` configuration directive has been added. Defined regex patterns are matched against file path.

* ``pyreverse`` now supports automatic coloring of package and class diagrams for .dot files by passing the ``--colorized`` option.

* Added handling of floating point values when parsing configuration from pyproject.toml
69 changes: 44 additions & 25 deletions pylint/pyreverse/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Copyright (c) 2020 Peter Kolbus <peter.kolbus@gmail.com>
# Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2021 Mark Byrne <mbyrnepr2@gmail.com>
# Copyright (c) 2021 Andreas Finkler <andi.finkler@gmail.com>

# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/LICENSE
Expand Down Expand Up @@ -123,8 +124,7 @@
short="k",
action="store_true",
default=False,
help="don't show attributes and methods in the class boxes; \
this disables -f values",
help="don't show attributes and methods in the class boxes; this disables -f values",
),
),
(
Expand All @@ -138,37 +138,56 @@
help="create a *.<format> output file if format available.",
),
),
(
"colorized",
dict(
dest="colorized",
action="store_true",
default=False,
help="Use colored output. Classes/modules of the same package get the same color.",
),
),
(
"max-color-depth",
dict(
dest="max_color_depth",
action="store",
default=2,
metavar="<depth>",
type="int",
help="Use separate colors up to package depth of <depth>",
),
),
(
"ignore",
{
"type": "csv",
"metavar": "<file[,file...]>",
"dest": "ignore_list",
"default": ("CVS",),
"help": "Files or directories to be skipped. They "
"should be base names, not paths.",
},
dict(
type="csv",
metavar="<file[,file...]>",
dest="ignore_list",
default=("CVS",),
help="Files or directories to be skipped. They should be base names, not paths.",
),
),
(
"project",
{
"default": "",
"type": "string",
"short": "p",
"metavar": "<project name>",
"help": "set the project name.",
},
dict(
default="",
type="string",
short="p",
metavar="<project name>",
help="set the project name.",
),
),
(
"output-directory",
{
"default": "",
"type": "string",
"short": "d",
"action": "store",
"metavar": "<output_directory>",
"help": "set the output directory path.",
},
dict(
default="",
type="string",
short="d",
action="store",
metavar="<output_directory>",
help="set the output directory path.",
),
),
)

Expand Down
92 changes: 84 additions & 8 deletions pylint/pyreverse/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

"""Utilities for creating VCG and Dot diagrams"""

import itertools
import os

import astroid
from astroid import modutils

from pylint.graph import DotBackend
from pylint.pyreverse.utils import is_exception
from pylint.pyreverse.vcgutils import VCGPrinter
Expand Down Expand Up @@ -49,7 +53,7 @@ def write_packages(self, diagram):
"""write a package diagram"""
# sorted to get predictable (hence testable) results
for i, obj in enumerate(sorted(diagram.modules(), key=lambda x: x.title)):
self.printer.emit_node(i, label=self.get_title(obj), shape="box")
self.printer.emit_node(i, **self.get_package_properties(obj))
obj.fig_id = i
# package dependencies
for rel in diagram.get_relationships("depends"):
Expand All @@ -61,7 +65,7 @@ def write_classes(self, diagram):
"""write a class diagram"""
# sorted to get predictable (hence testable) results
for i, obj in enumerate(sorted(diagram.objects, key=lambda x: x.title)):
self.printer.emit_node(i, **self.get_values(obj))
self.printer.emit_node(i, **self.get_class_properties(obj))
obj.fig_id = i
# inheritance links
for rel in diagram.get_relationships("specialization"):
Expand Down Expand Up @@ -90,7 +94,11 @@ def get_title(self, obj):
"""get project title"""
raise NotImplementedError

def get_values(self, obj):
def get_package_properties(self, obj):
"""get label and shape for packages."""
raise NotImplementedError

def get_class_properties(self, obj):
"""get label and shape for classes."""
raise NotImplementedError

Expand All @@ -111,6 +119,28 @@ def __init__(self, config):
fontcolor="green", arrowtail="none", arrowhead="diamond", style="solid"
),
]
self.available_colors = itertools.cycle(
[
"aliceblue",
"antiquewhite",
"aquamarine",
"burlywood",
"cadetblue",
"chartreuse",
"chocolate",
"coral",
"cornflowerblue",
"cyan",
"darkgoldenrod",
"darkseagreen",
"dodgerblue",
"forestgreen",
"gold",
"hotpink",
"mediumspringgreen",
]
)
self.used_colors = {}
DiagramWriter.__init__(self, config, styles)

def set_printer(self, file_name, basename):
Expand All @@ -123,7 +153,41 @@ def get_title(self, obj):
"""get project title"""
return obj.title

def get_values(self, obj):
def get_style(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def get_style(self):
def get_style(self) -> str:

Could you add typing on new functions please ? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

"""get style of object"""
if not self.config.colorized:
return "solid"
return "filled"

def get_color(self, obj):
"""get shape color"""
if not self.config.colorized:
return "black"
qualified_name = obj.node.qname()
if modutils.is_standard_module(qualified_name.split(".", maxsplit=1)[0]):
return "grey"
depth = self.config.max_color_depth
if isinstance(obj.node, astroid.ClassDef):
package = qualified_name.rsplit(".", maxsplit=2)[0]
elif obj.node.package:
package = qualified_name
else:
package = qualified_name.rsplit(".", maxsplit=1)[0]
base_name = ".".join(package.split(".", depth)[:depth])
if base_name not in self.used_colors:
self.used_colors[base_name] = next(self.available_colors)
return self.used_colors[base_name]

def get_package_properties(self, obj):
"""get label and shape for packages."""
return dict(
label=self.get_title(obj),
shape="box",
color=self.get_color(obj),
style=self.get_style(),
)

def get_class_properties(self, obj):
"""get label and shape for classes.

The label contains all attributes and methods
Expand All @@ -140,9 +204,14 @@ def get_values(self, obj):
args = []
label = r"{}{}({})\l".format(label, func.name, ", ".join(args))
label = "{%s}" % label
if is_exception(obj.node):
return dict(fontcolor="red", label=label, shape="record")
return dict(label=label, shape="record")
values = dict(
label=label,
shape="record",
fontcolor="red" if is_exception(obj.node) else "black",
style=self.get_style(),
color=self.get_color(obj),
)
return values

def close_graph(self):
"""print the dot graph into <file_name>"""
Expand Down Expand Up @@ -184,7 +253,14 @@ def get_title(self, obj):
"""get project title in vcg format"""
return r"\fb%s\fn" % obj.title

def get_values(self, obj):
def get_package_properties(self, obj):
"""get label and shape for packages."""
return dict(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to use NamedTuple (or a child class) here, so we can have proper typing and not use dict everywhere.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that a NamedTuple would be the cleaner solution. However, the current (as in: this PR) implementation of the DiagramWriter class and implementations of DotBackend and VCGPrinter do not allow for this. DotBackend and VCGPrinter rely on the data returned by the get_package_properties and get_class_properties methods, and the data structures they require are not the same.

That being said: I have already started working on the PlantUML backend, and while doing so I extracted a common interface for the Printer classes. Using a NamedTuple should be possible then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your other comment, it your implementation of plantuml make you upgrade the code with NamedTuple and proper typing it will be nice to rebase on it once it's merged.

label=self.get_title(obj),
shape="box",
)

def get_class_properties(self, obj):
"""get label and shape for classes.

The label contains all attributes and methods
Expand Down
8 changes: 4 additions & 4 deletions tests/data/classes_No_Name.dot
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
digraph "classes_No_Name" {
charset="utf-8"
rankdir=BT
"0" [label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record"];
"1" [label="{DoNothing|\l|}", shape="record"];
"2" [label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record"];
"3" [label="{Specialization|TYPE : str\lrelation\ltop : str\l|}", shape="record"];
"0" [color="black", fontcolor="black", label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record", style="solid"];
"1" [color="black", fontcolor="black", label="{DoNothing|\l|}", shape="record", style="solid"];
"2" [color="black", fontcolor="black", label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record", style="solid"];
"3" [color="black", fontcolor="black", label="{Specialization|TYPE : str\lrelation\ltop : str\l|}", shape="record", style="solid"];
"3" -> "0" [arrowhead="empty", arrowtail="none"];
"0" -> "2" [arrowhead="empty", arrowtail="node", style="dashed"];
"1" -> "0" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"];
Expand Down
12 changes: 12 additions & 0 deletions tests/data/classes_colorized.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
digraph "classes_colorized" {
charset="utf-8"
rankdir=BT
"0" [color="aliceblue", fontcolor="black", label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record", style="filled"];
"1" [color="aliceblue", fontcolor="black", label="{DoNothing|\l|}", shape="record", style="filled"];
"2" [color="aliceblue", fontcolor="black", label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record", style="filled"];
"3" [color="aliceblue", fontcolor="black", label="{Specialization|TYPE : str\lrelation\ltop : str\l|}", shape="record", style="filled"];
"3" -> "0" [arrowhead="empty", arrowtail="none"];
"0" -> "2" [arrowhead="empty", arrowtail="node", style="dashed"];
"1" -> "0" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"];
"1" -> "3" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"];
}
38 changes: 38 additions & 0 deletions tests/data/classes_vcg.vcg
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
graph:{
title:"classes_vcg"
layoutalgorithm:dfs
late_edge_labels:yes
port_sharing:no
manhattan_edges:yes
node: {title:"0" label:"\fbAncestor\fn\n\f____________\n\f08attr : str\n\f08cls_member\n\f____________\n\f10get_value()\n\f10set_value()"
shape:box
}
node: {title:"1" label:"\fbDoNothing\fn\n\f___________"
shape:box
}
node: {title:"2" label:"\fbInterface\fn\n\f___________\n\f10get_value()\n\f10set_value()"
shape:box
}
node: {title:"3" label:"\fbSpecialization\fn\n\f________________\n\f08TYPE : str\n\f08relation\n\f08top : str\n\f________________"
shape:box
}
edge: {sourcename:"3" targetname:"0" arrowstyle:solid
backarrowstyle:none
backarrowsize:10
}
edge: {sourcename:"0" targetname:"2" arrowstyle:solid
backarrowstyle:none
linestyle:dotted
backarrowsize:10
}
edge: {sourcename:"1" targetname:"0" label:"cls_member"
arrowstyle:solid
backarrowstyle:none
textcolor:green
}
edge: {sourcename:"1" targetname:"3" label:"relation"
arrowstyle:solid
backarrowstyle:none
textcolor:green
}
}
6 changes: 3 additions & 3 deletions tests/data/packages_No_Name.dot
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
digraph "packages_No_Name" {
charset="utf-8"
rankdir=BT
"0" [label="data", shape="box"];
"1" [label="data.clientmodule_test", shape="box"];
"2" [label="data.suppliermodule_test", shape="box"];
"0" [color="black", label="data", shape="box", style="solid"];
"1" [color="black", label="data.clientmodule_test", shape="box", style="solid"];
"2" [color="black", label="data.suppliermodule_test", shape="box", style="solid"];
"1" -> "2" [arrowhead="open", arrowtail="none"];
}
8 changes: 8 additions & 0 deletions tests/data/packages_colorized.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
digraph "packages_colorized" {
charset="utf-8"
rankdir=BT
"0" [color="aliceblue", label="data", shape="box", style="filled"];
"1" [color="aliceblue", label="data.clientmodule_test", shape="box", style="filled"];
"2" [color="aliceblue", label="data.suppliermodule_test", shape="box", style="filled"];
"1" -> "2" [arrowhead="open", arrowtail="none"];
}
20 changes: 20 additions & 0 deletions tests/data/packages_vcg.vcg
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
graph:{
title:"packages_vcg"
layoutalgorithm:dfs
late_edge_labels:yes
port_sharing:no
manhattan_edges:yes
node: {title:"0" label:"\fbdata\fn"
shape:box
}
node: {title:"1" label:"\fbdata.clientmodule_test\fn"
shape:box
}
node: {title:"2" label:"\fbdata.suppliermodule_test\fn"
shape:box
}
edge: {sourcename:"1" targetname:"2" arrowstyle:solid
backarrowstyle:none
backarrowsize:0
}
}