Completed
Push — master ( 4647ae...c1a5b0 )
by Max
02:48
created

Namespace.get_namespace()   A

Complexity

Conditions 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 12
ccs 9
cts 9
cp 1
crap 3
rs 9.4285
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
    parent_object = dct.parent_object
52 6
    index = mro.index(parent_object)
53 6
    mro = mro[index:]
54 6
    return _mro_to_chained(mro, dct.path)
55
56
57 6
def _retarget(ns_proxy):
58
    """Convert a class lookup to an instance lookup, if needed."""
59 6
    dct, instance, owner = _PROXY_INFOS[ns_proxy]
60 6
    if instance is None and isinstance(type(owner), _Namespaceable):
61 6
        instance, owner = owner, type(owner)
62 6
        dct = Namespace.get_namespace(owner, dct.path)
63 6
        ns_proxy = _NamespaceProxy(dct, instance, owner)
64 6
    return ns_proxy
65
66
67 6
class _NamespaceProxy(_Proxy):
68
69
    """Proxy object for manipulating and querying namespaces."""
70
71 6
    __slots__ = '__weakref__',
72
73 6
    def __init__(self, dct, instance, owner):
74 6
        _PROXY_INFOS[self] = dct, instance, owner
75
76 6
    def __dir__(self):
77 6
        return collections.ChainMap(_instance_map(self), _mro_map(self))
78
79 6
    def __getattribute__(self, name):
80 6
        self = _retarget(self)
81 6
        _, instance, owner = _PROXY_INFOS[self]
82 6
        instance_map = _instance_map(self)
83 6
        mro_map = _mro_map(self)
84 6
        instance_value = ops.get(instance_map, name)
85 6
        mro_value = ops.get(mro_map, name)
86 6
        if ops.is_data(mro_value):
87 6
            return mro_value.get(instance, owner)
88 6
        elif issubclass(owner, type) and ops.has_get(instance_value):
89 6
            return instance_value.get(None, instance)
90 6
        elif instance_value is not None:
91 6
            return instance_value.object
92 6
        elif ops.has_get(mro_value):
93 6
            return mro_value.get(instance, owner)
94 6
        elif mro_value is not None:
95 6
            return mro_value.object
96
        else:
97 6
            raise AttributeError(name)
98
99 6
    def __setattr__(self, name, value):
100 6
        self = _retarget(self)
101 6
        dct, instance, owner = _PROXY_INFOS[self]
102 6
        if instance is None:
103 6
            real_map = Namespace.get_namespace(owner, dct.path)
104 6
            real_map[name] = value
105 6
            return
106 6
        mro_map = _mro_map(self)
107 6
        target_value = ops.get_data(mro_map, name)
108 6
        if target_value is not None:
109
            # These lines will be called on a data descriptor.
110 6
            target_value.set(instance, value)
111 6
            return
112 6
        instance_map = Namespace.get_namespace(instance, dct.path)
113 6
        instance_map[name] = value
114
115 6
    def __delattr__(self, name):
116 6
        self = _retarget(self)
117 6
        dct, instance, owner = _PROXY_INFOS[self]
118 6
        real_map = Namespace.get_namespace(owner, dct.path)
119 6
        if instance is None:
120 6
            ops.delete(real_map, name)
121 6
            return
122 6
        value = ops.get_data(real_map, name)
123 6
        if value is not None:
124
            # These lines will be called on a data descriptor.
125 6
            value.delete(instance)
126 6
            return
127 6
        instance_map = Namespace.get_namespace(instance, dct.path)
128 6
        ops.delete(instance_map, name)
129
130
131 6
_NAMESPACE_INFOS = weakref.WeakKeyDictionary()
132
133
134 6
class Namespace(dict):
135
136
    """Namespace."""
137
138 6
    __slots__ = (
139
        'name', 'scope', 'parent', 'active', 'parent_object', 'needs_setup')
140
141 6
    __namespaces = {}
142
143 6
    def __init__(self, *args, **kwargs):
144 6
        super().__init__(*args, **kwargs)
145 6
        bad_values = tuple(
146
            value for value in self.values() if
147
            isinstance(value, (Namespace, _Proxy)))
148 6
        if bad_values:
149 6
            raise ValueError('Bad values: {}'.format(bad_values))
150 6
        self.name = None
151 6
        self.scope = None
152 6
        self.parent = None
153 6
        self.active = False
154 6
        self.parent_object = None
155 6
        self.needs_setup = False
156
157 6
    @classmethod
158
    def premake(cls, name, parent):
159
        """Return an empty namespace with the given name and parent."""
160 6
        self = cls()
161 6
        self.name = name
162 6
        self.parent = parent
163 6
        return self
