Completed
Push — develop ( b4a21c...b1df34 )
by Jace
02:37
created

Mappable   A

Complexity

Total Complexity 7

Size/Duplication

Total Lines 39
Duplicated Lines 0 %

Test Coverage

Coverage 100%
Metric Value
wmc 7
dl 0
loc 39
ccs 15
cts 15
cp 1
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __delitem__() 0 4 1
A __iter__() 0 4 1
A __getattribute__() 0 4 1
A __getitem__() 0 4 1
A __setitem__() 0 4 1
A __setattr__() 0 4 1
A append() 0 4 1
1
"""Base classes for mapping."""
2
3 1
import abc
4 1
import functools
5
6 1
from .. import common
7
8
9 1
log = common.logger(__name__)
10
11 1
TAG = '_modified_by_yorm'
12
13
14 1
def fetch_before(method):
15
    """Decorator for methods that should fetch before call."""
16
17 1
    if getattr(method, TAG, False):
18 1
        return method
19
20 1
    @functools.wraps(method)
21
    def wrapped(self, *args, **kwargs):
22
        """Decorated method."""
23 1
        if not _private_call(method, args):
24 1
            mapper = common.get_mapper(self)
25 1
            if mapper and mapper.modified:
26 1
                log.debug("Fetching before call: %s", method.__name__)
27 1
                mapper.fetch()
28 1
                if mapper.store_after_fetch:
29 1
                    mapper.store()
30 1
                    mapper.modified = False
31
32 1
        return method(self, *args, **kwargs)
33
34 1
    setattr(wrapped, TAG, True)
35
36 1
    return wrapped
37
38
39 1
def store_after(method):
40
    """Decorator for methods that should store after call."""
41
42 1
    if getattr(method, TAG, False):
43 1
        return method
44
45 1
    @functools.wraps(method)
46
    def wrapped(self, *args, **kwargs):
47
        """Decorated method."""
48 1
        result = method(self, *args, **kwargs)
49
50 1
        if not _private_call(method, args):
51 1
            mapper = common.get_mapper(self)
52 1
            if mapper and mapper.auto:
53 1
                log.debug("Storing after call: %s", method.__name__)
54 1
                mapper.store()
55
56 1
        return result
57
58 1
    setattr(wrapped, TAG, True)
59
60 1
    return wrapped
61
62
63 1
def _private_call(method, args, prefix='_'):
64
    """Determine if a call's first argument is a private variable name."""
65 1
    if method.__name__ in ('__getattribute__', '__setattr__'):
66 1
        assert isinstance(args[0], str)
67 1
        return args[0].startswith(prefix)
68
    else:
69 1
        return False
70
71
72
# TODO: move these methods inside of `Container`
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