sdoc.sdoc2.node.TableNode   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 364
Duplicated Lines 94.51 %

Test Coverage

Coverage 23.27%

Importance

Changes 0
Metric Value
wmc 60
eloc 159
dl 344
loc 364
ccs 37
cts 159
cp 0.2327
rs 3.6
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A TableNode.__init__() 26 26 1
A TableNode.prune_whitespace() 14 14 2
A TableNode.has_header() 19 19 4
C TableNode.split_by_vertical_separators() 46 46 10
A TableNode.prepare_content_tree() 23 23 4
A TableNode.number() 16 16 2
B TableNode.split_by_new_lines() 24 24 8
A TableNode.is_inline_command() 5 5 1
A TableNode.parse_vertical_separators() 14 14 4
A TableNode.divide_text_nodes() 24 24 4
A TableNode.reset_data() 12 12 1
A TableNode.generate_table() 16 16 2
B TableNode.get_column_alignments() 20 20 6
A TableNode.get_command() 5 5 1
A TableNode.setup_label() 7 7 1
B TableNode.generate_output_rows() 28 28 8
A TableNode.is_block_command() 5 5 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.TableNode 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 csv
2 1
import io
3 1
import re
4 1
from typing import Any, Dict, List, Optional
5
6 1
from cleo.io.io import IO
7
8 1
from sdoc.sdoc2 import in_scope, out_scope
9 1
from sdoc.sdoc2.helper.Enumerable import Enumerable
10 1
from sdoc.sdoc2.node.CaptionNode import CaptionNode
11 1
from sdoc.sdoc2.node.LabelNode import LabelNode
12 1
from sdoc.sdoc2.node.Node import Node
13 1
from sdoc.sdoc2.NodeStore import NodeStore
14
15
16 1 View Code Duplication
class TableNode(Node):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
17
    """
18
    SDoc2 node for table.
19
    """
20
21
    # ------------------------------------------------------------------------------------------------------------------
22 1
    def __init__(self, io_object: IO, options: Dict[str, str]):
23
        """
24
        Object constructor.
25
26
        :param OutputStyle io_object: The IO object.
27
        :param dict[str,str] options: The options of this table.
28
        """
29
        super().__init__(io_object, 'table', options)
30
31
        self.rows: List[List[str]] = []
32
        """
33
        The table rows.
34
        """
35
36
        self.column_headers: List[str] = []
37
        """
38
        The column headers of the table (if any).
39
        """
40
41
        self.alignments: List[Optional[str]] = []
42
        """
43
        The text alignments in the table columns.
44
        """
45
46
        self.caption: Optional[str] = None
47
        """
48
        The caption for the table.
49
        """
50
51
    # ------------------------------------------------------------------------------------------------------------------
52 1
    def get_command(self) -> str:
53
        """
54
        Returns the command of this node, i.e. table.
55
        """
56
        return 'table'
57
58
    # ------------------------------------------------------------------------------------------------------------------
59 1
    def is_block_command(self) -> bool:
60
        """
61
        Returns True.
62
        """
63
        return True
64
65
    # ------------------------------------------------------------------------------------------------------------------
66 1
    def is_inline_command(self) -> bool:
67
        """
68
        Returns False.
69
        """
70
        return False
71
72
    # ------------------------------------------------------------------------------------------------------------------
73 1
    def number(self, numbers: Dict[str, Any]) -> None:
74
        """
75
        Numbers all numerable nodes such as chapters, sections, figures, and, items.
76
77
        :param dict[str,any] numbers: The current numbers.
78
        """
79
        if 'table' not in numbers:
80
            numbers['table'] = Enumerable()
81
            numbers['table'].generate_numeration(1)
82
            numbers['table'].increment_last_level()
83
        else:
84
            numbers['table'].increment_last_level()
85
86
        self._options['number'] = numbers['table'].get_string()
87
88
        super().number(numbers)
89
90
    # ------------------------------------------------------------------------------------------------------------------
91 1
    def prepare_content_tree(self) -> None:
92
        """
93
        Prepares this node for further processing.
94
        """
95
        table_nodes = []
96
97
        for node_id in self.child_nodes:
98
            node = in_scope(node_id)
99
100
            if isinstance(node, LabelNode):
101
                self.setup_label(node)
102
103
            elif isinstance(node, CaptionNode):
104
                self.caption = node.argument
105
106
            else:
107
                table_nodes.append(node)
108
109
            node.prepare_content_tree()
110
111
            out_scope(node)
112
113
        self.generate_table(table_nodes)
114
115
    # ------------------------------------------------------------------------------------------------------------------
116 1
    def setup_label(self, node):
117
        """
118
        Sets the data of a label to current table.
119
120
        :param sdoc.sdoc2.node.LabelNode.LabelNode node: The label node.
121
        """
122
        self._options['id'] = node.argument
123
124
    # ------------------------------------------------------------------------------------------------------------------
125 1
    def generate_table(self, nodes):
126
        """
127
        Generates the table node.
128
129
        :param list[sdoc.sdoc2.node.Node.Node] nodes: The list with nodes.
130
        """
131
        table_data = TableNode.divide_text_nodes(nodes)
132
        split_data = TableNode.split_by_new_lines(table_data)
133
        rows = self.generate_output_rows(split_data)
134
135
        if self.has_header(rows):
136
            self.column_headers = rows[0]
137
            self.rows = rows[2:]
138
            self.alignments = self.get_column_alignments(rows[1])
139
        else:
140
            self.rows = rows
141
142
    # ------------------------------------------------------------------------------------------------------------------
143 1
    @staticmethod
144 1
    def divide_text_nodes(nodes: List[Any]) -> List[Any]:
145
        """
146
        Divides text nodes from other type of nodes.
147
148
        :param: list[any] nodes: The list with nodes.
149
150
        :rtype: list[any]
151
        """
152
        table_data = []
153
        table_text_repr = ''
154
155
        for node in nodes:
156
            if node.get_command() == 'TEXT':
157
                table_text_repr += node.argument
158
            else:
159
                table_data.append(table_text_repr)
160
                table_data.append(node)
161
                table_text_repr = ''
162
163
        if table_text_repr:
164
            table_data.append(table_text_repr)
165
166
        return table_data
167
168
    # ------------------------------------------------------------------------------------------------------------------
169 1
    @staticmethod
170 1
    def split_by_new_lines(table_data: List[Any]) -> List[Any]:
171
        """
172
        Splits data by newline symbols.
173
174
        :param list[any] table_data:
175
176
        :rtype: list[any]
177
        """
178
        split_data = []
179
180
        for data in table_data:
181
            if isinstance(data, str):
182
                split_data.append(data.split('\n'))
183
            else:
184
                split_data.append(data)
185
186
        for data in split_data:
187
            if isinstance(data, list):
188
                for element in data:
189
                    if element.isspace() or element == '':
190
                        data.remove(element)
191
192
        return split_data
193
194
    # ------------------------------------------------------------------------------------------------------------------
195 1
    def generate_output_rows(self, split_data: List[Any]) -> List[List[Any]]:
196
        """
197
        Generates the rows for final representation.
198
199
        :param list[any] split_data: The split data.
200
201
        :rtype: list[list[any]]
202
        """
203
        separated_data = TableNode.split_by_vertical_separators(split_data)
204
205
        rows = []
206
207
        for item in separated_data:
208
            row = []
209
210
            for data in item:
211
                if data and isinstance(data, str) and not data.isspace():
212
                    string = io.StringIO(data)
213
                    self.parse_vertical_separators(string, row)
214
215
                else:
216
                    if data:
217
                        row.append(data)
218
219
            if row:
220
                rows.append(row)
221
222
        return rows
223
224
    # ------------------------------------------------------------------------------------------------------------------
225 1
    @staticmethod
226 1
    def split_by_vertical_separators(split_data: List[Any]) -> List[List[Any]]:
227
        """
228
        Splits data by vertical separators and creates rows with data.
229
230
        :param list[any] split_data: The split data.
231
232
        :rtype: list[list[any]]
233
        """
234
        rows = []
235
        row = []
236
237
        for item in split_data:
238
            # If we have a list we pass for each element.
239
            # In list we have only text elements.
240
            if isinstance(item, list):
