Completed
Push — master ( a9ec99...7962cd )
by Max
02:16
created

_NamespaceBase.__getattribute__()   A

Complexity

Conditions 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 3
c 6
b 0
f 0
dl 0
loc 8
rs 9.4285
ccs 8
cts 8
cp 1
crap 3
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 6
import collections.abc
11 6
import functools
12 6
import itertools
13 6
import weakref
14
15 6
from . import ops
16 6
from .descriptor_inspector import _DescriptorInspector
17 6
from .flags import ENABLE_SET_NAME
18 6
from .proxy import _Proxy
19 6
from .scope_proxy import _ScopeProxy
20
21
22 6
_PROXY_INFOS = weakref.WeakKeyDictionary()
23
24
25 6
def _mro_to_chained(mro, path):
26
    """Return a chained map of lookups for the given namespace and mro."""
27 6
    return collections.ChainMap(*[
28
        Namespace.get_namespace(cls, path) for cls in
29
        itertools.takewhile(
30
            functools.partial(Namespace.no_blocker, path),
31
            (cls for cls in mro if isinstance(cls, _Namespaceable))) if
32
        Namespace.namespace_exists(path, cls)])
33
34
35 6
def _instance_map(ns_proxy):
36
    """Return a map, possibly chained, of lookups for the given instance."""
37 6
    dct, instance, _ = _PROXY_INFOS[ns_proxy]
38 6
    if instance is not None:
39 6
        if isinstance(instance, _Namespaceable):
40 6
            return _mro_to_chained(instance.__mro__, dct.path)
41
        else:
42 6
            return Namespace.get_namespace(instance, dct.path)
43
    else:
44 6
        return {}
45
46
47 6
def _mro_map(ns_proxy):
48
    """Return a chained map of lookups for the given owner class."""
49 6
    dct, _, owner = _PROXY_INFOS[ns_proxy]
50 6
    mro = owner.__mro__
51 6
    mro = mro[mro.index(dct.parent_object):]
52 6
    return _mro_to_chained(mro, dct.path)
53
54
55 6
def _retarget(ns_proxy):
56
    """Convert a class lookup to an instance lookup, if needed."""
57 6
    dct, instance, owner = _PROXY_INFOS[ns_proxy]
58 6
    if instance is None and isinstance(type(owner), _Namespaceable):
59 6
        instance, owner = owner, type(owner)
60 6
        dct = Namespace.get_namespace(owner, dct.path)
61 6
        ns_proxy = _NamespaceProxy(dct, instance, owner)
62 6
    return ns_proxy
63
64
65 6
class _NamespaceProxy(_Proxy):
66
67
    """Proxy object for manipulating and querying namespaces."""
68
69 6
    __slots__ = '__weakref__',
70
71 6
    def __init__(self, dct, instance, owner):
72 6
        _PROXY_INFOS[self] = dct, instance, owner
73
74 6
    def __dir__(self):
75 6
        return collections.ChainMap(_instance_map(self), _mro_map(self))
76
77 6
    def __getattribute__(self, name):
78 6
        self = _retarget(self)
79 6
        _, instance, owner = _PROXY_INFOS[self]
80 6
        instance_map = _instance_map(self)
81 6
        mro_map = _mro_map(self)
82 6
        instance_value = ops.get(instance_map, name)
83 6
        mro_value = ops.get(mro_map, name)
84 6
        if ops.is_data(mro_value):
85 6
            return mro_value.get(instance, owner)
86 6
        elif issubclass(owner, type) and ops.has_get(instance_value):
87 6
            return instance_value.get(None, instance)
88 6
        elif instance_value is not None:
89 6
            return instance_value.object
90 6
        elif ops.has_get(mro_value):
91 6
            return mro_value.get(instance, owner)
92 6
        elif mro_value is not None:
93 6
            return mro_value.object
94
        else:
95 6
            raise AttributeError(name)
96
97 6
    def __setattr__(self, name, value):
98 6
        self = _retarget(self)
99 6
        dct, instance, owner = _PROXY_INFOS[self]
100 6
        if instance is None:
101 6
            real_map = Namespace.get_namespace(owner, dct.path)
