Passed
Pull Request — develop (#143)
by Jamie
02:18
created

_unpack_parsed_fields()   A

Complexity

Conditions 3

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 1
cts 1
cp 1
rs 10
cc 3
crap 3
1
"""Functions to interact with mapped classes and instances."""
2
3 1
import inspect
4 1
import logging
5
import string
6 1
import glob
7
import types
8 1
import parse
9
10
from . import common, exceptions
11 1
12
log = logging.getLogger(__name__)
13
14
15
def create(class_or_instance, *args, overwrite=False, **kwargs):
16
    """Create a new mapped object.
17 1
18 1
    NOTE: Calling this function is unnecessary with 'auto_create' enabled.
19
20 1
    """
21 1
    instance = _instantiate(class_or_instance, *args, **kwargs)
22 1
    mapper = common.get_mapper(instance, expected=True)
23
24 1
    if mapper.exists and not overwrite:
25
        msg = "{!r} already exists".format(mapper.path)
26
        raise exceptions.DuplicateMappingError(msg)
27 1
28
    return load(save(instance))
29 1
30 1
31
def find(class_or_instance, *args, create=False, **kwargs):  # pylint: disable=redefined-outer-name
32 1
    """Find a matching mapped object or return None."""
33 1
    instance = _instantiate(class_or_instance, *args, **kwargs)
34 1
    mapper = common.get_mapper(instance, expected=True)
35 1
36
    if mapper.exists:
37 1
        return instance
38
    elif create:
39
        return save(instance)
40 1
    else:
41
        return None
42 1
43 1
44
class GlobFormatter(string.Formatter):
45
    """
46 1
    Uses '*' for all unknown fields
47
    """
48
49
    WILDCARD = object()
50
51
    def get_field(self, field_name, args, kwargs):
52
        try:
53 1
            return super().get_field(field_name, args, kwargs)
54
        except (KeyError, IndexError, AttributeError):
55 1
            return self.WILDCARD, None
56
57 1
    def get_value(self, key, args, kwargs):
58
        try:
59
            return super().get_value(key, args, kwargs)
60 1
        except (KeyError, IndexError, AttributeError):
61
            return self.WILDCARD
62
63
    def convert_field(self, value, conversion):
64
        if value is self.WILDCARD:
65
            return self.WILDCARD
66 1
        else:
67
            return super().convert_field(value, conversion)
68 1
69 1
    def format_field(self, value, format_spec):
70 1
        if value is self.WILDCARD:
71
            return '*'
72 1
        else:
73 1
            return super().format_field(value, format_spec)
74
75 1
76
def _unpack_parsed_fields(pathfields):
77 1
    return {
78
        (k[len('self.'):] if k.startswith('self.') else k): v
79
        for k, v in pathfields.items()
80 1
    }
81
82 1
83
def match(cls_or_path, factory=None, **kwargs):
84 1
    """match(class, [callable], ...) -> instance, ...
85
    match(str, callable, ...) -> instance, ...
86 1
87
    Yield all matching mapped objects. Can be used two ways:
88
    * With a YORM-decorated class, optionally with a factory callable
89 1
    * With a Python 3-style string template and a factory callable
90 1
91 1
    The factory callable must accept keyuword arguments, extracted from the file
92
    name merged with those passed to match(). If no factory is given, the class
93 1
    itself is used as the factory (same signature).
94 1
    """
95
    if isinstance(cls_or_path, type):
96 1
        path_format = common.path_formats[cls_or_path]
97
        # Let KeyError fail through
98
        if factory is None:
99
            factory = cls_or_path
100
    else:
101
        path_format = cls_or_path
102
        if factory is None:
103
            raise TypeError("Factory must be given if a string template is used")
104
105
    log.debug((path_format, factory, kwargs))
106
    gf = GlobFormatter()
107
    mock = types.SimpleNamespace(**kwargs)
108
109
    kwargs['self'] = mock
110
    posix_pattern = gf.vformat(path_format, (), kwargs.copy())
111
    del kwargs['self']
112
    py_pattern = parse.compile(path_format)
113
114
    for filename in glob.iglob(posix_pattern):
115
        pathfields = py_pattern.parse(filename).named
116
        fields = _unpack_parsed_fields(pathfields)
117
        fields.update(kwargs)
118
        yield factory(**fields)
119
120
121
def load(instance):
122
    """Force the loading of a mapped object's file.
123
124
    NOTE: Calling this function is unnecessary. It exists for the
125
        aesthetic purpose of having symmetry between save and load.
126
127
    """
128
    mapper = common.get_mapper(instance, expected=True)
129
130
    mapper.load()
131
132
    return instance
133
134
135
def save(instance):
136
    """Save a mapped object to file.
137
138
    NOTE: Calling this function is unnecessary with 'auto_save' enabled.
139
140
    """
141
    mapper = common.get_mapper(instance, expected=True)
142
143
    if mapper.deleted:
144
        msg = "{!r} was deleted".format(mapper.path)
145
        raise exceptions.DeletedFileError(msg)
146
147
    if not mapper.exists:
148
        mapper.create()
149
150
    mapper.save()
151
152
    return instance
153
154
155
def delete(instance):
156
    """Delete a mapped object's file."""
157
    mapper = common.get_mapper(instance, expected=True)
158
159
    mapper.delete()
160
161
    return None
162
163
164
def _instantiate(class_or_instance, *args, **kwargs):
165
    if inspect.isclass(class_or_instance):
166
        instance = class_or_instance(*args, **kwargs)
167
    else:
168
        assert not args
169
        instance = class_or_instance
170
171
    return instance
172