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

yorm.bases.wrapped()   A

Complexity

Conditions 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4
Metric Value
cc 4
dl 0
loc 12
ccs 8
cts 8
cp 1
crap 4
rs 9.2
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