Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate __init__ with converters inline. #80

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:/hynek/attrs/issues/76>`_
- Instantiation of ``attrs`` classes with converters is now significantly faster.
`#80 <https:/hynek/attrs/pull/80>`_


----
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 @@ -411,8 +412,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 @@ -470,22 +469,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 @@ -503,10 +486,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 @@ -515,81 +506,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