Completed
Push — master ( d2162f...9d06e5 )
by Max
51s
created

_get()   A

Complexity

Conditions 3

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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