parent
514e643b8a
commit
9fc3788266
@ -0,0 +1,48 @@
|
||||
from types import MappingProxyType
|
||||
|
||||
|
||||
class NamespaceProxy():
|
||||
"""
|
||||
Read-only proxy of a mapping (like a dict) allowing item access via attributes. Mapping keys
|
||||
that are not strings will be ignored, and attribute access to any names starting with "__"
|
||||
will still be passed to the actual object attributes.
|
||||
|
||||
Being a proxy, attributes available and their values will change as the underlying backing
|
||||
dict is changed.
|
||||
|
||||
Intended for sitatuations where a dynamic mapping needs to be passed out to client code but
|
||||
you'd like to heavily suggest that it not be modified.
|
||||
|
||||
Note that only the top-level mapping is read only - if the attribute values themselves are
|
||||
mutable, they may still be modified via the NamespaceProxy.
|
||||
"""
|
||||
|
||||
def __init__(self, backing_dict):
|
||||
"""
|
||||
Create a new NamespaceProxy, with attribute access to the underlying backing dict passed
|
||||
in.
|
||||
"""
|
||||
object.__setattr__(self, "_dict_proxy", MappingProxyType(backing_dict))
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name.startswith("__"):
|
||||
return object.__getattribute__(self, name)
|
||||
return object.__getattribute__(self, "_dict_proxy")[name]
|
||||
|
||||
def __setattr__(self, *args):
|
||||
raise TypeError("NamespaceProxy does not allow attributes to be modified")
|
||||
|
||||
def __delattr__(self, *args):
|
||||
raise TypeError("NamespaceProxy does not allow attributes to be modified")
|
||||
|
||||
def __repr__(self):
|
||||
keys = sorted(object.__getattribute__(self, "_dict_proxy"))
|
||||
items = ("{}={!r}".format(k, object.__getattribute__(
|
||||
self, "_dict_proxy")[k]) for k in keys)
|
||||
return "{}({})".format(type(self).__name__, ", ".join(items))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return (object.__getattribute__(self, "_dict_proxy") ==
|
||||
object.__getattribute__(other, "_dict_proxy"))
|
||||
return False
|
||||
@ -0,0 +1,65 @@
|
||||
# pylint: disable=comparison-with-callable, pointless-statement
|
||||
import pytest
|
||||
from namespace_proxy import NamespaceProxy
|
||||
|
||||
|
||||
def test_namespace_access():
|
||||
def dummy_func(val_a):
|
||||
return F"ret {val_a}"
|
||||
|
||||
back_dict = {'int': 1, 'str': 'string', 'func': dummy_func}
|
||||
proxy = NamespaceProxy(back_dict)
|
||||
assert proxy.int == 1
|
||||
assert proxy.str == 'string'
|
||||
assert proxy.func == dummy_func
|
||||
assert proxy.func(5) == 'ret 5'
|
||||
|
||||
|
||||
def test_namespace_errors():
|
||||
back_dict = {'b': 2}
|
||||
proxy = NamespaceProxy(back_dict)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
proxy.a
|
||||
|
||||
with pytest.raises(TypeError, match="does not allow attributes to be modified"):
|
||||
proxy.b = 3
|
||||
|
||||
with pytest.raises(TypeError, match="does not allow attributes to be modified"):
|
||||
del proxy.b
|
||||
|
||||
|
||||
def test_namespace_update():
|
||||
back_dict = {'b': 2}
|
||||
proxy = NamespaceProxy(back_dict)
|
||||
with pytest.raises(KeyError):
|
||||
proxy.a
|
||||
back_dict['a'] = 3
|
||||
assert proxy.a == 3
|
||||
|
||||
|
||||
def test_dunder_passthrough():
|
||||
back_dict = {'a': 1}
|
||||
proxy = NamespaceProxy(back_dict)
|
||||
d = dir(proxy)
|
||||
print(d)
|
||||
assert proxy.__class__ == NamespaceProxy
|
||||
|
||||
|
||||
def test_equality():
|
||||
back_dict = {'a': 1}
|
||||
proxy = NamespaceProxy(back_dict)
|
||||
proxy_b = NamespaceProxy(back_dict)
|
||||
proxy_c = NamespaceProxy({'c': 3})
|
||||
assert proxy == proxy_b
|
||||
assert proxy != proxy_c
|
||||
assert proxy != back_dict
|
||||
assert proxy is not False
|
||||
assert proxy
|
||||
|
||||
|
||||
def test_repr():
|
||||
back_dict = {'a': 1, 'b': 2}
|
||||
proxy = NamespaceProxy(back_dict)
|
||||
assert str(proxy) == "NamespaceProxy(a=1, b=2)"
|
||||
assert repr(proxy) == "NamespaceProxy(a=1, b=2)"
|
||||
Loading…
Reference in new issue