diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04ad409a..90e82ffa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,8 @@ Changes: - Converts now work with frozen classes. `#76 `_ +- Instantiation of ``attrs`` classes with converters is now significantly faster. + `#80 `_ - Pickling now works with ``__slots__`` classes. `#81 `_ diff --git a/src/attr/_make.py b/src/attr/_make.py index d2f144ed..1bcf4413 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -9,6 +9,7 @@ from .exceptions import FrozenInstanceError # This is used at least twice, so cache it here. _obj_setattr = object.__setattr__ +_init_convert_pat = '__attr_convert_{}' class _Nothing(object): @@ -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 @@ -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. @@ -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): @@ -541,34 +532,56 @@ def _attrs_to_script(attrs, frozen): "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( @@ -576,29 +589,43 @@ def _attrs_to_script(attrs, frozen): 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: @@ -606,8 +633,8 @@ def _attrs_to_script(attrs, frozen): 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}): @@ -615,7 +642,7 @@ def __init__(self, {args}): """.format( args=", ".join(args), lines="\n ".join(lines) if lines else "pass", - ), validators_for_globals + ), names_for_globals class Attribute(object): diff --git a/tests/test_make.py b/tests/test_make.py index c178d310..dd626906 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function 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 @@ -20,6 +20,7 @@ from attr._make import ( fields, make_class, validate, + Factory, ) from .utils import simple_attr, simple_attrs @@ -389,6 +390,33 @@ class TestConvert(object): 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.