241
                for element in item:
242
243
                    # If element starts with '|' we just add element to row.
244
                    if element.strip().startswith('|'):
245
                        row.append(element)
246
247
                    # If element ends with '|' we add row to rows list, reset row to empty list
248
                    # and append there current element. We do it because we know that if string ends with '|',
249
                    # after it we have non-text element.
250
                    elif element.strip().endswith('|'):
251
                        row = TableNode.reset_data(row, rows)
252
                        row.append(element)
253
254
                    # If we have element which not starts and not ends in '|' we do this block.
255
                    else:
256
                        # If last added element of row is not string we add row to rows list, reset row to empty.
257
                        if row and not isinstance(row[-1], str):
258
                            row = TableNode.reset_data(row, rows)
259
                        # And just add full element with text like a row to list of rows.
260
                        rows.append([element])
261
262
            # Do this block if element is not a list.
263
            else:
264
                # If last added element not ends with '|' we add row to rows list, and reset row to empty list.
265
                if row and not row[-1].strip().endswith('|'):
266
                    row = TableNode.reset_data(row, rows)
267
                # Add item to row.
268
                row.append(item)
269
270
        return rows
271
272
    # ------------------------------------------------------------------------------------------------------------------
273 1
    @staticmethod
274 1
    def reset_data(row: List[Any], rows: List[List[any]]) -> List[Any]:
275
        """
276
        Appends row with data to list of rows, and clear row from any elements.
277
278
        Warning! This method changes original list 'rows'.
279
280
        :param list[any] row: The row with elements
281
        :param list[list[any]] rows: The list with many rows.
282
        """
283
        rows.append(row)
284
        return []
285
286
    # ------------------------------------------------------------------------------------------------------------------
287 1
    def parse_vertical_separators(self, string: io.StringIO, row: List[any]) -> None:
288
        """
289
        Splits row by vertical separator for future output.
290
291
        :param StringIO string: The string which we will separate.
292
        :param list[mixed] row: The list with the row in which we append data.
293
        """
294
        reader = csv.reader(string, delimiter='|')
295
        for line in reader:
296
            new_row = self.prune_whitespace(line)
297
298
            for item in new_row:
299
                if item:
300
                    row.append(item)
301
302
    # ------------------------------------------------------------------------------------------------------------------
303 1
    @staticmethod
304 1
    def has_header(rows: List[str]) -> bool:
305
        """
306
        Returns True if the table has a table header.
307
308
        :param list[str] rows: The second row of the table.
309
        """
310
        is_header = True
311
312
        if len(rows) == 1:
313
            return False
314
315
        for align in rows[1]:
316
            header_part = re.findall(':?---+-*:?', align)
317
            if not header_part:
318
                is_header = False
319
                break
320
321
        return is_header
322
323
    # ------------------------------------------------------------------------------------------------------------------
324 1
    @staticmethod
325 1
    def get_column_alignments(row: List[str]) -> List[str]:
326
        """
327
        Sets alignments on table columns.
328
329
        :param list[str] row: The row with hyphens for creating column headers.
330
        """
331
        alignments = []
332
        for hyphens in row:
333
            hyphens = hyphens.strip()
334
            if hyphens.startswith(':') and hyphens.endswith(':'):
335
                alignments.append('center')
336
            elif hyphens.startswith(':'):
337
                alignments.append('left')
338
            elif hyphens.endswith(':'):
339
                alignments.append('right')
340
            else:
341
                alignments.append('')
342
343
        return alignments
344
345
    # ------------------------------------------------------------------------------------------------------------------
346 1
    @staticmethod
347 1
    def prune_whitespace(row: List[str]) -> List[str]:
348
        """
349
        Strips whitespaces from the text of an each cell.
350
351
        :param list[str] row: The row with text of an each cell.
352
        """
353
        clear_row = []
354
        for item in row:
355
            clear_text = item.strip()
356
            clear_text = re.sub(r'\s+', ' ', clear_text)
357
            clear_row.append(clear_text)
358
359
        return clear_row
360
361
362
# ----------------------------------------------------------------------------------------------------------------------
363
NodeStore.register_block_command('table', TableNode)
364