Completed
Push — master ( d0bff5...613540 )
by P.R.
02:06
created

NodeStore._append_to_content_tree()   F

Complexity

Conditions 10

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 10.1626

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 15
cts 17
cp 0.8824
rs 3.1304
c 0
b 0
f 0
cc 10
crap 10.1626

How to fix   Complexity   

Complexity

Complex classes like NodeStore._append_to_content_tree() 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 1
    _io = None
47
    """
48
    Styled output formatter.
49
50
    :type: None|sdoc.style.SdocStyle.SdocStyle
51
    """
52
53
    # ------------------------------------------------------------------------------------------------------------------
54 1
    def __init__(self, io):
55
        """
56
        Object constructor.
57
        """
58 1
        NodeStore._io = io
59
60 1
        self.format = 'html'
61
        """
62
        The output format.
63
64
        :type: str
65
        """
66
67 1
        self.nested_nodes = []
68
        """
69
        The stack of nested nodes (only filled when creating all nodes).
70
71
        :type: list[sdoc.sdoc2.node.Node.Node]
72
        """
73
74 1
        self.nodes = {}
75
        """
76
        The actual node store. Map from node ID to node.
77
78
        :type: dict[int,sdoc.sdoc2.node.Node.Node]
79
        """
80
81 1
        self._enumerable_numbers = {}
82
        """
83
        The current numbers of enumerable nodes (e.g. headings, figures).
84
85
        :type: dict[str,sdoc.sdoc2.helper.Enumerable.Enumerable]
86
        """
87
88 1
        self.labels = {}
89 1
        """
90
        The identifiers of labels which refers on each heading node.
91
92
        :type: dict[str,str]
93
        """
94
95
    # ------------------------------------------------------------------------------------------------------------------
96 1
    @staticmethod
97 1
    def error(message, node=None):
98
        """
99
        Logs an error.
100
101
        :param str message: The error message.this message will be appended with 'at filename:line.column' ot the token.
102
        :param sdoc.sdoc2.node.Node.Node node: The node where the error occurred.
103
        """
104 1
        NodeStore._errors += 1
105
106 1
        messages = [message]
107 1
        if node:
108 1
            filename = node.position.file_name
109 1
            line_number = node.position.start_line
110 1
            column_number = node.position.start_column + 1
111 1
            messages.append('Position: {0!s}:{1:d}.{2:d}'.format(filename, line_number, column_number))
112 1
        NodeStore._io.error(messages)
113
114
    # ------------------------------------------------------------------------------------------------------------------
115 1
    @staticmethod
116
    def get_formatter(output_type, name_formatter):
117
        """
118
        Returns the formatter for special type.
119
120
        :param str output_type: The type of output formatter (e.g. 'html')
121
        :param str name_formatter: The name of formatter (e.g. 'smile')
122
123
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
124
        """
125
        return formatters[output_type][name_formatter]
126
127
    # ------------------------------------------------------------------------------------------------------------------
128 1
    def end_block_node(self, command):
129
        """
130
        Signals the end of a block command.
131
132
        :param string command: The name of the inline command.
133
        """
134
        # Pop none block command nodes from the stack.
135 1
        while self.nested_nodes and not self.nested_nodes[-1].is_block_command():
136 1
            self.nested_nodes.pop()
137
138 1
        if not self.nested_nodes:
139
            # @todo position
140
            raise RuntimeError("Unexpected \\end{{{0!s}}}.".format(command))
141
142
        # Get the last node on the block stack.
143 1
        node = self.nested_nodes[-1]
144
145 1
        if node.name != command:
146
            # @todo position \end
147
            # @todo position \begin
148
            raise RuntimeError("\\begin{{{0!s}}} and \\end{{{1!s}}} do not match.".format(node.name, command))
149
150
        # Pop the last node of the block stack.
151 1
        self.nested_nodes.pop()
152
153
    # ------------------------------------------------------------------------------------------------------------------
154 1
    def in_scope(self, node_id):
155
        """
156
        Retrieves a node based on its ID.
157
158
        :param int node_id: The node ID.
159
160
        :rtype: sdoc.sdoc2.node.Node.Node
161
        """
162
        return self.nodes[node_id]
163
164
    # ------------------------------------------------------------------------------------------------------------------
165 1
    def out_scope(self, node):
166
        """
167
        Marks a node as not longer in scope.
168
169
        :param sdoc.sdoc2.node.Node.Node node: The node.
170
        """
171 1
        pass
172
173
    # ------------------------------------------------------------------------------------------------------------------
174 1
    @staticmethod
175
    def register_inline_command(command, constructor):
176
        """
177
        Registers a node constructor for an inline command.
178
179
        :param str command: The name of the inline command.
180
        :param callable constructor: The node constructor.
181
        """
182 1
        inline_creators[command] = constructor
183
184
    # ------------------------------------------------------------------------------------------------------------------
185 1
    @staticmethod
186
    def register_formatter(command, output_format, formatter):
187
        """
188
        Registers a output formatter constructor for a command.
189
190
        :param str command: The name of the command.
191
        :param str output_format: The output format the formatter generates.
192
        :param callable formatter: The formatter for generating the content of the node in the output format.
193
        """
194 1
        if output_format not in formatters:
195 1
            formatters[output_format] = {}
196
197 1
        formatters[output_format][command] = formatter
198
199
    # ------------------------------------------------------------------------------------------------------------------
200 1
    @staticmethod
201
    def register_block_command(command, constructor):
202
        """
203
        Registers a node constructor for a block command.
204
205
        :param string command: The name of the inline command.
206
        :param callable constructor: The node constructor.
207
        """
208 1
        block_creators[command] = constructor
209
210
    # ------------------------------------------------------------------------------------------------------------------
