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

yorm.sync_object()   B

Complexity

Conditions 3

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

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