Completed
Push — master ( 9d2694...4eafe1 )
by Max
01:52
created

Namespaceable.__getattribute__()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
ccs 4
cts 5
cp 0.8
crap 2.032
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 6
    def __setitem__(self, key, value):
157 6
        if (
158
                self.parent_object is not None and
159
                isinstance(value, Namespace) and value.name != key):
160 6
            value.push(key, self.scope)
161 6
            value.add(self.parent_object)
162 6
        super().__setitem__(key, value)
163
164 6
    def __enter__(self):
165 6
        self.activate()
166 6
        return self
167
168 6
    def __exit__(self, exc_type, exc_value, traceback):
169 6
        if self.name is None:
170 6
            raise RuntimeError('Namespace must be named.')
171 6
        self.deactivate()
172
173 6
    @property
174
    def path(self):
175
        """Return the full path of the namespace."""
176 6
        if self.name is None or self.parent is None:
177
            raise ValueError
178 6
        if isinstance(self.parent, Namespace):
179 6
            parent_path = self.parent.path
180
        else:
181 6
            parent_path = ()
182 6
        return parent_path + (self.name,)
183
184 6
    @classmethod
185
    def __get_helper(cls, target, path):
186
        """Return the namespace for `target` at `path`, create if needed."""
187 6
        if isinstance(target, _Namespaceable):
188 6
            path_ = target, path
189 6
            namespaces = cls.__namespaces
190
        else:
191 6
            path_ = path
192 6
            try:
193 6
                namespaces = target.__namespaces
194 6
            except AttributeError:
195 6
                namespaces = {}
196 6
                target.__namespaces = namespaces
197 6
        return path_, namespaces
198
199 6
    @classmethod
200
    def namespace_exists(cls, path, target):
201
        """Return whether the given namespace exists."""
202 6
        path_, namespaces = cls.__get_helper(target, path)
203 6
        return path_ in namespaces
204
205 6
    @classmethod
206
    def get_namespace(cls, target, path):
207
        """Return a namespace with given target and path, create if needed."""
208 6
        path_, namespaces = cls.__get_helper(target, path)
209 6
        try:
210 6
            return namespaces[path_]
211 6
        except KeyError:
212 6
            if len(path) == 1:
213 6
                parent = {}
214
            else:
215 6
                parent = cls.get_namespace(target, path[:-1])
216 6
            return namespaces.setdefault(path_, cls.premake(path[-1], parent))
217
218 6
    @classmethod
219
    def no_blocker(cls, path, cls_):
220
        """Return False if there's a non-Namespace object in the path."""
221 6
        try:
222 6
            namespace = vars(cls_)
223 6
            for name in path:
224 6
                namespace = namespace[name]
225 6
                if not isinstance(namespace, cls):
226 6
                    return False
227 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...
228 6
            pass
229 6
        return True
230
231 6
    def add(self, target):
232
        """Add self as a namespace under target."""
233 6
        path, namespaces = self.__get_helper(target, self.path)
234 6
        res = namespaces.setdefault(path, self)
235 6
        if res is self:
236 6
            self.parent_object = target
237
238 6
    def set_if_none(self, name, value):
239
        """Set the attribute `name` to `value`, if it's initially None."""
240 6
        if getattr(self, name) is None:
241 6
            setattr(self, name, value)
242
243 6
    def push(self, name, scope):
244
        """Bind self to the given name and scope, and activate."""
245 6
        self.set_if_none('name', name)
246 6
        self.set_if_none('scope', scope)
247 6
        self.set_if_none('parent', scope.dicts[0])
248 6
        if name != self.name:
249 6
            raise ValueError('Cannot rename namespace')
250 6
        if scope is not self.scope:
251
            raise ValueError('Cannot reuse namespace')
252 6
        if scope.dicts[0] is not self.parent:
253
            raise ValueError('Cannot reparent namespace')
254 6
        self.scope.namespaces.append(self)
255 6
        self.activate()
256
257 6
    def activate(self):
258
        """Take over as the scope for the target."""
259 6
        if self.scope is not None and not self.active:
260 6
            if self.scope.dicts[0] is not self.parent:
261
                raise ValueError('Cannot reparent namespace')
262 6
            self.active = True
263 6
            self.scope.dicts.insert(0, self)
264
265 6
    def deactivate(self):
266
        """Stop being the scope for the target."""
267 6
        if self.scope is not None and self.active:
268 6
            self.active = False
269 6
            self.scope.dicts.pop(0)
270
271 6
    def __get__(self, instance, owner):
272 6
        return _NamespaceProxy(self, instance, owner)
273
274
275 6
class _NamespaceScope(collections.abc.MutableMapping):
276
277
    """The class creation namespace for _Namespaceables."""
278
279 6
    __slots__ = 'dicts', 'namespaces', 'proxies', 'scope_proxy'
280
281 6
    def __init__(self, dct):
282 6
        self.dicts = [dct]
283 6
        self.namespaces = []
284 6
        self.proxies = proxies = weakref.WeakKeyDictionary()
285
286 6
        class ScopeProxy(_ScopeProxy):
287
288
            """Local version of ScopeProxy for this scope."""
