Completed
Push — master ( 1ec367...3840e1 )
by Max
59s
created

_delete()   A

Complexity

Conditions 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
c 2
b 0
f 0
dl 0
loc 6
rs 9.4285
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
import collections.abc
11
import functools
12
import itertools
13
import sys
14
import weakref
15
16
17
ENABLE_SET_NAME = sys.version_info >= (3, 6)
18
19
20
_PROXY_INFOS = weakref.WeakKeyDictionary()
21
_SENTINEL = object()
22
23
24
class _DescriptorInspector(collections.namedtuple('_DescriptorInspector',
25
                                                  ['object', 'dict'])):
26
27
    """Wrapper around objects. Provides access to descriptor protocol."""
28
29
    __slots__ = ()
30
31
    def __new__(cls, obj):
32
        dct = collections.ChainMap(*[vars(cls) for cls in type(obj).__mro__])
33
        return super().__new__(cls, obj, dct)
34
35
    @property
36
    def has_get(self):
37
        """Return whether self.object's mro provides __get__."""
38
        return '__get__' in self.dict
39
40
    @property
41
    def has_set(self):
42
        """Return whether self.object's mro provides __set__."""
43
        return '__set__' in self.dict
44
45
    @property
46
    def has_delete(self):
47
        """Return whether self.object's mro provides __delete__."""
48
        return '__delete__' in self.dict
49
50
    if ENABLE_SET_NAME:
51
        @property
52
        def has_set_name(self):
53
            """Return whether self.object's mro provides __set_name__."""
54
            return '__set_name__' in self.dict
55
56
        def set_name(self, owner, name):
57
            """Call __set_name__, bypassing descriptor protocol."""
58
            self.get_as_attribute('__set_name__')(self.object, owner, name)
59
60
        @property
61
        def has_non_data(self):
62
            """Return whether self.object's mro provides non-data methods."""
63
            return self.has_get or self.has_set_name
64
    else:
65
        has_non_data = has_get
66
67
    @property
68
    def is_data(self):
69
        """Returns whether self.object is a data descriptor."""
70
        return self.has_set or self.has_delete
71
72
    @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
    @property
78
    def is_descriptor(self):
79
        """Returns whether self.object is a descriptor."""
80
        return self.has_non_data or self.is_data
81
82
    def get_as_attribute(self, key):
83
        """Return attribute with the given name, or raise AttributeError."""
84
        try:
85
            return self.dict[key]
86
        except KeyError:
87
            raise AttributeError(key)
88
89
    def get(self, instance, owner):
90
        """Return the result of __get__, bypassing descriptor protocol."""
91
        return self.get_as_attribute('__get__')(self.object, instance, owner)
92
93
    def set(self, instance, value):
94
        """Call __set__, bypassing descriptor protocol."""
95
        self.get_as_attribute('__set__')(self.object, instance, value)
96
97
    def delete(self, instance):
98
        """Call __delete__, bypassing descriptor protocol."""
99
        self.get_as_attribute('__delete__')(self.object, instance)
100
101
102
def _no_blocker(dct, cls):
103
    """Return True if there's a non-Namespace object in the path for cls."""
104
    try:
105
        namespace = vars(cls)
106
        for name in dct.path:
107
            namespace = namespace[name]
108
            if not isinstance(namespace, Namespace):
109
                return False
110
    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
        pass
112
    return True
113
114
115
def _mro_to_chained(mro, dct):
116
    """Return a chained map of lookups for the given namespace and mro."""
117
    mro = (cls for cls in mro if isinstance(cls, Namespaceable))
118
    mro = itertools.takewhile(functools.partial(_no_blocker, dct), mro)
119
    mro = (cls for cls in mro if Namespace.namespace_exists(cls, dct.path))
120
    return collections.ChainMap(
121
        *[Namespace.get_namespace(cls, dct.path) for cls in mro])
