Skip to content

Commit

Permalink
massive update
Browse files Browse the repository at this point in the history
  • Loading branch information
pohmelie committed Nov 29, 2017
1 parent f9bf08a commit 112be8f
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 39 deletions.
30 changes: 15 additions & 15 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -148,20 +148,20 @@ Skin(defaultdict(<class 'list'>, {'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`.
63 changes: 41 additions & 22 deletions skin.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,57 @@
import copy
import functools


__all__ = ("Skin",)
__version__ = "0.0.4"
__version__ = "0.0.5"
version = tuple(map(int, __version__.split(".")))


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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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
Expand Down
23 changes: 21 additions & 2 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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")

0 comments on commit 112be8f

Please sign in to comment.