164
165
    # Hold up. Do we need a symmetric addon to __delitem__?
166
    # I forget how this works.
167 6
    def __setitem__(self, key, value):
168 6
        if (
169
                self.scope is not None and
170
                isinstance(value, self.scope.scope_proxy)):
171 6
            value = self.scope.proxies[value]
172 6
        if isinstance(value, (_ScopeProxy, _NamespaceProxy)):
173 6
            raise ValueError('Cannot move scopes between classes.')
174 6
        if isinstance(value, Namespace):
175 6
            value.push(key, self.scope, self)
176 6
            value.add(self.parent_object)
177 6
        super().__setitem__(key, value)
178
179 6
    def __enter__(self):
180 6
        self.needs_setup = True
181 6
        self.activate()
182 6
        return self
183
184 6
    def __exit__(self, exc_type, exc_value, traceback):
185 6
        if self.name is None:
186 6
            raise RuntimeError('Namespace must be named.')
187 6
        self.deactivate()
188
189 6
    @property
190
    def path(self):
191
        """Return the full path of the namespace."""
192 6
        if self.name is None or self.parent is None:
193
            # This line can be hit by Namespace().path.
194 6
            raise ValueError
195 6
        if isinstance(self.parent, Namespace):
196 6
            parent_path = self.parent.path
197
        else:
198 6
            parent_path = ()
199 6
        return parent_path + (self.name,)
200
201 6
    @classmethod
202
    def __get_helper(cls, target, path):
203
        """Return the namespace for `target` at `path`, create if needed."""
204 6
        if isinstance(target, _Namespaceable):
205 6
            path_ = target, path
206 6
            namespaces = cls.__namespaces
207
        else:
208 6
            path_ = path
209 6
            try:
210 6
                namespaces = _NAMESPACE_INFOS[target]
211 6
            except KeyError:
212 6
                namespaces = {}
213 6
                _NAMESPACE_INFOS[target] = namespaces
214 6
        return path_, namespaces
215
216 6
    @classmethod
217
    def namespace_exists(cls, path, target):
218
        """Return whether the given namespace exists."""
219 6
        path_, namespaces = cls.__get_helper(target, path)
220 6
        return path_ in namespaces
221
222 6
    @classmethod
223
    def get_namespace(cls, target, path):
224
        """Return a namespace with given target and path, create if needed."""
225 6
        path_, namespaces = cls.__get_helper(target, path)
226 6
        try:
227 6
            return namespaces[path_]
228 6
        except KeyError:
229 6
            if len(path) == 1:
230 6
                parent = {}
231
            else:
232 6
                parent = cls.get_namespace(target, path[:-1])
233 6
            return namespaces.setdefault(path_, cls.premake(path[-1], parent))
234
235 6
    @classmethod
236
    def no_blocker(cls, path, cls_):
237
        """Return False if there's a non-Namespace object in the path."""
238 6
        try:
239 6
            namespace = vars(cls_)
240 6
            for name in path:
241 6
                namespace = namespace[name]
242 6
                if not isinstance(namespace, cls):
243 6
                    return False
244 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...
245 6
            pass
246 6
        return True
247
248 6
    def add(self, target):
249
        """Add self as a namespace under target."""
250 6
        if target is not None:
251 6
            path, namespaces = self.__get_helper(target, self.path)
252 6
            res = namespaces.setdefault(path, self)
253 6
            if res is self:
254 6
                self.parent_object = target
255
256 6
    def set_if_none(self, name, value):
257
        """Set the attribute `name` to `value`, if it's initially None."""
258 6
        if getattr(self, name) is None:
259 6
            setattr(self, name, value)
260
261 6
    def validate_assignment(self, name, scope):
262
        """Confirm the instance can be assigned to the given name and scope."""
263 6
        if name != self.name:
264 6
            raise ValueError('Cannot rename namespace')
265 6
        if scope is not self.scope:
266
            # It should be possible to hit this line by assigning a namespace
267
            # into another class. It may not be, however.
268 6
            raise ValueError('Cannot reuse namespace')
269
270 6
    def validate_parent(self, parent):
271
        """Confirm that the instance has the correct parent dict."""
272 6
        if parent is not self.parent:
273
            # This line can be hit by assigning a namespace into another
274
            # namespace.
275 6
            raise ValueError('Cannot reparent namespace')
276
277 6
    def push(self, name, scope, parent):
278
        """Bind self to the given name and scope, and activate."""
279 6
        self.set_if_none('name', name)
280 6
        self.set_if_none('scope', scope)
281 6
        self.set_if_none('parent', parent)
282 6
        self.validate_assignment(name, scope)
283 6
        self.validate_parent(parent)
284 6
        self.scope.namespaces.append(self)
285 6
        if self.needs_setup:
