diff options
Diffstat (limited to 'factory')
-rw-r--r-- | factory/__init__.py | 17 | ||||
-rw-r--r-- | factory/alchemy.py | 21 | ||||
-rw-r--r-- | factory/base.py | 40 | ||||
-rw-r--r-- | factory/compat.py | 10 | ||||
-rw-r--r-- | factory/containers.py | 2 | ||||
-rw-r--r-- | factory/declarations.py | 26 | ||||
-rw-r--r-- | factory/django.py | 120 | ||||
-rw-r--r-- | factory/faker.py | 101 | ||||
-rw-r--r-- | factory/fuzzy.py | 53 | ||||
-rw-r--r-- | factory/helpers.py | 3 | ||||
-rw-r--r-- | factory/mogo.py | 6 | ||||
-rw-r--r-- | factory/mongoengine.py | 2 | ||||
-rw-r--r-- | factory/utils.py | 36 |
13 files changed, 282 insertions, 155 deletions
diff --git a/factory/__init__.py b/factory/__init__.py index 8fc8ef8..c8bc396 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.4.1' +__version__ = '2.6.1' __author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>' @@ -40,9 +40,7 @@ from .base import ( use_strategy, ) -# Backward compatibility; this should be removed soon. -from .mogo import MogoFactory -from .django import DjangoModelFactory +from .faker import Faker from .declarations import ( LazyAttribute, @@ -83,3 +81,12 @@ from .helpers import ( post_generation, ) +# Backward compatibility; this should be removed soon. +from . import alchemy +from . import django +from . import mogo +from . import mongoengine + +MogoFactory = mogo.MogoFactory +DjangoModelFactory = django.DjangoModelFactory + diff --git a/factory/alchemy.py b/factory/alchemy.py index 3c91411..a9aab23 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -19,7 +19,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from __future__ import unicode_literals -from sqlalchemy.sql.functions import max from . import base @@ -28,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), ] @@ -38,27 +38,12 @@ class SQLAlchemyModelFactory(base.Factory): class Meta: abstract = True - _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() - _OLDSTYLE_ATTRIBUTES.update({ - 'FACTORY_SESSION': 'sqlalchemy_session', - }) - - @classmethod - def _setup_next_sequence(cls, *args, **kwargs): - """Compute the next available PK, based on the 'pk' database field.""" - session = cls._meta.sqlalchemy_session - model = cls._meta.model - pk = getattr(model, model.__mapper__.primary_key[0].name) - max_pk = session.query(max(pk)).one()[0] - if isinstance(max_pk, int): - return max_pk + 1 if max_pk else 1 - else: - return 1 - @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" 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/base.py b/factory/base.py index 9e07899..0f2af59 100644 --- a/factory/base.py +++ b/factory/base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,6 @@ # THE SOFTWARE. import logging -import warnings from . import containers from . import declarations @@ -109,23 +108,6 @@ class FactoryMetaClass(type): attrs_meta = attrs.pop('Meta', None) - oldstyle_attrs = {} - converted_attrs = {} - for old_name, new_name in base_factory._OLDSTYLE_ATTRIBUTES.items(): - if old_name in attrs: - oldstyle_attrs[old_name] = new_name - converted_attrs[new_name] = attrs.pop(old_name) - if oldstyle_attrs: - warnings.warn( - "Declaring any of %s at class-level is deprecated" - " and will be removed in the future. Please set them" - " as %s attributes of a 'class Meta' attribute." % ( - ', '.join(oldstyle_attrs.keys()), - ', '.join(oldstyle_attrs.values()), - ), - PendingDeprecationWarning, 2) - attrs_meta = type('Meta', (object,), converted_attrs) - base_meta = resolve_attribute('_meta', bases) options_class = resolve_attribute('_options_class', bases, FactoryOptions) @@ -194,6 +176,7 @@ class FactoryOptions(object): OptionDefault('strategy', CREATE_STRATEGY, inherit=True), OptionDefault('inline_args', (), inherit=True), OptionDefault('exclude', (), inherit=True), + OptionDefault('rename', {}, inherit=True), ] def _fill_from_meta(self, meta, base_meta): @@ -322,14 +305,6 @@ class BaseFactory(object): _meta = FactoryOptions() - _OLDSTYLE_ATTRIBUTES = { - 'FACTORY_FOR': 'model', - 'ABSTRACT_FACTORY': 'abstract', - 'FACTORY_STRATEGY': 'strategy', - 'FACTORY_ARG_PARAMETERS': 'inline_args', - 'FACTORY_HIDDEN_ARGS': 'exclude', - } - # ID to use for the next 'declarations.Sequence' attribute. _counter = None @@ -435,10 +410,16 @@ class BaseFactory(object): retrieved DeclarationDict. """ decls = cls._meta.declarations.copy() - decls.update(extra_defs) + decls.update(extra_defs or {}) return decls @classmethod + def _rename_fields(cls, **kwargs): + for old_name, new_name in cls._meta.rename.items(): + kwargs[new_name] = kwargs.pop(old_name) + return kwargs + + @classmethod def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" return kwargs @@ -467,6 +448,7 @@ class BaseFactory(object): **kwargs: arguments to pass to the creation function """ model_class = cls._get_model_class() + kwargs = cls._rename_fields(**kwargs) kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. @@ -709,7 +691,7 @@ class StubFactory(Factory): @classmethod def build(cls, **kwargs): - raise UnsupportedStrategy() + return cls.stub(**kwargs) @classmethod def create(cls, **kwargs): diff --git a/factory/compat.py b/factory/compat.py index 7747b1a..737d91a 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -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/containers.py b/factory/containers.py index 5116320..0ae354b 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/declarations.py b/factory/declarations.py index 5e7e734..f0dbfe5 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,6 @@ import itertools -import warnings import logging from . import compat @@ -161,12 +160,19 @@ class Iterator(OrderedDeclaration): def __init__(self, iterator, cycle=True, getter=None): super(Iterator, self).__init__() self.getter = getter + self.iterator = None if cycle: - iterator = itertools.cycle(iterator) - self.iterator = utils.ResetableIterator(iterator) + self.iterator_builder = lambda: utils.ResetableIterator(itertools.cycle(iterator)) + else: + self.iterator_builder = lambda: utils.ResetableIterator(iterator) def evaluate(self, sequence, obj, create, extra=None, containers=()): + # Begin unrolling as late as possible. + # This helps with ResetableIterator(MyModel.objects.all()) + if self.iterator is None: + self.iterator = self.iterator_builder() + logger.debug("Iterator: Fetching next value from %r", self.iterator) value = next(iter(self.iterator)) if self.getter is None: @@ -195,7 +201,7 @@ class Sequence(OrderedDeclaration): self.type = type def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("Sequence: Computing next value of %r for seq=%d", self.function, sequence) + logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, sequence) return self.function(self.type(sequence)) @@ -209,7 +215,7 @@ class LazyAttributeSequence(Sequence): of counter for the 'function' attribute. """ def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%d, obj=%r", + logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%r", self.function, sequence, obj) return self.function(obj, self.type(sequence)) @@ -502,14 +508,6 @@ class RelatedFactory(PostGenerationDeclaration): def __init__(self, factory, factory_related_name='', **defaults): super(RelatedFactory, self).__init__() - if factory_related_name == '' and defaults.get('name') is not None: - warnings.warn( - "Usage of RelatedFactory(SomeFactory, name='foo') is deprecated" - " and will be removed in the future. Please use the" - " RelatedFactory(SomeFactory, 'foo') or" - " RelatedFactory(SomeFactory, factory_related_name='foo')" - " syntax instead", PendingDeprecationWarning, 2) - factory_related_name = defaults.pop('name') self.name = factory_related_name self.defaults = defaults diff --git a/factory/django.py b/factory/django.py index 2b6c463..b3c508c 100644 --- a/factory/django.py +++ b/factory/django.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -32,8 +32,10 @@ import functools """factory_boy extensions for use with the Django framework.""" try: + import django from django.core import files as django_files except ImportError as e: # pragma: no cover + django = None django_files = None import_failure = e @@ -45,6 +47,8 @@ from .compat import BytesIO, is_string logger = logging.getLogger('factory.generate') +DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS + def require_django(): """Simple helper to ensure Django is available.""" @@ -52,10 +56,41 @@ def require_django(): raise import_failure +_LAZY_LOADS = {} + +def get_model(app, model): + """Wrapper around django's get_model.""" + if 'get_model' not in _LAZY_LOADS: + _lazy_load_get_model() + + _get_model = _LAZY_LOADS['get_model'] + return _get_model(app, model) + + +def _lazy_load_get_model(): + """Lazy loading of get_model. + + get_model loads django.conf.settings, which may fail if + the settings haven't been configured yet. + """ + if django is None: + def get_model(app, model): + raise import_failure + + elif django.VERSION[:2] < (1, 7): + from django.db.models.loading import get_model + + else: + from django import apps as django_apps + get_model = django_apps.apps.get_model + _LAZY_LOADS['get_model'] = get_model + + class DjangoOptions(base.FactoryOptions): def _build_default_options(self): return super(DjangoOptions, self)._build_default_options() + [ base.OptionDefault('django_get_or_create', (), inherit=True), + base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True), ] def _get_counter_reference(self): @@ -84,18 +119,12 @@ class DjangoModelFactory(base.Factory): class Meta: abstract = True # Optional, but explicit. - _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() - _OLDSTYLE_ATTRIBUTES.update({ - 'FACTORY_DJANGO_GET_OR_CREATE': 'django_get_or_create', - }) - @classmethod def _load_model_class(cls, definition): if is_string(definition) and '.' in definition: app, model = definition.split('.', 1) - from django.db.models import loading as django_loading - return django_loading.get_model(app, model) + return get_model(app, model) return definition @@ -104,25 +133,17 @@ class DjangoModelFactory(base.Factory): if model_class is None: raise base.AssociatedClassError("No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) + try: - return model_class._default_manager # pylint: disable=W0212 + manager = model_class.objects except AttributeError: - return model_class.objects - - @classmethod - def _setup_next_sequence(cls): - """Compute the next available PK, based on the 'pk' database field.""" - - model = cls._get_model_class() # pylint: disable=E1101 - manager = cls._get_manager(model) + # When inheriting from an abstract model with a custom + # manager, the class has no 'objects' field. + manager = model_class._default_manager - try: - return 1 + manager.values_list('pk', flat=True - ).order_by('-pk')[0] - except (IndexError, TypeError): - # IndexError: No instance exist yet - # TypeError: pk isn't an integer type - return 1 + if cls._meta.database != DEFAULT_DB_ALIAS: + manager = manager.using(cls._meta.database) + return manager @classmethod def _get_or_create(cls, model_class, *args, **kwargs): @@ -160,24 +181,22 @@ class DjangoModelFactory(base.Factory): obj.save() -class FileField(declarations.PostGenerationDeclaration): +class FileField(declarations.ParameteredAttribute): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' + EXTEND_CONTAINERS = True def __init__(self, **defaults): require_django() - self.defaults = defaults - super(FileField, self).__init__() + super(FileField, self).__init__(**defaults) def _make_data(self, params): """Create data for the field.""" return params.get('data', b'') - def _make_content(self, extraction_context): + def _make_content(self, params): path = '' - params = dict(self.defaults) - params.update(extraction_context.extra) if params.get('from_path') and params.get('from_file'): raise ValueError( @@ -185,12 +204,7 @@ class FileField(declarations.PostGenerationDeclaration): "be non-empty when calling factory.django.FileField." ) - if extraction_context.did_extract: - # Should be a django.core.files.File - content = extraction_context.value - path = content.name - - elif params.get('from_path'): + if params.get('from_path'): path = params['from_path'] f = open(path, 'rb') content = django_files.File(f, name=path) @@ -212,19 +226,13 @@ class FileField(declarations.PostGenerationDeclaration): filename = params.get('filename', default_filename) return filename, content - def call(self, obj, create, extraction_context): + def generate(self, sequence, obj, create, params): """Fill in the field.""" - if extraction_context.did_extract and extraction_context.value is None: - # User passed an empty value, don't fill - return - filename, content = self._make_content(extraction_context) - field_file = getattr(obj, extraction_context.for_field) - try: - field_file.save(filename, content, save=create) - finally: - content.file.close() - return field_file + params.setdefault('__sequence', sequence) + params = base.DictFactory.simple_generate(create, **params) + filename, content = self._make_content(params) + return django_files.File(content.file, filename) class ImageField(FileField): @@ -278,6 +286,9 @@ class mute_signals(object): logger.debug('mute_signals: Disabling signal handlers %r', signal.receivers) + # Note that we're using implementation details of + # django.signals, since arguments to signal.connect() + # are lost in signal.receivers self.paused[signal] = signal.receivers signal.receivers = [] @@ -287,8 +298,17 @@ class mute_signals(object): receivers) signal.receivers = receivers + if django.VERSION[:2] >= (1, 6): + with signal.lock: + # Django uses some caching for its signals. + # Since we're bypassing signal.connect and signal.disconnect, + # we have to keep messing with django's internals. + signal.sender_receivers_cache.clear() self.paused = {} + def copy(self): + return mute_signals(*self.signals) + def __call__(self, callable_obj): if isinstance(callable_obj, base.FactoryMetaClass): # Retrieve __func__, the *actual* callable object. @@ -297,7 +317,8 @@ class mute_signals(object): @classmethod @functools.wraps(generate_method) def wrapped_generate(*args, **kwargs): - with self: + # A mute_signals() object is not reentrant; use a copy everytime. + with self.copy(): return generate_method(*args, **kwargs) callable_obj._generate = wrapped_generate @@ -306,7 +327,8 @@ class mute_signals(object): else: @functools.wraps(callable_obj) def wrapper(*args, **kwargs): - with self: + # A mute_signals() object is not reentrant; use a copy everytime. + with self.copy(): return callable_obj(*args, **kwargs) return wrapper diff --git a/factory/faker.py b/factory/faker.py new file mode 100644 index 0000000..5411985 --- /dev/null +++ b/factory/faker.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2015 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +"""Additional declarations for "faker" attributes. + +Usage: + + class MyFactory(factory.Factory): + class Meta: + model = MyProfile + + first_name = factory.Faker('name') +""" + + +from __future__ import absolute_import +from __future__ import unicode_literals + +import contextlib + +import faker +import faker.config + +from . import declarations + +class Faker(declarations.OrderedDeclaration): + """Wrapper for 'faker' values. + + Args: + provider (str): the name of the Faker field + locale (str): the locale to use for the faker + + All other kwargs will be passed to the underlying provider + (e.g ``factory.Faker('ean', length=10)`` + calls ``faker.Faker.ean(length=10)``) + + Usage: + >>> foo = factory.Faker('name') + """ + def __init__(self, provider, locale=None, **kwargs): + self.provider = provider + self.provider_kwargs = kwargs + self.locale = locale + + def generate(self, extra_kwargs): + kwargs = {} + kwargs.update(self.provider_kwargs) + kwargs.update(extra_kwargs) + faker = self._get_faker(self.locale) + return faker.format(self.provider, **kwargs) + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + return self.generate(extra or {}) + + _FAKER_REGISTRY = {} + _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE + + @classmethod + @contextlib.contextmanager + def override_default_locale(cls, locale): + old_locale = cls._DEFAULT_LOCALE + cls._DEFAULT_LOCALE = locale + try: + yield + finally: + cls._DEFAULT_LOCALE = old_locale + + @classmethod + def _get_faker(cls, locale=None): + if locale is None: + locale = cls._DEFAULT_LOCALE + + if locale not in cls._FAKER_REGISTRY: + cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale) + + return cls._FAKER_REGISTRY[locale] + + @classmethod + def add_provider(cls, provider, locale=None): + """Add a new Faker provider for the specified locale""" + cls._get_faker(locale).add_provider(provider) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 94599b7..a7e834c 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -34,6 +34,25 @@ from . import compat from . import declarations +_random = random.Random() + + +def get_random_state(): + """Retrieve the state of factory.fuzzy's random generator.""" + return _random.getstate() + + +def set_random_state(state): + """Force-set the state of factory.fuzzy's random generator.""" + return _random.setstate(state) + + +def reseed_random(seed): + """Reseed factory.fuzzy's random generator.""" + r = random.Random(seed) + set_random_state(r.getstate()) + + class BaseFuzzyAttribute(declarations.OrderedDeclaration): """Base class for fuzzy attributes. @@ -81,7 +100,7 @@ class FuzzyText(BaseFuzzyAttribute): """ def __init__(self, prefix='', length=12, suffix='', - chars=string.ascii_letters, **kwargs): + chars=string.ascii_letters, **kwargs): super(FuzzyText, self).__init__(**kwargs) self.prefix = prefix self.suffix = suffix @@ -89,19 +108,27 @@ class FuzzyText(BaseFuzzyAttribute): self.chars = tuple(chars) # Unroll iterators def fuzz(self): - chars = [random.choice(self.chars) for _i in range(self.length)] + chars = [_random.choice(self.chars) for _i in range(self.length)] return self.prefix + ''.join(chars) + self.suffix class FuzzyChoice(BaseFuzzyAttribute): - """Handles fuzzy choice of an attribute.""" + """Handles fuzzy choice of an attribute. + + Args: + choices (iterable): An iterable yielding options; will only be unrolled + on the first call. + """ def __init__(self, choices, **kwargs): - self.choices = list(choices) + self.choices = None + self.choices_generator = choices super(FuzzyChoice, self).__init__(**kwargs) def fuzz(self): - return random.choice(self.choices) + if self.choices is None: + self.choices = list(self.choices_generator) + return _random.choice(self.choices) class FuzzyInteger(BaseFuzzyAttribute): @@ -119,7 +146,7 @@ class FuzzyInteger(BaseFuzzyAttribute): super(FuzzyInteger, self).__init__(**kwargs) def fuzz(self): - return random.randrange(self.low, self.high + 1, self.step) + return _random.randrange(self.low, self.high + 1, self.step) class FuzzyDecimal(BaseFuzzyAttribute): @@ -137,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) @@ -155,7 +182,7 @@ class FuzzyFloat(BaseFuzzyAttribute): super(FuzzyFloat, self).__init__(**kwargs) def fuzz(self): - return random.uniform(self.low, self.high) + return _random.uniform(self.low, self.high) class FuzzyDate(BaseFuzzyAttribute): @@ -175,7 +202,7 @@ class FuzzyDate(BaseFuzzyAttribute): self.end_date = end_date.toordinal() def fuzz(self): - return datetime.date.fromordinal(random.randint(self.start_date, self.end_date)) + return datetime.date.fromordinal(_random.randint(self.start_date, self.end_date)) class BaseFuzzyDateTime(BaseFuzzyAttribute): @@ -215,7 +242,7 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute): delta = self.end_dt - self.start_dt microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400)) - offset = random.randint(0, microseconds) + offset = _random.randint(0, microseconds) result = self.start_dt + datetime.timedelta(microseconds=offset) if self.force_year is not None: @@ -270,10 +297,10 @@ class FuzzyDateTime(BaseFuzzyDateTime): def _check_bounds(self, start_dt, end_dt): if start_dt.tzinfo is None: raise ValueError( - "FuzzyDateTime only handles aware datetimes, got start=%r" + "FuzzyDateTime requires timezone-aware datetimes, got start=%r" % start_dt) if end_dt.tzinfo is None: raise ValueError( - "FuzzyDateTime only handles aware datetimes, got end=%r" + "FuzzyDateTime requires timezone-aware datetimes, got end=%r" % end_dt) super(FuzzyDateTime, self)._check_bounds(start_dt, end_dt) diff --git a/factory/helpers.py b/factory/helpers.py index 19431df..60a4d75 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,7 +28,6 @@ import logging from . import base from . import declarations -from . import django @contextlib.contextmanager diff --git a/factory/mogo.py b/factory/mogo.py index 5541043..aa9f28b 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -37,10 +37,10 @@ class MogoFactory(base.Factory): @classmethod def _build(cls, model_class, *args, **kwargs): - return model_class.new(*args, **kwargs) + return model_class(*args, **kwargs) @classmethod def _create(cls, model_class, *args, **kwargs): - instance = model_class.new(*args, **kwargs) + instance = model_class(*args, **kwargs) instance.save() return instance diff --git a/factory/mongoengine.py b/factory/mongoengine.py index e3ab99c..f50b727 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/utils.py b/factory/utils.py index 276977a..15dba0a 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -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) @@ -110,15 +110,29 @@ def _safe_repr(obj): return obj_repr.decode('utf-8') -def log_pprint(args=(), kwargs=None): - kwargs = kwargs or {} - return ', '.join( - [_safe_repr(arg) for arg in args] + - [ - '%s=%s' % (key, _safe_repr(value)) - for key, value in kwargs.items() - ] - ) +class log_pprint(object): + """Helper for properly printing args / kwargs passed to an object. + + Since it is only used with factory.debug(), the computation is + performed lazily. + """ + __slots__ = ['args', 'kwargs'] + + def __init__(self, args=(), kwargs=None): + self.args = args + self.kwargs = kwargs or {} + + def __repr__(self): + return repr(str(self)) + + def __str__(self): + return ', '.join( + [_safe_repr(arg) for arg in self.args] + + [ + '%s=%s' % (key, _safe_repr(value)) + for key, value in self.kwargs.items() + ] + ) class ResetableIterator(object): |