Completed
Push — develop ( ac1289...37ee14 )
by Jace
6s
created

yorm._ordered()   A

Complexity

Conditions 3

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3
Metric Value
cc 3
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 3
rs 9.4285
1
"""Functions and decorators."""
2
3 1
import uuid
4 1
from collections import OrderedDict
5
6 1
from . import common, exceptions
7 1
from .bases.mappable import patch_methods
8 1
from .mapper import Mapper
9
10 1
log = common.logger(__name__)
11
12 1
UUID = 'UUID'
13
14
15 1
def sync(*args, **kwargs):
16
    """Convenience function to forward calls based on arguments.
17
18
    This function will call either:
19
20
    * `sync_object` - when given an unmapped object
21
    * `sync_instances` - when used as the class decorator
22
23
    Consult the signature of each call for more information.
24
25
    """
26 1
    if 'path_format' in kwargs or args and isinstance(args[0], str):
27 1
        return sync_instances(*args, **kwargs)
28
    else:
29 1
        return sync_object(*args, **kwargs)
30
31
32 1
def sync_object(instance, path, attrs=None, existing=None, **kwargs):
33
    """Enable YAML mapping on an object.
34
35
    :param instance: object to patch with YAML mapping behavior
36
    :param path: file path for dump/load
37
    :param attrs: dictionary of attribute names mapped to converter classes
38
    :param existing: indicate if file is expected to exist or not
39
    :param auto: automatically store attributes to file
40
    :param strict: ignore new attributes in files
41
42
    """
43 1
    log.info("Mapping %r to %s...", instance, path)
44 1
    _check_base(instance, mappable=False)
45
46 1
    patch_methods(instance)
47
48 1
    attrs = _ordered(attrs) or common.attrs[instance.__class__]
49 1
    mapper = Mapper(instance, path, attrs, **kwargs)
50 1
    common.set_mapper(instance, mapper)
51 1
    _check_existance(mapper, existing)
52
53 1
    if mapper.auto:
54 1
        if not mapper.exists:
55 1
            mapper.create()
56 1
            mapper.store()
57 1
        mapper.fetch()
58
59 1
    log.info("Mapped %r to %s", instance, path)
60 1
    return instance
61
62
63 1
def sync_instances(path_format, format_spec=None, attrs=None, **kwargs):
64
    """Class decorator to enable YAML mapping after instantiation.
65
66
    :param path_format: formatting string to create file paths for dump/load
67
    :param format_spec: dictionary to use for string formatting
68
    :param attrs: dictionary of attribute names mapped to converter classes
69
    :param existing: indicate if file is expected to exist or not
70
    :param auto: automatically store attributes to file
71
72
    """
73 1
    format_spec = format_spec or {}
74 1
    attrs = attrs or OrderedDict()
75
76 1
    def decorator(cls):
77
        """Class decorator to map instances to files.."""
78
79 1
        init = cls.__init__
80
81 1
        def modified_init(self, *_args, **_kwargs):
82
            """Modified class __init__ that maps the resulting instance."""
83 1
            init(self, *_args, **_kwargs)
84
85 1
            log.info("Mapping instance of %r to '%s'...", cls, path_format)
86
87 1
            format_values = {}
88 1
            for key, value in format_spec.items():
89 1
                format_values[key] = getattr(self, value)
90 1
            if '{' + UUID + '}' in path_format:
91 1
                format_values[UUID] = uuid.uuid4().hex
92 1
            format_values['self'] = self
93
94 1
            common.attrs[cls].update(attrs)
95 1
            common.attrs[cls].update(common.attrs[self.__class__])
96 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...
97 1
            sync_object(self, path, **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...
98
99 1
        modified_init.__doc__ = init.__doc__
100 1
        cls.__init__ = modified_init
101
102 1
        return cls
103
104 1
    return decorator
105
106
107 1
def attr(**kwargs):
108
    """Class decorator to map attributes to types.
109
110
    :param kwargs: keyword arguments mapping attribute name to converter class
111
112
    """
113 1
    if len(kwargs) != 1:
114 1
        raise ValueError("Single attribute required: {}".format(kwargs))
115
116 1
    def decorator(cls):
117
        """Class decorator."""
118 1
        previous = common.attrs[cls]
119 1
        common.attrs[cls] = OrderedDict()
120 1
        for name, converter in kwargs.items():
121 1
            common.attrs[cls][name] = converter
122 1
        for name, converter in previous.items():
123 1
            common.attrs[cls][name] = converter
124
125 1
        return cls
126
127 1
    return decorator
128
129
130 1
def update(instance, fetch=True, force=True, store=True):
131
    """Synchronize changes between a mapped object and its file.
132
133
    :param instance: object with patched YAML mapping behavior
134
    :param fetch: update the object with changes from its file
135
    :param force: even if the file appears unchanged
136
    :param store: update the file with changes from the object
137
138
    """
139 1
    _check_base(instance, mappable=True)
140
141 1
    if fetch:
142 1
        update_object(instance, force=False)
143 1
    if store:
144 1
        update_file(instance)
145 1
    if fetch:
146 1
        update_object(instance, force=force)
147
148
149 1
def update_object(instance, existing=True, force=True):
150
    """Synchronize changes into a mapped object from its file.
151
152
    :param instance: object with patched YAML mapping behavior
153
    :param existing: indicate if file is expected to exist or not
154
    :param force: update the object even if the file appears unchanged
155
156
    """
157 1
    log.info("Manually updating %r from file...", instance)
158 1
    _check_base(instance, mappable=True)
159
160 1
    mapper = common.get_mapper(instance)
161 1
    _check_existance(mapper, existing)
162
163 1
    if mapper.modified or force:
164 1
        mapper.fetch()
165
166
167 1
def update_file(instance, existing=None, force=True):
168
    """Synchronize changes into a mapped object's file.
169
170
    :param instance: object with patched YAML mapping behavior
171
    :param existing: indicate if file is expected to exist or not
172
    :param force: update the file even if automatic sync is off
173
174
    """
175 1
    log.info("Manually saving %r to file...", instance)
176 1
    _check_base(instance, mappable=True)
177
178 1
    mapper = common.get_mapper(instance)
179 1
    _check_existance(mapper, existing)
180
181 1
    if mapper.auto or force:
182 1
        if not mapper.exists:
183 1
            mapper.create()
184 1
        mapper.store()
185
186
187 1
def synced(obj):
188
    """Determine if an object is already mapped to a file."""
189 1
    return bool(common.get_mapper(obj))
190
191
192 1
def _ordered(data):
193
    """Sort a dictionary-like object by key."""
194 1
    if data is None:
195 1
        return None
196 1
    return OrderedDict(sorted(data.items(), key=lambda pair: pair[0]))
197
198
199 1
def _check_base(obj, mappable=True):
200
    """Confirm an object's base class is `Mappable` as required."""
201 1
    if mappable and not synced(obj):
202 1
        raise exceptions.MappingError("{} is not mapped".format(repr(obj)))
203 1
    if not mappable and synced(obj):
204 1
        raise exceptions.MappingError("{} is already mapped".format(repr(obj)))
205
206
207 1
def _check_existance(mapper, existing=None):
208
    """Confirm the expected state of the file.
209
210
    :param existing: indicate if file is expected to exist or not
211
212
    """
213 1
    if existing is True:
214 1
        if not mapper.exists:
215 1
            raise exceptions.FileMissingError
216 1
    elif existing is False:
217 1
        if mapper.exists:
218
            raise exceptions.FileAlreadyExistsError
219