Completed
Push — master ( 6a32f2...18db63 )
by Max
01:55
created

_is_namespaceable()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
"""Class Namespaces (internal module)
2
3
All of the guts of the class namespace implementation.
4
5
"""
6
7
8
# See https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/
9
10 6
import collections.abc
11 6
import functools
12 6
import itertools
13 6
import sys
14 6
import weakref
15
16
17 6
ENABLE_SET_NAME = sys.version_info >= (3, 6)
18
19
20 6
_PROXY_INFOS = weakref.WeakKeyDictionary()
21 6
_SENTINEL = object()
22
23
24 6
class _DescriptorInspector(collections.namedtuple('_DescriptorInspector',
25
                                                  ['object', 'dict'])):
26
27
    """Wrapper around objects. Provides access to descriptor protocol."""
28
29 6
    __slots__ = ()
30
31 6
    def __new__(cls, obj):
32 6
        dct = collections.ChainMap(*[vars(cls) for cls in type(obj).__mro__])
33 6
        return super().__new__(cls, obj, dct)
34
35 6
    @property
36
    def has_get(self):
37
        """Return whether self.object's mro provides __get__."""
38 6
        return '__get__' in self.dict
39
40 6
    @property
41
    def has_set(self):
42
        """Return whether self.object's mro provides __set__."""
43 6
        return '__set__' in self.dict
44
45 6
    @property
46
    def has_delete(self):
47
        """Return whether self.object's mro provides __delete__."""
48 6
        return '__delete__' in self.dict
49
50 6
    if ENABLE_SET_NAME:
51 2
        @property
52
        def has_set_name(self):
53
            """Return whether self.object's mro provides __set_name__."""
54 2
            return '__set_name__' in self.dict
55
56 2
        def set_name(self, owner, name):
57
            """Call __set_name__, bypassing descriptor protocol."""
58 2
            self.get_as_attribute('__set_name__')(self.object, owner, name)
59
60 2
        @property
61
        def has_non_data(self):
62
            """Return whether self.object's mro provides non-data methods."""
63 2
            return self.has_get or self.has_set_name
64
    else:
65 4
        has_non_data = has_get
66
67 6
    @property
68
    def is_data(self):
69
        """Returns whether self.object is a data descriptor."""
70 6
        return self.has_set or self.has_delete
71
72 6
    @property
73
    def is_non_data(self):
74
        """Returns whether self.object is a non-data descriptor."""
75
        return self.has_non_data and not self.is_data
76
77 6
    @property
78
    def is_descriptor(self):
79
        """Returns whether self.object is a descriptor."""
80 6
        return self.has_non_data or self.is_data
81
82 6
    def get_as_attribute(self, key):
83
        """Return attribute with the given name, or raise AttributeError."""
84 6
        try:
85 6
            return self.dict[key]
86
        except KeyError:
87
            raise AttributeError(key)
88
89 6
    def get(self, instance, owner):
90
        """Return the result of __get__, bypassing descriptor protocol."""
91 6
        return self.get_as_attribute('__get__')(self.object, instance, owner)
92
93 6
    def set(self, instance, value):
94
        """Call __set__, bypassing descriptor protocol."""
95
        self.get_as_attribute('__set__')(self.object, instance, value)
96
97 6
    def delete(self, instance):
98
        """Call __delete__, bypassing descriptor protocol."""
99
        self.get_as_attribute('__delete__')(self.object, instance)
100
101
102 6
def _no_blocker(dct, cls):
103
    """Return True if there's a non-Namespace object in the path for cls."""
104 6
    try:
105 6
        namespace = vars(cls)
106 6
        for name in dct.path:
107 6
            namespace = namespace[name]
108 6
            if not isinstance(namespace, Namespace):
109 6
                return False
110 6
    except KeyError:
1 ignored issue
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
111 6
        pass
112 6
    return True
113
114
115 6
def _is_namespaceable(cls):
116
    """Partial function for filter."""
117 6
    return isinstance(cls, Namespaceable)
118
119
120 6
def _has_namespace_at(path, cls):
121
    """Return whether there is a namespace for cls at the given path?"""
122 6
    return Namespace.namespace_exists(cls, path)
123
124
125 6
def _mro_to_chained(mro, dct):
126
    """Return a chained map of lookups for the given namespace and mro."""
