Test Failed
Push — master ( 714282...4cb748 )
by P.R.
01:55 queued 13s
created

sdoc.sdoc2.node.TableNode   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 364
Duplicated Lines 94.51 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 60
eloc 159
dl 344
loc 364
ccs 0
cts 159
cp 0
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
import csv
2
import io
3
import re
4
from typing import Any, Dict, List, Optional
5
6
from cleo.styles import OutputStyle
7
8
from sdoc.sdoc2 import in_scope, out_scope
9
from sdoc.sdoc2.helper.Enumerable import Enumerable
10
from sdoc.sdoc2.node.CaptionNode import CaptionNode
11
from sdoc.sdoc2.node.LabelNode import LabelNode
12
from sdoc.sdoc2.node.Node import Node
13
from sdoc.sdoc2.NodeStore import NodeStore
14
15
16 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
    def __init__(self, in_out: OutputStyle, options: Dict[str, str]):
23
        """
24
        Object constructor.
25
26
        :param OutputStyle in_out: The IO object.
27
        :param dict[str,str] options: The options of this table.
28
        """
29
        super().__init__(in_out, '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
    def get_command(self) -> str:
53
        """
54
        Returns the command of this node, i.e. table.
55
        """
56
        return 'table'
57
58
    # ------------------------------------------------------------------------------------------------------------------
59
    def is_block_command(self) -> bool:
60
        """
61
        Returns True.
62
        """
63
        return True
64
65
    # ------------------------------------------------------------------------------------------------------------------
66
    def is_inline_command(self) -> bool:
67
        """
68
        Returns False.
69
        """
70
        return False
71
72
    # ------------------------------------------------------------------------------------------------------------------
73
    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
    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
    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
    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
    @staticmethod
144
    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
    @staticmethod
170
    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
    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
    @staticmethod
226
    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
    @staticmethod
274
    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
    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
    @staticmethod
304
    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
    @staticmethod
325
    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
    @staticmethod
347
    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