Completed
Push — develop ( e4f288...dec8f0 )
by Jace
02:13
created

yorm.bases.patch_methods()   F

Complexity

Conditions 11

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 11
Metric Value
cc 11
dl 0
loc 24
ccs 19
cts 19
cp 1
crap 11
rs 3.3409

How to fix   Complexity   

Complexity

Complex classes like yorm.bases.patch_methods() 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
"""Base classes for mapping."""
0 ignored issues
show
Bug introduced by
There seems to be a cyclic import (yorm.bases -> yorm.bases.mappable -> yorm.mapper).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (yorm.bases -> yorm.bases.mappable -> yorm.mapper -> yorm.types -> yorm.types.containers -> yorm.types.standard).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (yorm.bases -> yorm.bases.mappable -> yorm.mapper -> yorm.types -> yorm.types.standard).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (yorm.bases -> yorm.bases.convertible).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (yorm.bases -> yorm.bases.mappable -> yorm.mapper -> yorm.types -> yorm.types.extended -> yorm.types.containers).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (yorm.bases -> yorm.bases.mappable -> yorm.mapper -> yorm.types -> yorm.types.containers).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
2
3 1
import abc
4 1
import functools
5
6 1
from .. import common
7 1
from ..mapper import get_mapper
8
9
10 1
log = common.logger(__name__)
11
12 1
TAG = '_modified_by_yorm'
13
14
15 1
def fetch_before(method):
16
    """Decorator for methods that should fetch before call."""
17
18 1
    if getattr(method, TAG, False):
19 1
        return method
20
21 1
    @functools.wraps(method)
22
    def wrapped(self, *args, **kwargs):
23
        """Decorated method."""
24 1
        if not _private_call(method, args):
25 1
            mapper = get_mapper(self)
26 1
            if mapper and mapper.modified:
27 1
                log.debug("Fetching before call: %s", method.__name__)
28 1
                mapper.fetch()
29 1
                if mapper.auto_store:
30 1
                    mapper.store()
31 1
                    mapper.modified = False
32
33 1
        return method(self, *args, **kwargs)
34
35 1
    setattr(wrapped, TAG, True)
36
37 1
    return wrapped
38
39
40 1
def store_after(method):
41
    """Decorator for methods that should store after call."""
42
43 1
    if getattr(method, TAG, False):
44 1
        return method
45
46 1
    @functools.wraps(method)
47
    def wrapped(self, *args, **kwargs):
48
        """Decorated method."""
49 1
        result = method(self, *args, **kwargs)
50
51 1
        if not _private_call(method, args):
52 1
            mapper = get_mapper(self)
53 1
            if mapper and mapper.auto:
54 1
                log.debug("Storing after call: %s", method.__name__)
55 1
                mapper.store()
56
57 1
        return result
58
59 1
    setattr(wrapped, TAG, True)
60
61 1
    return wrapped
62
63
64 1
def _private_call(method, args, prefix='_'):
65
    """Determine if a call's first argument is a private variable name."""
66 1
    if method.__name__ in ('__getattribute__', '__setattr__'):
67 1
        assert isinstance(args[0], str)
68 1
        return args[0].startswith(prefix)
69
    else:
70 1
        return False
71
72
73 1
class Mappable(metaclass=abc.ABCMeta):
74
    """Base class for objects with attributes mapped to file."""
75
76
    # pylint: disable=no-member
77
78 1
    @fetch_before
79
    def __getattribute__(self, name):
80
        """Trigger object update when reading attributes."""
81 1
        return object.__getattribute__(self, name)
82
83 1
    @store_after
84
    def __setattr__(self, name, value):
85
        """Trigger file update when setting attributes."""
86 1
        super().__setattr__(name, value)
87
88 1
    @fetch_before
89
    def __iter__(self):
90
        """Trigger object update when iterating."""
91 1
        return super().__iter__()
92
93 1
    @fetch_before
94
    def __getitem__(self, key):
95
        """Trigger object update when reading an index."""
96 1
        return super().__getitem__(key)
97
98 1
    @store_after
99
    def __setitem__(self, key, value):
100
        """Trigger file update when setting an index."""
101 1
        super().__setitem__(key, value)
102
103 1
    @store_after
104
    def __delitem__(self, key):
105
        """Trigger file update when deleting an index."""
106 1
        super().__delitem__(key)
107
108 1
    @store_after
109
    def append(self, value):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
110
        """Trigger file update when appending items."""
111 1
        super().append(value)
112
113
114 1
def patch_methods(instance):
115 1
    log.debug("Patching methods on: %r", instance)
116 1
    cls = instance.__class__
117
118
    # TODO: determine a way to share the lists of methods to patch
119 1
    for name in ['__getattribute__', '__iter__', '__getitem__']:
120 1
        try:
121 1
            method = getattr(cls, name)
122 1
        except AttributeError:
123 1
            log.trace("No method: %s", name)
124
        else:
125 1
            modified_method = fetch_before(method)
126 1
            setattr(cls, name, modified_method)
127 1
            log.trace("Patched to fetch before call: %s", name)
128
129 1
    for name in ['__setattr__', '__setitem__', '__delitem__', 'append']:
130 1
        try:
131 1
            method = getattr(cls, name)
132 1
        except AttributeError:
133 1
            log.trace("No method: %s", name)
134
        else:
135 1
            modified_method = store_after(method)
136 1
            setattr(cls, name, modified_method)
137
            log.trace("Patched to store after call: %s", name)
138