127 6
    return collections.ChainMap(*[
128
        Namespace.get_namespace(cls, dct.path) for cls in
129
        filter(
130
            functools.partial(_has_namespace_at, dct.path),
131
            itertools.takewhile(
132
                functools.partial(_no_blocker, dct),
133
                filter(_is_namespaceable, mro)))])
134
135
136 6
def _instance_map(ns_proxy):
137
    """Return a map, possibly chained, of lookups for the given instance."""
138 6
    dct, instance, _ = _PROXY_INFOS[ns_proxy]
139 6
    if instance is not None:
140 6
        if isinstance(instance, Namespaceable):
141 6
            return _mro_to_chained(instance.__mro__, dct)
142
        else:
143 6
            return Namespace.get_namespace(instance, dct.path)
144
    else:
145 6
        return {}
146
147
148 6
def _mro_map(ns_proxy):
149
    """Return a chained map of lookups for the given owner class."""
150 6
    dct, _, owner = _PROXY_INFOS[ns_proxy]
151 6
    mro = owner.__mro__
152 6
    mro = mro[mro.index(dct.parent_object):]
153 6
    return _mro_to_chained(mro, dct)
154
155
156 6
def _retarget(ns_proxy):
157
    """Convert a class lookup to an instance lookup, if needed."""
158 6
    dct, instance, owner = _PROXY_INFOS[ns_proxy]
159 6
    if instance is None and isinstance(type(owner), Namespaceable):
160 6
        instance, owner = owner, type(owner)
161 6
        dct = Namespace.get_namespace(owner, dct.path)
162 6
        ns_proxy = _NamespaceProxy(dct, instance, owner)
163 6
    return ns_proxy
164
165
166 6
def _get(a_map, name):
167
    """Return a _DescriptorInspector around the attribute, or None."""
168 6
    try:
169 6
        value = a_map[name]
170 6
    except KeyError:
171 6
        return None
172
    else:
173 6
        return _DescriptorInspector(value)
174
175
176 6
def _delete(dct, name):
177
    """Attempt to delete `name` from `dct`. Raise AttributeError if missing."""
178 6
    try:
179 6
        del dct[name]
180 6
    except KeyError:
181 6
        raise AttributeError(name)
182
183
184 6
def _has_get(value):
185
    """Return whether the value is a wrapped getter descriptor."""
186 6
    return value is not None and value.has_get
187
188
189 6
def _is_data(value):
190
    """Return whether the value is a wrapped data descriptor."""
191 6
    return value is not None and value.is_data
192
193
194 6
class _NamespaceProxy:
195
196
    """Proxy object for manipulating and querying namespaces."""
197
198 6
    __slots__ = '__weakref__',
199
200 6
    def __init__(self, dct, instance, owner):
201 6
        _PROXY_INFOS[self] = dct, instance, owner
202
203 6
    def __dir__(self):
204 6
        return collections.ChainMap(_instance_map(self), _mro_map(self))
205
206 6
    def __getattribute__(self, name):
207 6
        self = _retarget(self)
208 6
        dct, instance, owner = _PROXY_INFOS[self]
209 6
        if owner is None:
210 6
            return dct[name]
211 6
        instance_map = _instance_map(self)
212 6
        mro_map = _mro_map(self)
213 6
        instance_value = _get(instance_map, name)
214 6
        mro_value = _get(mro_map, name)
215 6
        if _is_data(mro_value):
216 6
            return mro_value.get(instance, owner)
217 6
        elif issubclass(owner, type) and _has_get(instance_value):
218 6
            return instance_value.get(None, instance)
219 6
        elif instance_value is not None:
220 6
            return instance_value.object
221 6
        elif _has_get(mro_value):
222 6
            return mro_value.get(instance, owner)
223 6
        elif mro_value is not None:
224 6
            return mro_value.object
225
        else:
226 6
            raise AttributeError(name)
227
228 6
    def __setattr__(self, name, value):
229 6
        self = _retarget(self)
230 6
        dct, instance, owner = _PROXY_INFOS[self]
231 6
        if owner is None:
232 6
            dct[name] = value
233 6
            return
234 6
        if instance is None:
235 6
            real_map = Namespace.get_namespace(owner, dct.path)
236 6
            real_map[name] = value
237 6
            return
238 6
        mro_map = _mro_map(self)
239 6
        try:
240 6
            target_value = mro_map[name]
241 6
        except KeyError:
242 6
            pass
243
        else:
244 6
            target_value = _DescriptorInspector(target_value)
245 6
            if target_value.is_data:
