Completed
Push — master ( 7962cd...0243d6 )
by Max
01:50
created

_Namespaceable.__new__()   B

Complexity

Conditions 7

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
c 1
b 0
f 0
dl 0
loc 16
ccs 13
cts 13
cp 1
crap 7
rs 7.3333
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
            # These lines will be called on a data descriptor.
108 6
            target_value.set(instance, value)
109 6
            return
110 6
        instance_map = Namespace.get_namespace(instance, dct.path)
111 6
        instance_map[name] = value
112
113 6
    def __delattr__(self, name):
114 6
        self = _retarget(self)
115 6
        dct, instance, owner = _PROXY_INFOS[self]
116 6
        real_map = Namespace.get_namespace(owner, dct.path)
117 6
        if instance is None:
118 6
            ops.delete(real_map, name)
119 6
            return
120 6
        value = ops.get_data(real_map, name)
121 6
        if value is not None:
122
            # These lines will be called on a data descriptor.
123 6
            value.delete(instance)
124 6
            return
125 6
        instance_map = Namespace.get_namespace(instance, dct.path)
126 6
        ops.delete(instance_map, name)
127
128
129 6
class Namespace(dict):
130
131
    """Namespace."""
132
133 6
    __slots__ = 'name', 'scope', 'parent', 'active', 'parent_object'
134
135 6
    __namespaces = {}
136
137 6
    def __init__(self, *args, **kwargs):
138 6
        super().__init__(*args, **kwargs)
139 6
        bad_values = tuple(
140
            value for value in self.values() if
141
            isinstance(value, (Namespace, _Proxy)))
142 6
        if bad_values:
143 6
            raise ValueError('Bad values: {}'.format(bad_values))
144 6
        self.name = None
145 6
        self.scope = None
146 6
        self.parent = None
147 6
        self.active = False
148 6
        self.parent_object = None
149
150 6
    @classmethod
151
    def premake(cls, name, parent):
152
        """Return an empty namespace with the given name and parent."""
153 6
        self = cls()
154 6
        self.name = name
155 6
        self.parent = parent
156 6
        return self
157
158
    # Hold up. Do we need a symmetric addon to __delitem__?
159
    # I forget how this works.
160 6
    def __setitem__(self, key, value):
161 6
        if (
162
                self.parent_object is not None and
163
                isinstance(value, Namespace) and value.name != key):
164 6
            value.push(key, self.scope)
165 6
            value.add(self.parent_object)
166 6
        super().__setitem__(key, value)
167
168 6
    def __enter__(self):
169 6
        self.activate()
170 6
        return self
171
172 6
    def __exit__(self, exc_type, exc_value, traceback):
173 6
        if self.name is None:
174 6
            raise RuntimeError('Namespace must be named.')
175 6
        self.deactivate()
176
177 6
    @property
178
    def path(self):
179
        """Return the full path of the namespace."""
180 6
        if self.name is None or self.parent is None:
181
            # This line can be hit by Namespace().path.
182
            raise ValueError
183 6
        if isinstance(self.parent, Namespace):
184 6
            parent_path = self.parent.path
185
        else:
186 6
            parent_path = ()
187 6
        return parent_path + (self.name,)
188
189 6
    @classmethod
190
    def __get_helper(cls, target, path):
191
        """Return the namespace for `target` at `path`, create if needed."""
192 6
        if isinstance(target, _Namespaceable):
193 6
            path_ = target, path
194 6
            namespaces = cls.__namespaces
195
        else:
196 6
            path_ = path
197 6
            try:
198 6
                namespaces = target.__namespaces
199 6
            except AttributeError:
200 6
                namespaces = {}
201 6
                target.__namespaces = namespaces
202 6
        return path_, namespaces
203
204 6
    @classmethod
205
    def namespace_exists(cls, path, target):
206
        """Return whether the given namespace exists."""
207 6
        path_, namespaces = cls.__get_helper(target, path)
208 6
        return path_ in namespaces
209
210 6
    @classmethod
211
    def get_namespace(cls, target, path):
212
        """Return a namespace with given target and path, create if needed."""
213 6
        path_, namespaces = cls.__get_helper(target, path)
214 6
        try:
215 6
            return namespaces[path_]
216 6
        except KeyError:
217 6
            if len(path) == 1:
218 6
                parent = {}
219
            else:
220 6
                parent = cls.get_namespace(target, path[:-1])
