Completed
Push — develop ( 1ca3e5...47068b )
by Jace
02:03
created

yorm.attr()   A

Complexity

Conditions 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3
Metric Value
cc 3
dl 0
loc 14
rs 9.4285
ccs 6
cts 6
cp 1
crap 3

1 Method

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