sdoc.sdoc2.node.Node   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 360
Duplicated Lines 97.22 %

Test Coverage

Coverage 88.71%

Importance

Changes 0
Metric Value
wmc 46
eloc 127
dl 350
loc 360
ccs 110
cts 124
cp 0.8871
rs 8.72
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A Node.set_id_heading_node() 7 7 1
A Node.is_list_element() 5 5 1
A Node.number() 12 12 2
A Node.is_phrasing() 5 5 1
A Node.__init__() 46 46 2
A Node.change_ref_argument() 23 23 5
A Node.append_label_list_in_node_store() 19 19 4
A Node.is_document_root() 5 5 1
A Node.set_option_value() 8 8 1
A Node.append_child_node() 7 7 1
A Node.modify_label_list() 30 30 4
A Node.argument() 6 6 1
A Node.get_command() 6 6 1
A Node.parse_labels() 8 8 2
A Node.print_info() 14 14 2
A Node.prepare_content_tree() 10 10 2
A Node.is_inline_command() 6 6 1
A Node.get_hierarchy_level() 7 7 1
A Node.is_block_command() 6 6 1
A Node.is_hierarchy_root() 5 5 1
A Node._remove_child_nodes() 8 8 2
A Node.get_enumerated_items() 23 23 4
A Node.get_hierarchy_name() 5 5 1
A Node.get_option_value() 7 7 2
A Node.io() 6 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like sdoc.sdoc2.node.Node often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1 1
import abc
2 1
from typing import Any, Dict, List, Optional, Tuple, Union
3
4 1
from cleo.io.io import IO
5
6 1
from sdoc.sdoc2 import in_scope, node_store, out_scope
7 1
from sdoc.sdoc2.Position import Position
8
9
10 1 View Code Duplication
class Node(metaclass=abc.ABCMeta):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
11
    """
12
    Abstract class for SDoc2 nodes.
13
    """
14
15
    # ------------------------------------------------------------------------------------------------------------------
16 1
    def __init__(self, io: IO, name: str, options: Optional[Dict[str, str]] = None, argument: str = ''):
17
        """
18
        Object constructor.
19
20
        :param OutputStyle io: The IO object.
21
        :param str name: The (command) name of this node.
22
        :param dict[str,str] options: The options of this node.
23
        :param str argument: The argument of this node (inline commands only).
24
        """
25 1
        self._io: IO = io
26
        """
27
        The IO object.
28
        """
29
30 1
        self.id: int = 0
31
        """
32
        The ID of this SDoc2 node.
33
        """
34
35 1
        self.name: str = name
36
        """
37
        The (command) name of this node.
38
        """
39
40 1
        self._argument: str = argument
41
        """
42
        The argument of this node (inline commands only).
43
        """
44
45 1
        self._options: Dict[str, Any] = options if options else {}
46
        """
47
        The options of this node.
48
        """
49
50 1
        self.child_nodes: List[int] = []
51
        """
52
        The ID's of the SDoc2 child nodes of this SDoc2 node.
53
        """
54
55 1
        self.position: Optional[Position] = None
56
        """
57
        The position where this node is defined.
58
        """
59
60 1
        self.labels: List[int] = []
61 1
        """
62
        The list of labels in the node.
63
        """
64
65
    # ------------------------------------------------------------------------------------------------------------------
66 1
    @property
67 1
    def io(self) -> IO:
68
        """
69
        Getter for io.
70
        """
71 1
        return self._io
72
73
    # ------------------------------------------------------------------------------------------------------------------
74 1
    @property
75 1
    def argument(self) -> str:
76
        """
77
        Getter for argument.
78
        """
79 1
        return self._argument
80
81
    # ------------------------------------------------------------------------------------------------------------------
82 1
    @argument.setter
83 1
    def argument(self, new_argument: str) -> None:
84
        """
85
        Setter for argument.
86
87
        :param str new_argument: The new argument.
88
        """
89
        self._argument = new_argument
90
91
    # ------------------------------------------------------------------------------------------------------------------
92 1
    def print_info(self, level: int) -> None:
93
        """
94
        Temp function for development.
95
96
        :param int level: the level of block commands.
97
        """
98
        self.io.write_line("{0!s}{1:4d} {2!s}".format(' ' * 4 * level, self.id, self.name))
99
100
        for node_id in self.child_nodes:
101
            node = in_scope(node_id)
102
103
            node.print_info(level + 1)
104
105
            out_scope(node)
106
107
    # ------------------------------------------------------------------------------------------------------------------