221 6
            return namespaces.setdefault(path_, cls.premake(path[-1], parent))
222
223 6
    @classmethod
224
    def no_blocker(cls, path, cls_):
225
        """Return False if there's a non-Namespace object in the path."""
226 6
        try:
227 6
            namespace = vars(cls_)
228 6
            for name in path:
229 6
                namespace = namespace[name]
230 6
                if not isinstance(namespace, cls):
231 6
                    return False
232 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...
233 6
            pass
234 6
        return True
235
236 6
    def add(self, target):
237
        """Add self as a namespace under target."""
238 6
        path, namespaces = self.__get_helper(target, self.path)
239 6
        res = namespaces.setdefault(path, self)
240 6
        if res is self:
241 6
            self.parent_object = target
242
243 6
    def set_if_none(self, name, value):
244
        """Set the attribute `name` to `value`, if it's initially None."""
245 6
        if getattr(self, name) is None:
246 6
            setattr(self, name, value)
247
248 6
    def push(self, name, scope):
249
        """Bind self to the given name and scope, and activate."""
250 6
        self.set_if_none('name', name)
251 6
        self.set_if_none('scope', scope)
252 6
        self.set_if_none('parent', scope.dicts[0])
253 6
        if name != self.name:
254 6
            raise ValueError('Cannot rename namespace')
255 6
        if scope is not self.scope:
256
            # It should be possible to hit this line by assigning a namespace
257
            # into another class. It may not be, however.
258
            raise ValueError('Cannot reuse namespace')
259 6
        if scope.dicts[0] is not self.parent:
260
            # This line can be hit by assigning a namespace into another
261
            # namespace.
262
            raise ValueError('Cannot reparent namespace')
263 6
        self.scope.namespaces.append(self)
264 6
        self.activate()
265
266 6
    def activate(self):
267
        """Take over as the scope for the target."""
268 6
        if self.scope is not None and not self.active:
269 6
            if self.scope.dicts[0] is not self.parent:
270
                # This line can be hit by entering a namespace not under its
271
                # parent.
272
                raise ValueError('Cannot reparent namespace')
273 6
            self.active = True
274 6
            self.scope.dicts.insert(0, self)
275
276 6
    def deactivate(self):
277
        """Stop being the scope for the target."""
278 6
        if self.scope is not None and self.active:
279 6
            self.active = False
280 6
            self.scope.dicts.pop(0)
281
282 6
    def __get__(self, instance, owner):
283 6
        return _NamespaceProxy(self, instance, owner)
284
285
286 6
class _NamespaceScope(collections.abc.MutableMapping):
287
288
    """The class creation namespace for _Namespaceables."""
289
290 6
    __slots__ = 'dicts', 'namespaces', 'proxies', 'scope_proxy'
291
292 6
    def __init__(self, dct):
293 6
        self.dicts = [dct]
294 6
        self.namespaces = []
295 6
        self.proxies = proxies = weakref.WeakKeyDictionary()
296
297 6
        class ScopeProxy(_ScopeProxy):
298
299
            """Local version of ScopeProxy for this scope."""
300
301 6
            __slots__ = ()
302
303 6
            def __init__(self, dct):
304 6
                super().__init__(dct, proxies)
305
306 6
        self.scope_proxy = ScopeProxy
307
308
    # Mapping methods need to know about the dot syntax.
309
    # Possibly namespaces themselves should know. Would simplify some things.
310
311 6
    def __getitem__(self, key):
312 6
        value = collections.ChainMap(*self.dicts)[key]
313 6
        if isinstance(value, Namespace):
314 6
            value = self.scope_proxy(value)
315 6
        return value
316
317 6
    def __setitem__(self, key, value):
318 6
        dct = self.dicts[0]
319 6
        if isinstance(value, self.scope_proxy):
320 6
            value = self.proxies[value]
321 6
        if isinstance(value, Namespace) and value.name != key:
322 6
            value.push(key, self)
323 6
        dct[key] = value
324
325 6
    def __delitem__(self, key):
326 6
        del self.dicts[0][key]
327
328
    # These functions are incorrect and need to be rewritten.
329 6
    def __iter__(self):
330
        return iter(collections.ChainMap(*self.dicts))
331
332 6
    def __len__(self):
333
        return len(collections.ChainMap(*self.dicts))
