From 0aee8a4454065e5e4d4d5a45ef47ebc5a5b86759 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Wed, 21 Mar 2018 17:07:14 -0500 Subject: [PATCH 01/17] updated django-faker to work with newer django/python --- .coveragerc | 3 +- .travis.yml | 23 +- README.rst | 59 +++-- django_faker/__init__.py | 70 ++---- django_faker/constraints.py | 63 ++++++ django_faker/guessers.py | 195 ++++++++++++++-- django_faker/populator.py | 189 +++++++--------- django_faker/templatetags/fakers.py | 74 +++--- django_faker/tests.py | 212 ------------------ .../__init__.py | 0 django_faker_tests/models.py | 121 ++++++++++ django_faker_tests/settings.py | 78 +++++++ .../templates/faker/preview.html | 0 django_faker_tests/tests/__init__.py | 0 django_faker_tests/tests/test_base.py | 165 ++++++++++++++ django_faker_tests/tests/test_example.py | 26 +++ django_faker_tests/tests/test_name_fields.py | 31 +++ django_faker_tests/tests/test_relational.py | 83 +++++++ {django_faker => django_faker_tests}/urls.py | 0 requirements.txt | 3 +- runtests.py | 79 +++---- setup.py | 10 +- 22 files changed, 988 insertions(+), 496 deletions(-) create mode 100644 django_faker/constraints.py delete mode 100644 django_faker/tests.py rename django_faker/models.py => django_faker_tests/__init__.py (100%) create mode 100644 django_faker_tests/models.py create mode 100644 django_faker_tests/settings.py rename {django_faker => django_faker_tests}/templates/faker/preview.html (100%) create mode 100644 django_faker_tests/tests/__init__.py create mode 100644 django_faker_tests/tests/test_base.py create mode 100644 django_faker_tests/tests/test_example.py create mode 100644 django_faker_tests/tests/test_name_fields.py create mode 100644 django_faker_tests/tests/test_relational.py rename {django_faker => django_faker_tests}/urls.py (100%) diff --git a/.coveragerc b/.coveragerc index b8a32b3..f212303 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,2 @@ [paths] -source = django_faker/ -omit = django_faker/tests.py +source = django_faker/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 64a66a4..1259f77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,8 @@ python: - "3.4" env: matrix: - - DJANGO=Django==1.4 - - DJANGO=Django==1.5 - - DJANGO=Django==1.6 - - DJANGO=Django==1.7 - - DJANGO=Django==1.8 + - DJANGO=Django==1.11 + - DJANGO=Django==2.0 install: - pip install $DJANGO --use-mirrors - pip install . --use-mirrors @@ -24,17 +21,21 @@ script: coverage run --source=django_faker setup.py test matrix: exclude: - python: "2.6" - env: DJANGO=Django==1.7 + env: DJANGO=Django==2.0 - python: "2.6" - env: DJANGO=Django==1.8 + env: DJANGO=Django==2.0 + - python: "2.7" + env: DJANGO=Django==2.0 + - python: "2.7" + env: DJANGO=Django==2.0 - python: "3.3" - env: DJANGO=Django==1.4 + env: DJANGO=Django==1.11 - python: "3.3" - env: DJANGO=Django==1.5 + env: DJANGO=Django==1.11 - python: "3.4" - env: DJANGO=Django==1.4 + env: DJANGO=Django==1.11 - python: "3.4" - env: DJANGO=Django==1.5 + env: DJANGO=Django==1.11 after_success: - coverage report - pip install --quiet python-coveralls diff --git a/README.rst b/README.rst index 145a985..2e8783f 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Django-faker ============ -*Django-faker* uses `fake-factory`_ package to generate test data for Django models and templates. +*Django-faker* uses `faker`_ package to generate test data for Django models and templates. |pypi| |unix_build| |windows_build| |coverage| |downloads| |license| @@ -19,7 +19,6 @@ Configuration In django application `settings.py`:: INSTALLED_APPS = ( - # ... 'django_faker', ) @@ -41,23 +40,23 @@ Here is an example showing how to populate 5 `Game` and 10 `Player` objects:: from django_faker import Faker # this Populator is only a function thats return a django_faker.populator.Populator instance # correctly initialized with a faker.generator.Generator instance, configured as above - populator = Faker.getPopulator() + populator = Faker.get_populator() from myapp.models import Game, Player - populator.addEntity(Game,5) - populator.addEntity(Player,10) + populator.add_entity(Game,5) + populator.add_entity(Player,10) insertedPks = populator.execute() The populator uses name and column type guessers to populate each column with relevant data. -For instance, Django-faker populates a column named `first_name` using the `firstName` formatter, and a column with -a `datetime` instance using the `dateTime`. +For instance, Django-faker populates a column named `first_name` using the `first_name` formatter, and a column with +a `datetime` instance using the `date_time`. The resulting entities are therefore coherent. If Django-faker misinterprets a column name, you can still specify a custom -function to be used for populating a particular column, using the third argument to `addEntity()`:: +function to be used for populating a particular column, using the third argument to `add_entity()`:: - populator.addEntity(Player, 10, { - 'score': lambda x: populator.generator.randomInt(0,1000), + populator.add_entity(Player, 10, { + 'score': lambda x: populator.generator.random_int(0,1000), 'nickname': lambda x: populator.generator.email(), }) populator.execute() @@ -75,12 +74,36 @@ In the previous example, the `Player` and `Game` models share a relationship. Si Faker is smart enough to relate the populated `Player` entities to one of populated `Game` entities. +Relational Fields +------------------------ + +Django-faker will attempt to populate relational fields in the following manner: +1. From model instances added through `add_entity()` +2. From pre-existing values in the db + +If there aren't available values and the field can't be null, an `AttributeError` is thrown + +**One-to-one fields** +The Populator keeps track of what values have been used already so it doesn't violate the one-to-one constraint + +**Many-to-many fields** +The Populator randomly selects between 1-n values to assign to the object + +**Foreign key fields** +The Populator randomly selects 1 value to assign + +**unique/unique_together constraints** +Currently, django-faker tries to populate the field(s) and then if a constraint is violated it tries again. +This happens up to 1000 times and then an InvalidConstraint exception is thrown. +Future iterations will hopefully pick values from a generated set of options to guarantee correctness. + + Template tags and filter ~~~~~~~~~~~~~~~~~~~~~~~~ Django-faker offers a useful template tags and filters for interact with `PyFaker`_:: - {% fake 'name' as myname %}{% fake 'dateTimeBetween' '-10d' as mydate %} + {% fake 'name' as myname %}{% fake 'date_time_between' '-10d' as mydate %} {{ myname|title }} - {{ mydate|date:"M Y" }} @@ -90,20 +113,20 @@ Django-faker offers a useful template tags and filters for interact with `PyFake - {% fake 'randomInt' 10 20 as times %} + {% fake 'random_int' 10 20 as times %} {% for i in 10|get_range %} - - + + {% if 'boolean'|fake:25 %} - + {% endif %}
- {% fakestr 'streetAddress' %} + {% fakestr 'street_address' %} {% fakestr 'city' %} {% fakestr 'postcode' %} {% fakestr 'state' %}
- + {% if 'boolean'|fake:25 %} {% fakestr 'bs' %} {% endif %} @@ -168,7 +191,7 @@ Changelog - Add django template tag and filter -.. _fake-factory: https://www.github.com/joke2k/faker/ +.. _faker: https://www.github.com/joke2k/faker/ .. |pypi| image:: https://img.shields.io/pypi/v/django-faker.svg?style=flat-square&label=version :target: https://pypi.python.org/pypi/django-faker diff --git a/django_faker/__init__.py b/django_faker/__init__.py index ed816d8..b5e5eeb 100644 --- a/django_faker/__init__.py +++ b/django_faker/__init__.py @@ -1,14 +1,11 @@ """ - -Django-faker uses python-faker to generate test data for Django models and templates. - +Django-faker uses a generator (eg faker) to generate test data for Django models and templates. """ +from faker import Faker as FakerGenerator +from .populator import Populator - -__version__ = '0.2.1' - -class Faker(object): +class DjangoFaker(object): instance = None populators = {} @@ -17,24 +14,22 @@ class Faker(object): @classmethod def __new__(cls, *args, **kwargs): if cls.instance is None: - cls.instance = super(Faker, cls).__new__(*args, **kwargs) + cls.instance = super(DjangoFaker, cls).__new__(*args, **kwargs) return cls.instance def __init__(self): -# assert False, "Cannot create a instance of Faker" pass - @staticmethod - def getCodename(locale=None, providers=None): + def get_codename(locale=None, providers=None): """ codename = locale[-Provider]* """ from django.conf import settings # language - locale = locale or getattr(settings,'FAKER_LOCALE', getattr(settings,'LANGUAGE_CODE', None)) + locale = locale or getattr(settings, 'FAKER_LOCALE', getattr(settings, 'LANGUAGE_CODE', None)) # providers - providers = providers or getattr(settings,'FAKER_PROVIDERS', None) + providers = providers or getattr(settings, 'FAKER_PROVIDERS', None) codename = locale or 'default' @@ -43,68 +38,49 @@ def getCodename(locale=None, providers=None): return codename - @classmethod - def getGenerator(cls, locale=None, providers=None, codename=None): + def get_generator(cls, locale=None, providers=None, codename=None): """ use a codename to cache generators """ - codename = codename or cls.getCodename(locale, providers) + codename = codename or cls.get_codename(locale, providers) if codename not in cls.generators: - from faker import Faker as FakerGenerator # initialize with faker.generator.Generator instance # and remember in cache - cls.generators[codename] = FakerGenerator( locale, providers ) - cls.generators[codename].seed( cls.generators[codename].randomInt() ) + cls.generators[codename] = FakerGenerator(locale, providers) + cls.generators[codename].seed(cls.generators[codename].random_int()) return cls.generators[codename] - - @classmethod - def getPopulator(cls, locale=None, providers=None): + def get_populator(cls, locale=None, providers=None): """ uses: - from django_faker import Faker - pop = Faker.getPopulator() + from django_faker import DjangoFaker + pop = DjangoFaker.get_populator() from myapp import models - pop.addEntity(models.MyModel, 10) - pop.addEntity(models.MyOtherModel, 10) + pop.add_entity(models.MyModel, 10) + pop.add_entity(models.MyOtherModel, 10) pop.execute() - pop = Faker.getPopulator('it_IT') + pop = Faker.get_populator('it_IT') - pop.addEntity(models.MyModel, 10) - pop.addEntity(models.MyOtherModel, 10) + pop.add_entity(models.MyModel, 10) + pop.add_entity(models.MyOtherModel, 10) pop.execute() """ - codename = cls.getCodename(locale, providers) + codename = cls.get_codename(locale, providers) if codename not in cls.populators: + generator = cls.generators.get(codename, None) or cls.get_generator(codename=codename) - generator = cls.generators.get(codename, None) or cls.getGenerator(codename=codename) - - from django_faker import populator - - cls.populators[codename] = populator.Populator( generator ) + cls.populators[codename] = Populator(generator) return cls.populators[codename] - -# if not cls.populator: -# cls.populator= populators.Populator( -# # initialize with faker.generator.Generator instance -# FakerGenerator( -# -# getattr(settings,'FAKER_LOCALE', getattr(settings,'LANGUAGE_CODE', locale)), -# -# getattr(settings,'FAKER_PROVIDERS', providers) -# ) -# ) - diff --git a/django_faker/constraints.py b/django_faker/constraints.py new file mode 100644 index 0000000..c43336c --- /dev/null +++ b/django_faker/constraints.py @@ -0,0 +1,63 @@ +# Credit for constraints goes to django-autofixture: +# https://github.com/gregmuellegger/django-autofixture/blob/master/autofixture/constraints.py +# Copyright (c) 2010, Gregor Müllegger +# All rights reserved. + +from django.db.models.fields import related + + +class InvalidConstraint(Exception): + def __init__(self, fields, *args, **kwargs): + self.fields = fields + super(InvalidConstraint, self).__init__(*args, **kwargs) + + +def _is_unique_field(field): + if not field.unique: + return False + if field.primary_key: + # Primary key fields should not generally be checked for unique constraints, except when the + # primary key is a OneToOne mapping to an external table not via table inheritance, in which + # case we don't want to create new objects which will overwrite existing objects. + return (isinstance(field, related.OneToOneField) and + not issubclass(field.model, field.remote_field.model)) + return True + + +def validate_unique_constraint(instance): + error_fields = [] + for field in instance._meta.fields: # pylint: disable=protected-access + if _is_unique_field(field): + value = getattr(instance, field.name) + + # If the value is none and the field allows nulls, skip it + if value is None and field.null: + continue + + check = {field.name: value} + + if instance._meta.default_manager.filter(**check).exists(): # pylint: disable=protected-access + error_fields.append(field) + if error_fields: + raise InvalidConstraint(error_fields) + + +def validate_unique_together_constraint(instance): + if not instance._meta.unique_together: # pylint: disable=protected-access + return + error_fields = [] + for unique_fields in instance._meta.unique_together: # pylint: disable=protected-access + check = {} + for field_name in unique_fields: + if not instance._meta.get_field(field_name).primary_key: # pylint: disable=protected-access + check[field_name] = getattr(instance, field_name) + if all(e is None for e in check.values()): + continue + + if instance._meta.default_manager.filter(**check).exists(): # pylint: disable=protected-access + error_fields.extend([ + instance._meta.get_field(field_name) # pylint: disable=protected-access + for field_name in unique_fields + ]) + if error_fields: + raise InvalidConstraint(error_fields) diff --git a/django_faker/guessers.py b/django_faker/guessers.py index d3db451..182cfd2 100644 --- a/django_faker/guessers.py +++ b/django_faker/guessers.py @@ -1,33 +1,186 @@ +import random import re +import pytz +from django.db.models import ForeignKey, ManyToManyField, OneToOneField, ImageField, FileField, \ + PositiveSmallIntegerField, BigIntegerField, SmallIntegerField, PositiveIntegerField, IntegerField, DecimalField, \ + FloatField, SlugField, URLField, EmailField, TextField, UUIDField, CharField, BinaryField, BooleanField, \ + NullBooleanField, DateField, DateTimeField, DurationField, TimeField, FilePathField, GenericIPAddressField +from constraints import InvalidConstraint -class Name(object): + +class FieldGuesser(object): + """ + Used to determine how to handle a given field + """ def __init__(self, generator): """ :param generator Generator """ self.generator = generator - def guessFormat(self, name): + def guess_format(self, field): """ - :param name: - :type name: str + :param field: + :type field: Django Model Field + Field Guessers attempt to return: + b. a function to generate a value given a set of inserted objects """ + raise NotImplementedError('Subclass Guesser and implement this') + + +class NameGuesser(FieldGuesser): + def guess_format(self, field): # pylint: disable=too-many-return-statements,too-many-branches + # Don't try to deal with relational fields or fields with choices + if isinstance(field, (ForeignKey, ManyToManyField, OneToOneField)) or field.choices: + return + generator = self.generator + + name = field.name name = name.lower() + if re.findall(r'^is[_A-Z]', name): + return lambda x: generator.boolean() + if re.findall(r'(_a|A)t$', name): + return lambda x: generator.date_time(tzinfo=pytz.UTC) + if name in ('first_name', 'firstname'): + return lambda x: generator.first_name() + if name in ('last_name', 'lastname'): + return lambda x: generator.last_name() + if name == 'name': + return lambda x: generator.word() + if name in ('username', 'login', 'nickname'): + return lambda x: generator.user_name() + if name in ('email', 'email_address'): + return lambda x: generator.email() + if name in ('phone_number', 'phonenumber', 'phone'): + return lambda x: generator.phone_number() + if name == 'address': + return lambda x: generator.address() + if name == 'city': + return lambda x: generator.city() + if name in ('streetaddress', 'street_address'): + return lambda x: generator.street_address() + if name in ('postcode', 'zipcode'): + return lambda x: generator.postcode() + if name == 'state': + return lambda x: generator.state_abbr() + if name == 'country': + return lambda x: generator.country() + if name == 'title': + return lambda x: generator.sentence() + if name in ('body', 'summary', 'description'): + return lambda x: generator.paragraph() + + +class RelationGuesser(FieldGuesser): + def guess_format(self, field): + if isinstance(field, (ForeignKey, ManyToManyField, OneToOneField)): + return relation_wrapper(field) + + +def relation_wrapper(local_field): + def build_relation(inserted): + related_model = local_field.remote_field.model + # Try to choose from the recently created + if related_model in inserted and inserted[related_model]: + choices = inserted[related_model] + if isinstance(local_field, OneToOneField): + # Only allow unused related model primary keys + unused = { + local_field.related_query_name(): None + } + allowed = related_model.objects.filter(**unused) + choices = [obj.pk for obj in allowed if obj.pk in inserted[related_model]] + if choices: + if isinstance(local_field, ManyToManyField): + num = random.randint(1, len(choices)) + random.shuffle(choices) + return related_model.objects.filter(pk__in=choices[0:num]) + return related_model.objects.get(pk=random.choice(choices)) + + # Try to choose from already created objects + choices = related_model.objects.all() + if isinstance(local_field, OneToOneField): + unused = { + local_field.related_query_name(): None + } + choices = related_model.objects.filter(**unused) + choices = choices.order_by('?') + if not choices: + if local_field.null or local_field.blank: + return None + raise InvalidConstraint( + 'Relation "%s.%s" with "%s" cannot be null. Check order of addEntity list and ' + 'that there are enough objects for one-to-one relations' % ( + local_field.model.__name__, local_field.name, related_model.__name__, + )) + if isinstance(local_field, ManyToManyField): + num = random.randint(1, len(choices)) + return choices[:num] + return choices[0] + + return build_relation + + +class FieldTypeGuesser(FieldGuesser): + def guess_format(self, field): # pylint: disable=too-many-return-statements,too-many-branches generator = self.generator - if re.findall(r'^is[_A-Z]', name): return lambda x:generator.boolean() - if re.findall(r'(_a|A)t$', name): return lambda x:generator.dateTime() - - if name in ('first_name','firstname'): return lambda x: generator.firstName() - if name in ('last_name','lastname'): return lambda x: generator.lastName() - - if name in ('username','login','nickname'): return lambda x:generator.userName() - if name in ('email','email_address'): return lambda x:generator.email() - if name in ('phone_number','phonenumber','phone'): return lambda x:generator.phoneNumber() - if name == 'address' : return lambda x:generator.address() - if name == 'city' : return lambda x: generator.city() - if name == 'streetaddress' : return lambda x: generator.streetaddress() - if name in ('postcode','zipcode'): return lambda x: generator.postcode() - if name == 'state' : return lambda x: generator.state() - if name == 'country' : return lambda x: generator.country() - if name == 'title' : return lambda x: generator.sentence() - if name in ('body','summary', 'description'): return lambda x: generator.text() \ No newline at end of file + if field.choices: + return lambda x: generator.random_element(field.choices)[0] + + if isinstance(field, PositiveSmallIntegerField): + return lambda x: generator.random_int(0, 32767) + if isinstance(field, BigIntegerField): + return lambda x: generator.random_int(-9223372036854775808, 9223372036854775807) + if isinstance(field, SmallIntegerField): + return lambda x: generator.random_int(0, 65535) + if isinstance(field, PositiveIntegerField): + return lambda x: generator.random_int(0, 2147483647) + if isinstance(field, IntegerField): + return lambda x: generator.random_int(-2147483648, 2147483647) + + if isinstance(field, DecimalField): + return lambda x: generator.pydecimal(left_digits=field.max_digits - field.decimal_places, + right_digits=field.decimal_places) + if isinstance(field, FloatField): + return lambda x: generator.pyfloat(left_digits=field.max_digits - field.decimal_places, + right_digits=field.decimal_places) + + if isinstance(field, SlugField): + return lambda x: generator.slug() + if isinstance(field, URLField): + return lambda x: generator.uri() + if isinstance(field, EmailField): + return lambda x: generator.email() + if isinstance(field, TextField): + return lambda x: generator.paragraph() + if isinstance(field, UUIDField): + return lambda x: generator.uuid4() + if isinstance(field, CharField): + return lambda x: generator.text(field.max_length) if field.max_length >= 5 else generator.word() + + if isinstance(field, BinaryField): + return lambda x: generator.binary(length=1048576) # limit for performance + if isinstance(field, BooleanField): + return lambda x: generator.boolean() + if isinstance(field, NullBooleanField): + return lambda x: generator.null_boolean() + + if isinstance(field, DateField): + return lambda x: generator.date() + if isinstance(field, DateTimeField): + return lambda x: generator.date_time(tzinfo=pytz.UTC) + if isinstance(field, DurationField): + return lambda x: generator.time_delta() + if isinstance(field, TimeField): + return lambda x: generator.time() + + if isinstance(field, ImageField): + return lambda x: None + if isinstance(field, FileField): + return lambda x: None + if isinstance(field, FilePathField): + return lambda x: "/" + + if isinstance(field, GenericIPAddressField): + protocolIp = generator.random_elements(['ipv4', 'ipv6']) + return lambda x: getattr(generator, protocolIp)() diff --git a/django_faker/populator.py b/django_faker/populator.py index 623ba55..e7025ca 100644 --- a/django_faker/populator.py +++ b/django_faker/populator.py @@ -1,46 +1,8 @@ -import random -from django_faker.guessers import Name -from django.db.models.fields import * -from django.db.models import ForeignKey, ManyToManyField, OneToOneField, ImageField - - -class FieldTypeGuesser(object): - - def __init__(self, generator): - """ - :param generator: Generator - """ - self.generator = generator - - def guessFormat(self, field): - - generator = self.generator - if isinstance(field, BooleanField): return lambda x: generator.boolean() - if isinstance(field, NullBooleanField): return lambda x: generator.nullBoolean() - if isinstance(field, DecimalField): return lambda x: generator.pydecimal(rightDigits=field.decimal_places) - if isinstance(field, SmallIntegerField): return lambda x: generator.randomInt(0,65535) - if isinstance(field, IntegerField): return lambda x: generator.randomInt(0,4294967295) - if isinstance(field, BigIntegerField): return lambda x: generator.randomInt(0,18446744073709551615) - if isinstance(field, FloatField): return lambda x: generator.pyfloat() - if isinstance(field, CharField): - if field.choices: - return lambda x: generator.randomElement(field.choices)[0] - return lambda x: generator.text(field.max_length) if field.max_length >= 5 else generator.word() - if isinstance(field, TextField): return lambda x: generator.text() - - if isinstance(field, DateTimeField): return lambda x: generator.dateTime() - if isinstance(field, DateField): return lambda x: generator.date() - if isinstance(field, TimeField): return lambda x: generator.time() - - if isinstance(field, URLField): return lambda x: generator.uri() - if isinstance(field, SlugField): return lambda x: generator.slug() - if isinstance(field, IPAddressField): - protocolIp = generator.randomElements(['ipv4','ipv6']) - return lambda x: getattr(generator,protocolIp)() - if isinstance(field, EmailField): return lambda x: generator.email() - if isinstance(field, ImageField): return lambda x: None - - raise AttributeError(field) +from django.db.models import AutoField, ManyToManyField +from django.db.models.fields.reverse_related import ForeignObjectRel +from .constraints import validate_unique_constraint, validate_unique_together_constraint, \ + InvalidConstraint +from .guessers import NameGuesser, RelationGuesser, FieldTypeGuesser class ModelPopulator(object): @@ -49,68 +11,76 @@ def __init__(self, model): :param model: Generator """ self.model = model - self.fieldFormatters = {} - - def guessFieldFormatters(self, generator): + self.field_formatters = {} + def guess_field_formatters(self, generator): + """ + Figure out how to populate the model's fields given a certain generator + :param generator: should be able to create lots of types of fake values + :return: a map of field name -> value generation function + """ formatters = {} model = self.model - nameGuesser = Name(generator) - fieldTypeGuesser = FieldTypeGuesser(generator) - - for field in model._meta.fields: - # yield field.name, getattr(self, field.name) - fieldName = field.name - if isinstance(field, (ForeignKey,ManyToManyField,OneToOneField)): - relatedModel = field.rel.to - - def build_relation(inserted): - if relatedModel in inserted and inserted[relatedModel]: - return relatedModel.objects.get(pk=random.choice(inserted[relatedModel])) - if not field.null: - try : - # try to retrieve random object from relatedModel - relatedModel.objects.order_by('?')[0] - except IndexError: - raise Exception('Relation "%s.%s" with "%s" cannot be null, check order of addEntity list' % ( - field.model.__name__, field.name, relatedModel.__name__, - )) - return None - - formatters[fieldName] = build_relation - continue - if isinstance(field, AutoField): - continue + # field guesser order matters - whichever matches first has precedence + field_guessers = [ + NameGuesser(generator), + RelationGuesser(generator), + FieldTypeGuesser(generator) + ] - formatter = nameGuesser.guessFormat(fieldName) - if formatter: - formatters[fieldName] = formatter + for field in model._meta.get_fields(): # pylint: disable=protected-access + # Skip auto fields because they'll be generated on insertion + if isinstance(field, (AutoField, ForeignObjectRel)): continue - formatter = fieldTypeGuesser.guessFormat(field) - if formatter: - formatters[fieldName] = formatter - continue + # attempt to set + for guesser in field_guessers: + formatter = guesser.guess_format(field) + if formatter: + formatters[field.name] = formatter + break - return formatters + if not formatters[field.name]: + # No formatter set, something weird must've happened + raise AttributeError(field) - def execute(self, using, insertedEntities): + return formatters + def execute(self, using, inserted_entities): obj = self.model() - - for field, format in self.fieldFormatters.items(): - if format: - value = format(insertedEntities) if hasattr(format,'__call__') else format - setattr(obj, field, value) - - obj.save(using=using) - - return obj.pk + # try up to 1000 times to create an acceptable object + # Ideally, we'd generate an acceptable list of unique fields/tuples of unique together fields + # But that is harder when dealing with all sorts of generic fields, relational fields, etc + # A good future improvement + count = 0 + while count < 1000: + many_fields = {} + for field, fmt in self.field_formatters.items(): + if fmt: + value = fmt(inserted_entities) if hasattr(fmt, '__call__') else fmt + if isinstance(self.model._meta.get_field(field), # pylint: disable=protected-access + ManyToManyField): + if value: + many_fields[field] = value + else: + setattr(obj, field, value) + try: + validate_unique_constraint(obj) + validate_unique_together_constraint(obj) + obj.save(using=using) + # set many to many once id is created + for field, val in many_fields.items(): + getattr(obj, field).set(val) + obj.save(using=using) + return obj.pk + except InvalidConstraint as e: + if count == 1000: + raise e + count += 1 class Populator(object): - def __init__(self, generator): """ :param generator: Generator @@ -120,8 +90,7 @@ def __init__(self, generator): self.quantities = {} self.orders = [] - - def addEntity(self, model, number, customFieldFormatters=None): + def add_entity(self, model, number, custom_field_formatters=None): """ Add an order for the generation of $number records for $entity. @@ -129,15 +98,15 @@ def addEntity(self, model, number, customFieldFormatters=None): :type model: Model :param number: int The number of entities to populate :type number: integer - :param customFieldFormatters: optional dict with field as key and callable as value - :type customFieldFormatters: dict or None + :param custom_field_formatters: optional dict with field as key and callable as value + :type custom_field_formatters: dict or None """ if not isinstance(model, ModelPopulator): model = ModelPopulator(model) - model.fieldFormatters = model.guessFieldFormatters( self.generator ) - if customFieldFormatters: - model.fieldFormatters.update(customFieldFormatters) + model.field_formatters = model.guess_field_formatters(self.generator) + if custom_field_formatters: + model.field_formatters.update(custom_field_formatters) klass = model.model self.entities[klass] = model @@ -152,19 +121,19 @@ def execute(self, using=None): :rtype: A list of the inserted PKs """ if not using: - using = self.getConnection() + using = self.get_connection() - insertedEntities = {} + inserted_entities = {} for klass in self.orders: number = self.quantities[klass] - if klass not in insertedEntities: - insertedEntities[klass] = [] - for i in range(0,number): - insertedEntities[klass].append( self.entities[klass].execute(using, insertedEntities) ) + if klass not in inserted_entities: + inserted_entities[klass] = [] + for _ in range(0, number): + inserted_entities[klass].append(self.entities[klass].execute(using, inserted_entities)) - return insertedEntities + return inserted_entities - def getConnection(self): + def get_connection(self): """ use the first connection available :rtype: Connection @@ -175,7 +144,9 @@ def getConnection(self): raise AttributeError('No class found from entities. Did you add entities to the Populator ?') klass = list(klass)[0] - return klass.objects._db - - + return klass.objects._db # pylint: disable=protected-access + def clear(self): + self.entities = {} + self.quantities = {} + self.orders = [] diff --git a/django_faker/templatetags/fakers.py b/django_faker/templatetags/fakers.py index 139e6b2..2935c18 100644 --- a/django_faker/templatetags/fakers.py +++ b/django_faker/templatetags/fakers.py @@ -1,10 +1,12 @@ from inspect import getargspec +from django.template.base import TemplateSyntaxError from django import template -from django.template.base import TagHelperNode, TemplateSyntaxError, parse_bits +from django.template.library import parse_bits register = template.Library() -from django_faker import Faker +from django_faker import DjangoFaker + def optional_assignment_tag(func=None, takes_context=None, name=None): """ @@ -14,7 +16,7 @@ def optional_assignment_tag(func=None, takes_context=None, name=None): def dec(func): params, varargs, varkw, defaults = getargspec(func) - class AssignmentNode(TagHelperNode): + class AssignmentNode(template.Node): def __init__(self, takes_context, args, kwargs, target_var=None): super(AssignmentNode, self).__init__(takes_context, args, kwargs) self.target_var = target_var @@ -54,8 +56,9 @@ def compile_func(parser, token): else: raise TemplateSyntaxError("Invalid arguments provided to assignment_tag") + @optional_assignment_tag(name='fake') -def do_fake( formatter, *args, **kwargs ): +def do_fake(formatter, *args, **kwargs): """ call a faker format uses: @@ -67,33 +70,36 @@ def do_fake( formatter, *args, **kwargs ): {% fake 'name' %} """ - return Faker.getGenerator().format( formatter, *args, **kwargs ) - - -#@register.assignment_tag(name='fake') -#def fake_tag_as( formatter, *args, **kwargs ): -# """ -# call a faker format -# uses: -# -# {% fake "formatterName" *args **kwargs as myvar %} -# -# """ -# return Faker.getGenerator().format( formatter, *args, **kwargs ) - -#@register.simple_tag(name='fakestr') -#def fake_tag_str( formatter, *args, **kwargs ): -# """ -# call a faker format -# uses: -# -# {% fakestr "formatterName" *values **kwargs %} -# """ -# if formatter == 'dateTimeThisCentury' : print args, kwargs -# return Faker.getGenerator().format( formatter, *args, **kwargs ) + return DjangoFaker.get_generator().format(formatter, *args, **kwargs) + + +@register.assignment_tag(name='fake') +def fake_tag_as(formatter, *args, **kwargs): + """ + call a faker format + uses: + + {% fake "formatterName" *args **kwargs as myvar %} + + """ + return DjangoFaker.get_generator().format(formatter, *args, **kwargs) + + +@register.simple_tag(name='fakestr') +def fake_tag_str(formatter, *args, **kwargs): + """ + call a faker format + uses: + + {% fakestr "formatterName" *values **kwargs %} + """ + if formatter == 'dateTimeThisCentury': + print args, kwargs + return DjangoFaker.get_generator().format(formatter, *args, **kwargs) + @register.filter(name='fake') -def do_fake_filter( formatter, arg=None ): +def do_fake_filter(formatter, arg=None): """ call a faker format uses: @@ -105,11 +111,11 @@ def do_fake_filter( formatter, arg=None ): """ args = [] if not arg is None: args.append(arg) - return Faker.getGenerator().format( formatter, *args ) + return DjangoFaker.get_generator().format(formatter, *args) @register.filter(name='or_fake') -def do_or_fake_filter( value, formatter ): +def do_or_fake_filter(value, formatter): """ call a faker if value is None uses: @@ -118,12 +124,12 @@ def do_or_fake_filter( value, formatter ): """ if not value: - value = Faker.getGenerator().format( formatter ) + value = DjangoFaker.get_generator().format(formatter) return value @register.filter -def get_range( value ): +def get_range(value): """ http://djangosnippets.org/snippets/1357/ @@ -143,4 +149,4 @@ def get_range( value ): Instead of 3 one may use the variable set in the views """ - return range( value ) \ No newline at end of file + return range(value) diff --git a/django_faker/tests.py b/django_faker/tests.py deleted file mode 100644 index f7f9f00..0000000 --- a/django_faker/tests.py +++ /dev/null @@ -1,212 +0,0 @@ -from faker import Faker -from django_faker.populator import Populator -from django_faker import Faker as DjangoFaker - -from django.db import models -from django.utils import unittest -from django.template import Context, TemplateSyntaxError -from django.template import Template - -fake = Faker() - -class Game(models.Model): - - title= models.CharField(max_length=200) - slug= models.SlugField(max_length=200) - description= models.TextField() - created_at= models.DateTimeField() - updated_date= models.DateField() - updated_time= models.TimeField() - active= models.BooleanField() - max_score= models.BigIntegerField() - - -class Player(models.Model): - - nickname= models.CharField(max_length=100) - score= models.BigIntegerField() - last_login_at= models.DateTimeField() - - game= models.ForeignKey(Game) - - -class Action(models.Model): - - ACTION_FIRE='fire' - ACTION_MOVE='move' - ACTION_STOP='stop' - - - ACTIONS = ( - (ACTION_FIRE,'Fire'), - (ACTION_MOVE,'Move'), - (ACTION_STOP,'Stop'), - ) - - name= models.CharField(max_length=4, choices=ACTIONS) - executed_at= models.DateTimeField() - - actor= models.ForeignKey(Player,related_name='actions', null=True) - target= models.ForeignKey(Player, related_name='enemy_actions+', null=True) - - -class PopulatorTestCase(unittest.TestCase): - - def testPopulation(self): - - generator = fake - populator = Populator(generator) - populator.addEntity( Game, 10 ) - self.assertEqual(len(populator.execute()[Game]), 10) - - def testGuesser(self): - - generator = fake - - def title_fake(arg): - title_fake.count += 1 - name = generator.company() - return name - title_fake.count = 0 - - populator = Populator(generator) - populator.addEntity( Game, 10, { - 'title': title_fake - } ) - self.assertEqual(len(populator.execute()[Game]), title_fake.count) - - def testFormatter(self): - - generator = fake - - populator = Populator( generator ) - - populator.addEntity(Game,5) - populator.addEntity(Player, 10, { - 'score': lambda x: fake.randomInt(0,1000), - 'nickname': lambda x: fake.email() - }) - populator.addEntity(Action,30) - - insertedPks = populator.execute() - - self.assertTrue( len(insertedPks[Game]) == 5 ) - self.assertTrue( len(insertedPks[Player]) == 10 ) - - self.assertTrue( any([0 <= p.score <= 1000 and '@' in p.nickname for p in Player.objects.all() ]) ) - - -class TemplateTagsTestCase(unittest.TestCase): - - @staticmethod - def render( template, context=None ): - t = Template("{% load fakers %}"+template) - c = Context(context or {}) - text= t.render(c) - return text - - # do_fake: fake - def testSimpleFakeTag(self): - self.assertNotEqual(self.render("{% fake 'name' as myname %}{{ myname }}"),"") - - def testSimpleFakeTagWithArguments(self): - self.assertNotEqual(self.render("{% fake 'dateTimeBetween' '-10d' as mydate %}{{ mydate }}"),"") - - def testSimpleFakeTagFormatterNotFoundRaisesException(self): - with self.assertRaises(AttributeError): - self.render("{% fake 'notFoundedFake' as foo %}") - - def testSimpleFakeTagOptionalAssignment(self): - self.assertNotEqual(self.render("{% fake 'name' %}"),"") - self.assertEqual(len(self.render("{% fake 'md5' %}")),32) - - # do_fake_filter: fake - def testFakeFilterTag(self): - self.assertIn(self.render("{{ 'randomElement'|fake:'testString' }}"),'testString') - - def testFakeFilterWithValueFromContext(self): - mylist = [100,200,300] - rendered = self.render("{{ 'randomElement'|fake:mylist }}", {'mylist': mylist}) - self.assertIn(rendered, [unicode(el) for el in mylist]) - - def testFakeFilterFormatterNotFoundRaisesException(self): - with self.assertRaises(AttributeError): - self.render("{{ 'notFoundedFake'|fake:mylist }}", {'mylist': [100,200,300]}) - - def testFakeFilterAsIfCondition(self): - self.assertEqual(self.render("{% if 'boolean'|fake:100 %}True forever{% endif %}"), "True forever") - self.assertEqual(self.render("{% if 'boolean'|fake:0 %}{% else %}False forever{% endif %}"), "False forever") - - def testFakeFilterAsForInRange(self): - times = 10 - rendered = self.render("{% for word in 'words'|fake:times %}{{ word }}\n{% endfor %}",{'times':times}) - words = [word for word in rendered.split('\n') if word.strip()] - self.assertEqual( len(words) , times) - - # do_or_fake_filter: or_fake - def testOrFakeFilterTag(self): - self.assertEqual(len(self.render("{{ unknown_var|or_fake:'md5' }}")), 32) - - - def testFullXmlContact(self): - self.assertTrue(self.render(""" - - {% fake 'randomInt' 10 20 as times %} - {% for i in 10|get_range %} - - - {% if 'boolean'|fake:25 %} - - {% endif %} -
- {% fake 'streetAddress' %} - {% fake 'city' %} - {% fake 'postcode' %} - {% fake 'state' %} -
- - {% if 'boolean'|fake:25 %} - {% fake 'bs' %} - {% endif %} - {% if 'boolean'|fake:33 %} - - {% endif %} - - {% if 'boolean'|fake:15 %} -
- -
- {% endif %} -
- {% endfor %} -
-""")) - - -class APIDjangoFakerTestCase(unittest.TestCase): - - def testDjangoFakerSingleton(self): - - self.assertEqual( DjangoFaker() , DjangoFaker() ) - self.assertIs( DjangoFaker() , DjangoFaker() ) - - def testFakerCacheGenerator(self): - - self.assertEqual( DjangoFaker().getGenerator(), DjangoFaker().getGenerator() ) - self.assertIs( DjangoFaker().getGenerator(), DjangoFaker().getGenerator() ) - self.assertIs( DjangoFaker().getGenerator(codename='default'), DjangoFaker().getGenerator(codename='default') ) - - self.assertEqual( DjangoFaker().getGenerator(locale='it_IT'), DjangoFaker().getGenerator(locale='it_IT') ) - self.assertIs( DjangoFaker().getGenerator(locale='it_IT'), DjangoFaker().getGenerator(locale='it_IT') ) - - def testFakerCachePopulator(self): - - self.assertEqual( DjangoFaker().getPopulator(), DjangoFaker().getPopulator() ) - self.assertIs( DjangoFaker().getPopulator(), DjangoFaker().getPopulator() ) - self.assertIs( DjangoFaker().getPopulator().generator, DjangoFaker().getPopulator().generator ) - - self.assertEqual( DjangoFaker().getPopulator(locale='it_IT'), DjangoFaker().getPopulator(locale='it_IT') ) - self.assertIs( DjangoFaker().getPopulator(locale='it_IT'), DjangoFaker().getPopulator(locale='it_IT') ) - \ No newline at end of file diff --git a/django_faker/models.py b/django_faker_tests/__init__.py similarity index 100% rename from django_faker/models.py rename to django_faker_tests/__init__.py diff --git a/django_faker_tests/models.py b/django_faker_tests/models.py new file mode 100644 index 0000000..1f83845 --- /dev/null +++ b/django_faker_tests/models.py @@ -0,0 +1,121 @@ +from django.db import models + + +# 'Realistic' Example Models + +class Game(models.Model): + title = models.CharField(max_length=200) + slug = models.SlugField(max_length=200) + description = models.TextField() + created_at = models.DateTimeField() + updated_date = models.DateField() + updated_time = models.TimeField() + active = models.BooleanField() + max_score = models.BigIntegerField() + + +class Player(models.Model): + nickname = models.CharField(max_length=100) + score = models.BigIntegerField() + last_login_at = models.DateTimeField() + + game = models.ForeignKey(Game, on_delete=models.CASCADE) + + +class Action(models.Model): + ACTION_FIRE = 'fire' + ACTION_MOVE = 'move' + ACTION_STOP = 'stop' + + ACTIONS = ( + (ACTION_FIRE, 'Fire'), + (ACTION_MOVE, 'Move'), + (ACTION_STOP, 'Stop'), + ) + + name = models.CharField(max_length=4, choices=ACTIONS) + executed_at = models.DateTimeField() + + actor = models.ForeignKey(Player, related_name='actions', null=True, on_delete=models.CASCADE) + target = models.ForeignKey(Player, related_name='enemy_actions+', null=True, on_delete=models.CASCADE) + + +class Assessment(models.Model): + slug = models.SlugField(max_length=2, unique=True) + + +class Question(models.Model): + title = models.TextField(max_length=255) + order = models.SmallIntegerField() + important = models.BooleanField() + + +class AnswerSubmission(models.Model): + file = models.BinaryField(max_length=2000) + first_name = models.TextField(max_length=255) + last_name = models.TextField(max_length=255) + + +class Company(models.Model): + name = models.TextField(max_length=255) + address = models.TextField(max_length=4000) + city = models.TextField(max_length=255) + state = models.TextField(max_length=255) + + +class Answer(models.Model): + assessment = models.ForeignKey(Assessment, related_name="answers", on_delete=models.CASCADE) + question = models.ForeignKey(Question, related_name="answers", on_delete=models.CASCADE) + submission = models.OneToOneField(AnswerSubmission, related_name="answer", on_delete=models.CASCADE) + company = models.ManyToManyField(Company, related_name="answers") + + class Meta: + unique_together = (("assessment", "question"),) + +# Relational Models + + +class ToOne(models.Model): + thing = models.TextField() + + +class One(models.Model): + to_one = models.OneToOneField(ToOne, related_name="one", on_delete=models.CASCADE) + + +class ToMany(models.Model): + thing = models.TextField() + + +class Many(models.Model): + to_many = models.ManyToManyField(ToMany, related_name="one") + + +class Key(models.Model): + thing = models.TextField() + + +class Foreign(models.Model): + key = models.ForeignKey(Key, related_name="foreigns", null=False, on_delete=models.CASCADE) + key_null = models.ForeignKey(ToOne, related_name="foreigns", null=True, on_delete=models.CASCADE) + + +# Named fields + +class NamedFields(models.Model): + is_boolean = models.TextField() + created_at = models.TextField() + first_name = models.TextField() + last_name = models.TextField() + name = models.TextField() + login = models.TextField() + email = models.TextField() + phone_number = models.TextField() + address = models.TextField() + city = models.TextField() + street_address = models.TextField() + zipcode = models.TextField() + state = models.TextField() + country = models.TextField() + title = models.TextField() + description = models.TextField() diff --git a/django_faker_tests/settings.py b/django_faker_tests/settings.py new file mode 100644 index 0000000..ebaab02 --- /dev/null +++ b/django_faker_tests/settings.py @@ -0,0 +1,78 @@ +import django +import os +import warnings + + +warnings.simplefilter('always') + +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) + +DATABASES = { + 'default': { + 'NAME': os.path.join(PROJECT_ROOT, 'db.sqlite'), + 'ENGINE': 'django.db.backends.sqlite3', + } +} + +SITE_ID = 1 + +# Set in order to catch timezone aware vs unaware comparisons +USE_TZ = True + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True +USE_L10N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media') + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '0' + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.admin', + + 'django_faker', + 'django_faker_tests', + 'django_faker_tests.tests', +) + +MIDDLEWARE_CLASSES = () + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + # 'APP_DIRS': True, # can't use both APP_DIRS and loaders + 'OPTIONS': { + # 1.8 version - django.template.context_processors + 'context_processors': ("django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.template.context_processors.request", + ), + 'loaders': ( + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ), + }, + }, +] + +if django.VERSION < (1, 6): + TEST_RUNNER = 'discover_runner.DiscoverRunner' +else: + TEST_RUNNER = 'django.test.runner.DiscoverRunner' \ No newline at end of file diff --git a/django_faker/templates/faker/preview.html b/django_faker_tests/templates/faker/preview.html similarity index 100% rename from django_faker/templates/faker/preview.html rename to django_faker_tests/templates/faker/preview.html diff --git a/django_faker_tests/tests/__init__.py b/django_faker_tests/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_faker_tests/tests/test_base.py b/django_faker_tests/tests/test_base.py new file mode 100644 index 0000000..52bb795 --- /dev/null +++ b/django_faker_tests/tests/test_base.py @@ -0,0 +1,165 @@ +from faker import Faker +from django_faker.populator import Populator +from django_faker import DjangoFaker + +from django import test +from django.template import Context +from django.template import Template +from ..models import Game, Player, Action + +fake = Faker() + + +class PopulatorTestCase(test.TestCase): + + def testPopulation(self): + generator = fake + populator = Populator(generator) + populator.add_entity(Game, 10) + self.assertEqual(len(populator.execute()[Game]), 10) + + def testGuesser(self): + generator = fake + + def title_fake(arg): + title_fake.count += 1 + name = generator.company() + return name + + title_fake.count = 0 + + populator = Populator(generator) + populator.add_entity(Game, 10, { + 'title': title_fake + }) + self.assertEqual(len(populator.execute()[Game]), title_fake.count) + + def testFormatter(self): + generator = fake + + populator = Populator(generator) + + populator.add_entity(Game, 5) + populator.add_entity(Player, 10, { + 'score': lambda x: fake.random_int(0, 1000), + 'nickname': lambda x: fake.email() + }) + populator.add_entity(Action, 30) + + insertedPks = populator.execute() + + self.assertTrue(len(insertedPks[Game]) == 5) + self.assertTrue(len(insertedPks[Player]) == 10) + + self.assertTrue(any([0 <= p.score <= 1000 and '@' in p.nickname for p in Player.objects.all()])) + + +class TemplateTagsTestCase(test.TestCase): + + @staticmethod + def render(template, context=None): + t = Template("{% load fakers %}" + template) + c = Context(context or {}) + text = t.render(c) + return text + + # do_fake: fake + def testSimpleFakeTag(self): + self.assertNotEqual(self.render("{% fake 'name' as myname %}{{ myname }}"), "") + + def testSimpleFakeTagWithArguments(self): + self.assertNotEqual(self.render("{% fake 'date_time_between' '-10d' as mydate %}{{ mydate }}"), "") + + def testSimpleFakeTagFormatterNotFoundRaisesException(self): + with self.assertRaises(AttributeError): + self.render("{% fake 'notFoundedFake' as foo %}") + + def testSimpleFakeTagOptionalAssignment(self): + self.assertNotEqual(self.render("{% fake 'name' %}"), "") + self.assertEqual(len(self.render("{% fake 'md5' %}")), 32) + + # do_fake_filter: fake + def testFakeFilterTag(self): + self.assertIn(self.render("{{ 'random_element'|fake:'test_string' }}"), 'test_string') + + def testFakeFilterWithValueFromContext(self): + mylist = [100, 200, 300] + rendered = self.render("{{ 'random_element'|fake:mylist }}", {'mylist': mylist}) + self.assertIn(rendered, [unicode(el) for el in mylist]) + + def testFakeFilterFormatterNotFoundRaisesException(self): + with self.assertRaises(AttributeError): + self.render("{{ 'notFoundedFake'|fake:mylist }}", {'mylist': [100, 200, 300]}) + + def testFakeFilterAsIfCondition(self): + self.assertEqual(self.render("{% if 'boolean'|fake:100 %}True forever{% endif %}"), "True forever") + self.assertEqual(self.render("{% if 'boolean'|fake:0 %}{% else %}False forever{% endif %}"), "False forever") + + def testFakeFilterAsForInRange(self): + times = 10 + rendered = self.render("{% for word in 'words'|fake:times %}{{ word }}\n{% endfor %}", {'times': times}) + words = [word for word in rendered.split('\n') if word.strip()] + self.assertEqual(len(words), times) + + # do_or_fake_filter: or_fake + def testOrFakeFilterTag(self): + self.assertEqual(len(self.render("{{ unknown_var|or_fake:'md5' }}")), 32) + + def testFullXmlContact(self): + self.assertTrue(self.render(""" + + {% fake 'random_int' 10 20 as times %} + {% for i in 10|get_range %} + + + {% if 'boolean'|fake:25 %} + + {% endif %} +
+ {% fake 'street_address' %} + {% fake 'city' %} + {% fake 'postcode' %} + {% fake 'state' %} +
+ + {% if 'boolean'|fake:25 %} + {% fake 'bs' %} + {% endif %} + {% if 'boolean'|fake:33 %} + + {% endif %} + + {% if 'boolean'|fake:15 %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+""")) + + +class APIDjangoFakerTestCase(test.TestCase): + + def test_django_faker_singleton(self): + self.assertEqual(DjangoFaker(), DjangoFaker()) + self.assertIs(DjangoFaker(), DjangoFaker()) + + def test_faker_cache_generator(self): + self.assertEqual(DjangoFaker().get_generator(), DjangoFaker().get_generator()) + self.assertIs(DjangoFaker().get_generator(), DjangoFaker().get_generator()) + self.assertIs(DjangoFaker().get_generator(codename='default'), DjangoFaker().get_generator(codename='default')) + + self.assertEqual(DjangoFaker().get_generator(locale='it_IT'), DjangoFaker().get_generator(locale='it_IT')) + self.assertIs(DjangoFaker().get_generator(locale='it_IT'), DjangoFaker().get_generator(locale='it_IT')) + + def test_faker_cache_populator(self): + self.assertEqual(DjangoFaker().get_generator(), DjangoFaker().get_generator()) + self.assertIs(DjangoFaker().get_generator(), DjangoFaker().get_generator()) + self.assertIs(DjangoFaker().get_generator(), DjangoFaker().get_generator()) + + self.assertEqual(DjangoFaker().get_generator(locale='it_IT'), DjangoFaker().get_generator(locale='it_IT')) + self.assertIs(DjangoFaker().get_generator(locale='it_IT'), DjangoFaker().get_generator(locale='it_IT')) diff --git a/django_faker_tests/tests/test_example.py b/django_faker_tests/tests/test_example.py new file mode 100644 index 0000000..6960046 --- /dev/null +++ b/django_faker_tests/tests/test_example.py @@ -0,0 +1,26 @@ +from django import test +from django_faker import DjangoFaker +from ..models import Assessment, Question, Answer, AnswerSubmission, Company + + +class ExampleTestCase(test.TestCase): + + def setUp(self): + # populator gets shared, need to reset it + populator = DjangoFaker.get_populator() + populator.clear() + + def test_complex(self): + """Should be able to populate a complex database""" + populator = DjangoFaker.get_populator() + populator.add_entity(Assessment, 5) + populator.add_entity(Question, 5) + populator.add_entity(AnswerSubmission, 5) + populator.add_entity(Company, 5) + populator.add_entity(Answer, 5) + results = populator.execute() + self.assertEqual(len(results[Assessment]), 5) + self.assertEqual(len(results[Question]), 5) + self.assertEqual(len(results[AnswerSubmission]), 5) + self.assertEqual(len(results[Company]), 5) + self.assertEqual(len(results[Answer]), 5) diff --git a/django_faker_tests/tests/test_name_fields.py b/django_faker_tests/tests/test_name_fields.py new file mode 100644 index 0000000..4349d18 --- /dev/null +++ b/django_faker_tests/tests/test_name_fields.py @@ -0,0 +1,31 @@ +from django import test +from django_faker import DjangoFaker +from ..models import NamedFields + + +class RelationalTestCse(test.TestCase): + + def setUp(self): + populator = DjangoFaker.get_populator() + populator.clear() + + def test_all_names(self): + """""" + populator = DjangoFaker.get_populator() + populator.add_entity(NamedFields, 1) + populator.execute() + named = NamedFields.objects.all()[0] + self.assertRegexpMatches(named.is_boolean, '(True)|(False)') + self.assertRegexpMatches(named.created_at, '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}') + self.assertRegexpMatches(named.first_name, '[A-Z][a-z]+?') + self.assertRegexpMatches(named.last_name, '[A-Z][a-z]+?') + self.assertRegexpMatches(named.name, '\w') + self.assertRegexpMatches(named.login, '\w') + self.assertRegexpMatches(named.email, '.+@.*\..*') + self.assertRegexpMatches(named.phone_number, '[\d\-x]+') + self.assertRegexpMatches(named.city, '([A-Z][a-z]+? ?)+') + self.assertRegexpMatches(named.zipcode, '\d+') + self.assertRegexpMatches(named.state, '[A-Z\']{2}') + self.assertRegexpMatches(named.country, '([A-Z][a-z]+? ?)+') + self.assertRegexpMatches(named.title, '[\w ]+.') + diff --git a/django_faker_tests/tests/test_relational.py b/django_faker_tests/tests/test_relational.py new file mode 100644 index 0000000..079fdcc --- /dev/null +++ b/django_faker_tests/tests/test_relational.py @@ -0,0 +1,83 @@ +from django import test +from django_faker import DjangoFaker +from django_faker.constraints import InvalidConstraint +from ..models import One, ToOne, Many, ToMany, Foreign, Key + + +class RelationalTestCse(test.TestCase): + + def setUp(self): + populator = DjangoFaker.get_populator() + populator.clear() + + def test_one_to_one(self): + """Should preserve the one-to-one constraint (each one should have a different to one)""" + populator = DjangoFaker.get_populator() + populator.add_entity(ToOne, 5) + populator.add_entity(One, 5) + populator.execute() + to_ones = ToOne.objects.all() + for one in One.objects.all(): + to_ones = to_ones.exclude(one=one) + self.assertEqual(0, len(to_ones)) + + def test_one_to_one_fail(self): + """Should throw an exception if """ + try: + populator = DjangoFaker.get_populator() + populator.add_entity(ToOne, 4) + populator.add_entity(One, 5) + populator.execute() + self.fail("each One should need an unused ToOne which should be impossible here") + except InvalidConstraint: + pass + + def test_one_to_one_existing(self): + """Should be able to use a mix of existing db and new entities""" + populator = DjangoFaker.get_populator() + to_one = ToOne(thing='stuff') + to_one.save() + populator.add_entity(ToOne, 1) + populator.add_entity(One, 2) + populator.execute() + to_ones = ToOne.objects.all() + for one in One.objects.all(): + to_ones = to_ones.exclude(one=one) + self.assertEqual(0, len(to_ones)) + + def test_many_to_many(self): + """Each many-to-many field assigns 1 or more object if they exist. + many-to-many fields won't error out if there are no available objects""" + populator = DjangoFaker.get_populator() + populator.add_entity(ToMany, 5) + populator.add_entity(Many, 5) + populator.execute() + for many in Many.objects.all(): + self.assertTrue(len(many.to_many.all()) > 0) + + def test_many_to_many_fail(self): + """Non-null many-to-many fields will raise exceptions if there aren't available objects""" + try: + populator = DjangoFaker.get_populator() + populator.add_entity(Many, 1) + populator.execute() + self.fail("All Many's require at least one ToMany to exist") + except InvalidConstraint: + pass + + def test_many_to_many_existing(self): + """Should be able to use existing db values if none added to populator""" + populator = DjangoFaker.get_populator() + to_many = ToMany(thing='stuff') + to_many.save() + populator.add_entity(Many, 1) + populator.execute() + self.assertEqual(1, len(Many.objects.all())) + + def test_foreign_key(self): + """Should be able to use one key for all the foreigns""" + populator = DjangoFaker.get_populator() + populator.add_entity(Key, 1) + populator.add_entity(Foreign, 5) + populator.execute() + self.assertEqual(5, len(Key.objects.all()[0].foreigns.all())) diff --git a/django_faker/urls.py b/django_faker_tests/urls.py similarity index 100% rename from django_faker/urls.py rename to django_faker_tests/urls.py diff --git a/requirements.txt b/requirements.txt index 10e29ad..c880a3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ django -fake-factory==0.5.3 +faker +coverage \ No newline at end of file diff --git a/runtests.py b/runtests.py index fe0ab13..9a438f6 100644 --- a/runtests.py +++ b/runtests.py @@ -1,42 +1,43 @@ #!/usr/bin/env python -import sys - -from django.conf import settings - -def configure(): - - from faker import Faker - - fake = Faker() - - settings.configure( - DATABASES={ - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - } - }, - INSTALLED_APPS=( - 'django_faker', - ), - SITE_ID=1, - SECRET_KEY=fake.sha1(), - ) - -if not settings.configured: configure() - - -from django.test.utils import get_runner - - -def runtests(): - TestRunner = get_runner(settings) - test_runner = TestRunner(verbosity=1, interactive=True, failfast=False) - failures = test_runner.run_tests(['django_faker', ]) - sys.exit(failures) - +import argparse +import os, sys + +os.environ['DJANGO_SETTINGS_MODULE'] = 'django_faker_tests.settings' + +# Adding current directory to ``sys.path``. +parent = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, parent) + + +def runtests(*argv): + argv = list(argv) or [ + 'django_faker_tests', + ] + opts = argparser.parse_args(argv) + + # test_coverage = None + # if opts.coverage: + # from coverage import coverage + # test_coverage = coverage( + # branch=True, + # source=['django_faker']) + # test_coverage.start() + + # Run tests. + from django.core.management import execute_from_command_line + execute_from_command_line([sys.argv[0], 'test'] + opts.appname) + # + # if opts.coverage: + # test_coverage.stop() + # + # # Report coverage to commandline. + # test_coverage.report(file=sys.stdout) + + +argparser = argparse.ArgumentParser(description='Process some integers.') +argparser.add_argument('appname', nargs='*') +argparser.add_argument('--no-coverage', dest='coverage', action='store_const', + const=False, default=True, help='Do not collect coverage data.') if __name__ == '__main__': - runtests() - - + runtests(*sys.argv[1:]) diff --git a/setup.py b/setup.py index 88dd5c4..2ac27d7 100644 --- a/setup.py +++ b/setup.py @@ -37,8 +37,14 @@ def read_file(filename): ], keywords='faker fixtures data test django', long_description=read_file('README.rst'), - install_requires=['django','fake-factory==0.2'], - tests_require=['django','fake-factory==0.2'], + install_requires=[ + 'Django>=1.11.0', + 'Faker>=0.8.11', + ], + tests_require=[ + 'Django>=1.11.0', + 'Faker>=0.8.11', + ], test_suite="runtests.runtests", zip_safe=False, ) From f6d9177306776cc9622e754a9303db8c21d39e89 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Wed, 21 Mar 2018 17:09:39 -0500 Subject: [PATCH 02/17] readme fix --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2e8783f..900e059 100644 --- a/README.rst +++ b/README.rst @@ -75,8 +75,7 @@ Faker is smart enough to relate the populated `Player` entities to one of popula Relational Fields ------------------------- - +~~~~~~~~~~~~~~~~~ Django-faker will attempt to populate relational fields in the following manner: 1. From model instances added through `add_entity()` 2. From pre-existing values in the db From 82b74bf6e2988ba00a02a19cc083ff66abc7ea34 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Wed, 21 Mar 2018 17:12:28 -0500 Subject: [PATCH 03/17] readme fix pt. 2 --- README.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 900e059..f017fa3 100644 --- a/README.rst +++ b/README.rst @@ -77,21 +77,22 @@ Faker is smart enough to relate the populated `Player` entities to one of popula Relational Fields ~~~~~~~~~~~~~~~~~ Django-faker will attempt to populate relational fields in the following manner: -1. From model instances added through `add_entity()` -2. From pre-existing values in the db -If there aren't available values and the field can't be null, an `AttributeError` is thrown +#. From model instances added through `add_entity()` +#. From pre-existing values in the db -**One-to-one fields** -The Populator keeps track of what values have been used already so it doesn't violate the one-to-one constraint +If there aren't available values and the field can't be null, an `AttributeError` is thrown. -**Many-to-many fields** -The Populator randomly selects between 1-n values to assign to the object +**One-to-one fields**: +The Populator keeps track of what values have been used already so it doesn't violate the one-to-one constraint. -**Foreign key fields** -The Populator randomly selects 1 value to assign +**Many-to-many fields**: +The Populator randomly selects between 1-n values to assign to the object. -**unique/unique_together constraints** +**Foreign key fields**: +The Populator randomly selects 1 value to assign. + +**unique/unique_together constraints**: Currently, django-faker tries to populate the field(s) and then if a constraint is violated it tries again. This happens up to 1000 times and then an InvalidConstraint exception is thrown. Future iterations will hopefully pick values from a generated set of options to guarantee correctness. From a482c046e588b68be5223622dffa1b9d83eb3baa Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 11:31:46 -0500 Subject: [PATCH 04/17] updated tests/template tags --- README.rst | 11 +- django_faker/constraints.py | 2 + django_faker/templatetags/fakers.py | 114 +++++++-------- django_faker_tests/settings.py | 8 +- django_faker_tests/tests/test_base.py | 138 +----------------- django_faker_tests/tests/test_example.py | 52 ++++++- .../tests/test_template_tags.py | 91 ++++++++++++ 7 files changed, 204 insertions(+), 212 deletions(-) create mode 100644 django_faker_tests/tests/test_template_tags.py diff --git a/README.rst b/README.rst index f017fa3..57c0cb8 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,7 @@ In django application `settings.py`:: INSTALLED_APPS = ( # ... 'django_faker', + 'django_faker_tests', ) FAKER_LOCALE = None # settings.LANGUAGE_CODE is loaded @@ -38,7 +39,7 @@ call `execute()` method. Here is an example showing how to populate 5 `Game` and 10 `Player` objects:: from django_faker import Faker - # this Populator is only a function thats return a django_faker.populator.Populator instance + # this Populator is only a function that returns a django_faker.populator.Populator instance # correctly initialized with a faker.generator.Generator instance, configured as above populator = Faker.get_populator() @@ -50,7 +51,7 @@ Here is an example showing how to populate 5 `Game` and 10 `Player` objects:: The populator uses name and column type guessers to populate each column with relevant data. For instance, Django-faker populates a column named `first_name` using the `first_name` formatter, and a column with -a `datetime` instance using the `date_time`. +a `datetime` instance using `date_time`. The resulting entities are therefore coherent. If Django-faker misinterprets a column name, you can still specify a custom function to be used for populating a particular column, using the third argument to `add_entity()`:: @@ -74,7 +75,7 @@ In the previous example, the `Player` and `Game` models share a relationship. Si Faker is smart enough to relate the populated `Player` entities to one of populated `Game` entities. -Relational Fields +More on Relational Fields ~~~~~~~~~~~~~~~~~ Django-faker will attempt to populate relational fields in the following manner: @@ -101,7 +102,7 @@ Future iterations will hopefully pick values from a generated set of options to Template tags and filter ~~~~~~~~~~~~~~~~~~~~~~~~ -Django-faker offers a useful template tags and filters for interact with `PyFaker`_:: +Django-faker offers a useful template tags and filters to interact with `faker`_:: {% fake 'name' as myname %}{% fake 'date_time_between' '-10d' as mydate %} @@ -152,7 +153,7 @@ Open `url.py` in your main application and add this url:: urlpatterns = patterns('', ... - url(r'', include('django_faker.urls')), + url(r'', include('django_faker_tests.urls')), ... ) diff --git a/django_faker/constraints.py b/django_faker/constraints.py index c43336c..082473d 100644 --- a/django_faker/constraints.py +++ b/django_faker/constraints.py @@ -1,3 +1,5 @@ +# coding=utf-8 +# coding=UTF-8 # Credit for constraints goes to django-autofixture: # https://github.com/gregmuellegger/django-autofixture/blob/master/autofixture/constraints.py # Copyright (c) 2010, Gregor Müllegger diff --git a/django_faker/templatetags/fakers.py b/django_faker/templatetags/fakers.py index 2935c18..e36f4fe 100644 --- a/django_faker/templatetags/fakers.py +++ b/django_faker/templatetags/fakers.py @@ -7,57 +7,57 @@ from django_faker import DjangoFaker - -def optional_assignment_tag(func=None, takes_context=None, name=None): - """ - https://groups.google.com/forum/?fromgroups=#!topic/django-developers/E0XWFrkRMGc - new template tags type - """ - def dec(func): - params, varargs, varkw, defaults = getargspec(func) - - class AssignmentNode(template.Node): - def __init__(self, takes_context, args, kwargs, target_var=None): - super(AssignmentNode, self).__init__(takes_context, args, kwargs) - self.target_var = target_var - - def render(self, context): - resolved_args, resolved_kwargs = self.get_resolved_arguments(context) - output = func(*resolved_args, **resolved_kwargs) - if self.target_var is None: - return output - else: - context[self.target_var] = output - return '' - - function_name = (name or - getattr(func, '_decorated_function', func).__name__) - - def compile_func(parser, token): - bits = token.split_contents()[1:] - if len(bits) < 2 or bits[-2] != 'as': - target_var = None - else: - target_var = bits[-1] - bits = bits[:-2] - args, kwargs = parse_bits(parser, bits, params, - varargs, varkw, defaults, takes_context, function_name) - return AssignmentNode(takes_context, args, kwargs, target_var) - - compile_func.__doc__ = func.__doc__ - register.tag(function_name, compile_func) - return func - if func is None: - # @register.assignment_tag(...) - return dec - elif callable(func): - # @register.assignment_tag - return dec(func) - else: - raise TemplateSyntaxError("Invalid arguments provided to assignment_tag") - - -@optional_assignment_tag(name='fake') +# *** Django allows for optional assignment out of box now with simple tags now *** +# def optional_assignment_tag(func=None, takes_context=None, name=None): +# """ +# https://groups.google.com/forum/?fromgroups=#!topic/django-developers/E0XWFrkRMGc +# new template tags type +# """ +# def dec(func): +# params, varargs, varkw, defaults = getargspec(func) +# +# class AssignmentNode(template.Node): +# def __init__(self, takes_context, args, kwargs, target_var=None): +# super(AssignmentNode, self).__init__(takes_context, args, kwargs) +# self.target_var = target_var +# +# def render(self, context): +# resolved_args, resolved_kwargs = self.get_resolved_arguments(context) +# output = func(*resolved_args, **resolved_kwargs) +# if self.target_var is None: +# return output +# else: +# context[self.target_var] = output +# return '' +# +# function_name = (name or +# getattr(func, '_decorated_function', func).__name__) +# +# def compile_func(parser, token): +# bits = token.split_contents()[1:] +# if len(bits) < 2 or bits[-2] != 'as': +# target_var = None +# else: +# target_var = bits[-1] +# bits = bits[:-2] +# args, kwargs = parse_bits(parser, bits, params, +# varargs, varkw, defaults, takes_context, function_name) +# return AssignmentNode(takes_context, args, kwargs, target_var) +# +# compile_func.__doc__ = func.__doc__ +# register.tag(function_name, compile_func) +# return func +# if func is None: +# # @register.assignment_tag(...) +# return dec +# elif callable(func): +# # @register.assignment_tag +# return dec(func) +# else: +# raise TemplateSyntaxError("Invalid arguments provided to assignment_tag") + + +@register.simple_tag(name='fake') def do_fake(formatter, *args, **kwargs): """ call a faker format @@ -73,18 +73,6 @@ def do_fake(formatter, *args, **kwargs): return DjangoFaker.get_generator().format(formatter, *args, **kwargs) -@register.assignment_tag(name='fake') -def fake_tag_as(formatter, *args, **kwargs): - """ - call a faker format - uses: - - {% fake "formatterName" *args **kwargs as myvar %} - - """ - return DjangoFaker.get_generator().format(formatter, *args, **kwargs) - - @register.simple_tag(name='fakestr') def fake_tag_str(formatter, *args, **kwargs): """ diff --git a/django_faker_tests/settings.py b/django_faker_tests/settings.py index ebaab02..6217458 100644 --- a/django_faker_tests/settings.py +++ b/django_faker_tests/settings.py @@ -43,8 +43,8 @@ 'django.contrib.sites', 'django.contrib.admin', - 'django_faker', - 'django_faker_tests', + 'django_faker', # get the template tags + 'django_faker_tests', # get the models, urls, etc 'django_faker_tests.tests', ) @@ -53,9 +53,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - # 'APP_DIRS': True, # can't use both APP_DIRS and loaders 'OPTIONS': { - # 1.8 version - django.template.context_processors 'context_processors': ("django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", @@ -75,4 +73,4 @@ if django.VERSION < (1, 6): TEST_RUNNER = 'discover_runner.DiscoverRunner' else: - TEST_RUNNER = 'django.test.runner.DiscoverRunner' \ No newline at end of file + TEST_RUNNER = 'django.test.runner.DiscoverRunner' diff --git a/django_faker_tests/tests/test_base.py b/django_faker_tests/tests/test_base.py index 52bb795..e038a7d 100644 --- a/django_faker_tests/tests/test_base.py +++ b/django_faker_tests/tests/test_base.py @@ -1,145 +1,9 @@ from faker import Faker -from django_faker.populator import Populator from django_faker import DjangoFaker - from django import test -from django.template import Context -from django.template import Template -from ..models import Game, Player, Action - -fake = Faker() - - -class PopulatorTestCase(test.TestCase): - - def testPopulation(self): - generator = fake - populator = Populator(generator) - populator.add_entity(Game, 10) - self.assertEqual(len(populator.execute()[Game]), 10) - - def testGuesser(self): - generator = fake - - def title_fake(arg): - title_fake.count += 1 - name = generator.company() - return name - - title_fake.count = 0 - - populator = Populator(generator) - populator.add_entity(Game, 10, { - 'title': title_fake - }) - self.assertEqual(len(populator.execute()[Game]), title_fake.count) - - def testFormatter(self): - generator = fake - - populator = Populator(generator) - - populator.add_entity(Game, 5) - populator.add_entity(Player, 10, { - 'score': lambda x: fake.random_int(0, 1000), - 'nickname': lambda x: fake.email() - }) - populator.add_entity(Action, 30) - - insertedPks = populator.execute() - - self.assertTrue(len(insertedPks[Game]) == 5) - self.assertTrue(len(insertedPks[Player]) == 10) - self.assertTrue(any([0 <= p.score <= 1000 and '@' in p.nickname for p in Player.objects.all()])) - -class TemplateTagsTestCase(test.TestCase): - - @staticmethod - def render(template, context=None): - t = Template("{% load fakers %}" + template) - c = Context(context or {}) - text = t.render(c) - return text - - # do_fake: fake - def testSimpleFakeTag(self): - self.assertNotEqual(self.render("{% fake 'name' as myname %}{{ myname }}"), "") - - def testSimpleFakeTagWithArguments(self): - self.assertNotEqual(self.render("{% fake 'date_time_between' '-10d' as mydate %}{{ mydate }}"), "") - - def testSimpleFakeTagFormatterNotFoundRaisesException(self): - with self.assertRaises(AttributeError): - self.render("{% fake 'notFoundedFake' as foo %}") - - def testSimpleFakeTagOptionalAssignment(self): - self.assertNotEqual(self.render("{% fake 'name' %}"), "") - self.assertEqual(len(self.render("{% fake 'md5' %}")), 32) - - # do_fake_filter: fake - def testFakeFilterTag(self): - self.assertIn(self.render("{{ 'random_element'|fake:'test_string' }}"), 'test_string') - - def testFakeFilterWithValueFromContext(self): - mylist = [100, 200, 300] - rendered = self.render("{{ 'random_element'|fake:mylist }}", {'mylist': mylist}) - self.assertIn(rendered, [unicode(el) for el in mylist]) - - def testFakeFilterFormatterNotFoundRaisesException(self): - with self.assertRaises(AttributeError): - self.render("{{ 'notFoundedFake'|fake:mylist }}", {'mylist': [100, 200, 300]}) - - def testFakeFilterAsIfCondition(self): - self.assertEqual(self.render("{% if 'boolean'|fake:100 %}True forever{% endif %}"), "True forever") - self.assertEqual(self.render("{% if 'boolean'|fake:0 %}{% else %}False forever{% endif %}"), "False forever") - - def testFakeFilterAsForInRange(self): - times = 10 - rendered = self.render("{% for word in 'words'|fake:times %}{{ word }}\n{% endfor %}", {'times': times}) - words = [word for word in rendered.split('\n') if word.strip()] - self.assertEqual(len(words), times) - - # do_or_fake_filter: or_fake - def testOrFakeFilterTag(self): - self.assertEqual(len(self.render("{{ unknown_var|or_fake:'md5' }}")), 32) - - def testFullXmlContact(self): - self.assertTrue(self.render(""" - - {% fake 'random_int' 10 20 as times %} - {% for i in 10|get_range %} - - - {% if 'boolean'|fake:25 %} - - {% endif %} -
- {% fake 'street_address' %} - {% fake 'city' %} - {% fake 'postcode' %} - {% fake 'state' %} -
- - {% if 'boolean'|fake:25 %} - {% fake 'bs' %} - {% endif %} - {% if 'boolean'|fake:33 %} - - {% endif %} - - {% if 'boolean'|fake:15 %} -
- -
- {% endif %} -
- {% endfor %} -
-""")) +fake = Faker() class APIDjangoFakerTestCase(test.TestCase): diff --git a/django_faker_tests/tests/test_example.py b/django_faker_tests/tests/test_example.py index 6960046..724a6bb 100644 --- a/django_faker_tests/tests/test_example.py +++ b/django_faker_tests/tests/test_example.py @@ -1,6 +1,54 @@ +from faker import Faker from django import test from django_faker import DjangoFaker -from ..models import Assessment, Question, Answer, AnswerSubmission, Company +from django_faker.populator import Populator +from ..models import Assessment, Question, Answer, AnswerSubmission, Company, Game, Player, Action + +fake = Faker() + + +class PopulatorTestCase(test.TestCase): + + def testPopulation(self): + generator = fake + populator = Populator(generator) + populator.add_entity(Game, 10) + self.assertEqual(len(populator.execute()[Game]), 10) + + def testGuesser(self): + generator = fake + + def title_fake(arg): + title_fake.count += 1 + name = generator.company() + return name + + title_fake.count = 0 + + populator = Populator(generator) + populator.add_entity(Game, 10, { + 'title': title_fake + }) + self.assertEqual(len(populator.execute()[Game]), title_fake.count) + + def testFormatter(self): + generator = fake + + populator = Populator(generator) + + populator.add_entity(Game, 5) + populator.add_entity(Player, 10, { + 'score': lambda x: fake.random_int(0, 1000), + 'nickname': lambda x: fake.email() + }) + populator.add_entity(Action, 30) + + insertedPks = populator.execute() + + self.assertTrue(len(insertedPks[Game]) == 5) + self.assertTrue(len(insertedPks[Player]) == 10) + + self.assertTrue(any([0 <= p.score <= 1000 and '@' in p.nickname for p in Player.objects.all()])) class ExampleTestCase(test.TestCase): @@ -11,7 +59,7 @@ def setUp(self): populator.clear() def test_complex(self): - """Should be able to populate a complex database""" + """Should be able to populate a complex set of models""" populator = DjangoFaker.get_populator() populator.add_entity(Assessment, 5) populator.add_entity(Question, 5) diff --git a/django_faker_tests/tests/test_template_tags.py b/django_faker_tests/tests/test_template_tags.py new file mode 100644 index 0000000..6f6b256 --- /dev/null +++ b/django_faker_tests/tests/test_template_tags.py @@ -0,0 +1,91 @@ +from django import test +from django.template import Context +from django.template import Template + + +class TemplateTagsTestCase(test.TestCase): + + @staticmethod + def render(template, context=None): + t = Template("{% load fakers %}" + template) + c = Context(context or {}) + text = t.render(c) + return text + + # do_fake: fake + def testSimpleFakeTag(self): + self.assertNotEqual(self.render("{% fake 'name' as myname %}{{ myname }}"), "") + + def testSimpleFakeTagWithArguments(self): + self.assertNotEqual(self.render("{% fake 'date_time_between' '-10d' as mydate %}{{ mydate }}"), "") + + def testSimpleFakeTagFormatterNotFoundRaisesException(self): + with self.assertRaises(AttributeError): + self.render("{% fake 'notFoundedFake' as foo %}") + + def testSimpleFakeTagOptionalAssignment(self): + self.assertNotEqual(self.render("{% fake 'name' %}"), "") + self.assertEqual(len(self.render("{% fake 'md5' %}")), 32) + + # do_fake_filter: fake + def testFakeFilterTag(self): + self.assertIn(self.render("{{ 'random_element'|fake:'test_string' }}"), 'test_string') + + def testFakeFilterWithValueFromContext(self): + mylist = [100, 200, 300] + rendered = self.render("{{ 'random_element'|fake:mylist }}", {'mylist': mylist}) + self.assertIn(rendered, [unicode(el) for el in mylist]) + + def testFakeFilterFormatterNotFoundRaisesException(self): + with self.assertRaises(AttributeError): + self.render("{{ 'notFoundedFake'|fake:mylist }}", {'mylist': [100, 200, 300]}) + + def testFakeFilterAsIfCondition(self): + self.assertEqual(self.render("{% if 'boolean'|fake:100 %}True forever{% endif %}"), "True forever") + self.assertEqual(self.render("{% if 'boolean'|fake:0 %}{% else %}False forever{% endif %}"), "False forever") + + def testFakeFilterAsForInRange(self): + times = 10 + rendered = self.render("{% for word in 'words'|fake:times %}{{ word }}\n{% endfor %}", {'times': times}) + words = [word for word in rendered.split('\n') if word.strip()] + self.assertEqual(len(words), times) + + # do_or_fake_filter: or_fake + def testOrFakeFilterTag(self): + self.assertEqual(len(self.render("{{ unknown_var|or_fake:'md5' }}")), 32) + + def testFullXmlContact(self): + self.assertTrue(self.render(""" + + {% fake 'random_int' 10 20 as times %} + {% for i in 10|get_range %} + + + {% if 'boolean'|fake:25 %} + + {% endif %} +
+ {% fake 'street_address' %} + {% fake 'city' %} + {% fake 'postcode' %} + {% fake 'state' %} +
+ + {% if 'boolean'|fake:25 %} + {% fake 'bs' %} + {% endif %} + {% if 'boolean'|fake:33 %} + + {% endif %} + + {% if 'boolean'|fake:15 %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+""")) From 6f61036d668646934a0b86fe2e76a0c6b95ec180 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 11:48:47 -0500 Subject: [PATCH 05/17] trying to get travis build right --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1259f77..d08c8d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ env: - DJANGO=Django==1.11 - DJANGO=Django==2.0 install: - - pip install $DJANGO --use-mirrors - - pip install . --use-mirrors + - pip install $DJANGO + - pip install . - pip install coverage branches: only: From a69071817bf8d7d5a42d04ddb66b9f791891759d Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 11:51:10 -0500 Subject: [PATCH 06/17] trying to get travis build right 2 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d08c8d7..6423526 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: - DJANGO=Django==2.0 install: - pip install $DJANGO + - pip install faker - pip install . - pip install coverage branches: From 2956c52843525a0af7212da5c78feb4be7522c5d Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 11:53:01 -0500 Subject: [PATCH 07/17] trying to get travis build right 3 --- django_faker/guessers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_faker/guessers.py b/django_faker/guessers.py index 182cfd2..7a427c0 100644 --- a/django_faker/guessers.py +++ b/django_faker/guessers.py @@ -5,7 +5,7 @@ PositiveSmallIntegerField, BigIntegerField, SmallIntegerField, PositiveIntegerField, IntegerField, DecimalField, \ FloatField, SlugField, URLField, EmailField, TextField, UUIDField, CharField, BinaryField, BooleanField, \ NullBooleanField, DateField, DateTimeField, DurationField, TimeField, FilePathField, GenericIPAddressField -from constraints import InvalidConstraint +from .constraints import InvalidConstraint class FieldGuesser(object): From f19648d4dc1a404d98b8981675324f06b867feab Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 11:56:15 -0500 Subject: [PATCH 08/17] trying to get travis build right 4 --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6423526..38f7f70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,7 @@ env: - DJANGO=Django==2.0 install: - pip install $DJANGO - - pip install faker - - pip install . - - pip install coverage + - pip install -e . branches: only: - master From 2c3bfb9ebf954c677826eee4a4aca9fe58932ed9 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 13:21:09 -0500 Subject: [PATCH 09/17] trying to get travis build right 5 ugh --- django_faker/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django_faker/__init__.py b/django_faker/__init__.py index b5e5eeb..31d36f0 100644 --- a/django_faker/__init__.py +++ b/django_faker/__init__.py @@ -4,6 +4,8 @@ from faker import Faker as FakerGenerator from .populator import Populator +__version__ = "0.2.1" + class DjangoFaker(object): From df67a73d0669f07204fc30c5326efd69eb2f608d Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 13:25:31 -0500 Subject: [PATCH 10/17] trying to get travis build right 6 --- django_faker/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_faker/__init__.py b/django_faker/__init__.py index 31d36f0..4f84d9a 100644 --- a/django_faker/__init__.py +++ b/django_faker/__init__.py @@ -1,8 +1,6 @@ """ Django-faker uses a generator (eg faker) to generate test data for Django models and templates. """ -from faker import Faker as FakerGenerator -from .populator import Populator __version__ = "0.2.1" @@ -51,6 +49,7 @@ def get_generator(cls, locale=None, providers=None, codename=None): if codename not in cls.generators: # initialize with faker.generator.Generator instance # and remember in cache + from faker import Faker as FakerGenerator cls.generators[codename] = FakerGenerator(locale, providers) cls.generators[codename].seed(cls.generators[codename].random_int()) @@ -83,6 +82,7 @@ def get_populator(cls, locale=None, providers=None): if codename not in cls.populators: generator = cls.generators.get(codename, None) or cls.get_generator(codename=codename) + from .populator import Populator cls.populators[codename] = Populator(generator) return cls.populators[codename] From 1e36df909a0b9506d0cab696c338d1ccb1539346 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 13:31:07 -0500 Subject: [PATCH 11/17] trying to get travis build right 7 shoulda changed branch --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 2ac27d7..b126cdd 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ def read_file(filename): tests_require=[ 'Django>=1.11.0', 'Faker>=0.8.11', + 'coverage>=4.5.1', ], test_suite="runtests.runtests", zip_safe=False, From e0490af30490a8d80618d25078cc932d40656106 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 13:34:21 -0500 Subject: [PATCH 12/17] trying to get travis build right 7 shoulda changed branch --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index b126cdd..9c628a7 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def read_file(filename): install_requires=[ 'Django>=1.11.0', 'Faker>=0.8.11', + 'coverage>=4.5.1', ], tests_require=[ 'Django>=1.11.0', From f4e01e61b2865d9edca289fa8332b5caf15fe33b Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 13:56:02 -0500 Subject: [PATCH 13/17] okay maybe just maybe --- .travis.yml | 8 +------- django_faker/templatetags/fakers.py | 2 +- runtests.py | 31 ++++++++++++++++------------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 38f7f70..e55099b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python sudo: false python: - - "2.6" - "2.7" - "3.3" - "3.4" @@ -16,13 +15,9 @@ branches: only: - master - develop -script: coverage run --source=django_faker setup.py test +script: python setup.py test matrix: exclude: - - python: "2.6" - env: DJANGO=Django==2.0 - - python: "2.6" - env: DJANGO=Django==2.0 - python: "2.7" env: DJANGO=Django==2.0 - python: "2.7" @@ -36,6 +31,5 @@ matrix: - python: "3.4" env: DJANGO=Django==1.11 after_success: - - coverage report - pip install --quiet python-coveralls - coveralls diff --git a/django_faker/templatetags/fakers.py b/django_faker/templatetags/fakers.py index e36f4fe..7d49c8a 100644 --- a/django_faker/templatetags/fakers.py +++ b/django_faker/templatetags/fakers.py @@ -82,7 +82,7 @@ def fake_tag_str(formatter, *args, **kwargs): {% fakestr "formatterName" *values **kwargs %} """ if formatter == 'dateTimeThisCentury': - print args, kwargs + print(args, kwargs) return DjangoFaker.get_generator().format(formatter, *args, **kwargs) diff --git a/runtests.py b/runtests.py index 9a438f6..0d51d55 100644 --- a/runtests.py +++ b/runtests.py @@ -15,23 +15,26 @@ def runtests(*argv): ] opts = argparser.parse_args(argv) - # test_coverage = None - # if opts.coverage: - # from coverage import coverage - # test_coverage = coverage( - # branch=True, - # source=['django_faker']) - # test_coverage.start() + test_coverage = None + if opts.coverage: + from coverage import coverage + test_coverage = coverage( + branch=True, + source=['django_faker']) + test_coverage.start() # Run tests. from django.core.management import execute_from_command_line - execute_from_command_line([sys.argv[0], 'test'] + opts.appname) - # - # if opts.coverage: - # test_coverage.stop() - # - # # Report coverage to commandline. - # test_coverage.report(file=sys.stdout) + failures = execute_from_command_line([sys.argv[0], 'test'] + opts.appname) + if failures: + sys.exit(failures) + + if opts.coverage: + test_coverage.stop() + + # Report coverage to commandline. + test_coverage.report(file=sys.stdout) + sys.exit() argparser = argparse.ArgumentParser(description='Process some integers.') From 9de4fcfa5609f7df61c9cfacc7b559d048d95241 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 14:01:47 -0500 Subject: [PATCH 14/17] python 3 compliant --- django_faker_tests/tests/test_template_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_faker_tests/tests/test_template_tags.py b/django_faker_tests/tests/test_template_tags.py index 6f6b256..7652c26 100644 --- a/django_faker_tests/tests/test_template_tags.py +++ b/django_faker_tests/tests/test_template_tags.py @@ -34,7 +34,7 @@ def testFakeFilterTag(self): def testFakeFilterWithValueFromContext(self): mylist = [100, 200, 300] rendered = self.render("{{ 'random_element'|fake:mylist }}", {'mylist': mylist}) - self.assertIn(rendered, [unicode(el) for el in mylist]) + self.assertIn(rendered, [str(el) for el in mylist]) def testFakeFilterFormatterNotFoundRaisesException(self): with self.assertRaises(AttributeError): From 5daeab9b643b3f2db798f42b8e325ee6e24c3b23 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 14:09:14 -0500 Subject: [PATCH 15/17] django 2 doesn't play well with python 3.3 --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e55099b..92d0fe8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python sudo: false python: - "2.7" - - "3.3" - "3.4" env: matrix: @@ -22,10 +21,6 @@ matrix: env: DJANGO=Django==2.0 - python: "2.7" env: DJANGO=Django==2.0 - - python: "3.3" - env: DJANGO=Django==1.11 - - python: "3.3" - env: DJANGO=Django==1.11 - python: "3.4" env: DJANGO=Django==1.11 - python: "3.4" From 4ba2a6e5ad6d0193dce2429805419e7ceab0d6b6 Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 22 Mar 2018 14:26:03 -0500 Subject: [PATCH 16/17] django 1.11 should be compatible with python 2.7 --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 92d0fe8..01ca17a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,10 +21,6 @@ matrix: env: DJANGO=Django==2.0 - python: "2.7" env: DJANGO=Django==2.0 - - python: "3.4" - env: DJANGO=Django==1.11 - - python: "3.4" - env: DJANGO=Django==1.11 after_success: - pip install --quiet python-coveralls - coveralls From a8934a424518f441b1230ba634d840d876a9447e Mon Sep 17 00:00:00 2001 From: Chance Raine Date: Thu, 29 Mar 2018 10:45:46 -0500 Subject: [PATCH 17/17] removed redundant statement from tavis.yml --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 01ca17a..a24469a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,6 @@ matrix: exclude: - python: "2.7" env: DJANGO=Django==2.0 - - python: "2.7" - env: DJANGO=Django==2.0 after_success: - pip install --quiet python-coveralls - coveralls