Completed
Push — develop ( 42c0e7...a8ebf0 )
by Jace
03:06
created

Mapper.load()   D

Complexity

Conditions 11

Size

Total Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 56
ccs 39
cts 39
cp 1
rs 4.4262
cc 11
crap 11

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Mapper.load() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Core object-file mapping functionality."""
2
3 1
import functools
4 1
from pprint import pformat
5 1
import logging
6
7 1
from . import common, diskutils, exceptions, types, settings
8 1
from .bases import Container
9
10 1
log = logging.getLogger(__name__)
11
12
13 1
def file_required(method):
14
    """Decorator for methods that require the file to exist."""
15
16 1
    @functools.wraps(method)
17
    def wrapped(self, *args, **kwargs):
18 1
        if self.deleted:
19 1
            msg = "File deleted: {}".format(self.path)
20 1
            raise exceptions.DeletedFileError(msg)
21 1
        elif self.missing and not settings.fake:
22 1
            msg = "File missing: {}".format(self.path)
23 1
            raise exceptions.MissingFileError(msg)
24
        else:
25 1
            return method(self, *args, **kwargs)
26
27 1
    return wrapped
28
29
30 1
def prevent_recursion(method):
31
    """Decorator to prevent indirect recursive calls."""
32
33 1
    @functools.wraps(method)
34
    def wrapped(self, *args, **kwargs):
35
        # pylint: disable=protected-access
36 1
        if self._activity:
37 1
            return
38 1
        self._activity = True
39 1
        result = method(self, *args, **kwargs)
40 1
        self._activity = False
41 1
        return result
42
43 1
    return wrapped
44
45
46 1
def prefix(obj):
47
    """Prefix a string with a fake designator if enabled."""
48 1
    fake = "(fake) " if settings.fake else ""
49 1
    name = obj if isinstance(obj, str) else "'{}'".format(obj)
50 1
    return fake + name
51
52
53 1
class Mapper:
54
    """Utility class to map an object's attributes to a file.
55
56
    To start mapping attributes to a file:
57
58
        create -> [empty] -> FILE
59
60
    When getting an attribute:
61
62
        FILE -> read -> [text] -> parse -> [dict] -> load -> ATTRIBUTES
63
64
    When setting an attribute:
65
66
        ATTRIBUTES -> save -> [dict] -> dump -> [text] -> write -> FILE
67
68
    After the mapped file is no longer needed:
69
70
        delete -> [null] -> FILE
71
72
    """
73
74 1
    def __init__(self, obj, path, attrs, *,
75
                 auto_create=True, auto_save=True, auto_track=False):
76 1
        self._obj = obj
77 1
        self.path = path
78 1
        self.attrs = attrs
79 1
        self.auto_create = auto_create
80 1
        self.auto_save = auto_save
81 1
        self.auto_track = auto_track
82
83 1
        self.exists = diskutils.exists(self.path)
84 1
        self.deleted = False
85 1
        self.auto_save_after_load = False
86
87 1
        self._activity = False
88 1
        self._timestamp = 0
89 1
        self._fake = ""
90
91 1
    def __str__(self):
92 1
        return str(self.path)
93
94 1
    @property
95
    def missing(self):
96 1
        return not self.exists
97
98 1
    @property
99
    def modified(self):
100
        """Determine if the file has been modified."""
101 1
        if settings.fake:
102 1
            changes = self._timestamp is not None
103 1
            return changes
104 1
        elif not self.exists:
105 1
            return True
106
        else:
107
            # TODO: this raises an exception is the file is missing
108 1
            was = self._timestamp
109 1
            now = diskutils.stamp(self.path)
110 1
            return was != now
111
112 1
    @modified.setter
113 1
    @file_required
114
    def modified(self, changes):
115
        """Mark the file as modified if there are changes."""
116 1
        if changes:
117 1
            log.debug("Marked %s as modified", prefix(self))
118 1
            self._timestamp = 0
119
        else:
120 1
            if settings.fake or self.path is None:
121 1
                self._timestamp = None
122
            else:
123 1
                self._timestamp = diskutils.stamp(self.path)
124 1
            log.debug("Marked %s as unmodified", prefix(self))
125
126 1
    @property
127
    def text(self):
128
        """Get file contents as a string."""
129 1
        log.info("Getting contents of %s...", prefix(self))
130 1
        if settings.fake:
131 1
            text = self._fake
132
        else:
133 1
            text = self._read()
134 1
        log.trace("Text read: \n%s", text[:-1])
135 1
        return text
136
137 1
    @text.setter
138
    def text(self, text):
139
        """Set file contents from a string."""
140 1
        log.info("Setting contents of %s...", prefix(self))
141 1
        if settings.fake:
142 1
            self._fake = text
143
        else:
144 1
            self._write(text)
145 1
        log.trace("Text wrote: \n%s", text.rstrip())
146 1
        self.modified = True
147
148 1
    @property
149
    def data(self):
150
        """Get the file values as a dictionary."""
151 1
        text = self._read()
152 1
        data = diskutils.parse(text, self.path)
153 1
        log.trace("Parsed data: \n%s", pformat(data))
154 1
        return data
155
156 1
    @data.setter
157
    def data(self, data):
158
        """Set the file values from a dictionary."""
159 1
        text = diskutils.dump(data, self.path)
160 1
        self._write(text)
161
162 1
    def create(self):
163
        """Create a new file for the object."""
164 1
        log.info("Creating %s for %r...", prefix(self), self._obj)
165 1
        if self.exists:
166 1
            log.warning("Already created: %s", self)
167 1
            return
168 1
        if not settings.fake:
169 1
            diskutils.touch(self.path)
170 1
        self.exists = True
171 1
        self.deleted = False
172
173 1
    @file_required
174 1
    @prevent_recursion
175
    def load(self):
176
        """Update the object's mapped attributes from its file."""
