Completed
Push — master ( c943b5...440d09 )
by Max
01:01
created

_retarget()   A

Complexity

Conditions 3

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
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
class _NamespaceProxy:
148
149
    """Proxy object for manipulating and querying namespaces."""
150
151
    __slots__ = '__weakref__',
152
153
    def __init__(self, dct, instance, owner):
154
        _PROXY_INFOS[self] = dct, instance, owner
155
156
    def __dir__(self):
157
        return collections.ChainMap(_instance_map(self), _mro_map(self))
158
159
    def __getattribute__(self, name):
160
        self = _retarget(self)
161
        dct, instance, owner = _PROXY_INFOS[self]
162
        if owner is None:
163
            return dct[name]
164
        instance_map = _instance_map(self)
165
        mro_map = _mro_map(self)
166
        try:
167
            instance_value = instance_map[name]
168
        except KeyError:
169
            instance_value = None
170
        else:
171
            instance_value = _DescriptorInspector(instance_value)
172
        try:
173
            mro_value = mro_map[name]
174
        except KeyError:
175
            mro_value = None
176
        else:
177
            mro_value = _DescriptorInspector(mro_value)
178
        if issubclass(owner, type):
179
            if mro_value is not None and mro_value.is_data:
180
                return mro_value.get(instance, owner)
181
            elif instance_value is not None and instance_value.has_get:
182
                return instance_value.get(None, instance)
183
            elif instance_value is not None:
184
                return instance_value.object
185
            elif mro_value is not None and mro_value.has_get:
186
                return mro_value.get(instance, owner)
187
            elif mro_value is not None:
188
                return mro_value.object
189
            else:
190
                raise AttributeError(name)
191
        else:
192
            if mro_value is not None and mro_value.is_data:
193
                return mro_value.get(instance, owner)
194
            elif instance_value is not None:
195
                return instance_value.object
196
            elif mro_value is not None and mro_value.has_get:
197
                return mro_value.get(instance, owner)
198
            elif mro_value is not None:
199
                return mro_value.object
200
            else:
201
                raise AttributeError(name)
202
203
    def __setattr__(self, name, value):
204
        self = _retarget(self)
205
        dct, instance, owner = _PROXY_INFOS[self]
206
        if owner is None:
207
            dct[name] = value
208
            return
209
        if instance is None:
210
            real_map = Namespace.get_namespace(owner, dct.path)
211
            real_map[name] = value
212
            return
213
        mro_map = _mro_map(self)
214
        try:
215
            target_value = mro_map[name]
216
        except KeyError:
217
            pass
218
        else:
219
            target_value = _DescriptorInspector(target_value)
220
            if target_value.is_data:
221
                target_value.set(instance, value)
222
                return
223
        instance_map = Namespace.get_namespace(instance, dct.path)
224
        instance_map[name] = value
225
226
    def __delattr__(self, name):
227
        self = _retarget(self)
228
        dct, instance, owner = _PROXY_INFOS[self]
229
        if owner is None:
230
            del dct[name]
231
            return
232
        real_map = Namespace.get_namespace(owner, dct.path)
233
        if instance is None:
234
            try:
235
                del real_map[name]
236
                return
237
            except KeyError:
238
                raise AttributeError(name)
239
        try:
240
            value = real_map[name]
241
        except KeyError:
242
            pass
243
        else:
244
            value = _DescriptorInspector(value)
245
            if value.is_data:
246
                value.delete(instance)
247
                return
248
        instance_map = Namespace.get_namespace(instance, dct.path)
249
        try:
250
            del instance_map[name]
251
        except KeyError:
252
            raise AttributeError(name)
253
254
    def __enter__(self):
255
        dct, _, _ = _PROXY_INFOS[self]
256
        return dct.__enter__()
257
258
    def __exit__(self, exc_type, exc_value, traceback):
259
        dct, _, _ = _PROXY_INFOS[self]
260
        return dct.__exit__(exc_type, exc_value, traceback)
261
262
263
class Namespace(dict):
264
265
    """Namespace."""
266
267
    __slots__ = 'name', 'scope', 'parent', 'active', 'parent_object'
268
269
    __namespaces = {}
270
271
    def __init__(self, *args, **kwargs):
272
        super().__init__(*args, **kwargs)
273
        bad_values = tuple(
274
            value for value in self.values() if
275
            isinstance(value, (Namespace, _NamespaceProxy)))
276
        if bad_values:
277
            raise ValueError('Bad values: {}'.format(bad_values))
278
        self.name = None
279
        self.scope = None
280
        self.parent = None
281
        self.active = False
282
        self.parent_object = None
283
284
    @classmethod
285
    def premake(cls, name, parent):
286
        """Return an empty namespace with the given name and parent."""
287
        self = cls()
288
        self.name = name
289
        self.parent = parent
290
        return self
291
292
    def __setitem__(self, key, value):
293
        if (
294
                self.parent_object is not None and
295
                isinstance(value, Namespace) and value.name != key):
296
            value.push(key, self.scope)
297
            value.add(self.parent_object)
298
        super().__setitem__(key, value)