108 1
    def _remove_child_nodes(self, node_list: List[int]) -> None:
109
        """
110
        Removes child nodes from list of child nodes of this node.
111
112
        :param list[int] node_list: The child nodes to be removed.
113
        """
114 1
        for node in node_list:
115 1
            self.child_nodes.remove(node)
116
117
    # ------------------------------------------------------------------------------------------------------------------
118 1
    def get_hierarchy_name(self) -> Optional[str]:
119
        """
120
        Returns the hierarchy name if this node is a part of a hierarchy. Otherwise, returns None.
121
        """
122 1
        return None
123
124
    # ------------------------------------------------------------------------------------------------------------------
125 1
    @abc.abstractmethod
126 1
    def get_command(self) -> str:
127
        """
128
        Returns command of this node.
129
        """
130
        raise NotImplementedError()
131
132
    # ------------------------------------------------------------------------------------------------------------------
133 1
    def get_hierarchy_level(self, parent_hierarchy_level: int = -1) -> int:
134
        """
135
        Returns the hierarchy level if this node is a part of a hierarchy.
136
137
        :param int parent_hierarchy_level: The hierarchy level of the parent node in the same hierarchy.
138
        """
139
        raise RuntimeError("This method MUST only be called when a node is a part of an hierarchy.")
140
141
    # ------------------------------------------------------------------------------------------------------------------
142 1
    def get_option_value(self, option_name: str) -> Any:
143
        """
144
        Returns the value of an option. Returns None if the option is not set.
145
146
        :param str option_name: The name of the option.
147
        """
148 1
        return self._options[option_name] if option_name in self._options else None
149
150
    # ------------------------------------------------------------------------------------------------------------------
151 1
    def set_option_value(self, option: str, value: str) -> None:
152
        """
153
        Sets value for option.
154
155
        :param str option: The name of an option
156
        :param str value: The value of an option
157
        """
158 1
        self._options[option] = value
159
160
    # ------------------------------------------------------------------------------------------------------------------
161 1
    @abc.abstractmethod
162 1
    def is_block_command(self) -> bool:
163
        """
164
        Returns True if this node is created by a block command. Otherwise, returns False.
165
        """
166
        raise NotImplementedError()
167
168
    # ------------------------------------------------------------------------------------------------------------------
169 1
    def is_document_root(self) -> bool:
170
        """
171
        Returns True if this node is a document root node. Otherwise, returns False.
172
        """
173 1
        return False
174
175
    # ------------------------------------------------------------------------------------------------------------------
176 1
    def is_hierarchy_root(self) -> bool:
177
        """
178
        Returns True if this node can be the root of a hierarchy. Otherwise, returns False.
179
        """
180
        return False
181
182
    # ------------------------------------------------------------------------------------------------------------------
183 1
    @abc.abstractmethod
184 1
    def is_inline_command(self) -> bool:
185
        """
186
        Returns True if this node is created by a inline command. Otherwise returns False.
187
        """
188
        raise NotImplementedError()
189
190
    # ------------------------------------------------------------------------------------------------------------------
191 1
    def is_phrasing(self) -> bool:
192
        """
193
        Returns True if this node is a phrasing node, i.e. is a part of a paragraph. Otherwise, returns False.
194
        """
195 1
        return False
196
197
    # ------------------------------------------------------------------------------------------------------------------
198 1
    def is_list_element(self) -> bool:
199
        """
200
        Returns True if this node is a list element, e.g. an item in itemize. Otherwise, returns False.
201
        """
202 1
        return False
203
204
    # ------------------------------------------------------------------------------------------------------------------
205 1
    def append_child_node(self, child_node) -> None:
206
        """
207
        Appends a child node to the list of child nodes of the node.
208
209
        :param sdoc.sdoc2.node.Node.Node child_node: The new child node
210
        """
211 1
        self.child_nodes.append(child_node.id)
212
213
    # ------------------------------------------------------------------------------------------------------------------
214 1
    def prepare_content_tree(self) -> None:
215
        """
216
        Prepares this node for further processing.
217
        """
218 1
        for node_id in self.child_nodes:
219 1
            node = in_scope(node_id)
220
221 1
            node.prepare_content_tree()
222
223 1
            out_scope(node)
224
225
    # ------------------------------------------------------------------------------------------------------------------
226 1
    def number(self, numbers: Dict[str, str]) -> None:
227
        """
228
        Numbers all numerable nodes such as chapters, sections, figures, and, items.
229
230
        :param dict[str,str] numbers: The current numbers.
231
        """
