From f2c075c40fd331b7d26a9db72aad249b2165eac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Fri, 12 Feb 2016 17:31:04 +0100 Subject: factory: LazyFunction to just call a function in the simplest case No need to wrap it in a lambda to strip the object argument from LazyAttribute or the sequence argument from Sequence. --- README.rst | 5 +++++ docs/examples.rst | 2 ++ docs/introduction.rst | 29 +++++++++++++++++++++++++++++ docs/reference.rst | 40 ++++++++++++++++++++++++++++++++++++++-- factory/__init__.py | 1 + factory/declarations.py | 17 +++++++++++++++++ tests/test_base.py | 11 +++++++---- tests/test_containers.py | 23 +++++++++++++++++++++++ 8 files changed, 122 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 4d114e5..762fedb 100644 --- a/README.rst +++ b/README.rst @@ -226,6 +226,7 @@ These "lazy" attributes can be added as follows: first_name = 'Joe' last_name = 'Blow' email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower()) + date_joined = factory.LazyFunction(datetime.now) .. code-block:: pycon @@ -233,6 +234,10 @@ These "lazy" attributes can be added as follows: "joe.blow@example.com" +.. note:: ``LazyAttribute`` calls the function with the object being constructed as an argument, when + ``LazyFunction`` does not send any argument. + + Sequences """"""""" diff --git a/docs/examples.rst b/docs/examples.rst index e7f6057..6f26b7e 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -49,6 +49,7 @@ And now, we'll define the related factories: .. code-block:: python + import datetime import factory import random @@ -61,6 +62,7 @@ And now, we'll define the related factories: username = factory.Sequence(lambda n: 'john%s' % n) email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username) + date_joined = factory.LazyFunction(datetime.datetime.now) class ProfileFactory(factory.Factory): diff --git a/docs/introduction.rst b/docs/introduction.rst index d00154d..9a16c39 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -117,6 +117,35 @@ This is achieved with the :class:`~factory.Sequence` declaration: return 'user%d' % n +LazyFunction +------------ + +In simple cases, calling a function is enough to compute the value. If that function doesn't depend on the object +being built, use :class:`~factory.LazyFunction` to call that function; it should receive a function taking no +argument and returning the value for the field: + +.. code-block:: python + + class LogFactory(factory.Factory): + class Meta: + model = models.Log + + timestamp = factory.LazyFunction(datetime.now) + +.. code-block:: pycon + + >>> LogFactory() + + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + + + +.. note:: For complex cases when you happen to write a specific function, + the :meth:`~factory.@lazy_attribute` decorator should be more appropriate. + + LazyAttribute ------------- diff --git a/docs/reference.rst b/docs/reference.rst index b5ccd16..9e01213 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -90,7 +90,7 @@ The :class:`Factory` class model = Order exclude = ('now',) - now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow()) + now = factory.LazyFunction(datetime.datetime.utcnow) started_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(hours=1)) paid_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(minutes=50)) @@ -551,6 +551,42 @@ Faker smiley = factory.Faker('smiley') +LazyFunction +"""""""""""" + +.. class:: LazyFunction(method_to_call) + +The :class:`LazyFunction` is the simplest case where the value of an attribute +does not depend on the object being built. + +It takes as argument a method to call (function, lambda...); that method should +not take any argument, though keyword arguments are safe but unused, +and return a value. + +.. code-block:: python + + class LogFactory(factory.Factory): + class Meta: + model = models.Log + + timestamp = factory.LazyFunction(datetime.now) + +.. code-block:: pycon + + >>> LogFactory() + + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + + +Decorator +~~~~~~~~~ + +The class :class:`LazyFunction` does not provide a decorator. + +For complex cases, use :meth:`LazyAttribute.lazy_attribute` directly. + LazyAttribute """"""""""""" @@ -1041,7 +1077,7 @@ gains an "upward" semantic through the double-dot notation, as used in Python im >>> company.owner.language 'fr' -Obviously, this "follow parents" hability also handles overriding some attributes on call: +Obviously, this "follow parents" ability also handles overriding some attributes on call: .. code-block:: pycon diff --git a/factory/__init__.py b/factory/__init__.py index c8bc396..1fa581b 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -43,6 +43,7 @@ from .base import ( from .faker import Faker from .declarations import ( + LazyFunction, LazyAttribute, Iterator, Sequence, diff --git a/factory/declarations.py b/factory/declarations.py index f0dbfe5..9ab7462 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -57,6 +57,23 @@ class OrderedDeclaration(object): raise NotImplementedError('This is an abstract method') +class LazyFunction(OrderedDeclaration): + """Simplest OrderedDeclaration computed by calling the given function. + + Attributes: + function (function): a function without arguments and + returning the computed value. + """ + + def __init__(self, function, *args, **kwargs): + super(LazyFunction, self).__init__(*args, **kwargs) + self.function = function + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + logger.debug("LazyFunction: Evaluating %r on %r", self.function, obj) + return self.function() + + class LazyAttribute(OrderedDeclaration): """Specific OrderedDeclaration computed using a lambda. diff --git a/tests/test_base.py b/tests/test_base.py index 24f64e5..dd74e35 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -134,25 +134,28 @@ class OptionsTests(unittest.TestCase): self.assertEqual(AbstractFactory, AbstractFactory._meta.counter_reference) def test_declaration_collecting(self): - lazy = declarations.LazyAttribute(lambda _o: 1) + lazy = declarations.LazyFunction(int) + lazy2 = declarations.LazyAttribute(lambda _o: 1) postgen = declarations.PostGenerationDeclaration() class AbstractFactory(base.Factory): x = 1 y = lazy + y2 = lazy2 z = postgen # Declarations aren't removed self.assertEqual(1, AbstractFactory.x) self.assertEqual(lazy, AbstractFactory.y) + self.assertEqual(lazy2, AbstractFactory.y2) self.assertEqual(postgen, AbstractFactory.z) # And are available in class Meta - self.assertEqual({'x': 1, 'y': lazy}, AbstractFactory._meta.declarations) + self.assertEqual({'x': 1, 'y': lazy, 'y2': lazy2}, AbstractFactory._meta.declarations) self.assertEqual({'z': postgen}, AbstractFactory._meta.postgen_declarations) def test_inherited_declaration_collecting(self): - lazy = declarations.LazyAttribute(lambda _o: 1) + lazy = declarations.LazyFunction(int) lazy2 = declarations.LazyAttribute(lambda _o: 2) postgen = declarations.PostGenerationDeclaration() postgen2 = declarations.PostGenerationDeclaration() @@ -178,7 +181,7 @@ class OptionsTests(unittest.TestCase): self.assertEqual({'z': postgen, 'b': postgen2}, OtherFactory._meta.postgen_declarations) def test_inherited_declaration_shadowing(self): - lazy = declarations.LazyAttribute(lambda _o: 1) + lazy = declarations.LazyFunction(int) lazy2 = declarations.LazyAttribute(lambda _o: 2) postgen = declarations.PostGenerationDeclaration() postgen2 = declarations.PostGenerationDeclaration() diff --git a/tests/test_containers.py b/tests/test_containers.py index 083b306..825e897 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -215,6 +215,29 @@ class AttributeBuilderTestCase(unittest.TestCase): ab = containers.AttributeBuilder(FakeFactory, extra={'one': seq2}) self.assertEqual({'one': 'yy1'}, ab.build(create=False)) + def test_lazy_function(self): + lf = declarations.LazyFunction(int) + + class FakeFactory(object): + @classmethod + def declarations(cls, extra): + d = {'one': 1, 'two': lf} + d.update(extra) + return d + + @classmethod + def _generate_next_sequence(cls): + return 1 + + ab = containers.AttributeBuilder(FakeFactory) + self.assertEqual({'one': 1, 'two': 0}, ab.build(create=False)) + + ab = containers.AttributeBuilder(FakeFactory, {'one': 4}) + self.assertEqual({'one': 4, 'two': 0}, ab.build(create=False)) + + ab = containers.AttributeBuilder(FakeFactory, {'one': 4, 'three': lf}) + self.assertEqual({'one': 4, 'two': 0, 'three': 0}, ab.build(create=False)) + def test_lazy_attribute(self): la = declarations.LazyAttribute(lambda a: a.one * 2) -- cgit v1.2.3