Skip to content

Instantly share code, notes, and snippets.

@egregius313
Created December 23, 2017 18:31
Show Gist options
  • Save egregius313/b4604cdfff448b2a1a947039aeba616e to your computer and use it in GitHub Desktop.
Save egregius313/b4604cdfff448b2a1a947039aeba616e to your computer and use it in GitHub Desktop.
Allow tuple unpacking on classes defined with @attr.s
# Allow tuple unpacking on classes defined with
# @attr.s decorator. Uses the attribute information
# from the definition of the class using attrs
#
# See also:
# http://www.attrs.org
# https://github.com/python-attrs/attrs
import hashlib
import linecache
from toolz import curry
__author__ = 'egregius313'
@curry
def unpackable(cls, attrs=None):
"""
Take a class defined using the @attr.s method
and define a __iter__ method to allow for easier
pattern matching variable definitions.
Unpacking order is the same as the order @attr.s
uses for methods like __init__
The `attrs' argument must be either iterable or None.
If the argument is iterable, only attributes with those names
will be used. Otherwise, all attributes defined with attr.ib()
will be unpacked.
>>> @unpackable
... @attr.s
... class Person:
... name = attr.ib()
... age = attr.ib()
...
>>> people = [
... Person('Jonathan', 35),
... Person('Rick', 26),
... Person('Samuel', 20),
... Person('Brian', 39),
... ]
...
>>> for name, age in people:
... print(name, 'is', age, 'years old.')
...
Jonathan is 35 years old
Rick is 26 years old
Samuel is 20 years old
Brian is 39 years old
"""
if hasattr(cls, '__iter__'):
return cls
if attrs is None:
attributes = tuple(a.name for a in cls.__attrs_attrs__)
else:
attributes = tuple(
a.name for a in cls.__attrs_attrs__
if a.name not in attrs
)
print(attributes)
lines = [
'def __iter__(self):',
]
if attributes:
for attrib in attributes:
lines.append(' yield self.%s' % attrib)
else:
lines.append(' yield from ()')
sha1 = hashlib.sha1()
sha1.update(repr(attributes).encode('utf-8'))
unique_filename = '<unpackable generated iter %s>' % (sha1.hexdigest(),)
# The additional newline helps inspect.getsource
script = '\n'.join(lines) + '\n'
globs = {}
locs = {}
bytecode = compile(script, unique_filename, "exec")
eval(bytecode, globs, locs)
linecache.cache[unique_filename] = (
len(script),
None,
script.splitlines(True),
unique_filename,
)
cls.__iter__ = locs['__iter__']
return cls
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment