From 648dfca0002af7d6699ac32ff22fc3dc31446f52 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Thu, 8 Sep 2016 01:37:03 +0200 Subject: [PATCH 1/4] Generate __init__ with converters inline. --- src/attr/_make.py | 137 +++++++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 55 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 764fe552f..63637ca45 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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): @@ -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 @@ -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. @@ -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): @@ -515,34 +506,56 @@ 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( @@ -550,29 +563,43 @@ def fmt_setter(attr_name, value): 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: @@ -580,8 +607,8 @@ def fmt_setter(attr_name, value): 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}): @@ -589,7 +616,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): From d176d81604e581ab45d1fd935d7d3ee9c97877be Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Thu, 8 Sep 2016 02:16:52 +0200 Subject: [PATCH 2/4] More robust converter testing. --- tests/test_make.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/test_make.py b/tests/test_make.py index c178d310d..00416ffa5 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -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 @@ -20,6 +20,7 @@ fields, make_class, validate, + Factory, ) from .utils import simple_attr, simple_attrs @@ -389,6 +390,33 @@ def test_convert(self): assert c.x == 2 assert c.y == 2 + @given(integers(), booleans()) + def test_convert_no_init(self, val, init): + """ + The attribute with the converter has init=False. + """ + 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_no_init_factory(self, val, init): + """ + The attribute with the converter has init=False. + """ + 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. From 7fa3fff14a8b5e09cdbb1e92f3a54c3f71086842 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Thu, 8 Sep 2016 18:54:44 +0200 Subject: [PATCH 3/4] Add changelog entry. --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 965de1b01..08be19e98 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 `_ ---- From 4da3a4eb93b990d19891327230a1f4a3bb0b4ea8 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Thu, 8 Sep 2016 19:00:46 +0200 Subject: [PATCH 4/4] Rename two tests to be clearer. --- tests/test_make.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_make.py b/tests/test_make.py index 00416ffa5..dd626906d 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -391,9 +391,9 @@ def test_convert(self): assert c.y == 2 @given(integers(), booleans()) - def test_convert_no_init(self, val, init): + def test_convert_property(self, val, init): """ - The attribute with the converter has init=False. + Property tests for attributes with convert. """ C = make_class("C", {"y": attr(), "x": attr(init=init, default=val, @@ -404,9 +404,9 @@ def test_convert_no_init(self, val, init): assert c.y == 2 @given(integers(), booleans()) - def test_convert_no_init_factory(self, val, init): + def test_convert_factory_property(self, val, init): """ - The attribute with the converter has init=False. + Property tests for attributes with convert, and a factory default. """ C = make_class("C", {"y": attr(), "x": attr(init=init,