diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | README.rst | 38 | ||||
-rw-r--r-- | dev_requirements.txt | 3 | ||||
-rw-r--r-- | docs/changelog.rst | 9 | ||||
-rw-r--r-- | docs/conf.py | 2 | ||||
-rw-r--r-- | docs/examples.rst | 2 | ||||
-rw-r--r-- | docs/fuzzy.rst | 2 | ||||
-rw-r--r-- | docs/introduction.rst | 29 | ||||
-rw-r--r-- | docs/orms.rst | 8 | ||||
-rw-r--r-- | docs/recipes.rst | 2 | ||||
-rw-r--r-- | docs/reference.rst | 57 | ||||
-rw-r--r-- | factory/__init__.py | 3 | ||||
-rw-r--r-- | factory/alchemy.py | 3 | ||||
-rw-r--r-- | factory/compat.py | 8 | ||||
-rw-r--r-- | factory/declarations.py | 17 | ||||
-rw-r--r-- | factory/fuzzy.py | 5 | ||||
-rw-r--r-- | factory/utils.py | 2 | ||||
-rwxr-xr-x | setup.py | 1 | ||||
-rw-r--r-- | tests/test_alchemy.py | 32 | ||||
-rw-r--r-- | tests/test_base.py | 11 | ||||
-rw-r--r-- | tests/test_containers.py | 23 | ||||
-rw-r--r-- | tests/test_fuzzy.py | 13 | ||||
-rw-r--r-- | tests/test_using.py | 30 |
24 files changed, 274 insertions, 32 deletions
diff --git a/.travis.yml b/.travis.yml index e1600bd..938787b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ +sudo: false language: python python: - "2.7" - "3.4" + - "3.5" - "pypy" script: @@ -7,13 +7,13 @@ EXAMPLES_DIR=examples COVERAGE = python $(shell which coverage) # Dependencies -DJANGO ?= 1.8 +DJANGO ?= 1.9 NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") ALCHEMY ?= 1.0 NEXT_ALCHEMY = $(shell python -c "v='$(ALCHEMY)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") -MONGOENGINE ?= 0.9 +MONGOENGINE ?= 0.10 NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt @@ -1,20 +1,24 @@ factory_boy =========== -.. image:: https://pypip.in/version/factory_boy/badge.svg +.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master + :target: http://travis-ci.org/rbarrois/factory_boy/ + +.. image:: https://img.shields.io/pypi/v/factory_boy.svg :target: http://factoryboy.readthedocs.org/en/latest/changelog.html :alt: Latest Version -.. image:: https://pypip.in/py_versions/factory_boy/badge.svg +.. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg :target: https://pypi.python.org/pypi/factory_boy/ :alt: Supported Python versions -.. image:: https://pypip.in/wheel/factory_boy/badge.svg +.. image:: https://img.shields.io/pypi/wheel/factory_boy.svg :target: https://pypi.python.org/pypi/factory_boy/ :alt: Wheel status -.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master - :target: http://travis-ci.org/rbarrois/factory_boy/ +.. image:: https://img.shields.io/pypi/l/factory_boy.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: License factory_boy is a fixtures replacement based on thoughtbot's `factory_girl <http://github.com/thoughtbot/factory_girl>`_. @@ -74,8 +78,9 @@ Links * Documentation: http://factoryboy.readthedocs.org/ * Repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ +* Mailing-list: `factoryboy@googlegroups.com <mailto:factoryboy@googlegroups.com>`_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library. +factory_boy supports Python 2.6, 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library. Download @@ -181,7 +186,7 @@ It is also possible to create a bunch of objects in a single call: Realistic, random values """""""""""""""""""""""" -Tests look better with random yet realistic values. +Demos look better with random yet realistic values; and those realistic values can also help discover bugs. For this, factory_boy relies on the excellent `fake-factory <https://pypi.python.org/pypi/fake-factory>`_ library: .. code-block:: python @@ -199,6 +204,10 @@ For this, factory_boy relies on the excellent `fake-factory <https://pypi.python <User: Lucy Murray> +.. note:: Use of fully randomized data in tests is quickly a problem for reproducing broken builds. + To that purpose, factory_boy provides helpers to handle the random seeds it uses. + + Lazy Attributes """"""""""""""" @@ -217,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 @@ -224,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 """"""""" @@ -328,6 +342,7 @@ Contributing factory_boy is distributed under the MIT License. Issues should be opened through `GitHub Issues <http://github.com/rbarrois/factory_boy/issues/>`_; whenever possible, a pull request should be included. +Questions and suggestions are welcome on the `mailing-list <mailto:factoryboy@googlegroups.com>`_. All pull request should pass the test suite, which can be launched simply with: @@ -348,7 +363,7 @@ To test with a specific framework version, you may use: .. code-block:: sh - $ make DJANGO=1.7 test + $ make DJANGO=1.9 test Valid options are: @@ -357,6 +372,13 @@ Valid options are: * ``ALCHEMY`` for ``SQLAlchemy`` +To avoid running ``mongoengine`` tests (e.g no mongo server installed), run: + +.. code-block:: sh + + $ make SKIP_MONGOENGINE=1 test + + Contents, indices and tables ---------------------------- diff --git a/dev_requirements.txt b/dev_requirements.txt index 22261a1..c78aa9d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,3 +8,6 @@ SQLAlchemy mongoengine mock wheel + +Sphinx +sphinx_rtd_theme diff --git a/docs/changelog.rst b/docs/changelog.rst index fa542f4..dc3f967 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +.. _v2.6.1: + +2.6.1 (2016-02-10) +------------------ + +*New:* + + - :issue:`262`: Allow optional forced flush on SQLAlchemy, courtesy of `Minjung <https://github.com/Minjung>`_. + .. _v2.6.0: 2.6.0 (2015-10-20) diff --git a/docs/conf.py b/docs/conf.py index c3512e0..d5b86f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,7 +114,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the 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/fuzzy.rst b/docs/fuzzy.rst index 6b06608..fde1af1 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -199,7 +199,7 @@ FuzzyDate FuzzyDateTime ------------- -.. class:: FuzzyDateTime(start_dt[, end_dt], tz=UTC, force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None) +.. class:: FuzzyDateTime(start_dt[, end_dt], force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None) The :class:`FuzzyDateTime` fuzzer generates random timezone-aware datetime within a given inclusive range. 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() + <Log: log at 2016-02-12 17:02:34> + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + <Log: log at 2016-02-11 17:02:34> + + +.. 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/orms.rst b/docs/orms.rst index 9b209bc..af20917 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -15,7 +15,7 @@ Django The first versions of factory_boy were designed specifically for Django, -but the library has now evolved to be framework-independant. +but the library has now evolved to be framework-independent. Most features should thus feel quite familiar to Django users. @@ -126,7 +126,7 @@ Extra fields :param str from_path: Use data from the file located at ``from_path``, and keep its filename :param file from_file: Use the contents of the provided file object; use its filename - if available + if available, unless ``filename`` is also provided. :param bytes data: Use the provided bytes as file contents :param str filename: The filename for the FileField @@ -333,6 +333,10 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. + .. attribute:: force_flush + + Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. + A (very) simple example: .. code-block:: python diff --git a/docs/recipes.rst b/docs/recipes.rst index df86bac..a627e8b 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -88,7 +88,7 @@ When a :class:`UserFactory` is instantiated, factory_boy will call Example: Django's Profile -""""""""""""""""""""""""" +~~~~~~~~~~~~~~~~~~~~~~~~~ Django (<1.5) provided a mechanism to attach a ``Profile`` to a ``User`` instance, using a :class:`~django.db.models.OneToOneField` from the ``Profile`` to the ``User``. diff --git a/docs/reference.rst b/docs/reference.rst index 44300a5..e2f63db 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() + <Log: log at 2016-02-12 17:02:34> + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + <Log: log at 2016-02-11 17:02:34> + +Decorator +~~~~~~~~~ + +The class :class:`LazyFunction` does not provide a decorator. + +For complex cases, use :meth:`LazyAttribute.lazy_attribute` directly. + LazyAttribute """"""""""""" @@ -1042,7 +1078,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 @@ -1413,6 +1449,23 @@ If a value if passed for the :class:`RelatedFactory` attribute, this disables 1 +.. note:: The target of the :class:`RelatedFactory` is evaluated *after* the initial factory has been instantiated. + This means that calls to :class:`factory.SelfAttribute` cannot go higher than this :class:`RelatedFactory`: + + .. code-block:: python + + class CountryFactory(factory.Factory): + class Meta: + model = Country + + lang = 'fr' + capital_city = factory.RelatedFactory(CityFactory, 'capital_of', + # factory.SelfAttribute('..lang') will crash, since the context of + # ``CountryFactory`` has already been evaluated. + main_lang=factory.SelfAttribute('capital_of.lang'), + ) + + PostGeneration """""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index 4a4a09f..1fa581b 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.6.0' +__version__ = '2.6.1' __author__ = 'Raphaƫl Barrois <raphael.barrois+fboy@polytechnique.org>' @@ -43,6 +43,7 @@ from .base import ( from .faker import Faker from .declarations import ( + LazyFunction, LazyAttribute, Iterator, Sequence, diff --git a/factory/alchemy.py b/factory/alchemy.py index 20da6cf..a9aab23 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -27,6 +27,7 @@ class SQLAlchemyOptions(base.FactoryOptions): def _build_default_options(self): return super(SQLAlchemyOptions, self)._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), + base.OptionDefault('force_flush', False, inherit=True), ] @@ -43,4 +44,6 @@ class SQLAlchemyModelFactory(base.Factory): session = cls._meta.sqlalchemy_session obj = model_class(*args, **kwargs) session.add(obj) + if cls._meta.force_flush: + session.flush() return obj diff --git a/factory/compat.py b/factory/compat.py index 785d174..737d91a 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -42,14 +42,6 @@ else: # pragma: no cover from io import BytesIO -if sys.version_info[:2] == (2, 6): # pragma: no cover - def float_to_decimal(fl): - return decimal.Decimal(str(fl)) -else: # pragma: no cover - def float_to_decimal(fl): - return decimal.Decimal(fl) - - try: # pragma: no cover # Python >= 3.2 UTC = datetime.timezone.utc 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/factory/fuzzy.py b/factory/fuzzy.py index 923d8b7..71d1884 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -164,7 +164,7 @@ class FuzzyDecimal(BaseFuzzyAttribute): super(FuzzyDecimal, self).__init__(**kwargs) def fuzz(self): - base = compat.float_to_decimal(_random.uniform(self.low, self.high)) + base = decimal.Decimal(str(_random.uniform(self.low, self.high))) return base.quantize(decimal.Decimal(10) ** -self.precision) @@ -217,6 +217,9 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute): """%s boundaries should have start <= end, got %r > %r""" % ( self.__class__.__name__, start_dt, end_dt)) + def _now(self): + raise NotImplementedError() + def __init__(self, start_dt, end_dt=None, force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, diff --git a/factory/utils.py b/factory/utils.py index 806b1ec..15dba0a 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -101,7 +101,7 @@ def import_object(module_name, attribute_name): def _safe_repr(obj): try: obj_repr = repr(obj) - except UnicodeError: + except Exception: return '<bad_repr object at %s>' % id(obj) try: # Convert to "text type" (= unicode) @@ -67,6 +67,7 @@ setup( "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 9d7288a..5d8f275 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -23,6 +23,7 @@ import factory from .compat import unittest +import mock try: @@ -55,6 +56,16 @@ class StandardFactory(SQLAlchemyModelFactory): foo = factory.Sequence(lambda n: 'foo%d' % n) +class ForceFlushingStandardFactory(SQLAlchemyModelFactory): + class Meta: + model = models.StandardModel + sqlalchemy_session = mock.MagicMock() + force_flush = True + + id = factory.Sequence(lambda n: n) + foo = factory.Sequence(lambda n: 'foo%d' % n) + + class NonIntegerPkFactory(SQLAlchemyModelFactory): class Meta: model = models.NonIntegerPk @@ -103,6 +114,27 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase): @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") +class SQLAlchemyForceFlushTestCase(unittest.TestCase): + def setUp(self): + super(SQLAlchemyForceFlushTestCase, self).setUp() + ForceFlushingStandardFactory.reset_sequence(1) + ForceFlushingStandardFactory._meta.sqlalchemy_session.rollback() + ForceFlushingStandardFactory._meta.sqlalchemy_session.reset_mock() + + def test_force_flush_called(self): + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory.create() + self.assertTrue(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + + def test_force_flush_not_called(self): + ForceFlushingStandardFactory._meta.force_flush = False + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory.create() + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory._meta.force_flush = True + + +@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def setUp(self): super(SQLAlchemyNonIntegerPkTestCase, self).setUp() 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) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 3f9c434..4c3873a 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -189,6 +189,19 @@ class FuzzyDecimalTestCase(unittest.TestCase): self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) + @unittest.skipIf(compat.PY2, "decimal.FloatOperation was added in Py3") + def test_no_approximation(self): + """We should not go through floats in our fuzzy calls unless actually needed.""" + fuzz = fuzzy.FuzzyDecimal(0, 10) + + decimal_context = decimal.getcontext() + old_traps = decimal_context.traps[decimal.FloatOperation] + try: + decimal_context.traps[decimal.FloatOperation] = True + fuzz.evaluate(2, None, None) + finally: + decimal_context.traps[decimal.FloatOperation] = old_traps + class FuzzyDateTestCase(unittest.TestCase): @classmethod diff --git a/tests/test_using.py b/tests/test_using.py index c7d2b85..0a893c1 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1924,6 +1924,36 @@ class PostGenerationTestCase(unittest.TestCase): self.assertEqual(3, related.one) self.assertEqual(4, related.two) + def test_related_factory_selfattribute(self): + class TestRelatedObject(object): + def __init__(self, obj=None, one=None, two=None): + obj.related = self + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactory(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = 3 + two = 2 + three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj', + two=factory.SelfAttribute('obj.two'), + ) + + obj = TestObjectFactory.build(two=4) + self.assertEqual(3, obj.one) + self.assertEqual(4, obj.two) + self.assertEqual(1, obj.related.one) + self.assertEqual(4, obj.related.two) + + class RelatedFactoryExtractionTestCase(unittest.TestCase): def setUp(self): |