122
123
124
def _instance_map(ns_proxy):
125
    """Return a map, possibly chained, of lookups for the given instance."""
126
    dct, instance, _ = _PROXY_INFOS[ns_proxy]
127
    if instance is not None:
128
        if isinstance(instance, Namespaceable):
129
            return _mro_to_chained(instance.__mro__, dct)
130
        else:
131
            return Namespace.get_namespace(instance, dct.path)
132
    else:
133
        return {}
134
135
136
def _mro_map(ns_proxy):
137
    """Return a chained map of lookups for the given owner class."""
138
    dct, _, owner = _PROXY_INFOS[ns_proxy]
139
    mro = owner.__mro__
140
    mro = mro[mro.index(dct.parent_object):]
141
    return _mro_to_chained(mro, dct)
142
143
144
def _retarget(ns_proxy):
145
    """Convert a class lookup to an instance lookup, if needed."""
146
    dct, instance, owner = _PROXY_INFOS[ns_proxy]
147
    if instance is None and isinstance(type(owner), Namespaceable):
148
        instance, owner = owner, type(owner)
149
        dct = Namespace.get_namespace(owner, dct.path)
150
        ns_proxy = _NamespaceProxy(dct, instance, owner)
151
    return ns_proxy
152
153
154
def _get(a_map, name):
155
    """Return a _DescriptorInspector around the attribute, or None."""
156
    try:
157
        value = a_map[name]
158
    except KeyError:
159
        return None
160
    else:
161
        return _DescriptorInspector(value)
162
163
164
def _delete(dct, name):
165
    """Attempt to delete `name` from `dct`. Raise AttributeError if missing."""
166
    try:
167
        del dct[name]
168
    except KeyError:
169
        raise AttributeError(name)
170
171
172
def _has_get(value):
173
    """Return whether the value is a wrapped getter descriptor."""
174
    return value is not None and value.has_get
175
176
177
def _is_data(value):
178
    """Return whether the value is a wrapped data descriptor."""
179
    return value is not None and value.is_data
180
181
182
class _NamespaceProxy:
183
184
    """Proxy object for manipulating and querying namespaces."""
185
186
    __slots__ = '__weakref__',
187
188
    def __init__(self, dct, instance, owner):
189
        _PROXY_INFOS[self] = dct, instance, owner
190
191
    def __dir__(self):
192
        return collections.ChainMap(_instance_map(self), _mro_map(self))
193
194
    def __getattribute__(self, name):
195
        self = _retarget(self)
196
        dct, instance, owner = _PROXY_INFOS[self]
197
        if owner is None:
198
            return dct[name]
199
        instance_map = _instance_map(self)
200
        mro_map = _mro_map(self)
201
        instance_value = _get(instance_map, name)
202
        mro_value = _get(mro_map, name)
203
        if _is_data(mro_value):
204
            return mro_value.get(instance, owner)
205
        elif issubclass(owner, type) and _has_get(instance_value):
206
            return instance_value.get(None, instance)
207
        elif instance_value is not None:
208
            return instance_value.object
209
        elif _has_get(mro_value):
210
            return mro_value.get(instance, owner)
211
        elif mro_value is not None:
212
            return mro_value.object
213
        else:
214
            raise AttributeError(name)
215
216
    def __setattr__(self, name, value):
217
        self = _retarget(self)
218
        dct, instance, owner = _PROXY_INFOS[self]
219
        if owner is None:
220
            dct[name] = value
221
            return
222
        if instance is None:
223
            real_map = Namespace.get_namespace(owner, dct.path)
224
            real_map[name] = value
225
            return
226
        mro_map = _mro_map(self)
227
        try:
228
            target_value = mro_map[name]
229
        except KeyError:
230
            pass
231
        else:
232
            target_value = _DescriptorInspector(target_value)
233
            if target_value.is_data:
234
                target_value.set(instance, value)
235
                return
236
        instance_map = Namespace.get_namespace(instance, dct.path)
