Skip to content

Commit

Permalink
Generate __init__ with converters inline
Browse files Browse the repository at this point in the history
Fixes #80
  • Loading branch information
Tinche authored and hynek committed Sep 10, 2016
1 parent e59c360 commit 5428710
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 56 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Changes:

- Converts now work with frozen classes.
`#76 <https://github.com/hynek/attrs/issues/76>`_
- Instantiation of ``attrs`` classes with converters is now significantly faster.
`#80 <https://github.com/hynek/attrs/pull/80>`_
- Pickling now works with ``__slots__`` classes.
`#81 <https://github.com/hynek/attrs/issues/81>`_

Expand Down
137 changes: 82 additions & 55 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

# This is used at least twice, so cache it here.
_obj_setattr = object.__setattr__
_init_convert_pat = '__attr_convert_{}'


class _Nothing(object):
Expand Down Expand Up @@ -414,8 +415,6 @@ def _add_init(cls, frozen):
globs.update({
"NOTHING": NOTHING,
"attr_dict": attr_dict,
"validate": validate,
"_convert": _convert
})
if frozen is True:
# Save the lookup overhead in __init__ if we need to circumvent
Expand Down Expand Up @@ -496,22 +495,6 @@ def validate(inst):
a.validator(inst, a, getattr(inst, a.name))


def _convert(inst, setattr_):
"""
Convert all attributes on *inst* that have a converter.
Uses *setattr_* to set the attributes on the class. Allows for
circumvention of frozen instances.
Leaves all exceptions through.
:param inst: Instance of a class with ``attrs`` attributes.
"""
for a in inst.__class__.__attrs_attrs__:
if a.convert is not None:
setattr_(a.name, a.convert(getattr(inst, a.name)))


