Total Complexity | 49 |
Total Lines | 412 |
Duplicated Lines | 0 % |
Complex classes like sdoc.sdoc2.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 | """ |
||
24 | class NodeStore: |
||
25 | """ |
||
26 | Class for creating, storing, and retrieving nodes. |
||
27 | |||
28 | @todo Make abstract and implement other document store classes. |
||
29 | """ |
||
30 | |||
31 | # ------------------------------------------------------------------------------------------------------------------ |
||
32 | def __init__(self): |
||
33 | """ |
||
34 | Object constructor. |
||
35 | """ |
||
36 | |||
37 | self.format = 'html' |
||
38 | """ |
||
39 | The output format. |
||
40 | |||
41 | :type: str |
||
42 | """ |
||
43 | |||
44 | self._formatters = {} |
||
45 | """ |
||
46 | Map from format name to map from inline and block commands to format creators. |
||
47 | |||
48 | :type: dict[str,dict[str,callable]] |
||
49 | """ |
||
50 | |||
51 | self.nested_nodes = [] |
||
52 | """ |
||
53 | The stack of nested nodes (only filled when creating all nodes). |
||
54 | |||
55 | :type: list[sdoc.sdoc2.node.Node.Node] |
||
56 | """ |
||
57 | |||
58 | self.nodes = {} |
||
59 | """ |
||
60 | The actual node store. Map from node ID to node. |
||
61 | |||
62 | :type: dict[int,sdoc.sdoc2.node.Node.Node] |
||
63 | """ |
||
64 | |||
65 | self._enumerable_numbers = {} |
||
66 | """ |
||
67 | The current numbers of enumerable nodes (e.g. headings, figures). |
||
68 | |||
69 | :type: dict[str,str] |
||
70 | """ |
||
71 | |||
72 | self.labels = {} |
||
73 | """ |
||
74 | The identifiers of labels which refers on each heading node. |
||
75 | |||
76 | :type: dict[str,str] |
||
77 | """ |
||
78 | |||
79 | # ------------------------------------------------------------------------------------------------------------------ |
||
80 | def end_block_node(self, command): |
||
81 | """ |
||
82 | Signals the end of a block command. |
||
83 | |||
84 | :param string command: The name of the inline command. |
||
85 | """ |
||
86 | # Pop none block command nodes from the stack. |
||
87 | while self.nested_nodes and not self.nested_nodes[-1].is_block_command(): |
||
88 | self.nested_nodes.pop() |
||
89 | |||
90 | if not self.nested_nodes: |
||
91 | # @todo position |
||
92 | raise RuntimeError("Unexpected \\end{{{0!s}}}.".format(command)) |
||
93 | |||
94 | # Get the last node on the block stack. |
||
95 | node = self.nested_nodes[-1] |
||
96 | |||
97 | if node.name != command: |
||
98 | # @todo position \end |
||
99 | # @todo position \begin |
||
100 | raise RuntimeError("\\begin{{{0!s}}} and \\end{{{1!s}}} do not match.".format(node.name, command)) |
||
101 | |||
102 | # Pop the last node of the block stack. |
||
103 | self.nested_nodes.pop() |
||
104 | |||
105 | # ------------------------------------------------------------------------------------------------------------------ |
||
106 | def in_scope(self, node_id): |
||
107 | """ |
||
108 | Retrieves a node based on its ID. |
||
109 | |||
110 | :param int node_id: The node ID. |
||
111 | |||
112 | :rtype: sdoc.sdoc2.node.Node.Node |
||
113 | """ |
||
114 | return self.nodes[node_id] |
||
115 | |||
116 | # ------------------------------------------------------------------------------------------------------------------ |
||
117 | def out_scope(self, node): |
||
118 | """ |
||
119 | Marks a node as not longer in scope. |
||
120 | |||
121 | :param sdoc.sdoc2.node.Node.Node node: The node. |
||
122 | """ |
||
123 | pass |
||
124 | |||
125 | # ------------------------------------------------------------------------------------------------------------------ |
||
126 | @staticmethod |
||
127 | def register_inline_command(command, constructor): |
||
128 | """ |
||
129 | Registers a node constructor for an inline command. |
||
130 | |||
131 | :param str command: The name of the inline command. |
||
132 | :param callable constructor: The node constructor. |
||
133 | """ |
||
134 | inline_creators[command] = constructor |
||
135 | |||
136 | # ------------------------------------------------------------------------------------------------------------------ |
||
137 | def register_formatter(self, command, output_format, formatter): |
||
138 | """ |
||
139 | Registers a output formatter constructor for a command. |
||
140 | |||
141 | :param str command: The name of the command. |
||
142 | :param str output_format: The output format the formatter generates. |
||
143 | :param callable formatter: The formatter for generating the content of the node in the output format. |
||
144 | """ |
||
145 | if output_format not in self._formatters: |
||
146 | self._formatters[output_format] = {} |
||
147 | |||
148 | self._formatters[output_format][command] = formatter |
||
149 | |||
150 | # ------------------------------------------------------------------------------------------------------------------ |
||
151 | @staticmethod |
||
152 | def register_block_command(command, constructor): |
||
153 | """ |
||
154 | Registers a node constructor for a block command. |
||
155 | |||
156 | :param string command: The name of the inline command. |
||
157 | :param callable constructor: The node constructor. |
||
158 | """ |
||
159 | block_creators[command] = constructor |
||
160 | |||
161 | # ------------------------------------------------------------------------------------------------------------------ |
||
162 | def create_inline_node(self, command, options=None, argument='', position=None): |
||
163 | """ |
||
164 | Creates a node based an inline command. |
||
165 | |||
166 | Note: The node is not stored nor appended to the content tree. |
||
167 | |||
168 | :param str command: The inline command. |
||
169 | :param dict options: The options. |
||
170 | :param str argument: The argument of the inline command. |
||
171 | :param None|sdoc.sdoc2.Position.Position position: The position of the node definition. |
||
172 | |||
173 | :rtype: sdoc.sdoc2.node.Node.Node |
||
174 | """ |
||
175 | if command not in inline_creators: |
||
176 | # @todo set error status |
||
177 | constructor = inline_creators['unknown'] |
||
178 | node = constructor(options, argument) |
||
179 | node.name = command |
||
180 | |||
181 | else: |
||
182 | # Create the new node. |
||
183 | constructor = inline_creators[command] |
||
184 | node = constructor(options, argument) |
||
185 | |||
186 | node.position = position |
||
187 | |||
188 | # Store the node and assign ID. |
||
189 | self.store_node(node) |
||
190 | |||
191 | return node |
||
192 | |||
193 | # ------------------------------------------------------------------------------------------------------------------ |
||
194 | def create_block_node(self, command, options, position=None): |
||
195 | """ |
||
196 | Creates a node based on a block command. |
||
197 | |||
198 | Note: The node is not appended to the content tree. |
||
199 | |||
200 | :param str command: The inline command. |
||
201 | :param dict[str,str] options: The options. |
||
202 | :param None|sdoc.sdoc2.Position.Position position: The position of the node definition. |
||
203 | |||
204 | :rtype: sdoc.sdoc2.node.Node.Node |
||
205 | """ |
||
206 | if command not in block_creators: |
||
207 | constructor = block_creators['unknown'] |
||
208 | # @todo set error status |
||
209 | |||
210 | else: |
||
211 | # Create the new node. |
||
212 | constructor = block_creators[command] |
||
213 | |||
214 | node = constructor(options) |
||
215 | node.position = position |
||
216 | |||
217 | # Store the node and assign ID. |
||
218 | self.store_node(node) |
||
219 | |||
220 | return node |
||
221 | |||
222 | # ------------------------------------------------------------------------------------------------------------------ |
||
223 | def append_inline_node(self, command, options, argument, position): |
||
224 | """ |
||
225 | Creates a node based an inline command and appends it to the end of the content tree. |
||
226 | |||
227 | :param str command: The inline command. |
||
228 | :param dict options: The options. |
||
229 | :param str argument: The argument of the inline command. |
||
230 | :param sdoc.sdoc2.Position.Position position: The position of the node definition. |
||
231 | |||
232 | :rtype: sdoc.sdoc2.node.Node.Node |
||
233 | """ |
||
234 | # Create the inline node. |
||
235 | node = self.create_inline_node(command, options, argument, position) |
||
236 | |||
237 | # Add the node to the node store. |
||
238 | self._append_to_content_tree(node) |
||
239 | |||
240 | return node |
||
241 | |||
242 | # ------------------------------------------------------------------------------------------------------------------ |
||
243 | def append_block_node(self, command, options, position): |
||
244 | """ |
||
245 | Creates a node based on a block command and appends it to the end of the content tree. |
||
246 | |||
247 | :param str command: The inline command. |
||
248 | :param dict options: The options. |
||
249 | :param sdoc.sdoc2.Position.Position position: The position of the node definition. |
||
250 | |||
251 | :rtype: sdoc.sdoc2.node.Node.Node |
||
252 | """ |
||
253 | # Create the block node. |
||
254 | node = self.create_block_node(command, options, position) |
||
255 | |||
256 | # Add the node to the node store. |
||
257 | self._append_to_content_tree(node) |
||
258 | |||
259 | return node |
||
260 | |||
261 | # ------------------------------------------------------------------------------------------------------------------ |
||
262 | def create_formatter(self, command, parent=None): |
||
263 | """ |
||
264 | Creates a formatter for generating the output of nodes in the requested output format. |
||
265 | |||
266 | :param str command: The inline of block command. |
||
267 | :param sdoc.sdoc2.formatter.Formatter.Formatter parent: The parent formatter. |
||
268 | |||
269 | :rtype: sdoc.sdoc2.formatter.Formatter.Formatter |
||
270 | """ |
||
271 | if self.format not in self._formatters: |
||
272 | raise RuntimeError("Unknown output format '{0!s}'.".format(self.format)) |
||
273 | |||
274 | if command not in self._formatters[self.format]: |
||
275 | # @todo use default none decorator with warning |
||
276 | raise RuntimeError("Unknown formatter '{0!s}' for format '{1!s}'.".format(command, self.format)) |
||
277 | |||
278 | constructor = self._formatters[self.format][command] |
||
279 | formatter = constructor(parent) |
||
280 | |||
281 | return formatter |
||
282 | |||
283 | # ------------------------------------------------------------------------------------------------------------------ |
||
284 | def _adjust_hierarchy(self, node): |
||
285 | """ |
||
286 | Adjust the hierarchy based on the hierarchy of a new node. |
||
287 | |||
288 | :param sdoc.sdoc2.node.Node.Node node: The new node. |
||
289 | """ |
||
290 | node_hierarchy_name = node.get_hierarchy_name() |
||
291 | parent_found = False |
||
292 | while self.nested_nodes and not parent_found: |
||
293 | parent_node = self.nested_nodes[-1] |
||
294 | parent_hierarchy_name = parent_node.get_hierarchy_name() |
||
295 | if parent_hierarchy_name != node_hierarchy_name: |
||
296 | if node.is_hierarchy_root(): |
||
297 | parent_found = True |
||
298 | else: |
||
299 | raise RuntimeError( |
||
300 | "Improper nesting of node '{0!s}' at {1!s} and node '{2!s}' at {3!s}.".format(parent_node.name, |
||
301 | parent_node.position, |
||
302 | node.name, |
||
303 | node.position)) |
||
304 | |||
305 | if not parent_found: |
||
306 | parent_hierarchy_level = parent_node.get_hierarchy_level() |
||
307 | node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level) |
||
308 | if parent_hierarchy_level >= node_hierarchy_level and len(self.nested_nodes) > 1: |
||
309 | self.nested_nodes.pop() |
||
310 | else: |
||
311 | parent_found = True |
||
312 | |||
313 | parent_node = self.nested_nodes[-1] |
||
314 | parent_hierarchy_level = parent_node.get_hierarchy_level() |
||
315 | node_hierarchy_level = node.get_hierarchy_level(parent_hierarchy_level) |
||
316 | |||
317 | if node_hierarchy_level - parent_hierarchy_level > 1: |
||
318 | # @todo position |
||
319 | print( |
||
320 | "Warning improper nesting of levels: {0:d} at {1!s} and {2:d} at {3!s}.".format(parent_hierarchy_level, |
||
321 | parent_node.position, |
||
322 | node_hierarchy_level, |
||
323 | node.position)) |
||
324 | |||
325 | # ------------------------------------------------------------------------------------------------------------------ |
||
326 | def store_node(self, node): |
||
327 | """ |
||
328 | Stores a node. If the node was not stored before assigns an ID to this node, otherwise the node replaces the |
||
329 | node stored under the same ID. Returns the ID if the node. |
||
330 | |||
331 | :param sdoc.sdoc2.node.Node.Node node: The node. |
||
332 | |||
333 | :rtype: int |
||
334 | """ |
||
335 | if not node.id: |
||
336 | # Add the node to the node store. |
||
337 | node_id = len(self.nodes) + 1 |
||
338 | node.id = node_id |
||
339 | |||
340 | self.nodes[node.id] = node |
||
341 | |||
342 | return node.id |
||
343 | |||
344 | # ------------------------------------------------------------------------------------------------------------------ |
||
345 | def _append_to_content_tree(self, node): |
||
346 | """ |
||
347 | Appends the node at the proper nesting level at the end of the content tree. |
||
348 | |||
349 | :param sdoc.sdoc2.node.Node.Node node: The node. |
||
350 | """ |
||
351 | if node.id == 1: |
||
352 | # The first node must be a document root. |
||
353 | if not node.is_document_root(): |
||
354 | # @todo position of block node. |
||
355 | raise RuntimeError("Node {0!s} is not a document root".format(node.name)) |
||
356 | |||
357 | self.nested_nodes.append(node) |
||
358 | |||
359 | else: |
||
360 | # All other nodes must not be a document root. |
||
361 | if node.is_document_root(): |
||
362 | # @todo position of block node. |
||
363 | raise RuntimeError("Unexpected {0!s}. Node is document root".format(node.name)) |
||
364 | |||
365 | # If the node is a part of a hierarchy adjust the nested nodes stack. |
||
366 | if node.get_hierarchy_name(): |
||
367 | self._adjust_hierarchy(node) |
||
368 | |||
369 | # Add the node to the list of child nodes of its parent node. |
||
370 | if self.nested_nodes: |
||
371 | parent_node = self.nested_nodes[-1] |
||
372 | |||
373 | # Pop from stack if we have two list element nodes (e.g. item nodes) in a row. |
||
374 | if node.is_list_element() and type(parent_node) == type(node): |
||
375 | self.nested_nodes.pop() |
||
376 | parent_node = self.nested_nodes[-1] |
||
377 | |||
378 | parent_node.child_nodes.append(node.id) |
||
379 | |||
380 | # Block commands and hierarchical nodes must be appended to the nested nodes. |
||
381 | if node.is_block_command() or node.get_hierarchy_name(): |
||
382 | self.nested_nodes.append(node) |
||
383 | |||
384 | # ------------------------------------------------------------------------------------------------------------------ |
||
385 | def prepare_content_tree(self): |
||
386 | """ |
||
387 | Prepares after parsing at SDoc2 level the content tree for further processing. |
||
388 | """ |
||
389 | # Currently, node with ID 1 is the document node. @todo Improve getting the document node. |
||
390 | self.nodes[1].prepare_content_tree() |
||
391 | |||
392 | # ------------------------------------------------------------------------------------------------------------------ |
||
393 | def number_numerable(self): |
||
394 | """ |
||
395 | Numbers all numerable nodes such as chapters, sections, figures, and, items. |
||
396 | """ |
||
397 | self.nodes[1].number(self._enumerable_numbers) |
||
398 | |||
399 | # ------------------------------------------------------------------------------------------------------------------ |
||
400 | def generate(self, one_file=False, file_per_chapter=False): |
||
401 | """ |
||
402 | Generates the document. |
||
403 | |||
404 | :param bool one_file: If True creates one output file for one source file. |
||
405 | :param bool file_per_chapter: If True creates one output file per one chapter of source file. |
||
406 | """ |
||
407 | # Generates whole HTML output file. |
||
408 | if one_file: |
||
409 | general_file = open('output.html', 'w') |
||
410 | formatter = self.create_formatter('document') |
||
411 | formatter.generate(self.nodes[1], general_file) |
||
412 | |||
413 | # Generates in mode 'output file on each chapter'. |
||
414 | if file_per_chapter: |
||
415 | formatter = self.create_formatter('document') |
||
416 | formatter.generate_chapter(self.nodes[1], None) |
||
417 | |||
418 | # ------------------------------------------------------------------------------------------------------------------ |
||
419 | def get_enumerated_items(self): |
||
420 | """ |
||
421 | Returns a list with tuples with command and number of enumerated nodes. |
||
422 | |||
423 | This method is intended for unit test only. |
||
424 | |||
425 | :rtype: list[(str,str)] |
||
426 | """ |
||
427 | return self.nodes[1].get_enumerated_items() |
||
428 | |||
429 | # ------------------------------------------------------------------------------------------------------------------ |
||
430 | def parse_labels(self): |
||
431 | """ |
||
432 | Method for parsing labels, setting additional arguments to nodes, and removing label nodes from tree. |
||
433 | """ |
||
434 | self.nodes[1].parse_labels() |
||
435 | self.nodes[1].change_ref_argument() |
||
436 | |||
438 |
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.