237
        instance_map[name] = value
238
239
    def __delattr__(self, name):
240
        self = _retarget(self)
241
        dct, instance, owner = _PROXY_INFOS[self]
242
        if owner is None:
243
            _delete(dct, name)
244
            return
245
        real_map = Namespace.get_namespace(owner, dct.path)
246
        if instance is None:
247
            _delete(real_map, name)
248
            return
249
        try:
250
            value = real_map[name]
251
        except KeyError:
252
            pass
253
        else:
254
            value = _DescriptorInspector(value)
255
            if value.is_data:
256
                value.delete(instance)
257
                return
258
        instance_map = Namespace.get_namespace(instance, dct.path)
259
        _delete(instance_map, name)
260
261
    def __enter__(self):
262
        dct, _, _ = _PROXY_INFOS[self]
263
        return dct.__enter__()
264
265
    def __exit__(self, exc_type, exc_value, traceback):
266
        dct, _, _ = _PROXY_INFOS[self]
267
        return dct.__exit__(exc_type, exc_value, traceback)
268
269
270
class Namespace(dict):
271
272
    """Namespace."""
273
274
    __slots__ = 'name', 'scope', 'parent', 'active', 'parent_object'
275
276
    __namespaces = {}
277
278
    def __init__(self, *args, **kwargs):
279
        super().__init__(*args, **kwargs)
280
        bad_values = tuple(
281
            value for value in self.values() if
282
            isinstance(value, (Namespace, _NamespaceProxy)))
283
        if bad_values:
284
            raise ValueError('Bad values: {}'.format(bad_values))
285
        self.name = None
286
        self.scope = None
287
        self.parent = None
288
        self.active = False
289
        self.parent_object = None
290
291
    @classmethod
292
    def premake(cls, name, parent):
293
        """Return an empty namespace with the given name and parent."""
294
        self = cls()
295
        self.name = name
296
        self.parent = parent
297
        return self
298
299
    def __setitem__(self, key, value):
300
        if (
301
                self.parent_object is not None and
302
                isinstance(value, Namespace) and value.name != key):
303
            value.push(key, self.scope)
304
            value.add(self.parent_object)
305
        super().__setitem__(key, value)
306
307
    def __enter__(self):
308
        self.activate()
309
        return self
310
311
    def __exit__(self, exc_type, exc_value, traceback):
312
        if self.name is None:
313
            raise RuntimeError('Namespace must be named.')
314
        self.deactivate()
315
316
    @property
317
    def path(self):
318
        """Return the full path of the namespace."""
319
        if self.name is None or self.parent is None:
320
            raise ValueError
321
        if isinstance(self.parent, Namespace):
322
            parent_path = self.parent.path
323
        else:
324
            parent_path = ()
325
        return parent_path + (self.name,)
326
327
    @classmethod
328
    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...
329
        if isinstance(target, Namespaceable):
330
            path_ = target, path
331
            namespaces = cls.__namespaces
332
        else:
333
            path_ = path
334
            try:
335
                namespaces = target.__namespaces
336
            except AttributeError:
337
                namespaces = {}
338
                target.__namespaces = namespaces
339
        return path_, namespaces
340
341
    @classmethod
342
    def namespace_exists(cls, target, path):
343
        """Return whether the given namespace exists."""
344
        path_, namespaces = cls.__get_helper(target, path)
345
        return path_ in namespaces
346
347
    @classmethod
348
    def get_namespace(cls, target, path):
349
        """Return a namespace with given target and path, create if needed."""
350
        path_, namespaces = cls.__get_helper(target, path)
351
        try:
352
            return namespaces[path_]
353
        except KeyError:
354
            if len(path) == 1:
355
                parent = {}
356
            else:
357
                parent = cls.get_namespace(target, path[:-1])
358
            return namespaces.setdefault(path_, cls.premake(path[-1], parent))
359
360
    def add(self, target):
