diff --git a/changelog.d/178.change.rst b/changelog.d/178.change.rst new file mode 100644 index 000000000..283f81aa5 --- /dev/null +++ b/changelog.d/178.change.rst @@ -0,0 +1 @@ +``attr.ib(factory=f)`` is now syntactic sugar for the common case of ``attr.ib(default=attr.Factory(f))``. diff --git a/changelog.d/356.change.rst b/changelog.d/356.change.rst new file mode 100644 index 000000000..283f81aa5 --- /dev/null +++ b/changelog.d/356.change.rst @@ -0,0 +1 @@ +``attr.ib(factory=f)`` is now syntactic sugar for the common case of ``attr.ib(default=attr.Factory(f))``. diff --git a/docs/examples.rst b/docs/examples.rst index 39f4150b4..7f0eb0991 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -282,6 +282,16 @@ The method receives the partially initialized instance which enables you to base C(x=1, y=2) +And since the case of ``attr.ib(default=attr.Factory(f))`` is so common, ``attrs`` also comes with syntactic sugar for it: + +.. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(factory=list) + >>> C() + C(x=[]) + .. _examples_validators: Validators diff --git a/src/attr/_make.py b/src/attr/_make.py index ca27f1b6a..e8f55d8b8 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -58,7 +58,8 @@ def __hash__(self): def attrib(default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, - convert=None, metadata=None, type=None, converter=None): + convert=None, metadata=None, type=None, converter=None, + factory=None): """ Create a new attribute on a class. @@ -83,6 +84,9 @@ def attrib(default=NOTHING, validator=None, :type default: Any value. + :param callable factory: Syntactic sugar for + ``default=attr.Factory(callable)``. + :param validator: :func:`callable` that is called by ``attrs``-generated ``__init__`` methods after the instance has been initialized. They receive the initialized instance, the :class:`Attribute`, and the @@ -137,6 +141,8 @@ def attrib(default=NOTHING, validator=None, .. deprecated:: 17.4.0 *convert* .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated *convert* to achieve consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. """ if hash is not None and hash is not True and hash is not False: raise TypeError( @@ -156,6 +162,18 @@ def attrib(default=NOTHING, validator=None, ) converter = convert + if factory is not None: + if default is not NOTHING: + raise ValueError( + "The `default` and `factory` arguments are mutually " + "exclusive." + ) + if not callable(factory): + raise ValueError( + "The `factory` argument must be a callable." + ) + default = Factory(factory) + if metadata is None: metadata = {} diff --git a/tests/test_make.py b/tests/test_make.py index 47955a97f..e148df390 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -525,6 +525,35 @@ class C(object): assert not isinstance(x, _CountingAttr) + def test_factory_sugar(self): + """ + Passing factory=f is syntactic sugar for passing default=Factory(f). + """ + @attr.s + class C(object): + x = attr.ib(factory=list) + + assert Factory(list) == attr.fields(C).x.default + + def test_sugar_factory_mutex(self): + """ + Passing both default and factory raises ValueError. + """ + with pytest.raises(ValueError, match="mutually exclusive"): + @attr.s + class C(object): + x = attr.ib(factory=list, default=Factory(list)) + + def test_sugar_callable(self): + """ + Factory has to be a callable to prevent people from passing Factory + into it. + """ + with pytest.raises(ValueError, match="must be a callable"): + @attr.s + class C(object): + x = attr.ib(factory=Factory(list)) + @attr.s class GC(object):