Passed
Pull Request — develop (#301)
by
unknown
01:43
created

Reducer_Project.reduce()   F

Complexity

Conditions 14

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 14
c 2
b 1
f 0
dl 0
loc 35
rs 2.7581

How to fix   Complexity   

Complexity

Complex classes like Reducer_Project.reduce() 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
#!/usr/bin/env python
2
3
import copy
4
import os
5
6
from typing import Generator
7
from typing import Set
8
9
from doorstop.gui.action import Action
10
from doorstop.gui.action import Action_ChangeCWD
11
from doorstop.gui.action import Action_ChangeProjectPath
12
from doorstop.gui.action import Action_CloseProject
13
from doorstop.gui.action import Action_LoadProject
14
from doorstop.gui.action import Action_SaveProject
15
from doorstop.gui.action import Action_ChangeSelectedDocument
16
from doorstop.gui.action import Action_ChangeSelectedItem
17
from doorstop.gui.action import Action_ChangeItemText
18
from doorstop.gui.action import Action_ChangeItemReference
19
from doorstop.gui.action import Action_ChangeItemActive
20
from doorstop.gui.action import Action_ChangeItemDerived
21
from doorstop.gui.action import Action_ChangeItemNormative
22
from doorstop.gui.action import Action_ChangeItemHeading
23
from doorstop.gui.action import Action_ChangeSelectedLink
24
from doorstop.gui.action import Action_ChangeLinkInception
25
from doorstop.gui.action import Action_ChangeItemAddLink
26
from doorstop.gui.action import Action_ChangeItemRemoveLink
27
from doorstop.gui.action import Action_ChangeExtendedName
28
from doorstop.gui.action import Action_ChangeExtendedValue
29
from doorstop.gui.action import Action_AddNewItemNextToSelection
30
from doorstop.gui.action import Action_RemoveSelectedItem
31
from doorstop.gui.action import Action_SelectedItem_Level_Indent
32
from doorstop.gui.action import Action_SelectedItem_Level_Dedent
33
from doorstop.gui.action import Action_SelectedItem_Level_Increment
34
from doorstop.gui.action import Action_SelectedItem_Level_Decrement
35
from doorstop.gui.action import Action_Import
36
37
from doorstop.gui.state import State
38
39
from doorstop.core.types import UID
40
41
from doorstop.core import builder
42
from doorstop.core import importer
43
44
from doorstop.core.document import Document
45
from doorstop.core.item import Item
46
47
from doorstop.common import DoorstopError
48
49
50
class Reducer(object):
51
    def __init__(self) -> None:
52
        pass
53
54
    def reduce(self, state: State, action: Action) -> State:  # pylint: disable=R0201
55
        return state
56
57
58
class Reducer_CWD(Reducer):
59
    def reduce(self, state: State, action: Action) -> State:
60
        result = state
61
        if isinstance(action, Action_ChangeCWD):
62
            newCWD = os.getcwd() if action.cwd is None else action.cwd
63
            if newCWD != result.cwd:
64
                result = copy.deepcopy(result)
65
                result.cwd = newCWD
66
        return result
67
68
69
class Reducer_Project(Reducer):
70
    def reduce(self, state: State, action: Action) -> State:
71
        result = state
72
        new_path = state.project_path
73
        new_tree = state.project_tree
74
        if isinstance(action, Action_ChangeProjectPath):
75
            new_path = action.project_path
76
            new_tree = None
77
        elif isinstance(action, Action_LoadProject):
78
            new_tree = None
79
        elif isinstance(action, Action_CloseProject):
80
            new_path = ""
81
            new_tree = None
82
        elif isinstance(action, Action_SaveProject):
83
            project_tree = state.project_tree
84
            if project_tree is not None:
85
                for c_curr_document in project_tree:
86
                    c_curr_document.save()
87
                    for c_curr_item in c_curr_document:
88
                        c_curr_item.save()
89
                    if state.session_pending_change:
90
                        result = copy.deepcopy(result)
91
                        result.session_pending_change = False
92
93
        if (new_path != result.project_path) or (new_tree != result.project_tree):
94
            result = copy.deepcopy(result)
95
            result.session_selected_item = None
96
            result.session_selected_link = None
97
            try:
98
                result.project_tree = None if "" == new_path else builder.build(cwd=state.cwd, root=new_path, is_auto_save=False)
99
            except DoorstopError:
100
                return state
101
            if result.project_tree is not None:
102
                new_path = result.project_tree.root
103
            result.project_path = new_path
104
        return result
105
106
107
class Reducer_Session(Reducer):
108
    def reduce(self, state: State, action: Action) -> State:
109
        result = state
110
        if isinstance(action, Action_ChangeSelectedDocument):
111
            new_selected_document = action.selected_document
112
            if new_selected_document != state.session_selected_document:
113
                result = copy.deepcopy(result)
114
                result.session_selected_document = new_selected_document
115
                result.session_selected_item = None
116
        elif isinstance(action, Action_ChangeSelectedItem):
117
            new_selected_item = action.selected_item
118
            if set(str(a) for a in new_selected_item) != set(str(b) for b in state.session_selected_item):
119
                result = copy.deepcopy(result)
120
121
                # Load in the state the selection by putting the freshly selecte item first so that the main selected item becomes one of the freshly selected item.
122
                previous_selection = [str(x) for x in state.session_selected_item]
123
                freshly_selected = [str(y) for y in new_selected_item if y not in previous_selection]
124
                unchanged_selection = [str(z) for z in new_selected_item if z in previous_selection]
125
                freshly_selected.extend(unchanged_selection)
126
                result.session_selected_item = tuple([UID(aa) for aa in freshly_selected])
127
        elif isinstance(action, Action_ChangeSelectedLink):
128
            new_selected_link = frozenset(set([x for x in result.session_selected_link]).union(action.selected_link) - action.unselected_link)
129
            if new_selected_link != result.session_selected_link:
130
                result = copy.deepcopy(result)
131
                result.session_selected_link = new_selected_link
132
        elif isinstance(action, Action_ChangeLinkInception):
133
            new_inception_link = action.inception_link
134
            if new_inception_link != result.session_link_inception:
135
                result = copy.deepcopy(result)
136
                result.session_link_inception = new_inception_link
137
        elif isinstance(action, Action_ChangeExtendedName):
138
            new_extended_name = action.extendedName
139
            if new_extended_name != result.session_extended_name:
140
                result = copy.deepcopy(result)
141
                result.session_extended_name = new_extended_name
142
        return result
143
144
145 View Code Duplication
class Reducer_Edit(Reducer):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
146
    def reduce(self, state: State, action: Action) -> State:
147
        result = state
148
        if isinstance(action, Action_ChangeItemText):
149
            project_tree = state.project_tree
150
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
151
152
            if old_item:
153
                old_item_text = old_item.text
154
                new_item_text = action.item_new_text
155
                if old_item_text != new_item_text:
156
                    result = copy.deepcopy(result)
157
                    project_tree = result.project_tree
158
                    if project_tree:
159
                        new_item = project_tree.find_item(action.item_uid)
160
                        new_item.text = new_item_text
161
                        result.session_pending_change = True
162
        if isinstance(action, Action_ChangeItemReference):
163
            project_tree = state.project_tree
164
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
165
166
            if old_item:
167
                old_item_reference = old_item.ref
168
                new_item_reference = action.item_new_reference
169
                if old_item_reference != new_item_reference:
170
                    result = copy.deepcopy(result)
171
                    project_tree = result.project_tree
172
                    if project_tree:
173
                        new_item = project_tree.find_item(action.item_uid)
174
                        new_item.ref = new_item_reference
175
                        result.session_pending_change = True
176
        elif isinstance(action, Action_ChangeItemActive):
177
            project_tree = state.project_tree
178
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
179
180
            if old_item:
181
                old_item_active = old_item.active
182
                new_item_active = action.item_new_active
183
                if old_item_active != new_item_active:
184
                    result = copy.deepcopy(result)
185
                    project_tree = result.project_tree
186
                    if project_tree:
187
                        new_item = project_tree.find_item(action.item_uid)
188
                        new_item.active = new_item_active
189
                        result.session_pending_change = True
190
        elif isinstance(action, Action_ChangeItemDerived):
191
            project_tree = state.project_tree
192
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
193
194
            if old_item:
195
                old_item_derived = old_item.derived
196
                new_item_derived = action.item_new_derived
197
                if old_item_derived != new_item_derived:
198
                    result = copy.deepcopy(result)
199
                    project_tree = result.project_tree
200
                    if project_tree:
201
                        new_item = project_tree.find_item(action.item_uid)
202
                        new_item.derived = new_item_derived
203
                        result.session_pending_change = True
204
        elif isinstance(action, Action_ChangeItemNormative):
205
            project_tree = state.project_tree
206
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
207
208
            if old_item:
209
                old_item_normative = old_item.normative
210
                new_item_normative = action.item_new_normative
211
                if old_item_normative != new_item_normative:
212
                    result = copy.deepcopy(result)
213
                    project_tree = result.project_tree
214
                    if project_tree:
215
                        new_item = project_tree.find_item(action.item_uid)
216
                        new_item.normative = new_item_normative
217
                        result.session_pending_change = True
218
        elif isinstance(action, Action_ChangeItemHeading):
219
            project_tree = state.project_tree
220
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
221
222
            if old_item:
223
                old_item_heading = old_item.heading
224
                new_item_heading = action.item_new_heading
225
                if old_item_heading != new_item_heading:
226
                    result = copy.deepcopy(result)
227
                    project_tree = result.project_tree
228
                    if project_tree:
229
                        new_item = project_tree.find_item(action.item_uid)
230
                        new_item.heading = new_item_heading
231
                        result.session_pending_change = True
232
        elif isinstance(action, Action_ChangeItemRemoveLink):
233
            project_tree = state.project_tree
234
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
235
236
            if old_item:
237
                item_link = action.item_link
238
                result = copy.deepcopy(result)
239
                project_tree = result.project_tree
240
                if project_tree:
241
                    new_item = project_tree.find_item(action.item_uid)
242
                    count_before = len(new_item.links)
243
                    new_item.links = [x for x in new_item.links if str(x) not in [str(y) for y in item_link]]
244
                    result.session_selected_link = frozenset([x for x in result.session_selected_link if str(x) not in [str(y) for y in item_link]])
245
                    count_after = len(new_item.links)
246
                    if count_before != count_after:
247 View Code Duplication
                        result.session_pending_change = True
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
248
        elif isinstance(action, Action_ChangeItemAddLink):
249
            new_link = action.new_link
250
            if "" != new_link:
251
252
                project_tree = state.project_tree
253
                old_item = project_tree.find_item(action.item_uid) if project_tree else None
254
255
                if old_item:
256
                    if new_link not in old_item.links:
257
                        result = copy.deepcopy(result)
258
                        project_tree = result.project_tree
259
                        if project_tree:
260
                            new_item = project_tree.find_item(action.item_uid)
261
                            links = new_item.links
262
                            links.append(UID(new_link))
263
                            new_item.links = links
264
                            result.session_pending_change = True
265
        elif isinstance(action, Action_ChangeExtendedValue):
266
            project_tree = state.project_tree
267
            old_item = project_tree.find_item(action.item_uid) if project_tree else None
268
            if old_item:
269
                old_item_extended_value = old_item.get(action.extendedName)
270
                new_item_extended_value = action.extendedValue
271
                if old_item_extended_value != new_item_extended_value:
272
                    result = copy.deepcopy(result)
273
                    project_tree = result.project_tree
274
                    if project_tree:
275
                        new_item = project_tree.find_item(action.item_uid)
276
                        if new_item_extended_value and new_item_extended_value.strip():
277
                            new_item.set(action.extendedName, new_item_extended_value)
278
                        else:
279
                            new_item.remove(action.extendedName)
280
                        result.session_pending_change = True
281
        elif isinstance(action, Action_AddNewItemNextToSelection):
282
            project_tree = state.project_tree
283
            if project_tree is not None:
284
                result = copy.deepcopy(result)
285
                project_tree = result.project_tree
286
                assert project_tree is not None
287
                session_selected_item_principal = state.session_selected_item_principal
288
                session_selected_item_principal_item = None if session_selected_item_principal is None else project_tree.find_item(session_selected_item_principal)
289
                document = project_tree.find_document(result.session_selected_document)
290
                new_item = document.add_item(level=None if session_selected_item_principal_item is None else session_selected_item_principal_item.level + 1)
291
                result.session_pending_change = True
292
                result.session_selected_item = (new_item.uid, )
293
        elif isinstance(action, Action_RemoveSelectedItem):
294
            project_tree = state.project_tree
295
            if (project_tree is not None) and (state.session_selected_item_principal is not None):
296
                result = copy.deepcopy(result)
297
298
                session_selected_item_principal = result.session_selected_item_principal
299
                assert session_selected_item_principal is not None
300
301
                project_tree = result.project_tree
302
                assert project_tree is not None
303
304
                document = project_tree.find_document(result.session_selected_document)
305
                item_before = None
306
                item = None
307
                item_after = None
308
                found_it = False
309
                for curr_neighboor in document.items:
310
                    if found_it:
311
                        if str(curr_neighboor.uid) not in result.session_selected_item:
312
                            item_after = curr_neighboor
313
                            break
314
                    else:
315
                        if str(session_selected_item_principal) == str(curr_neighboor.uid):
316
                            item = curr_neighboor
317
                            found_it = True
318
                        else:
319
                            if str(curr_neighboor.uid) not in result.session_selected_item:
320
                                item_before = curr_neighboor
321
322
                assert item is not None, str(session_selected_item_principal)
323
324
                new_selection = None
325
                if item_before is None:
326
                    if item_after is None:
327
                        new_selection = None
328
                    else:
329
                        new_selection = [item_after.uid]
330
                else:
331
                    if item_after is None:
332
                        new_selection = [item_before.uid]
333
                    else:
334
                        # Use heuristic to chose the best userfriendly new selection.
335
                        item_before_level = item_before.level.value
336
                        item_level = item.level.value
337
                        item_after_level = item_after.level.value
338
339
                        for idx, val in enumerate(item_level):
340
                            if val == item_after_level[idx]:
341
                                if val == item_before_level[idx]:
342
                                    continue  # They are both equaly similar
343
                                else:
344
                                    # The after looks more similar
345
                                    new_selection = [item_after.uid]
346
                                    break
347
                            else:
348
                                if val == item_before_level[idx]:
349
                                    # The before looks more similar
350
                                    new_selection = [item_before.uid]
351
                                    break
352
                                else:
353
                                    # They are both equaly not similar
354
                                    new_selection = [item_after.uid]
355
                                    break
356
357
                for c_currUID in result.session_selected_item:
358
                    item = project_tree.find_item(c_currUID)
359
                    if item is not None:
360
                        item = project_tree.remove_item(item)
361
                        result.session_pending_change |= item is not None
362
                        result.session_selected_item = new_selection
363
        elif isinstance(action, Action_Import):
364
            source = action.file_to_import
365
            if source:
366
                project_tree = state.project_tree
367
                if project_tree is not None:
368
                    resultX = copy.deepcopy(result)
369
                    project_tree = resultX.project_tree
370
                    assert project_tree is not None
371
372
                    the_document = None
373
                    try:
374
                        the_document = project_tree.find_document(state.session_selected_document)
375
                    except DoorstopError:
376
                        pass  # The document is not found.
377
                    if the_document is not None:
378
                        ext = source[source.rfind("."):]
379
                        func = importer.check(ext)
380
                        if func is not None:
381
                            func(is_auto_save=False, path=source, document=the_document, mapping=None)
382
                            result = resultX
383
                            result.session_pending_change = True
384
385
        return result
386
387
388
class Reducer_Level(Reducer):
389
    def reduce(self, state: State, action: Action) -> State:
390
        result = state
391
392
        def getNextFromStart(document: Document, uid_to_process: Set[str]) -> Generator[Item, None, None]:
393
            uid_to_process_left = set(uid_to_process)
394
            while uid_to_process_left:
395
                the_next_item_to_process = next((x for x in document.items if str(x.uid) in uid_to_process_left), None)  # pylint: disable=R1708
396
                if the_next_item_to_process is None: return
397
                yield the_next_item_to_process
398
                uid_to_process_left.remove(str(the_next_item_to_process.uid))
399
400
        def getNextFromEnd(document: Document, uid_to_process: Set[str]) -> Generator[Item, None, None]:
401
            uid_to_process_left = set(uid_to_process)
402
            while uid_to_process_left:
403
                the_next_item_to_process = next((x for x in reversed(document.items) if str(x.uid) in uid_to_process_left), None)  # pylint: disable=R1708
404
                if the_next_item_to_process is None: return
405
                yield the_next_item_to_process
406
                uid_to_process_left.remove(str(the_next_item_to_process.uid))
407
408
        if isinstance(action, (Action_SelectedItem_Level_Indent, Action_SelectedItem_Level_Dedent, Action_SelectedItem_Level_Increment, Action_SelectedItem_Level_Decrement)):
409
            project_tree = state.project_tree
410
            if (project_tree is not None) and (state.session_selected_item_principal is not None):
411
                result = copy.deepcopy(result)
412
                session_selected_item = result.session_selected_item
413
414
                project_tree = result.project_tree
415
                assert project_tree is not None
416
417
                document = project_tree.find_document(result.session_selected_document)
418
419
                for x in (getNextFromStart if isinstance(action, (Action_SelectedItem_Level_Decrement)) else getNextFromEnd)(document, session_selected_item):
420
                    result.session_pending_change = True
421
                    if isinstance(action, Action_SelectedItem_Level_Indent):
422
                        x.level >>= 1
423
                        document.reorder(keep=x)
424
                    elif isinstance(action, Action_SelectedItem_Level_Dedent):
425
                        x.level <<= 1
426
                        document.reorder(keep=x)
427
                    elif isinstance(action, Action_SelectedItem_Level_Increment):
428
                        x.level += 2
429
                        document.reorder(keep=x)
430
                    elif isinstance(action, Action_SelectedItem_Level_Decrement):
431
                        x.level -= 1
432
                        document.reorder(keep=x)
433
        return result
434
435
436
class Reducer_GUI(Reducer):
437
    def reduce(self, state: State, action: Action) -> State:
438
        result = state
439
        for curr_reducer in (Reducer_CWD(), Reducer_Project(), Reducer_Session(), Reducer_Edit(), Reducer_Level()):
440
            result = curr_reducer.reduce(result, action)
441
        return result
442