246
                target_value.set(instance, value)
247
                return
248 6
        instance_map = Namespace.get_namespace(instance, dct.path)
249 6
        instance_map[name] = value
250
251 6
    def __delattr__(self, name):
252 6
        self = _retarget(self)
253 6
        dct, instance, owner = _PROXY_INFOS[self]
254 6
        if owner is None:
255 6
            _delete(dct, name)
256 6
            return
257 6
        real_map = Namespace.get_namespace(owner, dct.path)
258 6
        if instance is None:
259 6
            _delete(real_map, name)
260 6
            return
261 6
        try:
262 6
            value = real_map[name]
263
        except KeyError:
264
            pass
265
        else:
266 6
            value = _DescriptorInspector(value)
267 6
            if value.is_data:
268
                value.delete(instance)
269
                return
270 6
        instance_map = Namespace.get_namespace(instance, dct.path)
271 6
        _delete(instance_map, name)
272
273 6
    def __enter__(self):
274 6
        dct, _, _ = _PROXY_INFOS[self]
275 6
        return dct.__enter__()
276
277 6
    def __exit__(self, exc_type, exc_value, traceback):
278 6
        dct, _, _ = _PROXY_INFOS[self]
279 6
        return dct.__exit__(exc_type, exc_value, traceback)
280
281
282 6
class Namespace(dict):
283
284
    """Namespace."""
285
286 6
    __slots__ = 'name', 'scope', 'parent', 'active', 'parent_object'
287
288 6
    __namespaces = {}
289
290 6
    def __init__(self, *args, **kwargs):
291 6
        super().__init__(*args, **kwargs)
292 6
        bad_values = tuple(
293
            value for value in self.values() if
294
            isinstance(value, (Namespace, _NamespaceProxy)))
295 6
        if bad_values:
296 6
            raise ValueError('Bad values: {}'.format(bad_values))
297 6
        self.name = None
298 6
        self.scope = None
299 6
        self.parent = None
300 6
        self.active = False
301 6
        self.parent_object = None
302
303 6
    @classmethod
304
    def premake(cls, name, parent):
305
        """Return an empty namespace with the given name and parent."""
306 6
        self = cls()
307 6
        self.name = name
308 6
        self.parent = parent
309 6
        return self
310
311 6
    def __setitem__(self, key, value):
312 6
        if (
313
                self.parent_object is not None and
314
                isinstance(value, Namespace) and value.name != key):
315 6
            value.push(key, self.scope)
316 6
            value.add(self.parent_object)
317 6
        super().__setitem__(key, value)
318
319 6
    def __enter__(self):
320 6
        self.activate()
321 6
        return self
322
323 6
    def __exit__(self, exc_type, exc_value, traceback):
324 6
        if self.name is None:
325 6
            raise RuntimeError('Namespace must be named.')
326 6
        self.deactivate()
327
328 6
    @property
329
    def path(self):
330
        """Return the full path of the namespace."""
331 6
        if self.name is None or self.parent is None:
332
            raise ValueError
333 6
        if isinstance(self.parent, Namespace):
334 6
            parent_path = self.parent.path
335
        else:
336 6
            parent_path = ()
337 6
        return parent_path + (self.name,)
338
339 6
    @classmethod
340
    def __get_helper(cls, target, path):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
341 6
        if isinstance(target, Namespaceable):
342 6
            path_ = target, path
343 6
            namespaces = cls.__namespaces
344
        else:
345 6
            path_ = path
346 6
            try:
347 6
                namespaces = target.__namespaces
348 6
            except AttributeError:
349 6
                namespaces = {}
350 6
                target.__namespaces = namespaces
351 6
        return path_, namespaces
352
353 6
    @classmethod
354
    def namespace_exists(cls, target, path):
355
        """Return whether the given namespace exists."""
356 6
        path_, namespaces = cls.__get_helper(target, path)
357 6
        return path_ in namespaces
358
359 6
    @classmethod
360
    def get_namespace(cls, target, path):
361
        """Return a namespace with given target and path, create if needed."""
362 6
        path_, namespaces = cls.__get_helper(target, path)
363 6
        try:
364 6
            return namespaces[path_]
365 6
        except KeyError:
366 6
            if len(path) == 1:
367 6
                parent = {}
368
            else:
369 6
                parent = cls.get_namespace(target, path[:-1])
370 6
            return namespaces.setdefault(path_, cls.premake(path[-1], parent))