361
        """Add self as a namespace under target."""
362
        path, namespaces = self.__get_helper(target, self.path)
363
        res = namespaces.setdefault(path, self)
364
        if res is self:
365
            self.parent_object = target
366
367
    def set_if_none(self, name, value):
368
        """Set the attribute `name` to `value`, if it's initially None."""
369
        if getattr(self, name) is None:
370
            setattr(self, name, value)
371
372
    def push(self, name, scope):
373
        """Bind self to the given name and scope, and activate."""
374
        self.set_if_none('name', name)
375
        self.set_if_none('scope', scope)
376
        self.set_if_none('parent', scope.dicts[0])
377
        if name != self.name:
378
            raise ValueError('Cannot rename namespace')
379
        if scope is not self.scope:
380
            raise ValueError('Cannot reuse namespace')
381
        if scope.dicts[0] is not self.parent:
382
            raise ValueError('Cannot reparent namespace')
383
        self.scope.namespaces.append(self)
384
        self.activate()
385
386
    def activate(self):
387
        """Take over as the scope for the target."""
388
        if self.scope is not None and not self.active:
389
            if self.scope.dicts[0] is not self.parent:
390
                raise ValueError('Cannot reparent namespace')
391
            self.active = True
392
            self.scope.dicts.insert(0, self)
393
394
    def deactivate(self):
395
        """Stop being the scope for the target."""
396
        if self.scope is not None and self.active:
397
            self.active = False
398
            self.scope.dicts.pop(0)
399
400
    def __get__(self, instance, owner):
401
        return _NamespaceProxy(self, instance, owner)
402
403
404
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...
405
406
    __slots__ = 'dicts', 'namespaces'
407
408
    def __init__(self, dct):
409
        self.dicts = [dct]
410
        self.namespaces = []
411
412
    def __getitem__(self, key):
413
        value = collections.ChainMap(*self.dicts)[key]
414
        if isinstance(value, Namespace):
415
            value = _NamespaceProxy(value, None, None)
416
        return value
417
418
    def __setitem__(self, key, value):
419
        dct = self.dicts[0]
420
        if isinstance(value, _NamespaceProxy):
421
            value, _, _ = _PROXY_INFOS[value]
422
        if isinstance(value, Namespace) and value.name != key:
423
            value.push(key, self)
424
        dct[key] = value
425
426
    def __delitem__(self, key):
427
        del self.dicts[0][key]
428
429
    def __iter__(self):
430
        return iter(collections.ChainMap(*self.dicts))
431
432
    def __len__(self):
433
        return len(collections.ChainMap(*self.dicts))
434
435
436
_NAMESPACE_SCOPES = weakref.WeakKeyDictionary()
437
438
439
class Namespaceable(type):
440
441
    """Metaclass for classes that can contain namespaces."""
442
443
    @classmethod
444
    def __prepare__(mcs, name, bases, **kwargs):
445
        return _NamespaceScope(super().__prepare__(name, bases, **kwargs))
446
447
    def __new__(mcs, name, bases, dct, **kwargs):
448
        cls = super().__new__(mcs, name, bases, dct.dicts[0], **kwargs)
449
        _NAMESPACE_SCOPES[cls] = dct
450
        for namespace in dct.namespaces:
451
            namespace.add(cls)
452
            if ENABLE_SET_NAME:
453
                for name, value in namespace.items():
454
                    wrapped = _DescriptorInspector(value)
455
                    if wrapped.has_set_name:
456
                        wrapped.set_name(cls, name)
457
        return cls
458
459
    def __setattr__(cls, name, value):
460
        if isinstance(value, Namespace) and value.name != name:
461
            value.push(name, _NAMESPACE_SCOPES[cls])
462
            value.add(cls)
463
        if issubclass(cls, Namespaceable):
464
            super().__setattr__(cls, name, value)
465
        else:
466
            super().__setattr__(name, value)
467