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

Add takes_self to Factory and @_CountingAttr.default #189

Merged
merged 2 commits into from
May 16, 2017
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ Changes:
- Validators can now be defined conveniently inline by using the attribute as a decorator.
Check out the `examples <http://www.attrs.org/en/stable/examples.html#validators>`_ to see it in action!
`#143 <https://github.com/python-attrs/attrs/issues/143>`_
- ``attr.Factory()`` now has a ``takes_self`` argument that makes the initializer to pass the partially initialized instance into the factory.
In other words you can define attribute defaults based on other attributes.
`#165`_
- Default factories can now also be defined inline using decorators.
They are *always* passed the partially initialized instance.
`#165`_
- Conversion can now be made optional using ``attr.converters.optional()``.
`#105 <https://github.com/python-attrs/attrs/issues/105>`_
`#173 <https://github.com/python-attrs/attrs/pull/173>`_
Expand All @@ -70,6 +76,7 @@ Changes:
`#155 <https://github.com/python-attrs/attrs/pull/155>`_

.. _`#136`: https://github.com/python-attrs/attrs/issues/136
.. _`#165`: https://github.com/python-attrs/attrs/issues/165


----
Expand Down
18 changes: 13 additions & 5 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,20 @@ Core
>>> @attr.s
... class C(object):
... x = attr.ib(default=attr.Factory(list))
... y = attr.ib(default=attr.Factory(
... lambda self: set(self.x),
... takes_self=True)
... )
>>> C()
C(x=[])
C(x=[], y=set())
>>> C([1, 2, 3])
C(x=[1, 2, 3], y={1, 2, 3})


.. autoexception:: attr.exceptions.FrozenInstanceError
.. autoexception:: attr.exceptions.AttrsAttributeNotFoundError
.. autoexception:: attr.exceptions.NotAnAttrsClassError
.. autoexception:: attr.exceptions.DefaultAlreadySetError


.. _helpers:
Expand Down Expand Up @@ -203,11 +210,12 @@ See :ref:`asdict` for examples.
>>> i1 == i2
False

``evolve`` creates a new instance using ``__init__``. This fact has several implications:
``evolve`` creates a new instance using ``__init__``.
This fact has several implications:

* private attributes should be specified without the leading underscore, just like in ``__init__``.
* attributes with ``init=False`` can't be set with ``evolve``.
* the usual ``__init__`` validators will validate the new values.
* private attributes should be specified without the leading underscore, just like in ``__init__``.
* attributes with ``init=False`` can't be set with ``evolve``.
* the usual ``__init__`` validators will validate the new values.

.. autofunction:: validate

Expand Down
15 changes: 15 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,21 @@ And sometimes you even want mutable objects as default values (ever used acciden

More information on why class methods for constructing objects are awesome can be found in this insightful `blog post <http://as.ynchrono.us/2014/12/asynchronous-object-initialization.html>`_.

Default factories can also be set using a decorator.
The method receives the partially initialiazed instance which enables you to base a default value on other attributes:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(default=1)
... y = attr.ib()
... @y.default
... def name_does_not_matter(self):
... return self.x + 1
>>> C()
C(x=1, y=2)


.. _examples_validators:

Expand Down
123 changes: 85 additions & 38 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@

from . import _config
from ._compat import PY2, iteritems, isclass, iterkeys, metadata_proxy
from .exceptions import FrozenInstanceError, NotAnAttrsClassError
from .exceptions import (
DefaultAlreadySetError,
FrozenInstanceError,
NotAnAttrsClassError,
)


# This is used at least twice, so cache it here.
_obj_setattr = object.__setattr__
_init_convert_pat = "__attr_convert_{}"
_init_factory_pat = "__attr_factory_{}"
_tuple_property_pat = " {attr_name} = property(itemgetter({index}))"
_empty_metadata_singleton = metadata_proxy({})

Expand Down Expand Up @@ -701,21 +706,26 @@ def fmt_setter_with_converter(attr_name, value_var):
attrs_to_validate.append(a)
attr_name = a.name
arg_name = a.name.lstrip("_")
has_factory = isinstance(a.default, Factory)
if has_factory and a.default.takes_self:
maybe_self = "self"
else:
maybe_self = ""
if a.init is False:
if isinstance(a.default, Factory):
if has_factory:
init_factory_name = _init_factory_pat.format(a.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)))
init_factory_name + "({0})".format(maybe_self)))
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)
init_factory_name + "({0})".format(maybe_self)
))
names_for_globals[init_factory_name] = a.default.factory
else:
if a.convert is not None:
lines.append(fmt_setter_with_converter(
Expand All @@ -731,7 +741,7 @@ def fmt_setter_with_converter(attr_name, value_var):
"attr_dict['{attr_name}'].default"
.format(attr_name=attr_name)
))
elif a.default is not NOTHING and not isinstance(a.default, Factory):
elif a.default is not NOTHING and not has_factory:
args.append(
"{arg_name}=attr_dict['{attr_name}'].default".format(
arg_name=arg_name,
Expand All @@ -743,28 +753,28 @@ def fmt_setter_with_converter(attr_name, value_var):
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):
elif has_factory:
args.append("{arg_name}=NOTHING".format(arg_name=arg_name))
lines.append("if {arg_name} is not NOTHING:"
.format(arg_name=arg_name))
init_factory_name = _init_factory_pat.format(a.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)
init_factory_name + "({0})".format(maybe_self)
))
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)
init_factory_name + "({0})".format(maybe_self)
))
names_for_globals[init_factory_name] = a.default.factory
else:
args.append(arg_name)
if a.convert is not None:
Expand Down Expand Up @@ -808,21 +818,21 @@ class Attribute(object):
"convert", "metadata",
)

