diff --git a/readme.md b/readme.md index f6820f1..ae4b617 100644 --- a/readme.md +++ b/readme.md @@ -90,9 +90,7 @@ Example: You have original dictionary `{"foo": "bar"}`, and you expect from skin that `Skin({"foo": "bar"}).foo` is `"bar"` string, not skin wrapper. But, `str`, `bytes`, etc. have `__getitiem__` method. That is why there is `allowed` and `forbidden` tuples. I hope defaults are enough for 99% usecases. In general: if `value` have no `__getitem__` or not allowed or forbidden you will get `SkinValueError` exception, which skin catches to determine if object can be wrapped. -Skin class have two hardcoded attributes: -* `value` — original object, which skin wraps -* `_skin_config` — some skin internals +**Skin class have only one accessible attribute: `value` — original object, which skin wraps** Skin supports both "item" and "attribute" notations: ``` python @@ -110,6 +108,8 @@ False True >>> ``` +Both objects `s.foo` and `s["foo"]` is instances of `Skin`, but since they are created dynamicaly they are not the same object. + Skin use strict order to find "items": * in case of attribute access: * skin attribute @@ -148,20 +148,20 @@ Skin(defaultdict(, {'foo': [1]})) >>> ``` -# Benchmark +# Benchmark (v0.0.5) ``` text Create instance: - Box 1.3039436460239813 - Dict 1.4458735270309262 - Skin 0.22381233097985387 - tri.struct 0.0160809819935821 + Box 0.7227337849326432 + Dict 0.8247780610108748 + Skin 0.14907896996010095 + tri.struct 0.014445346896536648 Access exist: - dict 0.010150779969990253 - Box 0.6168131970334798 - Dict 0.38859444903209805 - Skin 1.6113240469712764 + dict 0.005448702024295926 + Box 0.32549735193606466 + Dict 0.21359142300207168 + Skin 1.5485703510930762 Access non-exist: - Dict 0.5559089470189065 - Skin 1.0153888199711218 + Dict 0.2847607780713588 + Skin 1.007843557978049 ``` -`Skin` do not wrap object recursively, so it have constant creation time. In case of access `Skin` create wrappers every time, it is 2x-4x slower, than `Dict` and `Box`. +`Skin` do not wrap objects recursively, so it have constant creation time. In case of access, `Skin` create wrappers every time. That is why it is 3x-8x slower, than `Dict` and `Box`. diff --git a/skin.py b/skin.py index 40d9106..a449590 100644 --- a/skin.py +++ b/skin.py @@ -1,8 +1,9 @@ import copy +import functools __all__ = ("Skin",) -__version__ = "0.0.4" +__version__ = "0.0.5" version = tuple(map(int, __version__.split("."))) @@ -10,26 +11,47 @@ class SkinValueError(ValueError): pass +DEFAULT_VALUE = object() ANY = object() FORBIDDEN = (str, bytes, bytearray, memoryview, range) -DEFAULT_VALUE = object() +TRANSPARENT_ATTRIBUTES = {"value", "__class__", "__deepcopy__"} + + +def _wrapper_or_value(self, value=DEFAULT_VALUE, *, parent=None, parent_name=None): + try: + cls = self.__class__ + getter = super(cls, self).__getattribute__ + return cls(value, allowed=getter("allowed"), forbidden=getter("forbidden"), + _parent=parent, _parent_name=parent_name) + except SkinValueError: + return value class Skin: - def __init__(self, value=DEFAULT_VALUE, *, allowed=ANY, forbidden=FORBIDDEN, parent=None): + def __init__(self, value=DEFAULT_VALUE, *, allowed=ANY, forbidden=FORBIDDEN, _parent=None, _parent_name=None): if value is DEFAULT_VALUE: value = {} - if not hasattr(value, "__getitem__"): - raise SkinValueError("{!r} have no '__getitem__' method".format(value)) - if allowed is not ANY and not isinstance(value, allowed): - raise SkinValueError("{!r} not in allowed".format(value)) - if forbidden is ANY or isinstance(value, forbidden): - raise SkinValueError("{!r} in forbidden".format(value)) if isinstance(value, self.__class__): value = value.value - super().__setattr__("value", value) - super().__setattr__("_skin_config", dict(parent=parent, allowed=allowed, forbidden=forbidden)) + else: + if not hasattr(value, "__getitem__"): + raise SkinValueError("{!r} have no '__getitem__' method".format(value)) + if allowed is not ANY and not isinstance(value, allowed): + raise SkinValueError("{!r} not in allowed".format(value)) + if forbidden is ANY or isinstance(value, forbidden): + raise SkinValueError("{!r} in forbidden".format(value)) + setter = super().__setattr__ + setter("value", value) + setter("allowed", allowed) + setter("forbidden", forbidden) + setter("parent", _parent) + setter("parent_name", _parent_name) + + def __getattribute__(self, name): + if name not in TRANSPARENT_ATTRIBUTES: + raise AttributeError + return super().__getattribute__(name) def __getattr__(self, name): if hasattr(self.value, name): @@ -38,13 +60,9 @@ def __getattr__(self, name): def __getitem__(self, name): try: - return self.__class__(self.value[name]) - except SkinValueError: - return self.value[name] + return _wrapper_or_value(self, self.value[name]) except (KeyError, IndexError): - config = self._skin_config.copy() - config["parent"] = (self, name) - return self.__class__({}, **config) + return _wrapper_or_value(self, parent=self, parent_name=name) def __setattr__(self, name, value): if hasattr(self.value, name): @@ -54,9 +72,10 @@ def __setattr__(self, name, value): def __setitem__(self, name, value): self.value[name] = value - if self._skin_config["parent"] is not None: - skin, parent_name = self._skin_config["parent"] - skin[parent_name] = self.value + getter = super().__getattribute__ + parent = getter("parent") + if parent is not None: + parent[getter("parent_name")] = self.value def __delattr__(self, name): if hasattr(self.value, name): @@ -74,10 +93,10 @@ def __len__(self): return len(self.value) def __iter__(self): - return iter(self.value) + yield from map(functools.partial(_wrapper_or_value, self), self.value) def __reversed__(self): - return reversed(self.value) + yield from map(functools.partial(_wrapper_or_value, self), reversed(self.value)) def __contains__(self, item): return item in self.value diff --git a/tests.py b/tests.py index 102a114..a19c3bc 100644 --- a/tests.py +++ b/tests.py @@ -138,8 +138,12 @@ def test_repr(): def test_reversed(d, s): - for i, v in enumerate(reversed(s.b)): - assert v is d["b"][-1 - i] + ls = list(reversed(s.b))[::-1] + assert d["b"][:2] == ls[:2] + assert d["b"][2] != ls[2] + assert isinstance(ls[2], Skin) + assert d["b"][2] == ls[2].value + assert ls[2].a == 1 def test_defaultdict(): @@ -169,3 +173,18 @@ def test_deepcopy(): s.foo = [] t = copy.deepcopy(s) assert s.foo.value is not t.foo.value + + +def test_iteration(d, s): + ls = list(s.b) + assert d["b"][:2] == ls[:2] + assert d["b"][2] != ls[2] + assert isinstance(ls[2], Skin) + assert d["b"][2] == ls[2].value + assert ls[2].a == 1 + + +def test_config_inheritance(): + s1 = Skin(forbidden=(set,)) + s2 = s1.foo.bar.baz + assert super(Skin, s1).__getattribute__("forbidden") is super(Skin, s2).__getattribute__("forbidden")