def _attrs_to_script(attrs, frozen):
"""
Return a script of an initializer for *attrs* and a dict of globals.
Expand All @@ -529,10 +512,18 @@ def _attrs_to_script(attrs, frozen):
"_setattr = _cached_setattr.__get__(self, self.__class__)"
)

def fmt_setter(attr_name, value):
return "_setattr('%(attr_name)s', %(value)s)" % {
def fmt_setter(attr_name, value_var):
return "_setattr('%(attr_name)s', %(value_var)s)" % {
"attr_name": attr_name,
"value": value,
"value_var": value_var,
}

def fmt_setter_with_converter(attr_name, value_var):
conv_name = _init_convert_pat.format(attr_name)
return "_setattr('%(attr_name)s', %(conv)s(%(value_var)s))" % {
"attr_name": attr_name,
"value_var": value_var,
"conv": conv_name,
}
else:
def fmt_setter(attr_name, value):
Expand All @@ -541,81 +532,117 @@ def fmt_setter(attr_name, value):
"value": value,
}

def fmt_setter_with_converter(attr_name, value_var):
conv_name = _init_convert_pat.format(attr_name)
return "self.%(attr_name)s = %(conv)s(%(value_var)s)" % {
"attr_name": attr_name,
"value_var": value_var,
"conv": conv_name,
}

args = []
has_convert = False
attrs_to_validate = []

# This is a dictionary of names to validator callables. Injecting
# this into __init__ globals lets us avoid lookups.
validators_for_globals = {}
# This is a dictionary of names to validator and converter callables.
# Injecting this into __init__ globals lets us avoid lookups.
names_for_globals = {}

for a in attrs:
if a.validator is not None:
attrs_to_validate.append(a)
if a.convert is not None:
has_convert = True
attr_name = a.name
arg_name = a.name.lstrip("_")
if a.init is False:
if isinstance(a.default, Factory):
lines.append(fmt_setter(
attr_name,
"attr_dict['{attr_name}'].default.factory()"
.format(attr_name=attr_name)
))
if a.convert is not None:
lines.append(fmt_setter_with_converter(
attr_name,
"attr_dict['{attr_name}'].default.factory()"
.format(attr_name=attr_name)))
conv_name = _init_convert_pat.format(a.name)
names_for_globals[conv_name] = a.convert
else:
lines.append(fmt_setter(
attr_name,
"attr_dict['{attr_name}'].default.factory()"
.format(attr_name=attr_name)
))
else:
lines.append(fmt_setter(
attr_name,
"attr_dict['{attr_name}'].default"
.format(attr_name=attr_name)
))
if a.convert is not None:
lines.append(fmt_setter_with_converter(
attr_name,
"attr_dict['{attr_name}'].default"
.format(attr_name=attr_name)
))
conv_name = _init_convert_pat.format(a.name)
names_for_globals[conv_name] = a.convert
else:
lines.append(fmt_setter(
attr_name,
"attr_dict['{attr_name}'].default"
.format(attr_name=attr_name)
))
elif a.default is not NOTHING and not isinstance(a.default, Factory):
args.append(
"{arg_name}=attr_dict['{attr_name}'].default".format(
arg_name=arg_name,
attr_name=attr_name,
)
)
lines.append(fmt_setter(attr_name, arg_name))
if a.convert is not None:
lines.append(fmt_setter_with_converter(attr_name, arg_name))
names_for_globals[_init_convert_pat.format(a.name)] = a.convert
else:
lines.append(fmt_setter(attr_name, arg_name))
elif a.default is not NOTHING and isinstance(a.default, Factory):
args.append("{arg_name}=NOTHING".format(arg_name=arg_name))
lines.append("if {arg_name} is not NOTHING:"
.format(arg_name=arg_name))
lines.append(" " + fmt_setter(attr_name, arg_name))
lines.append("else:")
lines.append(" " + fmt_setter(
attr_name,
"attr_dict['{attr_name}'].default.factory()"
.format(attr_name=attr_name)
))
if a.convert is not None:
lines.append(" " + fmt_setter_with_converter(attr_name,
arg_name))
lines.append("else:")
lines.append(" " + fmt_setter_with_converter(
attr_name,
"attr_dict['{attr_name}'].default.factory()"
.format(attr_name=attr_name)
))
names_for_globals[_init_convert_pat.format(a.name)] = a.convert
else:
lines.append(" " + fmt_setter(attr_name, arg_name))
lines.append("else:")
lines.append(" " + fmt_setter(
attr_name,
"attr_dict['{attr_name}'].default.factory()"
.format(attr_name=attr_name)
))
else:
args.append(arg_name)
lines.append(fmt_setter(attr_name, arg_name))
if a.convert is not None:
lines.append(fmt_setter_with_converter(attr_name, arg_name))
names_for_globals[_init_convert_pat.format(a.name)] = a.convert
else:
lines.append(fmt_setter(attr_name, arg_name))

if has_convert:
if frozen is True:
lines.append("_convert(self, _setattr)")
else:
lines.append("_convert(self, self.__setattr__)")
if attrs_to_validate: # we can skip this if there are no validators.
validators_for_globals["_config"] = _config
names_for_globals["_config"] = _config
lines.append("if _config._run_validators is False:")
lines.append(" return")
for a in attrs_to_validate:
val_name = "__attr_validator_{}".format(a.name)
attr_name = "__attr_{}".format(a.name)
lines.append("{}(self, {}, self.{})".format(val_name, attr_name,
a.name))
validators_for_globals[val_name] = a.validator
validators_for_globals[attr_name] = a
names_for_globals[val_name] = a.validator
names_for_globals[attr_name] = a

return """\
def __init__(self, {args}):
{lines}
""".format(
args=", ".join(args),
lines="\n ".join(lines) if lines else "pass",
), validators_for_globals
), names_for_globals


class Attribute(object):
Expand Down
30 changes: 29 additions & 1 deletion tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

from hypothesis import given
from hypothesis.strategies import booleans, sampled_from
from hypothesis.strategies import booleans, integers, sampled_from

from attr import _config
from attr._compat import PY2
Expand All @@ -20,6 +20,7 @@
fields,
make_class,
validate,
Factory,
)

from .utils import simple_attr, simple_attrs
Expand Down Expand Up @@ -389,6 +390,33 @@ def test_convert(self):
assert c.x == 2
assert c.y == 2

@given(integers(), booleans())
def test_convert_property(self, val, init):
"""
Property tests for attributes with convert.
"""
C = make_class("C", {"y": attr(),
"x": attr(init=init, default=val,
convert=lambda v: v + 1),
})
c = C(2)
assert c.x == val + 1
assert c.y == 2

@given(integers(), booleans())
def test_convert_factory_property(self, val, init):
"""
Property tests for attributes with convert, and a factory default.
"""
C = make_class("C", {"y": attr(),
"x": attr(init=init,
default=Factory(lambda: val),
convert=lambda v: v + 1),
})
c = C(2)
assert c.x == val + 1
assert c.y == 2

def test_convert_before_validate(self):
"""
Validation happens after conversion.
Expand Down

0 comments on commit 5428710

Please sign in to comment.