Getitem-objects «skin» for attribute-like access


License
Apache-2.0
Install
pip install skin==0.0.7

Documentation

travis coveralls pypi

Skin

Getitem-objects «skin» for attribute-like access.

Reason

addict, python-box, tri.struct, dotmap, ddict, easydict do not respect dict reference transparency.

addict

>>> from addict import Dict
>>> original = {"foo": [1, 2, 3]}
>>> d = Dict(original)
>>> d.foo
[1, 2, 3]
>>> type(d.foo)
<class 'list'>
>>> d.foo.append(4)
>>> original
{'foo': [1, 2, 3]}
>>> d.foo
[1, 2, 3, 4]
>>>

python-box

>>> from box import Box
>>> original = {"foo": [1, 2, 3]}
>>> b = Box(original)
>>> b.foo
<BoxList: [1, 2, 3]>
>>> type(b.foo)
<class 'box.BoxList'>
>>> b.foo.append(4)
>>> original
{'foo': [1, 2, 3]}
>>> b.foo
<BoxList: [1, 2, 3, 4]>
>>>

skin

>>> from skin import Skin
>>> original = {"foo": [1, 2, 3]}
>>> s = Skin(original)
>>> s.foo
Skin([1, 2, 3])
>>> type(s.foo)
<class 'skin.Skin'>
>>> type(s.foo.value)
<class 'list'>
>>> s.foo.value is original["foo"]
True
>>> s.foo.append(4)
>>> original
{'foo': [1, 2, 3, 4]}
>>>

Similar projects

And much more, since some of them are python 2 only.

Benchmark

Skin (skin) Dict (addict) DotMap (dotmap) DotAccessDict (ddict) Box (box) EasyDict (easydict) Dot (dot_access) dict (builtins)
Create from dict 4.0x 37.0x 45.5x 37.1x 19.0x 44.6x 4.2x 1.0x
Create from key-word arguments - 11.1x 6.4x 9.4x 16.3x 11.7x - 1.0x
Get exist element (attribute access) 33.6x 7.7x 7.1x 6.5x 132.4x 1.0x 25.4x -
Get exist element (item access) 31.1x 5.5x 3.7x 2.0x 154.1x 2.1x 26.8x 1.0x
Get non-exist element (attribute access) 1.8x 1.0x 1.4x 1.2x - - 1.3x -
Get non-exist element (item access) 1.6x 1.0x 1.4x - - - 1.2x -
Set exist element (attribute access) 10.6x 3.0x 2.3x 2.5x 47.7x 1.0x - -
Set exist element (item access) 29.0x 6.7x 3.9x 4.2x 164.6x 4.6x - 1.0x
Set non-exist element (attribute access) 1.6x 1.3x 1.0x 1.0x - - - -
Set non-exist element (item access) 1.5x 1.3x 1.0x - - - - -
Support items iteration - 2.9x 3.8x 2.7x 40.5x 1.0x - -
Support values iteration - 3.9x 4.2x 3.6x 59.3x 1.0x - -
Support len 13.9x 4.7x 4.3x 4.1x 80.5x 1.0x - -
Support copy 1.3x 1.0x - - - - - -
Support deepcopy 2.1x 1.1x 1.0x - 3.8x 1.6x - -
Wrapped modification affect original 1.0x - - - - - - -
Original modification affect wrapped 1.3x - - - - - 1.0x -
defaultdict as original 1.0x - - - - - - -
Non-dict as original 1.3x - - - - - 1.0x -

items and values support mean that values of iteration will be wrapped too.

Documentation

Skin(value=DEFAULT_VALUE, *, allowed=ANY, forbidden=FORBIDDEN)
  • value — any object with __getitem__ method (default: dict).
  • allowed — tuple of allowed types to wrap or skin.ANY for all types allowed (default: skin.ANY)
  • forbidden — tuple of forbidden types to wrap (default: (str, bytes, bytearray, memoryview, range))

What is allowed and forbidden?

Since skin target is not to recreate containers there should be a rule to determine is object container or endpoint-node. Some objects (listed above as forbidden) have __getitem__ method, but wont act like containers.

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 good 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 only one accessible attribute: value — original object, which skin wraps 🎉

Skin supports both "item" and "attribute" notations:

>>> s = Skin({"foo": "bar"})
>>> s.foo is s["foo"]
True
>>>

But, in case of nested containers:

>>> s = Skin({"foo": {"bar": "baz"}})
>>> s.foo is s["foo"]
False
>>> s.foo.value is s["foo"].value
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
    • value attribute
    • value item
    • orphan item
  • in case of item access:
    • value item
    • orphan item

Orphan item is just naming for item, which is not yet set. Example:

>>> s = Skin()
>>> s.foo.bar
Skin({})
>>> s
Skin({})
>>>

As you can see there is no "foo" or "bar" items. But in case of setting:

>>> s = Skin()
>>> s.foo.bar = "baz"
>>> s
Skin({'foo': {'bar': 'baz'}})
>>>

Since skin is just wrapper, which do not recreate container you can use any object with __getitem__:

>>> import collections
>>> s = Skin(collections.defaultdict(list))
>>> s.foo.append(1)
>>> s
Skin(defaultdict(<class 'list'>, {'foo': [1]}))
>>>