299
300
    def __enter__(self):
301
        self.activate()
302
        return self
303
304
    def __exit__(self, exc_type, exc_value, traceback):
305
        if self.name is None:
306
            raise RuntimeError('Namespace must be named.')
307
        self.deactivate()
308
309
    @property
310
    def path(self):
311
        """Return the full path of the namespace."""
312
        if self.name is None or self.parent is None:
313
            raise ValueError
314
        if isinstance(self.parent, Namespace):
315
            parent_path = self.parent.path
316
        else:
317
            parent_path = ()
318
        return parent_path + (self.name,)
319
320
    @classmethod
321
    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...
322
        if isinstance(target, Namespaceable):
323
            path_ = target, path
324
            namespaces = cls.__namespaces
325
        else:
326
            path_ = path
327
            try:
328
                namespaces = target.__namespaces
329
            except AttributeError:
330
                namespaces = {}
331
                target.__namespaces = namespaces
332
        return path_, namespaces
333
334
    @classmethod
335
    def namespace_exists(cls, target, path):
336
        """Return whether the given namespace exists."""
337
        path_, namespaces = cls.__get_helper(target, path)
338
        return path_ in namespaces
339
340
    @classmethod
341
    def get_namespace(cls, target, path):
342
        """Return a namespace with given target and path, create if needed."""
343
        path_, namespaces = cls.__get_helper(target, path)
344
        try:
345
            return namespaces[path_]
346
        except KeyError:
347
            if len(path) == 1:
348
                parent = {}
349
            else:
350
                parent = cls.get_namespace(target, path[:-1])
351
            return namespaces.setdefault(path_, cls.premake(path[-1], parent))
352
353
    def add(self, target):
354
        """Add self as a namespace under target."""
355
        path, namespaces = self.__get_helper(target, self.path)
356
        res = namespaces.setdefault(path, self)
357
        if res is self:
358
            self.parent_object = target
359
360
    def push(self, name, scope):
361
        """Bind self to the given name and scope, and activate."""
362
        if self.name is None:
363
            self.name = name
364
        if self.scope is None:
365
            self.scope = scope
366
        if self.parent is None:
367
            self.parent = scope.dicts[-1]
368
        if name != self.name:
369
            raise ValueError('Cannot rename namespace')
370
        if scope is not self.scope:
371
            raise ValueError('Cannot reuse namespace')
372
        if scope.dicts[-1] is not self.parent:
373
            raise ValueError('Cannot reparent namespace')
374
        self.scope.namespaces.append(self)
375
        self.activate()
376
377
    def activate(self):
378
        """Take over as the scope for the target."""
379
        if self.scope is not None and not self.active:
380
            if self.scope.dicts[-1] is not self.parent:
381
                raise ValueError('Cannot reparent namespace')
382
            self.active = True
383
            self.scope.dicts.append(self)
384
385
    def deactivate(self):
386
        """Stop being the scope for the target."""
387
        if self.scope is not None and self.active:
388
            self.active = False
389
            self.scope.dicts.pop()
390
391
    def __get__(self, instance, owner):
392
        return _NamespaceProxy(self, instance, owner)
393
394
395
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...
396
397
    __slots__ = 'dicts', 'namespaces'
398
399
    def __init__(self):
400
        self.dicts = [{}]
401
        self.namespaces = []
402
403
    def __getitem__(self, key):
404
        value = self.dicts[-1][key]
405
        if isinstance(value, Namespace):
406
            value = _NamespaceProxy(value, None, None)
407
        return value
408
409
    def __setitem__(self, key, value):
410
        dct = self.dicts[-1]
411
        if isinstance(value, _NamespaceProxy):
412
            value, _, _ = _PROXY_INFOS[value]
413
        if isinstance(value, Namespace) and value.name != key:
414
            value.push(key, self)
415
        dct[key] = value
416
417
    def __delitem__(self, key):
418
        del self.dicts[-1][key]
419
420
    def __iter__(self):
421
        return iter(collections.ChainMap(*self.dicts))
422
423
    def __len__(self):
424
        return len(collections.ChainMap(*self.dicts))
425
426
427
class Namespaceable(type):
428
429
    """Metaclass for classes that can contain namespaces."""
430
431
    @classmethod
432
    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...
433
        return _NamespaceScope()
434
435
    def __new__(mcs, name, bases, dct):
436
        cls = super().__new__(mcs, name, bases, dct.dicts[-1])
437
        cls.__scope = dct
438
        for namespace in dct.namespaces:
439
            namespace.add(cls)
440
            if ENABLE_SET_NAME:
441
                for name, value in namespace.items():
442
                    wrapped = _DescriptorInspector(value)
443
                    if wrapped.has_set_name:
444
                        wrapped.set_name(cls, name)
445
        return cls
446
447
    def __setattr__(cls, name, value):
448
        if isinstance(value, Namespace) and value.name != name:
449
            value.push(name, cls.__scope)
450
            value.add(cls)
451
        if issubclass(cls, Namespaceable):
452
            super().__setattr__(cls, name, value)
453
        else:
454
            super().__setattr__(name, value)
455