177 1
        log.info("Loading %r from %s...", self._obj, prefix(self))
178
179
        # Update all attributes
180 1
        attrs2 = self.attrs.copy()
181 1
        for name, data in self.data.items():
182 1
            attrs2.pop(name, None)
183
184
            # Find a matching converter
185 1
            try:
186 1
                converter = self.attrs[name]
187 1
            except KeyError:
188 1
                if self.auto_track:
189 1
                    converter = types.match(name, data)
190 1
                    self.attrs[name] = converter
191
                else:
192 1
                    msg = "Ignored unknown file attribute: %s = %r"
193 1
                    log.warning(msg, name, data)
194 1
                    continue
195
196
            # Convert the parsed value to the attribute's final type
197 1
            attr = getattr(self._obj, name, None)
198 1
            if isinstance(attr, converter) and \
199
                    issubclass(converter, Container):
200 1
                attr.update_value(data, auto_track=self.auto_track)
201
            else:
202 1
                log.trace("Converting attribute %r to %r", name, converter)
203 1
                attr = converter.to_value(data)
204 1
                setattr(self._obj, name, attr)
205 1
            self._remap(attr, self)
206 1
            log.trace("Value loaded: %s = %r", name, attr)
207
208
        # Add missing attributes
209 1
        for name, converter in attrs2.items():
210 1
            try:
211 1
                existing_attr = getattr(self._obj, name)
212 1
            except AttributeError:
213 1
                value = converter.create_default()
214 1
                msg = "Default value for missing object attribute: %s = %r"
215 1
                log.warning(msg, name, value)
216 1
                setattr(self._obj, name, value)
217 1
                self._remap(value, self)
218
            else:
219 1
                if issubclass(converter, Container) and \
220
                        not isinstance(existing_attr, converter):
221 1
                    msg = "Converting container attribute %r to %r"
222 1
                    log.trace(msg, name, converter)
223 1
                    value = converter.create_default()
224 1
                    setattr(self._obj, name, value)
225 1
                    self._remap(value, self)
226
227
        # Set meta attributes
228 1
        self.modified = False
229
230 1
    def _remap(self, obj, root):
231
        """Attach mapper on nested attributes."""
232 1
        if isinstance(obj, Container):
233 1
            common.set_mapper(obj, root)
234
235 1
            if isinstance(obj, dict):
236 1
                for obj2 in obj.values():
237 1
                    self._remap(obj2, root)
238
            else:
239 1
                assert isinstance(obj, list)
240 1
                for obj2 in obj:
241 1
                    self._remap(obj2, root)
242
243 1
    @file_required
244 1
    @prevent_recursion
245
    def save(self):
246
        """Format and save the object's mapped attributes to its file."""
247 1
        log.info("Saving %r to %s...", self._obj, prefix(self))
248
249
        # Format the data items
250 1
        data = self.attrs.__class__()
251 1
        for name, converter in self.attrs.items():
252 1
            try:
253 1
                value = getattr(self._obj, name)
254 1
            except AttributeError:
255 1
                data2 = converter.to_data(None)
256 1
                msg = "Default data for missing object attribute: %s = %r"
257 1
                log.warning(msg, name, data2)
258
            else:
259 1
                data2 = converter.to_data(value)
260
261 1
            log.trace("Data to save: %s = %r", name, data2)
262 1
            data[name] = data2
263
264
        # Save the formatted to disk
265 1
        self.data = data
266
267
        # Set meta attributes
268 1
        self.modified = True
269 1
        self.auto_save_after_load = self.auto_save
270
271 1
    def delete(self):
272
        """Delete the object's file from the file system."""
273 1
        if self.exists:
274 1
            log.info("Deleting %s...", prefix(self))
275 1
            diskutils.delete(self.path)
276
        else:
277 1
            log.warning("Already deleted: %s", self)
278 1
        self.exists = False
279 1
        self.deleted = True
280
281 1
    @file_required
282
    def _read(self):
283
        """Read text from the object's file."""
284 1
        if settings.fake:
285 1
            return self._fake
286 1
        elif not self.exists:
287
            return ""
288
        else:
289 1
            return diskutils.read(self.path)
290
291 1
    @file_required
292
    def _write(self, text):
293
        """Write text to the object's file."""
294 1
        if settings.fake:
295 1
            self._fake = text
296
        else:
297
            diskutils.write(text, self.path)
298