summaryrefslogtreecommitdiff
path: root/pylint/pyreverse/printer.py
blob: 7d4236026db2fa501853be38824e714d871a5e06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.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/main/LICENSE

"""
Base class defining the interface for a printer.
"""
from abc import ABC, abstractmethod
from enum import Enum
from typing import List, NamedTuple, Optional

from astroid import nodes

from pylint.pyreverse.utils import get_annotation_label


class NodeType(Enum):
    CLASS = "class"
    INTERFACE = "interface"
    PACKAGE = "package"


class EdgeType(Enum):
    INHERITS = "inherits"
    IMPLEMENTS = "implements"
    ASSOCIATION = "association"
    USES = "uses"


class Layout(Enum):
    LEFT_TO_RIGHT = "LR"
    RIGHT_TO_LEFT = "RL"
    TOP_TO_BOTTOM = "TB"
    BOTTOM_TO_TOP = "BT"


class NodeProperties(NamedTuple):
    label: str
    attrs: Optional[List[str]] = None
    methods: Optional[List[nodes.FunctionDef]] = None
    color: Optional[str] = None
    fontcolor: Optional[str] = None


class Printer(ABC):
    """Base class defining the interface for a printer"""

    def __init__(
        self,
        title: str,
        layout: Optional[Layout] = None,
        use_automatic_namespace: Optional[bool] = None,
    ):
        self.title: str = title
        self.layout = layout
        self.use_automatic_namespace = use_automatic_namespace
        self.lines: List[str] = []
        self._indent = ""
        self._open_graph()

    def _inc_indent(self):
        """increment indentation"""
        self._indent += "  "

    def _dec_indent(self):
        """decrement indentation"""
        self._indent = self._indent[:-2]

    @abstractmethod
    def _open_graph(self) -> None:
        """Emit the header lines, i.e. all boilerplate code that defines things like layout etc."""

    def emit(self, line: str, force_newline: Optional[bool] = True) -> None:
        if force_newline and not line.endswith("\n"):
            line += "\n"
        self.lines.append(self._indent + line)

    @abstractmethod
    def emit_node(
        self,
        name: str,
        type_: NodeType,
        properties: Optional[NodeProperties] = None,
    ) -> None:
        """Create a new node. Nodes can be classes, packages, participants etc."""

    @abstractmethod
    def emit_edge(
        self,
        from_node: str,
        to_node: str,
        type_: EdgeType,
        label: Optional[str] = None,
    ) -> None:
        """Create an edge from one node to another to display relationships."""

    @staticmethod
    def _get_method_arguments(method: nodes.FunctionDef) -> List[str]:
        if method.args.args:
            arguments: List[nodes.AssignName] = [
                arg for arg in method.args.args if arg.name != "self"
            ]
        else:
            arguments = []

        annotations = dict(zip(arguments, method.args.annotations[1:]))
        for arg in arguments:
            annotation_label = ""
            ann = annotations.get(arg)
            if ann:
                annotation_label = get_annotation_label(ann)
            annotations[arg] = annotation_label

        return [
            f"{arg.name}: {ann}" if ann else f"{arg.name}"
            for arg, ann in annotations.items()
        ]

    def generate(self, outputfile: str) -> None:
        """Generate and save the final outputfile."""
        self._close_graph()
        with open(outputfile, "w", encoding="utf-8") as outfile:
            outfile.writelines(self.lines)

    @abstractmethod
    def _close_graph(self) -> None:
        """Emit the lines needed to properly close the graph."""