From e260ce1e69a1259dc7ba3396e43866fe7b5994f4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 14 Mar 2018 16:53:53 +0100 Subject: [PATCH 1/6] Add syntactic sugar for attr.ib(default=attr.Factory) Fixes #178 --- changelog.d/178.change.rst | 1 + src/attr/_make.py | 16 +++++++++++++++- tests/test_make.py | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 changelog.d/178.change.rst diff --git a/changelog.d/178.change.rst b/changelog.d/178.change.rst new file mode 100644 index 000000000..f41698df7 --- /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/src/attr/_make.py b/src/attr/_make.py index ca27f1b6a..34422396e 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,14 @@ 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." + ) + default = Factory(factory) + if metadata is None: metadata = {} diff --git a/tests/test_make.py b/tests/test_make.py index 47955a97f..6ba3597a0 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -525,6 +525,27 @@ 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_factory_mutex(self): + """ + Passing both default and factory raises ValueError. + """ + with pytest.raises(ValueError) as ei: + @attr.s + class C(object): + x = attr.ib(factory=list, default=Factory(list)) + + assert "mutually exclusive" in ei.value.args[0] + @attr.s class GC(object): From 87ee94f16a6d94053ff859e8b1a31199a1e5c59c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 14 Mar 2018 17:38:27 +0100 Subject: [PATCH 2/6] Add example --- docs/examples.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/examples.rst b/docs/examples.rst index 39f4150b4..7ca02524f 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 that case: + +.. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(factory=list) + >>> C() + C(x=[]) + .. _examples_validators: Validators From cdc7969d5b403a55c70ea18d1057da5265cd4f0f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 14 Mar 2018 17:43:51 +0100 Subject: [PATCH 3/6] Enforce factory to be a callable --- src/attr/_make.py | 4 ++++ tests/test_make.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 34422396e..e8f55d8b8 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -168,6 +168,10 @@ def attrib(default=NOTHING, validator=None, "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: diff --git a/tests/test_make.py b/tests/test_make.py index 6ba3597a0..fda358393 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -535,7 +535,7 @@ class C(object): assert Factory(list) == attr.fields(C).x.default - def test_factory_mutex(self): + def test_sugar_factory_mutex(self): """ Passing both default and factory raises ValueError. """ @@ -546,6 +546,18 @@ class C(object): assert "mutually exclusive" in ei.value.args[0] + def test_sugar_callable(self): + """ + Factory has to be a callable to prevent people from passing Factory + into it. + """ + with pytest.raises(ValueError) as ei: + @attr.s + class C(object): + x = attr.ib(factory=Factory(list)) + + assert "must be a callable" in ei.value.args[0] + @attr.s class GC(object): From 4985a51a8bb5c6358d1b0f36700ae2599d56d6fe Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 14 Mar 2018 17:49:47 +0100 Subject: [PATCH 4/6] Add newsfragment --- changelog.d/178.change.rst | 2 +- changelog.d/356.change.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/356.change.rst diff --git a/changelog.d/178.change.rst b/changelog.d/178.change.rst index f41698df7..283f81aa5 100644 --- a/changelog.d/178.change.rst +++ b/changelog.d/178.change.rst @@ -1 +1 @@ -``attr.ib(factory=f)`` is now syntactic sugar for the common case of ``attr.ib(default=attr.Factory(f)``. +``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))``. From a3100c701383dd3dce6d9c16a20356bb795a4b11 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 14 Mar 2018 18:08:06 +0100 Subject: [PATCH 5/6] SO SHINY --- tests/test_make.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_make.py b/tests/test_make.py index fda358393..e148df390 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -539,25 +539,21 @@ def test_sugar_factory_mutex(self): """ Passing both default and factory raises ValueError. """ - with pytest.raises(ValueError) as ei: + with pytest.raises(ValueError, match="mutually exclusive"): @attr.s class C(object): x = attr.ib(factory=list, default=Factory(list)) - assert "mutually exclusive" in ei.value.args[0] - def test_sugar_callable(self): """ Factory has to be a callable to prevent people from passing Factory into it. """ - with pytest.raises(ValueError) as ei: + with pytest.raises(ValueError, match="must be a callable"): @attr.s class C(object): x = attr.ib(factory=Factory(list)) - assert "must be a callable" in ei.value.args[0] - @attr.s class GC(object): From 3060477841c638dcf4ba09e5ceebfd1963a8a24e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 14 Mar 2018 18:47:51 +0100 Subject: [PATCH 6/6] Avoid double "that case" --- docs/examples.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index 7ca02524f..7f0eb0991 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -282,7 +282,7 @@ 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 that case: +And since the case of ``attr.ib(default=attr.Factory(f))`` is so common, ``attrs`` also comes with syntactic sugar for it: .. doctest::