102 6
            real_map[name] = value
103 6
            return
104 6
        mro_map = _mro_map(self)
105 6
        target_value = ops.get_data(mro_map, name)
106 6
        if target_value is not None:
107
            target_value.set(instance, value)
108
            return
109 6
        instance_map = Namespace.get_namespace(instance, dct.path)
110 6
        instance_map[name] = value
111
112 6
    def __delattr__(self, name):
113 6
        self = _retarget(self)
114 6
        dct, instance, owner = _PROXY_INFOS[self]
115 6
        real_map = Namespace.get_namespace(owner, dct.path)
116 6
        if instance is None:
117 6
            ops.delete(real_map, name)
118 6
            return
119 6
        value = ops.get_data(real_map, name)
120 6
        if value is not None:
121
            value.delete(instance)
122
            return
123 6
        instance_map = Namespace.get_namespace(instance, dct.path)
124 6
        ops.delete(instance_map, name)
125
126
127 6
class Namespace(dict):
128
129
    """Namespace."""
130
131 6
    __slots__ = 'name', 'scope', 'parent', 'active', 'parent_object'
132
133 6
    __namespaces = {}
134
135 6
    def __init__(self, *args, **kwargs):
136 6
        super().__init__(*args, **kwargs)
137 6
        bad_values = tuple(
138
            value for value in self.values() if
139
            isinstance(value, (Namespace, _Proxy)))
140 6
        if bad_values:
141 6
            raise ValueError('Bad values: {}'.format(bad_values))
142 6
        self.name = None
143 6
        self.scope = None
144 6
        self.parent = None
145 6
        self.active = False
146 6
        self.parent_object = None
147
148 6
    @classmethod
149
    def premake(cls, name, parent):
150
        """Return an empty namespace with the given name and parent."""
151 6
        self = cls()
152 6
        self.name = name
153 6
        self.parent = parent
154 6
        return self
155
156
    # Hold up. Do we need a symmetric addon to __delitem__?
157
    # I forget how this works.
158 6
    def __setitem__(self, key, value):
159 6
        if (
160
                self.parent_object is not None and
161
                isinstance(value, Namespace) and value.name != key):
162 6
            value.push(key, self.scope)
163 6
            value.add(self.parent_object)
164 6
        super().__setitem__(key, value)
165
166 6
    def __enter__(self):
167 6
        self.activate()
168 6
        return self
169
170 6
    def __exit__(self, exc_type, exc_value, traceback):
171 6
        if self.name is None:
172 6
            raise RuntimeError('Namespace must be named.')
173 6
        self.deactivate()
174
175 6
    @property
176
    def path(self):
177
        """Return the full path of the namespace."""
178 6
        if self.name is None or self.parent is None:
179
            raise ValueError
180 6
        if isinstance(self.parent, Namespace):
181 6
            parent_path = self.parent.path
182
        else:
183 6
            parent_path = ()
184 6
        return parent_path + (self.name,)
185
186 6
    @classmethod
187
    def __get_helper(cls, target, path):
188
        """Return the namespace for `target` at `path`, create if needed."""
189 6
        if isinstance(target, _Namespaceable):
190 6
            path_ = target, path
191 6
            namespaces = cls.__namespaces
192
        else:
193 6
            path_ = path
194 6
            try:
195 6
                namespaces = target.__namespaces
196 6
            except AttributeError:
197 6
                namespaces = {}
198 6
                target.__namespaces = namespaces
199 6
        return path_, namespaces
200
201 6
    @classmethod
202
    def namespace_exists(cls, path, target):
203
        """Return whether the given namespace exists."""
204 6
        path_, namespaces = cls.__get_helper(target, path)
205 6
        return path_ in namespaces
206
207 6
    @classmethod
208
    def get_namespace(cls, target, path):
209
        """Return a namespace with given target and path, create if needed."""
210 6
        path_, namespaces = cls.__get_helper(target, path)
211 6
        try:
212 6
            return namespaces[path_]
213 6
        except KeyError:
214 6
            if len(path) == 1:
215 6
                parent = {}
216
            else:
217 6
                parent = cls.get_namespace(target, path[:-1])
