Completed
Push — develop ( b517c8...2e9112 )
by Jace
02:23
created

yorm.decorator()   B

Complexity

Conditions 4

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4
Metric Value
cc 4
dl 0
loc 28
ccs 18
cts 18
cp 1
crap 4
rs 8.5806
1
"""Functions and decorators."""
2
3 1
import uuid
4
5 1
from . import common, exceptions
6 1
from .bases.mappable import fetch_before, store_after
7 1
from .mapper import get_mapper, set_mapper
8
9 1
log = common.logger(__name__)
10
11 1
UUID = 'UUID'
12
13
14 1
def sync(*args, **kwargs):
15
    """Convenience function to forward calls based on arguments.
16
17
    This function will call either:
18
19
    * `sync_object` - when given an unmapped object
20
    * `sync_instances` - when used as the class decorator
21
22
    Consult the signature of each call for more information.
23
24
    """
25 1
    if 'path_format' in kwargs or args and isinstance(args[0], str):
26 1
        return sync_instances(*args, **kwargs)
27
    else:
28 1
        return sync_object(*args, **kwargs)
29
30
31 1
def sync_object(instance, path, attrs=None, existing=None, auto=True):
32
    """Enable YAML mapping on an object.
33
34
    :param instance: object to patch with YAML mapping behavior
35
    :param path: file path for dump/load
36
    :param attrs: dictionary of attribute names mapped to converter classes
37
    :param existing: indicate if file is expected to exist or not
38
    :param auto: automatically store attributes to file
39
40
    """
41 1
    log.info("Mapping %r to %s...", instance, path)
42 1
    _check_base(instance, mappable=False)
43
44 1
    attrs = attrs or common.attrs[instance.__class__]
45
46 1
    for name in ['__getattribute__', '__iter__', '__getitem__']:
47 1
        try:
48 1
            method = getattr(instance.__class__, name)
49 1
        except AttributeError:
50 1
            log.trace("No method: %s", name)
51
        else:
52 1
            modified_method = fetch_before(method)
53 1
            setattr(instance.__class__, name, modified_method)
54 1
            log.trace("Method patched to fetch: %s", name)
55
56 1
    for name in ['__setattr__', '__setitem__', '__delitem__', 'append']:
57 1
        try:
58 1
            method = getattr(instance.__class__, name)
59 1
        except AttributeError:
60 1
            log.trace("No method: %s", name)
61
        else:
62 1
            modified_method = store_after(method)
63 1
            setattr(instance.__class__, name, modified_method)
64 1
            log.trace("Method patched to store: %s", name)
65
66 1
    mapper = set_mapper(instance, path, attrs, auto=auto)
67 1
    _check_existance(mapper, existing)
68
69 1
    if mapper.auto:
70 1
        if not mapper.exists:
71 1
            mapper.create()
72 1
            mapper.store()
73 1
        mapper.fetch()
74
75 1
    log.info("Mapped %r to %s", instance, path)
76 1
    return instance
77
78
79 1
def sync_instances(path_format, format_spec=None, attrs=None, **kwargs):
80
    """Class decorator to enable YAML mapping after instantiation.
81
82
    :param path_format: formatting string to create file paths for dump/load
83
    :param format_spec: dictionary to use for string formatting
84
    :param attrs: dictionary of attribute names mapped to converter classes
85
    :param existing: indicate if file is expected to exist or not
86
    :param auto: automatically store attributes to file
87
88
    """
89 1
    format_spec = format_spec or {}
90 1
    attrs = attrs or {}
91
92 1
    def decorator(cls):
93
        """Class decorator to map instances to files.."""
94
95 1
        init = cls.__init__
96
97 1
        def modified_init(self, *_args, **_kwargs):
98
            """Modified class __init__ that maps the resulting instance."""
99 1
            init(self, *_args, **_kwargs)
100
101 1
            log.info("Mapping instance of %r to '%s'...", cls, path_format)