232 1
        for node_id in self.child_nodes:
233 1
            node = in_scope(node_id)
234
235 1
            node.number(numbers)
236
237 1
            out_scope(node)
238
239
    # ------------------------------------------------------------------------------------------------------------------
240 1
    def get_enumerated_items(self) -> List[Union[Tuple[str, str, str], List[Tuple[str, str]]]]:
241
        """
242
        Returns a list with a tuple with command and number of enumerated child nodes.
243
244
        This method is intended for unit test only.
245
        """
246 1
        items = list()
247
248
        # First append the enumeration of this node (if any).
249 1
        if 'number' in self._options:
250 1
            items.append((self.get_command(), self._options['number'], self._argument))
251
252
        # Second append the enumeration of child nodes (if any).
253 1
        for node_id in self.child_nodes:
254 1
            node = in_scope(node_id)
255
256 1
            tmp = node.get_enumerated_items()
257 1
            if tmp:
258 1
                items.append(tmp)
259
260 1
            out_scope(node)
261
262 1
        return items
263
264
    # ------------------------------------------------------------------------------------------------------------------
265 1
    def parse_labels(self) -> None:
266
        """
267
        Parses all labels and call methods to collect labels.
268
        """
269 1
        self.modify_label_list()
270
271 1
        if self.labels:
272 1
            self.set_id_heading_node()
273
274
    # ------------------------------------------------------------------------------------------------------------------
275 1
    def modify_label_list(self) -> None:
276
        """
277
        Creates label list for each heading node, and for node_store. Removes label nodes from child list.
278
        """
279 1
        obsolete_node_ids = []
280 1
        for node_id in self.child_nodes:
281 1
            node = in_scope(node_id)
282
283 1
            if node.get_command() == 'label':
284
                # Appending in Node labels list.
285 1
                self.labels.append(node.id)
286
287 1
                self.append_label_list_in_node_store(node)
288 1
                if self.get_option_value('number'):
289 1
                    label_arg = self.get_option_value('number')
290 1
                    title_attribute = self.argument
291
                else:
292
                    label_arg = self.argument
293
                    title_attribute = None
294
295 1
                node_store.labels[node.argument] = {'argument': label_arg, 'title': title_attribute}
296
297
                # Removing node from child nodes.
298 1
                obsolete_node_ids.append(node.id)
299
300 1
            node.parse_labels()
301
302 1
            out_scope(node)
303
304 1
        self._remove_child_nodes(obsolete_node_ids)
305
306
    # ------------------------------------------------------------------------------------------------------------------
307 1
    def append_label_list_in_node_store(self, node) -> None:
308
        """
309
        Appending in NodeStore labels list.
310
311
        :param sdoc.sdoc2.node.Node.Node node: The current node.
312
        """
313 1
        if node.argument not in node_store.labels:
314 1
            if self.argument:
315 1
                node_store.labels[node.argument] = self.argument
316
317
            else:
318
                if 'number' in self._options:
319
                    node_store.labels[node.argument] = self._options['number']
320
                else:
321
                    node_store.labels[node.argument] = node.argument
322
323
        else:
324
            # @todo log definitions of both labels
325
            raise NameError('Duplicate label', node.argument)
326
327
    # ------------------------------------------------------------------------------------------------------------------
328 1
    def set_id_heading_node(self) -> None:
329
        """
330
        Sets id to heading node. (Argument of first label)
331
        """
332 1
        node = in_scope(self.labels[0])
333 1
        self._options['id'] = node.argument
334 1
        out_scope(node)
335
336
    # ------------------------------------------------------------------------------------------------------------------
337 1
    def change_ref_argument(self) -> None:
338
        """
339
        Changes reference argument on number of depending on heading node.
340
        """
341 1
        for node_id in self.child_nodes:
342 1
            node = in_scope(node_id)
343
344 1
            if node.get_command() == 'ref':
345
346 1
                if node.argument in node_store.labels:
347 1
                    node.set_option_value('href', '#{0}'.format(node.argument))
348
349 1
                    if node_store.labels[node.argument]['title']:
350 1
                        node.set_option_value('title', node_store.labels[node.argument]['title'])
351
352 1
                    node.text = node_store.labels[node.argument]['argument']
353
354
                else:
355 1
                    node_store.error("Label '{}' not found".format(node.argument), node)
356
357 1
            node.change_ref_argument()
358
359 1
            out_scope(node)
360
361
# ----------------------------------------------------------------------------------------------------------------------
362