Completed
Push — master ( cd9ff5...1ec367 )
by Max
54s
created

Namespace.set_if_none()   A

Complexity

Conditions 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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