def __init__(self, name, default, _validator, repr, cmp, hash, init,
def __init__(self, name, _default, _validator, repr, cmp, hash, init,
convert=None, metadata=None):
# Cache this descriptor here to speed things up later.
__bound_setattr = _obj_setattr.__get__(self, Attribute)

__bound_setattr("name", name)
__bound_setattr("default", default)
__bound_setattr("validator", _validator)
__bound_setattr("repr", repr)
__bound_setattr("cmp", cmp)
__bound_setattr("hash", hash)
__bound_setattr("init", init)
__bound_setattr("convert", convert)
__bound_setattr("metadata", (metadata_proxy(metadata) if metadata
else _empty_metadata_singleton))
bound_setattr = _obj_setattr.__get__(self, Attribute)

bound_setattr("name", name)
bound_setattr("default", _default)
bound_setattr("validator", _validator)
bound_setattr("repr", repr)
bound_setattr("cmp", cmp)
bound_setattr("hash", hash)
bound_setattr("init", init)
bound_setattr("convert", convert)
bound_setattr("metadata", (metadata_proxy(metadata) if metadata
else _empty_metadata_singleton))

def __setattr__(self, name, value):
raise FrozenInstanceError()
Expand All @@ -832,8 +842,10 @@ def from_counting_attr(cls, name, ca):
inst_dict = {
k: getattr(ca, k)
for k
in Attribute.__slots__ + ("_validator",)
if k != "name" and k != "validator" # `validator` is a method
in Attribute.__slots__ + ("_validator", "_default")
if k != "name" and k not in (
"validator", "default",
) # exclude methods
}
return cls(name=name, **inst_dict)

Expand All @@ -850,16 +862,16 @@ def __setstate__(self, state):
"""
Play nice with pickle.
"""
__bound_setattr = _obj_setattr.__get__(self, Attribute)
bound_setattr = _obj_setattr.__get__(self, Attribute)
for name, value in zip(self.__slots__, state):
if name != "metadata":
__bound_setattr(name, value)
bound_setattr(name, value)
else:
__bound_setattr(name, metadata_proxy(value) if value else
_empty_metadata_singleton)
bound_setattr(name, metadata_proxy(value) if value else
_empty_metadata_singleton)


_a = [Attribute(name=name, default=NOTHING, _validator=None,
_a = [Attribute(name=name, _default=NOTHING, _validator=None,
repr=True, cmp=True, hash=(name != "metadata"), init=True)
for name in Attribute.__slots__]

Expand All @@ -877,15 +889,15 @@ class _CountingAttr(object):
*Internal* data structure of the attrs library. Running into is most
likely the result of a bug like a forgotten `@attr.s` decorator.
"""
__slots__ = ("counter", "default", "repr", "cmp", "hash", "init",
__slots__ = ("counter", "_default", "repr", "cmp", "hash", "init",
"metadata", "_validator", "convert")
__attrs_attrs__ = tuple(
Attribute(name=name, default=NOTHING, _validator=None,
Attribute(name=name, _default=NOTHING, _validator=None,
repr=True, cmp=True, hash=True, init=True)
for name
in ("counter", "default", "repr", "cmp", "hash", "init",)
in ("counter", "_default", "repr", "cmp", "hash", "init",)
) + (
Attribute(name="metadata", default=None, _validator=None,
Attribute(name="metadata", _default=None, _validator=None,
repr=True, cmp=True, hash=False, init=True),
)
cls_counter = 0
Expand All @@ -894,7 +906,7 @@ def __init__(self, default, validator, repr, cmp, hash, init, convert,
metadata):
_CountingAttr.cls_counter += 1
self.counter = _CountingAttr.cls_counter
self.default = default
self._default = default
# If validator is a list/tuple, wrap it using helper validator.
if validator and isinstance(validator, (list, tuple)):
self._validator = and_(*validator)
Expand All @@ -912,26 +924,61 @@ def validator(self, meth):
Decorator that adds *meth* to the list of validators.

Returns *meth* unchanged.

.. versionadded:: 17.1.0
"""
if self._validator is None:
self._validator = meth
else:
self._validator = and_(self._validator, meth)
return meth

def default(self, meth):
"""
Decorator that allows to set the default for an attribute.

Returns *meth* unchanged.

:raises DefaultAlreadySetError: If default has been set before.

.. versionadded:: 17.1.0
"""
if self._default is not NOTHING:
raise DefaultAlreadySetError()

self._default = Factory(meth, takes_self=True)

return meth


_CountingAttr = _add_cmp(_add_repr(_CountingAttr))


@attributes(slots=True)
@attributes(slots=True, init=False)
class Factory(object):
"""
Stores a factory callable.

If passed as the default value to :func:`attr.ib`, the factory is used to
generate a new value.

:param callable factory: A callable that takes either none or exactly one
mandatory positional argument depending on *takes_self*.
:param bool takes_self: Pass the partially initialized instance that is
being initialized as a positional argument.

.. versionadded:: 17.1.0 *takes_self*
"""
factory = attr()
takes_self = attr()

def __init__(self, factory, takes_self=False):
"""
`Factory` is part of the default machinery so if we want a default
value here, we have to implement it ourselves.
"""
self.factory = factory
self.takes_self = takes_self


def make_class(name, attrs, bases=(object,), **attributes_arguments):
Expand Down
9 changes: 9 additions & 0 deletions src/attr/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ class NotAnAttrsClassError(ValueError):

.. versionadded:: 16.2.0
"""


class DefaultAlreadySetError(RuntimeError):
"""
A default has been set using ``attr.ib()`` and is attempted to be reset
using the decorator.

.. versionadded:: 17.1.0
"""
27 changes: 23 additions & 4 deletions tests/test_dark_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ def test_fields(self, cls):
`attr.fields` works.
"""
assert (
Attribute(name="x", default=foo, _validator=None,
Attribute(name="x", _default=foo, _validator=None,
repr=True, cmp=True, hash=None, init=True),
Attribute(name="y", default=attr.Factory(list), _validator=None,
Attribute(name="y", _default=attr.Factory(list), _validator=None,
repr=True, cmp=True, hash=None, init=True),
) == attr.fields(cls)

Expand Down Expand Up @@ -158,9 +158,9 @@ def test_programmatic(self, slots, frozen):
"""
PC = attr.make_class("PC", ["a", "b"], slots=slots, frozen=frozen)
assert (
Attribute(name="a", default=NOTHING, _validator=None,
Attribute(name="a", _default=NOTHING, _validator=None,
repr=True, cmp=True, hash=None, init=True),
Attribute(name="b", default=NOTHING, _validator=None,
Attribute(name="b", _default=NOTHING, _validator=None,
repr=True, cmp=True, hash=None, init=True),
) == attr.fields(PC)

Expand Down Expand Up @@ -251,4 +251,23 @@ def test_subclassing_frozen_gives_frozen(self):

@pytest.mark.parametrize("cls", [WithMeta, WithMetaSlots])
def test_metaclass_preserved(self, cls):
"""
Metaclass data is preserved.
"""
assert Meta == type(cls)

def test_default_decorator(self):
"""
Default decorator sets the default and the respective method gets
called.
"""
@attr.s
class C(object):
x = attr.ib(default=1)
y = attr.ib()

@y.default
def compute(self):
return self.x + 1

assert C(1, 2) == C()
Loading