From 3dc5fb0a5aef95813c0098558ee97d2d18e3ab99 Mon Sep 17 00:00:00 2001 From: Tommy Beadle Date: Thu, 10 Nov 2016 23:16:48 -0500 Subject: [PATCH 1/3] Add ability for ``__post_init__`` method to be defined. If this method is defined for a class, it will get executed at the end of ``__init__``. Previously, there was no way to extend what was done during ``__init__`` when using ``@attr.s``. --- CHANGELOG.rst | 4 ++++ docs/examples.rst | 20 ++++++++++++++++++++ src/attr/_make.py | 10 ++++++++-- tests/test_make.py | 15 +++++++++++++++ tests/utils.py | 8 +++++++- 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 44b10ad45..51bd09c4c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,10 @@ Changes: `#96 `_ - Don't overwrite ``__name__`` with ``__qualname__`` for ``attr.s(slots=True)`` classes. `#99 `_ +- Allow for a ``__post_init__`` method that, if defined, will get executed at + the end of the ``__init__`` that gets constructed for an ``@attr.s``-decorated + class. + `#111 `_ ---- diff --git a/docs/examples.rst b/docs/examples.rst index a307aa3b0..f6366aad6 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -574,6 +574,26 @@ You can still have power over the attributes if you pass a dictionary of name: ` >>> i.y [] +Sometimes, you want to have your class's ``__init__`` method do more than just +the initialization, validation, etc. that gets done for you automatically when +using ``@attr.s``. +To do this, just define a ``__post_init__`` method in your class. +It will get called at the end of the ``__init__`` method that has been +autogenerated. + +.. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib() + ... y = attr.ib() + ... + ... def __post_init__(self): + ... self.z = self.x + self.y + >>> obj = C(x=1, y=2) + >>> obj.z + 3 + Finally, you can exclude single attributes from certain methods: .. doctest:: diff --git a/src/attr/_make.py b/src/attr/_make.py index 98fbc1d7d..b2033e5e0 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -450,7 +450,11 @@ def _add_init(cls, frozen): sha1.hexdigest() ) - script, globs = _attrs_to_script(attrs, frozen) + script, globs = _attrs_to_script( + attrs, + frozen, + getattr(cls, '__post_init__', False), + ) locs = {} bytecode = compile(script, unique_filename, "exec") attr_dict = dict((a.name, a) for a in attrs) @@ -544,7 +548,7 @@ def validate(inst): a.validator(inst, a, getattr(inst, a.name)) -def _attrs_to_script(attrs, frozen): +def _attrs_to_script(attrs, frozen, post_init): """ Return a script of an initializer for *attrs* and a dict of globals. @@ -684,6 +688,8 @@ def fmt_setter_with_converter(attr_name, value_var): a.name)) names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a + if post_init: + lines.append("self.__post_init__()") return """\ def __init__(self, {args}): diff --git a/tests/test_make.py b/tests/test_make.py index 9ea26299e..f665d59ba 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -305,6 +305,21 @@ class D(object): assert C.D.__name__ == "D" assert C.D.__qualname__ == C.__qualname__ + ".D" + def test_post_init(self): + """ + Verify that __post_init__ gets called if defined. + """ + @attributes + class C(object): + x = attr() + y = attr() + + def __post_init__(self2): + self2.z = self2.x + self2.y + + c = C(x=10, y=20) + assert 30 == getattr(c, 'z', None) + @attributes class GC(object): diff --git a/tests/utils.py b/tests/utils.py index 7d4f32882..5c620897d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -186,7 +186,13 @@ class HypClass: frozen_flag = draw(st.booleans()) if frozen is None else frozen slots_flag = draw(st.booleans()) if slots is None else slots - return make_class('HypClass', dict(zip(gen_attr_names(), attrs)), + cls_dict = dict(zip(gen_attr_names(), attrs)) + post_init_flag = draw(st.booleans()) + if post_init_flag: + def post_init(self): + pass + cls_dict['__post_init__'] = post_init + return make_class('HypClass', cls_dict, slots=slots_flag, frozen=frozen_flag) From 7e4cecdac223059a7288ef2d4a8390ecd1d92757 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 20 Nov 2016 14:15:29 +0100 Subject: [PATCH 2/3] Minor tweaks --- CHANGELOG.rst | 8 +++----- docs/examples.rst | 8 ++++---- src/attr/_make.py | 2 +- tests/utils.py | 9 +++++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51bd09c4c..e430db1ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -Versions are year-based with a strict backwards compatibility policy. +Versions follow `CalVer `_ with a strict backwards compatibility policy. The third digit is only for regressions. @@ -13,12 +13,10 @@ Changes: - Attributes now can have user-defined metadata which greatly improves ``attrs``'s extensibility. `#96 `_ +- Allow for a ``__post_init__`` method that -- if defined -- will get executed at the end of the ``attrs``-generated ``__init__`` method. + `#111 `_ - Don't overwrite ``__name__`` with ``__qualname__`` for ``attr.s(slots=True)`` classes. `#99 `_ -- Allow for a ``__post_init__`` method that, if defined, will get executed at - the end of the ``__init__`` that gets constructed for an ``@attr.s``-decorated - class. - `#111 `_ ---- diff --git a/docs/examples.rst b/docs/examples.rst index f6366aad6..78ae7612f 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -578,8 +578,7 @@ Sometimes, you want to have your class's ``__init__`` method do more than just the initialization, validation, etc. that gets done for you automatically when using ``@attr.s``. To do this, just define a ``__post_init__`` method in your class. -It will get called at the end of the ``__init__`` method that has been -autogenerated. +It will get called at the end of the generated ``__init__`` method. .. doctest:: @@ -587,12 +586,13 @@ autogenerated. ... class C(object): ... x = attr.ib() ... y = attr.ib() + ... z = attr.ib(init=False) ... ... def __post_init__(self): ... self.z = self.x + self.y >>> obj = C(x=1, y=2) - >>> obj.z - 3 + >>> obj + C(x=1, y=2, z=3) Finally, you can exclude single attributes from certain methods: diff --git a/src/attr/_make.py b/src/attr/_make.py index b2033e5e0..3bc353713 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -453,7 +453,7 @@ def _add_init(cls, frozen): script, globs = _attrs_to_script( attrs, frozen, - getattr(cls, '__post_init__', False), + getattr(cls, "__post_init__", False), ) locs = {} bytecode = compile(script, unique_filename, "exec") diff --git a/tests/utils.py b/tests/utils.py index 5c620897d..dcdfb1c26 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -165,7 +165,8 @@ def simple_attrs_with_metadata(draw): @st.composite def simple_classes(draw, slots=None, frozen=None): - """A strategy that generates classes with default non-attr attributes. + """ + A strategy that generates classes with default non-attr attributes. For example, this strategy might generate a class such as: @@ -196,8 +197,8 @@ def post_init(self): slots=slots_flag, frozen=frozen_flag) -# Ok, so st.recursive works by taking a base strategy (in this case, -# simple_classes) and a special function. This function receives a strategy, -# and returns another strategy (building on top of the base strategy). +# st.recursive works by taking a base strategy (in this case, simple_classes) +# and a special function. This function receives a strategy, and returns +# another strategy (building on top of the base strategy). nested_classes = st.recursive(simple_classes(), _create_hyp_nested_strategy, max_leaves=10) From 3964f9d37b1f3e5e9cea4531d7c1f642f6d63120 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 20 Nov 2016 14:39:34 +0100 Subject: [PATCH 3/3] Rename __post_init__ to __attrs_post_init__ This makes it clearer its coming from attrs and is consistent with __attrs__attrs__. --- CHANGELOG.rst | 2 +- docs/examples.rst | 4 ++-- src/attr/_make.py | 4 ++-- tests/test_make.py | 4 ++-- tests/utils.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e430db1ef..9c2d85110 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,7 +13,7 @@ Changes: - Attributes now can have user-defined metadata which greatly improves ``attrs``'s extensibility. `#96 `_ -- Allow for a ``__post_init__`` method that -- if defined -- will get executed at the end of the ``attrs``-generated ``__init__`` method. +- Allow for a ``__attrs_post_init__`` method that -- if defined -- will get executed at the end of the ``attrs``-generated ``__init__`` method. `#111 `_ - Don't overwrite ``__name__`` with ``__qualname__`` for ``attr.s(slots=True)`` classes. `#99 `_ diff --git a/docs/examples.rst b/docs/examples.rst index 78ae7612f..dbca8a465 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -577,7 +577,7 @@ You can still have power over the attributes if you pass a dictionary of name: ` Sometimes, you want to have your class's ``__init__`` method do more than just the initialization, validation, etc. that gets done for you automatically when using ``@attr.s``. -To do this, just define a ``__post_init__`` method in your class. +To do this, just define a ``__attrs_post_init__`` method in your class. It will get called at the end of the generated ``__init__`` method. .. doctest:: @@ -588,7 +588,7 @@ It will get called at the end of the generated ``__init__`` method. ... y = attr.ib() ... z = attr.ib(init=False) ... - ... def __post_init__(self): + ... def __attrs_post_init__(self): ... self.z = self.x + self.y >>> obj = C(x=1, y=2) >>> obj diff --git a/src/attr/_make.py b/src/attr/_make.py index 3bc353713..28923f54d 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -453,7 +453,7 @@ def _add_init(cls, frozen): script, globs = _attrs_to_script( attrs, frozen, - getattr(cls, "__post_init__", False), + getattr(cls, "__attrs_post_init__", False), ) locs = {} bytecode = compile(script, unique_filename, "exec") @@ -689,7 +689,7 @@ def fmt_setter_with_converter(attr_name, value_var): names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a if post_init: - lines.append("self.__post_init__()") + lines.append("self.__attrs_post_init__()") return """\ def __init__(self, {args}): diff --git a/tests/test_make.py b/tests/test_make.py index f665d59ba..e672305b3 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -307,14 +307,14 @@ class D(object): def test_post_init(self): """ - Verify that __post_init__ gets called if defined. + Verify that __attrs_post_init__ gets called if defined. """ @attributes class C(object): x = attr() y = attr() - def __post_init__(self2): + def __attrs_post_init__(self2): self2.z = self2.x + self2.y c = C(x=10, y=20) diff --git a/tests/utils.py b/tests/utils.py index dcdfb1c26..dd5049697 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -192,7 +192,7 @@ class HypClass: if post_init_flag: def post_init(self): pass - cls_dict['__post_init__'] = post_init + cls_dict['__attrs_post_init__'] = post_init return make_class('HypClass', cls_dict, slots=slots_flag, frozen=frozen_flag)