289
290 6
            __slots__ = ()
291
292 6
            def __init__(self, dct):
293 6
                super().__init__(dct, proxies)
294
295 6
        self.scope_proxy = ScopeProxy
296
297 6
    def __getitem__(self, key):
298 6
        value = collections.ChainMap(*self.dicts)[key]
299 6
        if isinstance(value, Namespace):
300 6
            value = self.scope_proxy(value)
301 6
        return value
302
303 6
    def __setitem__(self, key, value):
304 6
        dct = self.dicts[0]
305 6
        if isinstance(value, self.scope_proxy):
306 6
            value = self.proxies[value]
307 6
        if isinstance(value, Namespace) and value.name != key:
308 6
            value.push(key, self)
309 6
        dct[key] = value
310
311 6
    def __delitem__(self, key):
312 6
        del self.dicts[0][key]
313
314 6
    def __iter__(self):
315
        return iter(collections.ChainMap(*self.dicts))
316
317 6
    def __len__(self):
318
        return len(collections.ChainMap(*self.dicts))
319
320
321 6
_NAMESPACE_SCOPES = weakref.WeakKeyDictionary()
322
323
324 6
class _NamespaceBase:
325
326
    """Common base class for Namespaceable and its metaclass."""
327
328 6
    __slots__ = ()
329
330 6
    def __getattribute__(self, name):
331 6
        parent, is_namespace, name_ = name.rpartition('.')
332 6
        if is_namespace:
333 6
            self_ = self
334 6
            for element in parent.split('.'):
335 6
                self_ = getattr(self_, element)
336 6
            return getattr(getattr(self, parent), name_)
337 6
        return super(_NamespaceBase, type(self)).__getattribute__(self, name)
338
339 6
    def __setattr__(self, name, value):
340 6
        parent, is_namespace, name_ = name.rpartition('.')
341 6
        if is_namespace:
342 6
            setattr(getattr(self, parent), name_, value)
343 6
            return
344 6
        super(_NamespaceBase, type(self)).__setattr__(self, name, value)
345
346 6
    def __delattr__(self, name):
347 6
        parent, is_namespace, name_ = name.rpartition('.')
348 6
        if is_namespace:
349 6
            delattr(getattr(self, parent), name_)
350 6
            return
351
        super(_NamespaceBase, type(self)).__delattr__(self, name)
352
353
354 6
class _Namespaceable(_NamespaceBase, type):
355
356
    """Metaclass for classes that can contain namespaces.
357
358
    Using the metaclass directly is a bad idea. Use a base class instead.
359
    """
360
361 6
    @classmethod
362
    def __prepare__(mcs, name, bases, **kwargs):
363 6
        return _NamespaceScope(super().__prepare__(name, bases, **kwargs))
364
365 6
    def __new__(mcs, name, bases, dct, **kwargs):
366 6
        cls = super().__new__(mcs, name, bases, dct.dicts[0], **kwargs)
367 6
        if _DEFINED and not issubclass(cls, Namespaceable):
368
            raise ValueError(
369
                'Cannot create a _Namespaceable that does not inherit from '
370
                'Namespaceable')
371 6
        _NAMESPACE_SCOPES[cls] = dct
372 6
        for namespace in dct.namespaces:
373 6
            namespace.add(cls)
374 6
            if ENABLE_SET_NAME:
375 2
                for name, value in namespace.items():
376 2
                    wrapped = _DescriptorInspector(value)
377 2
                    if wrapped.has_set_name:
378 2
                        wrapped.set_name(cls, name)
379 6
        return cls
380
381 6
    def __setattr__(cls, name, value):
382 6
        if isinstance(value, Namespace) and value.name != name:
383 6
            value.push(name, _NAMESPACE_SCOPES[cls])
384 6
            value.add(cls)
385 6
        super(_Namespaceable, type(cls)).__setattr__(cls, name, value)
386
387
388 6
_DEFINED = False
389
390
391 6
class Namespaceable(_NamespaceBase, metaclass=_Namespaceable):
392
393
    """Base class for classes that can contain namespaces."""
394
395 6
    def __maps(self, parent):
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...
396
        path = tuple(parent.split('.'))
397
        instance_namespace = Namespace.get_namespace(self, path)
0 ignored issues
show
Unused Code introduced by
The variable instance_namespace seems to be unused.
Loading history...
398
        if isinstance(type(self), _Namespaceable):
399
            owner_namespace = Namespace.get_namespace(type(self), path)
0 ignored issues
show
Unused Code introduced by
The variable owner_namespace seems to be unused.
Loading history...
400
            instance_value = ops.get(instance_map, name)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'instance_map'
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'name'
Loading history...
Unused Code introduced by
The variable instance_value seems to be unused.
Loading history...
401
        else:
402
            owner_namespace = None
403
404 6
    def __getattribute__(self, name):
405 6
        parent, is_namespace, name = name.rpartition('.')
406 6
        if is_namespace:
407
            maps = self.__maps(parent)
0 ignored issues
show
Unused Code introduced by
The variable maps seems to be unused.
Loading history...
408 6
        return super(Namespaceable, type(self)).__getattribute__(self, name)
409
410
411
_DEFINED = True
412