218 6
            return namespaces.setdefault(path_, cls.premake(path[-1], parent))
219
220 6
    @classmethod
221
    def no_blocker(cls, path, cls_):
222
        """Return False if there's a non-Namespace object in the path."""
223 6
        try:
224 6
            namespace = vars(cls_)
225 6
            for name in path:
226 6
                namespace = namespace[name]
227 6
                if not isinstance(namespace, cls):
228 6
                    return False
229 6
        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...
230 6
            pass
231 6
        return True
232
233 6
    def add(self, target):
234
        """Add self as a namespace under target."""
235 6
        path, namespaces = self.__get_helper(target, self.path)
236 6
        res = namespaces.setdefault(path, self)
237 6
        if res is self:
238 6
            self.parent_object = target
239
240 6
    def set_if_none(self, name, value):
241
        """Set the attribute `name` to `value`, if it's initially None."""
242 6
        if getattr(self, name) is None:
243 6
            setattr(self, name, value)
244
245 6
    def push(self, name, scope):
246
        """Bind self to the given name and scope, and activate."""
247 6
        self.set_if_none('name', name)
248 6
        self.set_if_none('scope', scope)
249 6
        self.set_if_none('parent', scope.dicts[0])
250 6
        if name != self.name:
251 6
            raise ValueError('Cannot rename namespace')
252 6
        if scope is not self.scope:
253
            raise ValueError('Cannot reuse namespace')
254 6
        if scope.dicts[0] is not self.parent:
255
            raise ValueError('Cannot reparent namespace')
256 6
        self.scope.namespaces.append(self)
257 6
        self.activate()
258
259 6
    def activate(self):
260
        """Take over as the scope for the target."""
261 6
        if self.scope is not None and not self.active:
262 6
            if self.scope.dicts[0] is not self.parent:
263
                raise ValueError('Cannot reparent namespace')
264 6
            self.active = True
265 6
            self.scope.dicts.insert(0, self)
266
267 6
    def deactivate(self):
268
        """Stop being the scope for the target."""
269 6
        if self.scope is not None and self.active:
270 6
            self.active = False
271 6
            self.scope.dicts.pop(0)
272
273 6
    def __get__(self, instance, owner):
274 6
        return _NamespaceProxy(self, instance, owner)
275
276
277 6
class _NamespaceScope(collections.abc.MutableMapping):
278
279
    """The class creation namespace for _Namespaceables."""
280
281 6
    __slots__ = 'dicts', 'namespaces', 'proxies', 'scope_proxy'
282
283 6
    def __init__(self, dct):
284 6
        self.dicts = [dct]
285 6
        self.namespaces = []
286 6
        self.proxies = proxies = weakref.WeakKeyDictionary()
287
288 6
        class ScopeProxy(_ScopeProxy):
289
290
            """Local version of ScopeProxy for this scope."""
291
292 6
            __slots__ = ()
293
294 6
            def __init__(self, dct):
295 6
                super().__init__(dct, proxies)
296
297 6
        self.scope_proxy = ScopeProxy
298
299
    # Mapping methods need to know about the dot syntax.
300
    # Possibly namespaces themselves should know. Would simplify some things.
301
302 6
    def __getitem__(self, key):
303 6
        value = collections.ChainMap(*self.dicts)[key]
304 6
        if isinstance(value, Namespace):
305 6
            value = self.scope_proxy(value)
306 6
        return value
307
308 6
    def __setitem__(self, key, value):
309 6
        dct = self.dicts[0]
310 6
        if isinstance(value, self.scope_proxy):
311 6
            value = self.proxies[value]
312 6
        if isinstance(value, Namespace) and value.name != key:
313 6
            value.push(key, self)
314 6
        dct[key] = value
315
316 6
    def __delitem__(self, key):
317 6
        del self.dicts[0][key]
318
319 6
    def __iter__(self):
320
        return iter(collections.ChainMap(*self.dicts))
321
322 6
    def __len__(self):
323
        return len(collections.ChainMap(*self.dicts))
324
325
326 6
_NAMESPACE_SCOPES = weakref.WeakKeyDictionary()
327
328
329 6
class _NamespaceBase:
330
331
    """Common base class for Namespaceable and its metaclass."""