211 1
    def create_inline_node(self, command, options=None, argument='', position=None):
212
        """
213
        Creates a node based an inline command.
214
215
        Note: The node is not stored nor appended to the content tree.
216
217
        :param str command: The inline command.
218
        :param dict options: The options.
219
        :param str argument: The argument of the inline command.
220
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
221
222
        :rtype: sdoc.sdoc2.node.Node.Node
223
        """
224 1
        if command not in inline_creators:
225
            # @todo set error status
226 1
            constructor = inline_creators['unknown']
227 1
            node = constructor(self._io, options, argument)
228 1
            node.name = command
229
230
        else:
231
            # Create the new node.
232 1
            constructor = inline_creators[command]
233 1
            node = constructor(self._io, options, argument)
234
235 1
        node.position = position
236
237
        # Store the node and assign ID.
238 1
        self.store_node(node)
239
240 1
        return node
241
242
    # ------------------------------------------------------------------------------------------------------------------
243 1
    def create_block_node(self, command, options, position=None):
244
        """
245
        Creates a node based on a block command.
246
247
        Note: The node is not appended to the content tree.
248
249
        :param str command: The inline command.
250
        :param dict[str,str] options: The options.
251
        :param None|sdoc.sdoc2.Position.Position position: The position of the node definition.
252
253
        :rtype: sdoc.sdoc2.node.Node.Node
254
        """
255 1
        if command not in block_creators:
256
            constructor = block_creators['unknown']
257
            # @todo set error status
258
259
        else:
260
            # Create the new node.
261 1
            constructor = block_creators[command]
262
263 1
        node = constructor(self._io, options)
264 1
        node.position = position
265
266
        # Store the node and assign ID.
267 1
        self.store_node(node)
268
269 1
        return node
270
271
    # ------------------------------------------------------------------------------------------------------------------
272 1
    def append_inline_node(self, command, options, argument, position):
273
        """
274
        Creates a node based an inline command and appends it to the end of the content tree.
275
276
        :param str command: The inline command.
277
        :param dict options: The options.
278
        :param str argument: The argument of the inline command.
279
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
280
281
        :rtype: sdoc.sdoc2.node.Node.Node
282
        """
283
        # Create the inline node.
284 1
        node = self.create_inline_node(command, options, argument, position)
285
286
        # Add the node to the node store.
287 1
        self._append_to_content_tree(node)
288
289 1
        return node
290
291
    # ------------------------------------------------------------------------------------------------------------------
292 1
    def append_block_node(self, command, options, position):
293
        """
294
        Creates a node based on a block command and appends it to the end of the content tree.
295
296
        :param str command: The inline command.
297
        :param dict options: The options.
298
        :param sdoc.sdoc2.Position.Position position: The position of the node definition.
299
300
        :rtype: sdoc.sdoc2.node.Node.Node
301
        """
302
        # Create the block node.
303 1
        node = self.create_block_node(command, options, position)
304
305
        # Add the node to the node store.
306 1
        self._append_to_content_tree(node)
307
308 1
        return node
309
310
    # ------------------------------------------------------------------------------------------------------------------
311 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...
312
        """
313
        Creates a formatter for generating the output of nodes in the requested output format.
314
315
        :param cleo.styles.output_style.OutputStyle io: The IO object.
316
        :param str command: The inline of block command.
317
        :param sdoc.sdoc2.formatter.Formatter.Formatter parent: The parent formatter.
318
319
        :rtype: sdoc.sdoc2.formatter.Formatter.Formatter
320
        """
321 1
        if self.format not in formatters:
322
            raise RuntimeError("Unknown output format '{0!s}'.".format(self.format))
323
324 1
        if command not in formatters[self.format]:
325
            # @todo use default none decorator with warning
326
            raise RuntimeError("Unknown formatter '{0!s}' for format '{1!s}'.".format(command, self.format))
327
328 1
        constructor = formatters[self.format][command]
329 1
        formatter = constructor(io, parent)
330
331 1
        return formatter
332
333
    # ------------------------------------------------------------------------------------------------------------------
334 1
    def _adjust_hierarchy(self, node):
335
        """
336
        Adjust the hierarchy based on the hierarchy of a new node.
337
338
        :param sdoc.sdoc2.node.Node.Node node: The new node.
339
        """
340 1
        node_hierarchy_name = node.get_hierarchy_name()
341 1
        parent_found = False
342 1
        while self.nested_nodes and not parent_found:
343 1
            parent_node = self.nested_nodes[-1]
344 1
            parent_hierarchy_name = parent_node.get_hierarchy_name()
345 1
            if parent_hierarchy_name != node_hierarchy_name:
346 1
                if node.is_hierarchy_root():
347 1
                    parent_found = True
348
                else:
349
                    self.error("Improper nesting of node '{0!s}' at {1!s} and node '{2!s}' at {3!s}.".format(
350
                        parent_node.name, parent_node.position, node.name, node.position))
351
352 1
            if not parent_found:
353 1
                parent_hierarchy_level = parent_node.get_hierarchy_level()
354 1
                node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
355 1
                if parent_hierarchy_level >= node_hierarchy_level and len(self.nested_nodes) > 1:
356 1
                    self.nested_nodes.pop()
357
                else:
358 1
                    parent_found = True
359
360 1
        parent_node = self.nested_nodes[-1]
361 1
        parent_hierarchy_level = parent_node.get_hierarchy_level()
362 1
        node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level)
363
364 1
        if node_hierarchy_level - parent_hierarchy_level > 1:
365 1
            self.error("Improper nesting of levels:{0:d} at {1!s} and {2:d} at {3!s}.".format(
366
                parent_hierarchy_level, parent_node.position, node_hierarchy_level, node.position),
367
                node)
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 node in self.nodes.values():
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