Skip to content

Instantly share code, notes, and snippets.

@sherzberg
Forked from floer32/tupperware.py
Last active September 28, 2019 08:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sherzberg/7661076 to your computer and use it in GitHub Desktop.
Save sherzberg/7661076 to your computer and use it in GitHub Desktop.
from UserDict import IterableUserDict
import collections
def tupperware(mapping):
""" Convert mappings to 'tupperwares' recursively.
Lets you use dicts like they're JavaScript Object Literals (~=JSON)...
It recursively turns mappings (dictionaries) into namedtuples.
Thus, you can cheaply create an object whose attributes are accessible
by dotted notation (all the way down).
Use cases:
* Fake objects (useful for dependency injection when a Mock is actually
more complex than your requirements call for)
* Storing data (like fixtures) in a structured way, in Python code
(data whose initial definition reads nicely like JSON). You could do
this with dictionaries, but this solution is immutable, and its
dotted notation is arguably clearer in many contexts.
.. doctest::
>>> t = tupperware({
... 'foo': 'bar',
... 'baz': {'qux': 'quux'},
... 'tito': {
... 'tata': 'tutu',
... 'totoro': 'tots',
... 'frobnicator': ['this', 'is', 'not', 'a', 'mapping']
... },
... 'alist': [
... {'one': '1', 'a': 'A'},
... {'two': '2', 'b': 'B'},
... ]
... })
>>> t # doctest: +ELLIPSIS
Tupperware(baz=Tupperware(qux='quux'), tito=Tupperware(...), foo='bar', alist=[Tupperware(...), Tupperware(...)])
>>> t.tito # doctest: +ELLIPSIS
Tupperware(frobnicator=[...], tata='tutu', totoro='tots')
>>> t.tito.tata
'tutu'
>>> t.tito.frobnicator
['this', 'is', 'not', 'a', 'mapping']
>>> t.foo
'bar'
>>> t.baz.qux
'quux'
>>> t.alist[0].one
'1'
>>> t.alist[0].a
'A'
>>> t.alist[1].two
'2'
>>> t.alist[1].b
'B'
Args:
mapping: An object that might be a mapping. If it's a mapping, convert
it (and all of its contents that are mappings) to namedtuples
(called 'Tupperwares').
Returns:
A tupperware (a namedtuple (of namedtuples (of namedtuples (...)))).
If argument is not a mapping, it just returns it (this enables the
recursion).
"""
if (isinstance(mapping, collections.Mapping) and
not isinstance(mapping, ProtectedDict)):
for key, value in mapping.iteritems():
mapping[key] = tupperware(value)
return namedtuple_wrapper(**mapping)
elif isinstance(mapping, list):
return [tupperware(item) for item in mapping]
return mapping
def namedtuple_wrapper(**kwargs):
namedtuple = collections.namedtuple('Tupperware', kwargs)
return namedtuple(**kwargs)
class ProtectedDict(IterableUserDict):
""" A class that exists just to tell `tupperware` not to eat it.
`tupperware` eats all dicts you give it, recursively; but what if you
actually want a dictionary in there? This will stop it. Just do
ProtectedDict({...}) or ProtectedDict(kwarg=foo).
"""
if __name__ == "__main__":
import doctest
doctest.testmod()
@james7132
Copy link

james7132 commented Sep 28, 2019

collections.namedtuple is great for making immutable mappings. However, the produced object is still mutable if any lists were added. If full recursive immutability is desired, replace the elif statement with the following:

elif isinstance(mapping, collections.Sequence) and not isinstance(mapping, str):
    return tuple(tupperware(item) for item in mapping)

Also as of Python 3.8, the collections abstract base classes (Mapping, Sequence) will be moved to collections.abc, and this code will no longer work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment