From ebf519bd9b25e68d5948a5b7f866c0d95227678c Mon Sep 17 00:00:00 2001 From: Lucidiot Date: Sat, 5 Oct 2019 14:36:24 +0200 Subject: [PATCH] Fix getattr on namespaces, closes #2 --- objtools/collections.py | 62 +++++++++++++-------------------------- tests/test_collections.py | 2 +- 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/objtools/collections.py b/objtools/collections.py index 90831ef..ea99f29 100644 --- a/objtools/collections.py +++ b/objtools/collections.py @@ -3,10 +3,7 @@ This module provides helpers for working with collections (as in abstract base classes defined in the ``collections.abc`` standard Python module: iterables, iterators, mappings, etc.). """ -from typing import ( - Mapping, MutableMapping, Sequence, Set, - Iterable, Iterator, ByteString, Any, -) +from typing import Mapping, Dict, Sequence, Set, Iterable, ByteString, Any def namespacify(value: Any) -> Any: @@ -42,12 +39,11 @@ def namespacify(value: Any) -> Any: return value -class Namespace(MutableMapping): +class Namespace(Dict): """ - A class that maps items of a mapping to attributes. Takes the same - arguments as a classic ``dict``. Attributes and items are kept in sync; - deleting an item deletes the attribute, and updating an item updates the - attribute, and vice-versa. + A subclass of ``dict`` which allows accessing items using attributes. + Attributes and items are kept in sync; deleting an item deletes the + attribute, and updating an item updates the attribute, and vice-versa. Getting an attribute will call :meth:`namespacify` on the returned value, to allow easier manipulation of complex structures such as JSON documents: @@ -60,45 +56,29 @@ class Namespace(MutableMapping): 42 """ - def __init__(self, *args: Iterable, **kwargs: Any): - """ - :param \\*args: Mappings or iterables yielding - ``(key, value)`` two-tuples. - :param \\**kwargs: Key-value pairs set as attributes. - """ - for iterable in (*args, kwargs): - if isinstance(iterable, Mapping): - iterable = iterable.items() - for k, v in iterable: - setattr(self, k, v) - def __getitem__(self, name: Any) -> Any: + return namespacify(super().__getitem__(name)) + + def __getattr__(self, name: str) -> Any: try: - attr = getattr(self, name) - except AttributeError: - raise KeyError(repr(name)) - return namespacify(attr) + return self[name] + except KeyError: + raise AttributeError(repr(name)) - def __setitem__(self, name: Any, value: Any) -> None: - setattr(self, name, value) + def __setattr__(self, name: str, value: Any) -> None: + self[name] = value - def __delitem__(self, name: Any) -> None: + def __delattr__(self, name: str) -> None: try: - delattr(self, name) - except AttributeError: - raise KeyError(repr(name)) - - def __iter__(self) -> Iterator: - return iter(self.__dict__) - - def __len__(self) -> int: - return len(self.__dict__) + del self[name] + except KeyError as e: + raise AttributeError(str(e)) def __repr__(self) -> str: - return '{}({!r})'.format(self.__class__.__name__, self.__dict__) + return '{}({})'.format(self.__class__.__name__, super().__repr__()) def __str__(self) -> str: - return str(self.__dict__) + return str(dict(self)) def _repr_pretty_(self, p: Any, cycle: bool) -> None: # IPython's pretty printing @@ -106,11 +86,11 @@ class Namespace(MutableMapping): p.text('{}(...)'.format(self.__class__.__name__)) else: p.text('{}('.format(self.__class__.__name__)) - p.pretty(self.__dict__) + p.pretty(dict(self)) p.text(')') def copy(self) -> 'Namespace': """ Create a shallow copy of this namespace. """ - return self.__class__(self.__dict__) + return self.__class__(self) diff --git a/tests/test_collections.py b/tests/test_collections.py index 51293a4..ce0e5a6 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -55,7 +55,7 @@ class TestNamespace(TestCase): def test_init(self) -> None: ns: Namespace = Namespace() self.assertDictEqual(dict(ns), {}) - ns = Namespace({'b': 3}, {'a': 4}, a=1, b=2) + ns = Namespace({'b': 3}, a=1, b=2) self.assertDictEqual(dict(ns), {'a': 1, 'b': 2}) def test_getitem(self) -> None: