Completed
Pull Request — master (#45)
by Oleg
02:04
created

NodeStore   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 453
Duplicated Lines 0 %

Test Coverage

Coverage 89.13%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 53
c 4
b 0
f 0
dl 0
loc 453
ccs 123
cts 138
cp 0.8913
rs 7.4757

23 Methods

Rating   Name   Duplication   Size   Complexity  
A get_enumerated_items() 0 9 1
A register_formatter() 0 13 2
A number_numerable() 0 5 1
B __init__() 0 46 1
A append_inline_node() 0 18 1
A create_formatter() 0 21 3
A get_formatter() 0 11 1
F _append_to_content_tree() 0 38 10
A generate() 0 13 1
A generate_toc() 0 7 3
B end_block_node() 0 24 5
A out_scope() 0 7 1
A prepare_content_tree() 0 6 1
B create_inline_node() 0 30 2
A append_block_node() 0 17 1
A store_node() 0 17 2
A error() 0 17 2
A in_scope() 0 9 1
F _adjust_hierarchy() 0 35 9
A register_block_command() 0 9 1
A parse_labels() 0 6 1
A register_inline_command() 0 9 1
B create_block_node() 0 27 2

How to fix   Complexity   

Complex Class

Complex classes like NodeStore 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
"""
2
SDoc
3
4
Copyright 2016 Set Based IT Consultancy
5
6
Licence MIT
7
"""
8
# ----------------------------------------------------------------------------------------------------------------------
9
10 1
inline_creators = {}
0 ignored issues
show
Coding Style Naming introduced by
The name inline_creators does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
11
"""
12
Map from inline commands to node creators.
13
14
:type: dict[str,callable]
15
"""
16
17 1
block_creators = {}
0 ignored issues
show
Coding Style Naming introduced by
The name block_creators does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
18
"""
19
Map from block commands to object creators.
20
21
:type: dict[str,callable]
22
"""
23
24 1
formatters = {}
0 ignored issues
show
Coding Style Naming introduced by
The name formatters does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
25
"""
26
Map from format name to map from inline and block commands to format creators.
27
28
:type: dict[str,dict[str,callable]]
29
"""
30
31
32 1
class NodeStore:
33
    """
34
    Class for creating, storing, and retrieving nodes.
35
36
    @todo Make abstract and implement other document store classes.
37
    """
38
39 1
    errors = 0
40
    """
41
    The error count.
42
43
    :type: int
44
    """
45
46
    # ------------------------------------------------------------------------------------------------------------------
47 1
    def __init__(self, io):
48
        """
49
        Object constructor.
50
        """
51
52 1
        self._io = io
53
        """
54
        Styled output formatter.
55
56
        :type: sdoc.style.SdocStyle.SdocStyle
57
        """
58
59 1
        self.format = 'html'
60
        """
61
        The output format.
62
63
        :type: str
64
        """
65
66 1
        self.nested_nodes = []
67
        """
68
        The stack of nested nodes (only filled when creating all nodes).
69
70
        :type: list[sdoc.sdoc2.node.Node.Node]
71
        """
72
73 1
        self.nodes = {}
74
        """
75
        The actual node store. Map from node ID to node.
76
77
        :type: dict[int,sdoc.sdoc2.node.Node.Node]
78
        """
79
80 1
        self._enumerable_numbers = {}
81
        """
82
        The current numbers of enumerable nodes (e.g. headings, figures).
83
84
        :type: dict[str,sdoc.sdoc2.helper.Enumerable.Enumerable]
85
        """
86
87 1
        self.labels = {}
88 1
        """
89
        The identifiers of labels which refers on each heading node.
90
91
        :type: dict[str,str]
92
        """
93
94
    # ------------------------------------------------------------------------------------------------------------------
95 1
    @staticmethod
96 1
    def error(message, node=None):
97
        """
98
        Logs an error.
99
100
        :param str message: The error message.this message will be appended with 'at filename:line.column' ot the token.
101
        :param sdoc.sdoc2.node.Node.Node node: The node where the error occurred.
102
        """
103 1
        NodeStore.errors += 1
104
105 1
        messages = [message]
106 1
        if node:
107 1
            filename = node.position.file_name
108 1
            line_number = node.position.start_line
109 1
            column_number = node.position.start_column + 1
110 1
            messages.append('Position: {0!s}:{1:d}.{2:d}'.format(filename, line_number, column_number))
111 1
        node.io.warning(messages)
112
113
    # ------------------------------------------------------------------------------------------------------------------
114 1
    @staticmethod
115
    def get_formatter(output_type, name_formatter):
116
        """
117
        Returns the formatter for special type.
118
119
        :param str output_type: The type of output formatter (e.g. 'html')
120
        :param str name_formatter: The name of formatter (e.g. 'smile')
121
122
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
123
        """
124
        return formatters[output_type][name_formatter]
125
126
    # ------------------------------------------------------------------------------------------------------------------
127 1
    def end_block_node(self, command):
128
        """
129
        Signals the end of a block command.
130
131
        :param string command: The name of the inline command.
132
        """
133
        # Pop none block command nodes from the stack.
134 1
        while self.nested_nodes and not self.nested_nodes[-1].is_block_command():
135 1
            self.nested_nodes.pop()
136
137 1
        if not self.nested_nodes:
138
            # @todo position
139
            raise RuntimeError("Unexpected \\end{{{0!s}}}.".format(command))
140
141
        # Get the last node on the block stack.
142 1
        node = self.nested_nodes[-1]
143
144 1
        if node.name != command:
145
            # @todo position \end
146
            # @todo position \begin
147
            raise RuntimeError("\\begin{{{0!s}}} and \\end{{{1!s}}} do not match.".format(node.name, command))
148
149
        # Pop the last node of the block stack.
150 1
        self.nested_nodes.pop()
151
152
    # ------------------------------------------------------------------------------------------------------------------
153 1
    def in_scope(self, node_id):
154
        """
155
        Retrieves a node based on its ID.
156
157
        :param int node_id: The node ID.
158
159
        :rtype: sdoc.sdoc2.node.Node.Node
160
        """
161
        return self.nodes[node_id]
162
163
    # ------------------------------------------------------------------------------------------------------------------
164 1
    def out_scope(self, node):
165
        """
166
        Marks a node as not longer in scope.
167
168
        :param sdoc.sdoc2.node.Node.Node node: The node.
169
        """
170 1
        pass
171
172
    # ------------------------------------------------------------------------------------------------------------------
173 1
    @staticmethod
174
    def register_inline_command(command, constructor):
175
        """
176
        Registers a node constructor for an inline command.
177
178
        :param str command: The name of the inline command.
179
        :param callable constructor: The node constructor.
180
        """
181 1
        inline_creators[command] = constructor
182
183
    # ------------------------------------------------------------------------------------------------------------------
184 1
    @staticmethod
185
    def register_formatter(command, output_format, formatter):
186
        """
187
        Registers a output formatter constructor for a command.
188
189
        :param str command: The name of the command.
190
        :param str output_format: The output format the formatter generates.
191
        :param callable formatter: The formatter for generating the content of the node in the output format.
192
        """
193 1
        if output_format not in formatters:
194 1
            formatters[output_format] = {}
195
196 1
        formatters[output_format][command] = formatter
197
198
    # ------------------------------------------------------------------------------------------------------------------
199 1
    @staticmethod
200
    def register_block_command(command, constructor):
201
        """
202
        Registers a node constructor for a block command.
203
204
        :param string command: The name of the inline command.
205
        :param callable constructor: The node constructor.
206
        """
207 1
        block_creators[command] = constructor
208
209
    # ------------------------------------------------------------------------------------------------------------------
210 1
    def create_inline_node(self, command, options=None, argument='', position=None):
211
        """
212
        Creates a node based an inline command.
213
214
        Note: The node is not stored nor appended to the content tree.
215
216
        :param str command: The inline command.
217
        :param dict options: The options.
218
        :param str argument: The argument of the inline command.
219
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
220
221
        :rtype: sdoc.sdoc2.node.Node.Node
222
        """
223 1
        if command not in inline_creators:
224
            # @todo set error status
225
            constructor = inline_creators['unknown']
226
            node = constructor(self._io, options, argument)
227
            node.name = command
228
229
        else:
230
            # Create the new node.
231 1
            constructor = inline_creators[command]
232 1
            node = constructor(self._io, options, argument)
233
234 1
        node.position = position
235
236
        # Store the node and assign ID.
237 1
        self.store_node(node)
238
239 1
        return node
240
241
    # ------------------------------------------------------------------------------------------------------------------
242 1
    def create_block_node(self, command, options, position=None):
243
        """
244
        Creates a node based on a block command.
245
246
        Note: The node is not appended to the content tree.
247
248
        :param str command: The inline command.
249
        :param dict[str,str] options: The options.
250
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
251
252
        :rtype: sdoc.sdoc2.node.Node.Node
253
        """
254 1
        if command not in block_creators:
255
            constructor = block_creators['unknown']
256
            # @todo set error status
257
258
        else:
259
            # Create the new node.
260 1
            constructor = block_creators[command]
261
262 1
        node = constructor(self._io, options)
263 1
        node.position = position
264
265
        # Store the node and assign ID.
266 1
        self.store_node(node)
267
268 1
        return node
269
270
    # ------------------------------------------------------------------------------------------------------------------
271 1
    def append_inline_node(self, command, options, argument, position):
272
        """
273
        Creates a node based an inline command and appends it to the end of the content tree.
274
275
        :param str command: The inline command.
276
        :param dict options: The options.
277
        :param str argument: The argument of the inline command.
278
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
279
280
        :rtype: sdoc.sdoc2.node.Node.Node
281
        """
282
        # Create the inline node.
283 1
        node = self.create_inline_node(command, options, argument, position)
284
285
        # Add the node to the node store.
286 1
        self._append_to_content_tree(node)
287
288 1
        return node
289
290
    # ------------------------------------------------------------------------------------------------------------------
291 1
    def append_block_node(self, command, options, position):
292
        """
293
        Creates a node based on a block command and appends it to the end of the content tree.
294
295
        :param str command: The inline command.
296
        :param dict options: The options.
297
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
298
299
        :rtype: sdoc.sdoc2.node.Node.Node
300
        """
301
        # Create the block node.
302 1
        node = self.create_block_node(command, options, position)
303
304
        # Add the node to the node store.
305 1
        self._append_to_content_tree(node)
306
307 1
        return node
308
309
    # ------------------------------------------------------------------------------------------------------------------
310 1
    def create_formatter(self, io, command, parent=None):
0 ignored issues
show
Coding Style Naming introduced by
The name io does not conform to the argument naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
311
        """
312
        Creates a formatter for generating the output of nodes in the requested output format.
313
314
        :param cleo.styles.output_style.OutputStyle io: The IO object.
315
        :param str command: The inline of block command.
316
        :param sdoc.sdoc2.formatter.Formatter.Formatter parent: The parent formatter.
317
318
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
319
        """
320 1
        if self.format not in formatters:
321
            raise RuntimeError("Unknown output format '{0!s}'.".format(self.format))
322
323 1
        if command not in formatters[self.format]:
324
            # @todo use default none decorator with warning
325
            raise RuntimeError("Unknown formatter '{0!s}' for format '{1!s}'.".format(command, self.format))
326
327 1
        constructor = formatters[self.format][command]
328 1
        formatter = constructor(io, parent)
329
330 1
        return formatter
331
332
    # ------------------------------------------------------------------------------------------------------------------
333 1
    def _adjust_hierarchy(self, node):
334
        """
335
        Adjust the hierarchy based on the hierarchy of a new node.
336
337
        :param sdoc.sdoc2.node.Node.Node node: The new node.
338
        """
339 1
        node_hierarchy_name = node.get_hierarchy_name()
340 1
        parent_found = False
341 1
        while self.nested_nodes and not parent_found:
342 1
            parent_node = self.nested_nodes[-1]
343 1
            parent_hierarchy_name = parent_node.get_hierarchy_name()
344 1
            if parent_hierarchy_name != node_hierarchy_name:
345 1
                if node.is_hierarchy_root():
346 1
                    parent_found = True
347
                else:
348
                    raise RuntimeError("Improper nesting of node '{0!s}' at {1!s} and node '{2!s}' at {3!s}."
349
                                       .format(parent_node.name, parent_node.position, node.name, node.position))
350
351 1
            if not parent_found:
352 1
                parent_hierarchy_level = parent_node.get_hierarchy_level()
353 1
                node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
354 1
                if parent_hierarchy_level >= node_hierarchy_level and len(self.nested_nodes) > 1:
355 1
                    self.nested_nodes.pop()
356
                else:
357 1
                    parent_found = True
358
359 1
        parent_node = self.nested_nodes[-1]
360 1
        parent_hierarchy_level = parent_node.get_hierarchy_level()
361 1
        node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
362
363 1
        if node_hierarchy_level - parent_hierarchy_level > 1:
364
            # @todo position
365 1
            self._io.warning(("improper nesting of levels:{0:d} at {1!s} and {2:d} at {3!s}.")
366
                             .format(parent_hierarchy_level, parent_node.position,
367
                                     node_hierarchy_level, node.position))
368
369
    # ------------------------------------------------------------------------------------------------------------------
370 1
    def store_node(self, node):
371
        """
372
        Stores a node. If the node was not stored before assigns an ID to this node, otherwise the node replaces the
373
        node stored under the same ID. Returns the ID if the node.
374
375
        :param sdoc.sdoc2.node.Node.Node node: The node.
376
377
        :rtype: int
378
        """
379 1
        if not node.id:
380
            # Add the node to the node store.
381 1
            node_id = len(self.nodes) + 1
382 1
            node.id = node_id
383
384 1
        self.nodes[node.id] = node
385
386 1
        return node.id
387
388
    # ------------------------------------------------------------------------------------------------------------------
389 1
    def _append_to_content_tree(self, node):
390
        """
391
        Appends the node at the proper nesting level at the end of the content tree.
392
393
        :param sdoc.sdoc2.node.Node.Node node: The node.
394
        """
395 1
        if node.id == 1:
396
            # The first node must be a document root.
397 1
            if not node.is_document_root():
398
                # @todo position of block node.
399
                raise RuntimeError("Node {0!s} is not a document root".format(node.name))
400
401 1
            self.nested_nodes.append(node)
402
403
        else:
404
            # All other nodes must not be a document root.
405 1
            if node.is_document_root():
406
                # @todo position of block node.
407
                raise RuntimeError("Unexpected {0!s}. Node is document root".format(node.name))
408
409
            # If the node is a part of a hierarchy adjust the nested nodes stack.
410 1
            if node.get_hierarchy_name():
411 1
                self._adjust_hierarchy(node)
412
413
            # Add the node to the list of child nodes of its parent node.
414 1
            if self.nested_nodes:
415 1
                parent_node = self.nested_nodes[-1]
416
417
                # Pop from stack if we have two list element nodes (e.g. item nodes) in a row.
418 1
                if node.is_list_element() and type(parent_node) == type(node):
419 1
                    self.nested_nodes.pop()
420 1
                    parent_node = self.nested_nodes[-1]
421
422 1
                parent_node.child_nodes.append(node.id)
423
424
            # Block commands and hierarchical nodes must be appended to the nested nodes.
425 1
            if node.is_block_command() or node.get_hierarchy_name():
426 1
                self.nested_nodes.append(node)
427
428
    # ------------------------------------------------------------------------------------------------------------------
429 1
    def prepare_content_tree(self):
430
        """
431
        Prepares after parsing at SDoc2 level the content tree for further processing.
432
        """
433
        # Currently, node with ID 1 is the document node. @todo Improve getting the document node.
434 1
        self.nodes[1].prepare_content_tree()
435
436
    # ------------------------------------------------------------------------------------------------------------------
437 1
    def number_numerable(self):
438
        """
439
        Numbers all numerable nodes such as chapters, sections, figures, and, items.
440
        """
441 1
        self.nodes[1].number(self._enumerable_numbers)
442
443
    # ------------------------------------------------------------------------------------------------------------------
444 1
    def generate_toc(self):
445
        """
446
        Checks if we have table of contents in document. If yes, we generate table of contents.
447
        """
448 1
        for key, node in self.nodes.items():
0 ignored issues
show
Unused Code introduced by
The variable key seems to be unused.
Loading history...
449 1
            if node.get_command() == 'toc':
450
                node.generate_toc()
451
452
    # ------------------------------------------------------------------------------------------------------------------
453 1
    @staticmethod
454
    def generate(target_format):
455
        """
456
        Generates the document.
457
458
        :param sdoc.format.Format.Format target_format: The format which will generate file.
459
        """
460
        # Start generating file using specific formatter and check the errors.
461 1
        format_errors = target_format.generate()
462
463 1
        NodeStore.errors += format_errors
464
465 1
        return NodeStore.errors
466
467
    # ------------------------------------------------------------------------------------------------------------------
468 1
    def get_enumerated_items(self):
469
        """
470
        Returns a list with tuples with command and number of enumerated nodes.
471
472
        This method is intended for unit test only.
473
474
        :rtype: list[(str,str)]
475
        """
476
        return self.nodes[1].get_enumerated_items()
477
478
    # ------------------------------------------------------------------------------------------------------------------
479 1
    def parse_labels(self):
480
        """
481
        Method for parsing labels, setting additional arguments to nodes, and removing label nodes from tree.
482
        """
483 1
        self.nodes[1].parse_labels()
484 1
        self.nodes[1].change_ref_argument()
485
486
# ----------------------------------------------------------------------------------------------------------------------
487