Completed
Push — master ( d4c428...bc02b9 )
by Max
55s
created

_has_get()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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