332
333 6
    __slots__ = ()
334
335
    # Note: the dot format of invocation can "escape" self into other objects.
336
    # This is not intended behavior, and the result of using dots "too deeply"
337
    # should be considered undefined.
338
    # I would like it to be an error, if I can figure out how.
339
340 6
    @staticmethod
341
    def __is_proxy(value):
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...
342 6
        if not isinstance(value, _NamespaceProxy):
343
            raise ValueError('Given a dot attribute that went too deep.')
344 6
        return value
345
346 6
    def __getattribute__(self, name):
347 6
        parent, is_namespace, name_ = name.rpartition('.')
348 6
        if is_namespace:
349 6
            self_ = self
350 6
            for element in parent.split('.'):
351 6
                self_ = self.__is_proxy(getattr(self_, element))
352 6
            return getattr(getattr(self, parent), name_)
353 6
        return super(_NamespaceBase, type(self)).__getattribute__(self, name)
354
355 6
    def __setattr__(self, name, value):
356 6
        parent, is_namespace, name_ = name.rpartition('.')
357 6
        if is_namespace:
358 6
            setattr(self.__is_proxy(getattr(self, parent)), name_, value)
359 6
            return
360 6
        super(_NamespaceBase, type(self)).__setattr__(self, name, value)
361
362 6
    def __delattr__(self, name):
363 6
        parent, is_namespace, name_ = name.rpartition('.')
364 6
        if is_namespace:
365 6
            delattr(self.__is_proxy(getattr(self, parent)), name_)
366 6
            return
367
        super(_NamespaceBase, type(self)).__delattr__(self, name)
368
369
370 6
class _Namespaceable(_NamespaceBase, type):
371
372
    """Metaclass for classes that can contain namespaces.
373
374
    Using the metaclass directly is a bad idea. Use a base class instead.
375
    """
376
377 6
    @classmethod
378
    def __prepare__(mcs, name, bases, **kwargs):
379 6
        return _NamespaceScope(super().__prepare__(name, bases, **kwargs))
380
381 6
    def __new__(mcs, name, bases, dct, **kwargs):
382 6
        cls = super().__new__(mcs, name, bases, dct.dicts[0], **kwargs)
383 6
        if _DEFINED and not issubclass(cls, Namespaceable):
384
            raise ValueError(
385
                'Cannot create a _Namespaceable that does not inherit from '
386
                'Namespaceable')
387 6
        _NAMESPACE_SCOPES[cls] = dct
388 6
        for namespace in dct.namespaces:
389 6
            namespace.add(cls)
390 6
            if ENABLE_SET_NAME:
391 2
                for name, value in namespace.items():
392 2
                    wrapped = _DescriptorInspector(value)
393 2
                    if wrapped.has_set_name:
394 2
                        wrapped.set_name(cls, name)
395 6
        return cls
396
397 6
    def __setattr__(cls, name, value):
398 6
        if (
399
                '.' not in name and isinstance(value, Namespace) and
400
                value.name != name):
401 6
            value.push(name, _NAMESPACE_SCOPES[cls])
402 6
            value.add(cls)
403 6
        super(_Namespaceable, type(cls)).__setattr__(cls, name, value)
404
405
406 6
_DEFINED = False
407
408
409 6
class Namespaceable(_NamespaceBase, metaclass=_Namespaceable):
410
411
    """Base class for classes that can contain namespaces.
412
413
    A note for people extending the functionality:
414
    The base class for Namespaceable and its metaclass uses a non-standard
415
    super() invocation in its definitions of several methods. This was the only
416
    way I could find to mitigate some bugs I encountered with a standard
417
    invocation. If you override any of methods defined on built-in types, I
418
    recommend this form for maximal reusability:
419
420
    super(class, type(self)).__method__(self, ...)
421
422
    This avoids confusing error messages in case self is a subclass of class,
423
    in addition to being an instance.
424
425
    If you're not delegating above Namespaceable, you can probably use the
426
    standard invocation, unless you bring about the above situation on your own
427
    types.
428
    """
429
430
431
_DEFINED = True
432