Skip to content

Commit

Permalink
Merge branch 'pr/111'
Browse files Browse the repository at this point in the history
Fixes #111
  • Loading branch information
hynek committed Nov 20, 2016
2 parents 0fac921 + 3964f9d commit 1fbaa20
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 8 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Changelog
=========

Versions are year-based with a strict backwards compatibility policy.
Versions follow `CalVer <http://calver.org>`_ with a strict backwards compatibility policy.
The third digit is only for regressions.


Expand All @@ -13,6 +13,8 @@ Changes:

- Attributes now can have user-defined metadata which greatly improves ``attrs``'s extensibility.
`#96 <https://github.com/hynek/attrs/pull/96>`_
- Allow for a ``__attrs_post_init__`` method that -- if defined -- will get executed at the end of the ``attrs``-generated ``__init__`` method.
`#111 <https://github.com/hynek/attrs/pull/111>`_
- Don't overwrite ``__name__`` with ``__qualname__`` for ``attr.s(slots=True)`` classes.
`#99 <https://github.com/hynek/attrs/issues/99>`_

Expand Down
20 changes: 20 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``__attrs_post_init__`` method in your class.
It will get called at the end of the generated ``__init__`` method.

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib()
... y = attr.ib()
... z = attr.ib(init=False)
...
... def __attrs_post_init__(self):
... self.z = self.x + self.y
>>> obj = C(x=1, y=2)
>>> obj
C(x=1, y=2, z=3)

Finally, you can exclude single attributes from certain methods:

.. doctest::
Expand Down
10 changes: 8 additions & 2 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, "__attrs_post_init__", False),
)
locs = {}
bytecode = compile(script, unique_filename, "exec")
attr_dict = dict((a.name, a) for a in attrs)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.__attrs_post_init__()")

return """\
def __init__(self, {args}):
Expand Down
15 changes: 15 additions & 0 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 __attrs_post_init__ gets called if defined.
"""
@attributes
class C(object):
x = attr()
y = attr()

def __attrs_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):
Expand Down
17 changes: 12 additions & 5 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -186,12 +187,18 @@ 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['__attrs_post_init__'] = post_init
return make_class('HypClass', cls_dict,
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)

0 comments on commit 1fbaa20

Please sign in to comment.