334
335
336 6
_NAMESPACE_SCOPES = weakref.WeakKeyDictionary()
337
338
339 6
class _NamespaceBase:
340
341
    """Common base class for Namespaceable and its metaclass."""
342
343 6
    __slots__ = ()
344
345
    # Note: the dot format of invocation can "escape" self into other objects.
346
    # This is not intended behavior, and the result of using dots "too deeply"
347
    # should be considered undefined.
348
    # I would like it to be an error, if I can figure out how.
349
350 6
    @staticmethod
351
    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...
352 6
        if not isinstance(value, _NamespaceProxy):
353
            # This line can be hit by doing what the error message says.
354
            raise ValueError('Given a dot attribute that went too deep.')
355 6
        return value
356
357 6
    def __getattribute__(self, name):
358 6
        parent, is_namespace, name_ = name.rpartition('.')
359 6
        if is_namespace:
360 6
            self_ = self
361 6
            for element in parent.split('.'):
362 6
                self_ = self.__is_proxy(getattr(self_, element))
363 6
            return getattr(getattr(self, parent), name_)
364 6
        return super(_NamespaceBase, type(self)).__getattribute__(self, name)
365
366 6
    def __setattr__(self, name, value):
367 6
        parent, is_namespace, name_ = name.rpartition('.')
368 6
        if is_namespace:
369 6
            setattr(self.__is_proxy(getattr(self, parent)), name_, value)
370 6
            return
371 6
        super(_NamespaceBase, type(self)).__setattr__(self, name, value)
372
373 6
    def __delattr__(self, name):
374 6
        parent, is_namespace, name_ = name.rpartition('.')
375 6
        if is_namespace:
376 6
            delattr(self.__is_proxy(getattr(self, parent)), name_)
377 6
            return
378
        # This line can be hit by deleting an attribute that isn't a namespace.
379
        super(_NamespaceBase, type(self)).__delattr__(self, name)
380
381
382 6
class _Namespaceable(_NamespaceBase, type):
383
384
    """Metaclass for classes that can contain namespaces.
385
386
    Using the metaclass directly is a bad idea. Use a base class instead.
387
    """
388
389 6
    @classmethod
390
    def __prepare__(mcs, name, bases, **kwargs):
391 6
        return _NamespaceScope(super().__prepare__(name, bases, **kwargs))
392
393 6
    def __new__(mcs, name, bases, dct, **kwargs):
394 6
        cls = super().__new__(mcs, name, bases, dct.dicts[0], **kwargs)
395 6
        if _DEFINED and not issubclass(cls, Namespaceable):
396
            # This line can be hit with class(metaclass=type(Namespaceable)):
397 6
            raise ValueError(
398
                'Cannot create a _Namespaceable that does not inherit from '
399
                'Namespaceable')
400 6
        _NAMESPACE_SCOPES[cls] = dct
401 6
        for namespace in dct.namespaces:
402 6
            namespace.add(cls)
403 6
            if ENABLE_SET_NAME:
404 2
                for name, value in namespace.items():
405 2
                    wrapped = _DescriptorInspector(value)
406 2
                    if wrapped.has_set_name:
407 2
                        wrapped.set_name(cls, name)
408 6
        return cls
409
410 6
    def __setattr__(cls, name, value):
411 6
        if (
412
                '.' not in name and isinstance(value, Namespace) and
413
                value.name != name):
414 6
            value.push(name, _NAMESPACE_SCOPES[cls])
415 6
            value.add(cls)
416 6
        super(_Namespaceable, type(cls)).__setattr__(cls, name, value)
417
418
419 6
_DEFINED = False
420
421
422 6
class Namespaceable(_NamespaceBase, metaclass=_Namespaceable):
423
424
    """Base class for classes that can contain namespaces.
425
426
    A note for people extending the functionality:
427
    The base class for Namespaceable and its metaclass uses a non-standard
428
    super() invocation in its definitions of several methods. This was the only
429
    way I could find to mitigate some bugs I encountered with a standard
430
    invocation. If you override any of methods defined on built-in types, I
431
    recommend this form for maximal reusability:
432
433
    super(class, type(self)).__method__(self, ...)
434
435
    This avoids confusing error messages in case self is a subclass of class,
436
    in addition to being an instance.
437
438
    If you're not delegating above Namespaceable, you can probably use the
439
    standard invocation, unless you bring about the above situation on your own
440
    types.
441
    """
442
443
444
_DEFINED = True
445