Skip to content
This repository has been archived by the owner on Jun 17, 2023. It is now read-only.

Commit

Permalink
Employ the darkest of all magics
Browse files Browse the repository at this point in the history
Every cycle is sacred.
  • Loading branch information
hynek committed Aug 6, 2014
1 parent 66ad249 commit 3f9c685
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 24 deletions.
75 changes: 51 additions & 24 deletions characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
Python attributes without boilerplate.
"""

import sys


__version__ = "14.0dev"
__author__ = "Hynek Schlawack"
Expand All @@ -20,6 +22,15 @@
]


# I'm sorry. :(
if sys.version_info[0] == 2:
def exec_(code, locals_, globals_):
exec("exec code in locals_, globals_")
else: # pragma: no cover
def exec_(code, locals_, globals_):
exec(code, locals_, globals_)


class _Nothing(object):
"""
Sentinel class to indicate the lack of a value when ``None`` is ambiguous.
Expand Down Expand Up @@ -260,37 +271,53 @@ def with_init(attrs, defaults=None):
:param defaults: Default values if attributes are omitted on instantiation.
:type defaults: ``dict`` or ``None``
"""
if defaults is None:
defaults = {}

def init(self, *args, **kw):
"""
Attribute initializer automatically created by characteristic.
The original `__init__` method is renamed to `__original_init__` and
is called at the end with the initialized attributes removed from the
keyword arguments.
"""
for a in attrs:
v = kw.pop(a.name, NOTHING)
if v is NOTHING:
if a._default_value is not NOTHING:
v = a._default_value
elif a._default_factory is not None:
v = a._default_factory()
if v is NOTHING:
raise ValueError(
"Missing keyword value for '{0}'.".format(a.name)
attrs = _ensure_attributes(attrs, defaults or {})

setters = []
for i, a in enumerate(attrs):
# attrs is passed into the the exec later to enable default_value
# and default_factory. To find it, enumerate and 'i' are used.
setters.append(
"self.{0.name} = kw.pop('{0.name}', attrs[{1}]._default_value)"
.format(a, i)
)
if a._default_value is NOTHING:
# Can't use pop + big try/except because Python 2.6:
# http://bugs.python.org/issue10221
setters.append("if self.{0.name} is NOTHING:".format(a))
if a._default_factory is None:
setters.append(
" raise ValueError(\"Missing keyword value for "
"'{0.name}'.\")".format(a),
)
setattr(self, a.name, v)
self.__original_init__(*args, **kw)
else:
setters.append(
" self.{0.name} = attrs[{1}]._default_factory()"
.format(a, i)
)

script = """\
def init(self, *args, **kw):
'''
Attribute initializer automatically created by characteristic.
The original `__init__` method is renamed to `__original_init__` and
is called at the end with the initialized attributes removed from the
keyword arguments.
'''
{setters}
self.__original_init__(*args, **kw)
""".format(setters="\n ".join(setters))
locs = {}
exec_(script, {"NOTHING": NOTHING, "attrs": attrs}, locs)
init = locs["init"]

def wrap(cl):
cl.__original_init__ = cl.__init__
init._script = script # better for debugging
cl.__init__ = init
return cl

attrs = _ensure_attributes(attrs, defaults)
return wrap


Expand Down
14 changes: 14 additions & 0 deletions test_characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,22 @@ class C(object):
pass
o1 = C()
o2 = C()
assert o1.a == o2.a == []
assert o1.a is not o2.a

def test_default_only_if_necessary(self):
"""
Default value is only used if the value is actually missing.
"""
@with_init([Attribute("a", default_value=42),
Attribute("b", default_factory="abc")])
class C(object):
pass
o = object()
c = C(a=o, b=o)
assert c.a is o
assert c.b is o


@attributes(["a", "b"], create_init=True)
class MagicWithInitC(object):
Expand Down

0 comments on commit 3f9c685

Please sign in to comment.