Completed
Push — develop ( 292e6b...e2d982 )
by Jace
02:51
created

yorm.decorator()   A

Complexity

Conditions 4

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4
Metric Value
cc 4
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 4
rs 9.2
1
"""Functions and decorators."""
2
3 1
import uuid
4
5 1
from . import common, exceptions
6 1
from .bases.mappable import patch_methods
7 1
from .mapper import 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, **kwargs):
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
    :param strict: ignore new attributes in files
40
41
    """
42 1
    log.info("Mapping %r to %s...", instance, path)
43 1
    _check_base(instance, mappable=False)
44
45 1
    patch_methods(instance)
46
47 1
    attrs = attrs or common.attrs[instance.__class__]
48 1
    mapper = Mapper(instance, path, attrs, **kwargs)
49 1
    common.set_mapper(instance, mapper)
50 1
    _check_existance(mapper, existing)
51
52 1
    if mapper.auto:
53 1
        if not mapper.exists:
54 1
            mapper.create()
55 1
            mapper.store()
56 1
        mapper.fetch()
57
58 1
    log.info("Mapped %r to %s", instance, path)
59 1
    return instance
60
61
62 1
def sync_instances(path_format, format_spec=None, attrs=None, **kwargs):
63
    """Class decorator to enable YAML mapping after instantiation.
64
65
    :param path_format: formatting string to create file paths for dump/load
66
    :param format_spec: dictionary to use for string formatting
67
    :param attrs: dictionary of attribute names mapped to converter classes
68
    :param existing: indicate if file is expected to exist or not
69
    :param auto: automatically store attributes to file
70
71
    """
72 1
    format_spec = format_spec or {}
73 1
    attrs = attrs or {}
74
75 1
    def decorator(cls):
76
        """Class decorator to map instances to files.."""
77
78 1
        init = cls.__init__
79
80 1
        def modified_init(self, *_args, **_kwargs):
81
            """Modified class __init__ that maps the resulting instance."""
82 1
            init(self, *_args, **_kwargs)
83
84 1
            log.info("Mapping instance of %r to '%s'...", cls, path_format)
85
86 1
            format_values = {}
87 1
            for key, value in format_spec.items():
88 1
                format_values[key] = getattr(self, value)
89 1
            if '{' + UUID + '}' in path_format:
90 1
                format_values[UUID] = uuid.uuid4().hex
91 1
            format_values['self'] = self
92
93 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...
94 1
            attrs.update(common.attrs[self.__class__])
95 1
            attrs.update(common.attrs[cls])
96
97 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...
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
    def decorator(cls):
114
        """Class decorator."""
115 1
        for name, converter in kwargs.items():
116 1
            common.attrs[cls][name] = converter
117
118 1
        return cls
119
120 1
    return decorator
121
122
123 1
def update(instance, fetch=True, force=True, store=True):
124
    """Synchronize changes between a mapped object and its file.
125
126
    :param instance: object with patched YAML mapping behavior
127
    :param fetch: update the object with changes from its file
128
    :param force: even if the file appears unchanged
129
    :param store: update the file with changes from the object
130
131
    """
132 1
    _check_base(instance, mappable=True)
133
134 1
    if fetch:
135 1
        update_object(instance, force=False)
136 1
    if store:
137 1
        update_file(instance)
138 1
    if fetch:
139 1
        update_object(instance, force=force)
140
141
142 1
def update_object(instance, existing=True, force=True):
143
    """Synchronize changes into a mapped object from its file.
144
145
    :param instance: object with patched YAML mapping behavior
146
    :param existing: indicate if file is expected to exist or not
147
    :param force: update the object even if the file appears unchanged
148
149
    """
150 1
    log.info("Manually updating %r from file...", instance)
151 1
    _check_base(instance, mappable=True)
152
153 1
    mapper = common.get_mapper(instance)
154 1
    _check_existance(mapper, existing)
155
156 1
    if mapper.modified or force:
157 1
        mapper.fetch()
158
159
160 1
def update_file(instance, existing=None, force=True):
161
    """Synchronize changes into a mapped object's file.
162
163
    :param instance: object with patched YAML mapping behavior
164
    :param existing: indicate if file is expected to exist or not
165
    :param force: update the file even if automatic sync is off
166
167
    """
168 1
    log.info("Manually saving %r to file...", instance)
169 1
    _check_base(instance, mappable=True)
170
171 1
    mapper = common.get_mapper(instance)
172 1
    _check_existance(mapper, existing)
173
174 1
    if mapper.auto or force:
175 1
        if not mapper.exists:
176 1
            mapper.create()
177 1
        mapper.store()
178
179
180 1
def synced(obj):
181
    """Determine if an object is already mapped to a file."""
182 1
    return bool(common.get_mapper(obj))
183
184
185 1
def _check_base(obj, mappable=True):
186
    """Confirm an object's base class is `Mappable` as required."""
187 1
    if mappable and not synced(obj):
188 1
        raise exceptions.MappingError("{} is not mapped".format(repr(obj)))
189 1
    if not mappable and synced(obj):
190 1
        raise exceptions.MappingError("{} is already mapped".format(repr(obj)))
191
192
193 1
def _check_existance(mapper, existing=None):
194
    """Confirm the expected state of the file.
195
196
    :param existing: indicate if file is expected to exist or not
197
198
    """
199 1
    if existing is True:
200 1
        if not mapper.exists:
201 1
            raise exceptions.FileMissingError
202 1
    elif existing is False:
203 1
        if mapper.exists:
204
            raise exceptions.FileAlreadyExistsError
205