371
372 6
    def add(self, target):
373
        """Add self as a namespace under target."""
374 6
        path, namespaces = self.__get_helper(target, self.path)
375 6
        res = namespaces.setdefault(path, self)
376 6
        if res is self:
377 6
            self.parent_object = target
378
379 6
    def set_if_none(self, name, value):
380
        """Set the attribute `name` to `value`, if it's initially None."""
381 6
        if getattr(self, name) is None:
382 6
            setattr(self, name, value)
383
384 6
    def push(self, name, scope):
385
        """Bind self to the given name and scope, and activate."""
386 6
        self.set_if_none('name', name)
387 6
        self.set_if_none('scope', scope)
388 6
        self.set_if_none('parent', scope.dicts[0])
389 6
        if name != self.name:
390 6
            raise ValueError('Cannot rename namespace')
391 6
        if scope is not self.scope:
392
            raise ValueError('Cannot reuse namespace')
393 6
        if scope.dicts[0] is not self.parent:
394
            raise ValueError('Cannot reparent namespace')
395 6
        self.scope.namespaces.append(self)
396 6
        self.activate()
397
398 6
    def activate(self):
399
        """Take over as the scope for the target."""
400 6
        if self.scope is not None and not self.active:
401 6
            if self.scope.dicts[0] is not self.parent:
402
                raise ValueError('Cannot reparent namespace')
403 6
            self.active = True
404 6
            self.scope.dicts.insert(0, self)
405
406 6
    def deactivate(self):
407
        """Stop being the scope for the target."""
408 6
        if self.scope is not None and self.active:
409 6
            self.active = False
410 6
            self.scope.dicts.pop(0)
411
412 6
    def __get__(self, instance, owner):
413 6
        return _NamespaceProxy(self, instance, owner)
414
415
416 6
class _NamespaceScope(collections.abc.MutableMapping):
0 ignored issues
show
Coding Style introduced by
This class should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
417
418 6
    __slots__ = 'dicts', 'namespaces'
419
420 6
    def __init__(self, dct):
421 6
        self.dicts = [dct]
422 6
        self.namespaces = []
423
424 6
    def __getitem__(self, key):
425 6
        value = collections.ChainMap(*self.dicts)[key]
426 6
        if isinstance(value, Namespace):
427 6
            value = _NamespaceProxy(value, None, None)
428 6
        return value
429
430 6
    def __setitem__(self, key, value):
431 6
        dct = self.dicts[0]
432 6
        if isinstance(value, _NamespaceProxy):
433 6
            value, _, _ = _PROXY_INFOS[value]
434 6
        if isinstance(value, Namespace) and value.name != key:
435 6
            value.push(key, self)
436 6
        dct[key] = value
437
438 6
    def __delitem__(self, key):
439 6
        del self.dicts[0][key]
440
441 6
    def __iter__(self):
442
        return iter(collections.ChainMap(*self.dicts))
443
444 6
    def __len__(self):
445
        return len(collections.ChainMap(*self.dicts))
446
447
448 6
_NAMESPACE_SCOPES = weakref.WeakKeyDictionary()
449
450
451 6
class Namespaceable(type):
452
453
    """Metaclass for classes that can contain namespaces."""
454
455 6
    @classmethod
456
    def __prepare__(mcs, name, bases, **kwargs):
457 6
        return _NamespaceScope(super().__prepare__(name, bases, **kwargs))
458
459 6
    def __new__(mcs, name, bases, dct, **kwargs):
460 6
        cls = super().__new__(mcs, name, bases, dct.dicts[0], **kwargs)
461 6
        _NAMESPACE_SCOPES[cls] = dct
462 6
        for namespace in dct.namespaces:
463 6
            namespace.add(cls)
464 6
            if ENABLE_SET_NAME:
465 2
                for name, value in namespace.items():
466 2
                    wrapped = _DescriptorInspector(value)
467 2
                    if wrapped.has_set_name:
468 2
                        wrapped.set_name(cls, name)
469 6
        return cls
470
471 6
    def __setattr__(cls, name, value):
472 6
        if isinstance(value, Namespace) and value.name != name:
473 6
            value.push(name, _NAMESPACE_SCOPES[cls])
474 6
            value.add(cls)
475 6
        if issubclass(cls, Namespaceable):
476 6
            super().__setattr__(cls, name, value)
477
        else:
478
            super().__setattr__(name, value)
479