102
103 1
            format_values = {}
104 1
            for key, value in format_spec.items():
105 1
                format_values[key] = getattr(self, value)
106 1
            if '{' + UUID + '}' in path_format:
107 1
                format_values[UUID] = uuid.uuid4().hex
108 1
            format_values['self'] = self
109
110 1
            path = path_format.format(**format_values)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
111 1
            attrs.update(common.attrs[self.__class__])
112 1
            attrs.update(common.attrs[cls])
113
114 1
            sync_object(self, path, attrs, **kwargs)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
115
116 1
        modified_init.__doc__ = init.__doc__
117 1
        cls.__init__ = modified_init
118
119 1
        return cls
120
121 1
    return decorator
122
123
124 1
def attr(**kwargs):
125
    """Class decorator to map attributes to types.
126
127
    :param kwargs: keyword arguments mapping attribute name to converter class
128
129
    """
130 1
    def decorator(cls):
131
        """Class decorator."""
132 1
        for name, converter in kwargs.items():
133 1
            common.attrs[cls][name] = converter
134
135 1
        return cls
136
137 1
    return decorator
138
139
140 1
def update(instance, fetch=True, force=True, store=True):
141
    """Synchronize changes between a mapped object and its file.
142
143
    :param instance: object with patched YAML mapping behavior
144
    :param fetch: update the object with changes from its file
145
    :param force: even if the file appears unchanged
146
    :param store: update the file with changes from the object
147
148
    """
149 1
    _check_base(instance, mappable=True)
150
151 1
    if fetch:
152 1
        update_object(instance, force=False)
153 1
    if store:
154 1
        update_file(instance)
155 1
    if fetch:
156 1
        update_object(instance, force=force)
157
158
159 1
def update_object(instance, existing=True, force=True):
160
    """Synchronize changes into a mapped object from its file.
161
162
    :param instance: object with patched YAML mapping behavior
163
    :param existing: indicate if file is expected to exist or not
164
    :param force: update the object even if the file appears unchanged
165
166
    """
167 1
    log.info("Manually updating %r from file...", instance)
168 1
    _check_base(instance, mappable=True)
169
170 1
    mapper = get_mapper(instance)
171 1
    _check_existance(mapper, existing)
172
173 1
    if mapper.modified or force:
174 1
        mapper.fetch()
175
176
177 1
def update_file(instance, existing=None, force=True):
178
    """Synchronize changes into a mapped object's file.
179
180
    :param instance: object with patched YAML mapping behavior
181
    :param existing: indicate if file is expected to exist or not
182
    :param force: update the file even if automatic sync is off
183
184
    """
185 1
    log.info("Manually saving %r to file...", instance)
186 1
    _check_base(instance, mappable=True)
187
188 1
    mapper = get_mapper(instance)
189 1
    _check_existance(mapper, existing)
190
191 1
    if mapper.auto or force:
192 1
        if not mapper.exists:
193 1
            mapper.create()
194 1
        mapper.store()
195
196
197 1
def synced(obj):
198
    """Determine if an object is already mapped to a file."""
199 1
    return bool(get_mapper(obj))
200
201
202 1
def _check_base(obj, mappable=True):
203
    """Confirm an object's base class is `Mappable` as required."""
204 1
    if mappable and not synced(obj):
205 1
        raise exceptions.MappingError("{} is not mapped".format(repr(obj)))
206 1
    if not mappable and synced(obj):
207 1
        raise exceptions.MappingError("{} is already mapped".format(repr(obj)))
208
209
210 1
def _check_existance(mapper, existing=None):
211
    """Confirm the expected state of the file.
212
213
    :param existing: indicate if file is expected to exist or not
214
215
    """
216 1
    if existing is True:
217 1
        if not mapper.exists:
218 1
            raise exceptions.FileMissingError
219 1
    elif existing is False:
220 1
        if mapper.exists:
221
            raise exceptions.FileAlreadyExistsError
222