286 6
            self.activate()
287
288 6
    def activate(self):
289
        """Take over as the scope for the target."""
290 6
        if self.active:
291 6
            raise ValueError('Cannot double-activate.')
292 6
        if self.scope is not None:
293 6
            self.validate_parent(self.scope.head)
294 6
            self.active = True
295 6
            self.scope.push(self)
296 6
            self.needs_setup = False
297
298 6
    def deactivate(self):
299
        """Stop being the scope for the target."""
300 6
        if self.scope is not None and self.active:
301 6
            self.active = False
302 6
            self.scope.pop_()
303
304 6
    def total_length(self):
305
        """Return the total number of elements in the Namespace tree."""
306
        return len(self) + sum(
307
            ns.total_length() for ns in self.values() if
308
            isinstance(ns, Namespace))
309
310 6
    def iter_all(self, prefix):
311
        """Iterate over all elements in the Namespace tree."""
312 6
        for key, value in self.items():
313 6
            qualified_name = prefix + key
314 6
            yield qualified_name
315 6
            if isinstance(value, Namespace):
316 6
                yield from value.iter_all(qualified_name + '.')
317
318 6
    def __get__(self, instance, owner):
319 6
        return _NamespaceProxy(self, instance, owner)
320
321 6
    def __bool__(self):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
322 6
        return True
323
324
325 6
class _NamespaceScope(collections.abc.MutableMapping):
326
327
    """The class creation namespace for _Namespaceables."""
328
329 6
    __slots__ = '_dicts', 'namespaces', 'proxies', 'scope_proxy', 'finalized'
330
331 6
    def __init__(self, dct):
332 6
        self._dicts = [dct]
333 6
        self.namespaces = []
334 6
        self.proxies = proxies = weakref.WeakKeyDictionary()
335 6
        self.finalized = False
336
337 6
        class ScopeProxy(_ScopeProxy):
338
339
            """Local version of ScopeProxy for this scope."""
340
341 6
            __slots__ = ()
342
343 6
            def __init__(self, dct):
344 6
                super().__init__(dct, proxies)
345
346 6
        self.scope_proxy = ScopeProxy
347
348
    # Mapping methods need to know about the dot syntax.
349
    # Possibly namespaces themselves should know. Would simplify some things.
350
351 6
    @property
352
    def head(self):
353
        """The innermost Namespace scope."""
354 6
        return self._dicts[0]
355
356 6
    def finalize(self):
357
        """Mark the scope as no longer active, and return the head."""
358 6
        if len(self._dicts) != 1:
359 6
            raise ValueError('Cannot finalize a pushed scope!')
360 6
        self.finalized = True
361 6
        return self.head
362
363 6
    def push(self, dct):
364
        """Add a new active Namespace to the scope."""
365 6
        if self.finalized:
366 6
            raise ValueError('Cannot push a finalized scope!')
367 6
        self._dicts.insert(0, dct)
368
369 6
    def pop_(self):
370
        """Remove the current active Namespace from the scope."""
371 6
        if len(self._dicts) == 1:
372 6
            raise ValueError('Cannot pop from a basal scope!')
373 6
        self._dicts.pop(0)
374
375 6
    def __getitem__(self, key):
376 6
        value = collections.ChainMap(*self._dicts)[key]
377 6
        if isinstance(value, Namespace):
378 6
            value = self.scope_proxy(value)
379 6
        return value
380
381 6
    def _store(self, key, value, dct):
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...
382
        # We just entered the context successfully.
383 6
        if value is dct:
384 6
            dct = self._dicts[1]
385 6
        if isinstance(value, Namespace):
386 6
            value.push(key, self, dct)
387 6
        if isinstance(value, self.scope_proxy):
388 6
            value = self.proxies[value]
389 6
            value.validate_parent(dct)
390 6
            value.validate_assignment(key, self)
391 6
        return value, dct
392
393 6
    def __setitem__(self, key, value):
394 6
        dct = self.head
395 6
        value, dct = self._store(key, value, dct)
396 6
        if isinstance(value, (_ScopeProxy, _NamespaceProxy)):
397 6
            raise ValueError('Cannot move scopes between classes.')
398 6
        dct[key] = value
399
400 6
    def __delitem__(self, key):
401 6
        del self.head[key]
402
403
    # These functions are incorrect and need to be rewritten.
404 6
    def __iter__(self):
405 6
        if not self.finalized:
406 6
            raise ValueError('Iteration not defined on unfinalized scope.')
407 6
        for key, value in self.head.items():
408 6
            yield key
409 6
            if isinstance(value, Namespace):
410 6
                yield from value.iter_all(prefix=key + '.')
411
412 6
    def __len__(self):
413 6
        if not self.finalized:
414 6
            raise ValueError('Length not defined on unfinalized scope.')
415
        return len(self.head) + sum(
416
            ns.total_length() for ns in self.head.values() if
417
            isinstance(ns, Namespace))
418
419
420 6
_NAMESPACE_SCOPES = weakref.WeakKeyDictionary()
421
422
423 6
class _NamespaceBase:
424
425
    """Common base class for Namespaceable and its metaclass."""
426
427 6
    __slots__ = ()
428
429
    # Note: the dot format of invocation can "escape" self into other objects.
430
    # This is not intended behavior, and the result of using dots "too deeply"
431
    # should be considered undefined.
432
    # I would like it to be an error, if I can figure out how.
433
434 6
    @staticmethod
435
    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...
436 6
        if not isinstance(value, _NamespaceProxy):
437
            # This line can be hit by doing what the error message says.
438 6
            raise ValueError('Given a dot attribute that went too deep.')
439 6
        return value
440
441 6
    def __getattribute__(self, name):
442 6
        parent, is_namespace, name_ = name.rpartition('.')
443 6
        if is_namespace:
444 6
            self_ = self
445 6
            for element in parent.split('.'):
446 6
                self_ = self.__is_proxy(getattr(self_, element))
447 6
            return getattr(getattr(self, parent), name_)
448 6
        return super(_NamespaceBase, type(self)).__getattribute__(self, name)
449
450 6
    def __setattr__(self, name, value):
451 6
        parent, is_namespace, name_ = name.rpartition('.')
452 6
        if is_namespace:
453 6
            setattr(self.__is_proxy(getattr(self, parent)), name_, value)
454 6
            return
455 6
        super(_NamespaceBase, type(self)).__setattr__(self, name, value)
456
457 6
    def __delattr__(self, name):
458 6
        parent, is_namespace, name_ = name.rpartition('.')
459 6
        if is_namespace:
460 6
            delattr(self.__is_proxy(getattr(self, parent)), name_)
461 6
            return
462
        # This line can be hit by deleting an attribute that isn't a namespace.
463 6
        super(_NamespaceBase, type(self)).__delattr__(self, name)
464
465
466 6
class _Namespaceable(_NamespaceBase, type):
467
468
    """Metaclass for classes that can contain namespaces.
469
470
    Using the metaclass directly is a bad idea. Use a base class instead.
471
    """
472
473 6
    @classmethod
474
    def __prepare__(mcs, name, bases, **kwargs):
475 6
        return _NamespaceScope(super().__prepare__(name, bases, **kwargs))
476
477 6
    def __new__(mcs, name, bases, dct, **kwargs):
478 6
        cls = super().__new__(mcs, name, bases, dct.finalize(), **kwargs)
479 6
        if _DEFINED and not issubclass(cls, Namespaceable):
480
            # This line can be hit with class(metaclass=type(Namespaceable)):
481 6
            raise ValueError(
482
                'Cannot create a _Namespaceable that does not inherit from '
483
                'Namespaceable')
484 6
        _NAMESPACE_SCOPES[cls] = dct
485 6
        for namespace in dct.namespaces:
486 6
            namespace.add(cls)
487 6
            if ENABLE_SET_NAME:
488 2
                for name, value in namespace.items():
489 2
                    wrapped = _DescriptorInspector(value)
490 2
                    if wrapped.has_set_name:
491 2
                        wrapped.set_name(cls, name)
492 6
        return cls
493
494 6
    def __setattr__(cls, name, value):
495 6
        if (
496
                '.' not in name and isinstance(value, Namespace) and
497
                value.name != name):
498 6
            scope = _NAMESPACE_SCOPES[cls]
499 6
            value.push(name, scope, scope.head)
500 6
            value.add(cls)
501 6
        super(_Namespaceable, type(cls)).__setattr__(cls, name, value)
502
503
504 6
_DEFINED = False
505
506
507 6
class Namespaceable(_NamespaceBase, metaclass=_Namespaceable):
508
509
    """Base class for classes that can contain namespaces.
510
511
    A note for people extending the functionality:
512
    The base class for Namespaceable and its metaclass uses a non-standard
513
    super() invocation in its definitions of several methods. This was the only
514
    way I could find to mitigate some bugs I encountered with a standard
515
    invocation. If you override any of methods defined on built-in types, I
516
    recommend this form for maximal reusability:
517
518
    super(class, type(self)).__method__(self, ...)
519
520
    This avoids confusing error messages in case self is a subclass of class,
521
    in addition to being an instance.
522
523
    If you're not delegating above Namespaceable, you can probably use the
524
    standard invocation, unless you bring about the above situation on your own
525
    types.
526
    """
527
528
529
_DEFINED = True
530