Skip to content

Commit

Permalink
Merge pull request #129 from paulocheque/data-format
Browse files Browse the repository at this point in the history
New feature: String masks
  • Loading branch information
paulocheque committed Mar 29, 2020
2 parents 7ee8059 + 223647a commit c1dfd9b
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 10 deletions.
60 changes: 55 additions & 5 deletions README.mkd
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ Django Dynamic Fixture (DDF) is a complete and simple library to create dynamic

It lets you focus on your tests, instead of focusing on generating some dummy data which is boring and polutes the test source code.

Documentation
-------------
* [Basic Examples](#basic-examples)
* [Cheat Sheet](#cheat-sheet)
* <a href="http://django-dynamic-fixture.readthedocs.org/en/latest/index.html" target="_blank">Full Documentation</a>

http://django-dynamic-fixture.readthedocs.org/en/latest/index.html


Basic examples
Basic Examples
--------------

> Customize only the important details of the test:
Expand Down Expand Up @@ -66,3 +65,54 @@ Basic examples
assert book2 not in books
assert book1.main_author.name == 'Eistein'
```

Cheat Sheet
--------------

```python
# Import the main DDF features
from ddf import N, G, F, M, C, P, teach
```

```python
# `N` creates an instance of model without saving it to DB
instance = N(Book)
```

```python
# `G` creates an instance of model and save it into the DB
instance = G(Book)
```

```python
# `F` customize relationship objects
instance = G(Book, author=F(name='Eistein'))
# Same as `F`
instance = G(Book, author__name='Eistein')
```

```python
# `M` receives a data mask and create a random string using it
# Known symbols: `_`, `#` or `-`
# To escape known symbols: `!`
instance = N(Book, address=M('Street ___, ### !- --'))
assert instance.address == 'Street TPA, 632 - BR'
```

```python
# `C` copies data from one field to another
instance = N(Book, address_formatted=C('address'), address=D('Street ___, ### \- --'))
assert instance.address_formatted == 'Street TPA, 632 - BR'
```

```python
# `teach` teaches DDF in how to build an instance
teach(Book, address=M'Street ___, ### !- --'))
instance = G(Book)
assert instance.address == 'Street TPA, 632 - BR'
```

```python
# `P` print instance values for debugging
P(instance)
```
2 changes: 1 addition & 1 deletion ddf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Short alias to use: # `from ddf import *` instead of `from django_dynamic_fixture import *`
from django_dynamic_fixture import N, G, F, C, P, PRE_SAVE, POST_SAVE, __version__
from django_dynamic_fixture import N, G, F, C, M, P, PRE_SAVE, POST_SAVE, __version__
from django_dynamic_fixture import new, get, fixture, teach, look_up_alias
from django_dynamic_fixture.decorators import skip_for_database, only_for_database
from django_dynamic_fixture.fdf import FileSystemDjangoTestCase
Expand Down
3 changes: 2 additions & 1 deletion django_dynamic_fixture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from django.apps import apps

from django_dynamic_fixture.ddf import DynamicFixture, Copier, DDFLibrary, \
from django_dynamic_fixture.ddf import DynamicFixture, Copier, Mask, DDFLibrary, \
set_pre_save_receiver, set_post_save_receiver
from django_dynamic_fixture.django_helper import print_field_values, django_greater_than
from django_dynamic_fixture.fixture_algorithms.sequential_fixture import SequentialDataFixture, \
Expand Down Expand Up @@ -183,6 +183,7 @@ def _teach(model, ddf_lesson=None, **kwargs):
T = teach = _teach
F = fixture
C = Copier
M = Mask
P = print_field_values
DDFLibrary = DDFLibrary
PRE_SAVE = set_pre_save_receiver
Expand Down
46 changes: 46 additions & 0 deletions django_dynamic_fixture/ddf.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,50 @@ def eval_expression(self, instance):
six.reraise(InvalidCopierExpressionError, InvalidCopierExpressionError(self.expression, e), sys.exc_info()[2])


class Mask(object):
'''
Wrapper for an expression mask that will be used to generate a random string with a custom format.
The expression mask supports 4 special characters:
- `#` for random numbers;
- `-` for random uppercase chars;
- `_` for random lowercase chars;
- `!` to escape special chars;
Other characters will not be interpreted and it will be considered part of the final string.
Example of usage: D('###.___!----') => '510.kap-NGK'
'''
def __init__(self, expression):
self.expression = expression

def __str__(self):
return "D('%s')" % self.expression

def evaluate(self):
import random
import string
chars = []
escaped = False
for char in self.expression:
if escaped:
c = char
escaped = False
elif char == '#':
c = random.choice(string.digits)
elif char == '-':
c = random.choice(string.ascii_uppercase)
elif char == '_':
c = random.choice(string.ascii_lowercase)
elif char == '!':
escaped = True
continue
else:
c = char
chars.append(c)
return ''.join(chars)


class DDFLibrary(object):
instance = None
DEFAULT_KEY = 'ddf_default'
Expand Down Expand Up @@ -319,6 +363,8 @@ def _process_field_with_customized_fixture(self, instance, field, fixture, persi
data = self._get_data_from_custom_dynamic_fixture(field, fixture, persist_dependencies)
elif isinstance(fixture, Copier): # Copier (C)
data = self._get_data_from_custom_copier(instance, field, fixture)
elif isinstance(fixture, Mask): # Mask (M)
data = fixture.evaluate()
elif isinstance(fixture, DataFixture): # DataFixture
data = self._get_data_from_data_fixture(field, fixture)
elif callable(fixture): # callable with the field as parameters
Expand Down
3 changes: 2 additions & 1 deletion django_dynamic_fixture/models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class Meta:


class ModelWithStrings(models.Model):
string = models.CharField(max_length=1, unique=True)
char = models.CharField(max_length=1, unique=True)
string = models.CharField(max_length=50, unique=True)
text = models.TextField(unique=True)
slug = models.SlugField(unique=True)
commaseparated = models.CommaSeparatedIntegerField(max_length=100, unique=True)
Expand Down
6 changes: 6 additions & 0 deletions django_dynamic_fixture/tests/test_ddf_copier.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ def test_it_should_raise_a_invalid_configuration_error_if_expression_is_bugged(s
def test_it_should_raise_a_invalid_configuration_error_if_copier_has_cyclic_dependency(self):
with pytest.raises(InvalidConfigurationError):
self.ddf.get(ModelForCopy, int_a=Copier('int_b'), int_b=Copier('int_a'))

def test_it_must_copy_generated_data_mask_too(self):
import re
instance = self.ddf.get(ModelWithStrings, string=Mask('- _ #'), text=Copier('string'))
assert re.match(r'[A-Z]{1} [a-z]{1} [0-9]{1}', instance.string)
assert re.match(r'[A-Z]{1} [a-z]{1} [0-9]{1}', instance.text)
44 changes: 44 additions & 0 deletions django_dynamic_fixture/tests/test_mask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import re

from django.test import TestCase
import pytest

from django_dynamic_fixture.models_test import *
from django_dynamic_fixture.ddf import *
from django_dynamic_fixture.fixture_algorithms.sequential_fixture import SequentialDataFixture


class DDFTestCase(TestCase):
def setUp(self):
self.ddf = DynamicFixture(SequentialDataFixture())


class MaskTests(DDFTestCase):
def test_it_must_generate_random_numbers(self):
instance = self.ddf.get(ModelWithStrings, string=Mask('###'))
assert re.match(r'\d{3}', instance.string)

def test_it_must_generate_lower_ascii_chars(self):
instance = self.ddf.get(ModelWithStrings, string=Mask('___'))
assert re.match(r'[a-z]{3}', instance.string)

def test_it_must_generate_upper_ascii_chars(self):
instance = self.ddf.get(ModelWithStrings, string=Mask('---'))
assert re.match(r'[A-Z]{3}', instance.string)

def test_it_must_accept_pure_chars(self):
instance = self.ddf.get(ModelWithStrings, string=Mask('ABC123'))
assert re.match(r'ABC123', instance.string)

def test_it_must_be_able_to_escape_symbols(self):
instance = self.ddf.get(ModelWithStrings, string=Mask(r'!# !_ !-'))
assert '# _ -' == instance.string

def test_phone_mask(self):
instance = self.ddf.get(ModelWithStrings, string=Mask(r'+## (##) #####!-#####'))
assert re.match(r'\+\d{2} \(\d{2}\) \d{5}-\d{5}', instance.string)

def test_address_mask(self):
instance = self.ddf.get(ModelWithStrings, string=Mask(r'St. -______, ### !- -- --'))
assert re.match(r'St\. [A-Z]{1}[a-z]{6}, \d{3} - [A-Z]{2} [A-Z]{2}', instance.string)
11 changes: 9 additions & 2 deletions django_dynamic_fixture/tests/test_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from django.test import TransactionTestCase as TestCase

from django_dynamic_fixture.models_test import EmptyModel, ModelWithRelationships, ModelForLibrary
from django_dynamic_fixture import N, G, F, C, P, teach, look_up_alias, PRE_SAVE, POST_SAVE
from django_dynamic_fixture.models_test import EmptyModel, ModelWithRelationships, ModelForLibrary, ModelWithStrings
from django_dynamic_fixture import N, G, F, C, M, P, teach, look_up_alias, PRE_SAVE, POST_SAVE


class NShortcutTest(TestCase):
Expand Down Expand Up @@ -138,6 +138,13 @@ def test_copying_inside_many_to_many(self):
assert instance1.integer == instance1.integer_b


class MShortcutTest(TestCase):
def test_full_data_mask_sample(self):
import re
instance = G(ModelWithStrings, string=M(r'St. -______, ### !- -- --'))
assert re.match(r'St\. [A-Z]{1}[a-z]{6}, \d{3} - [A-Z]{2} [A-Z]{2}', instance.string)


class TeachingAndLessonsTest(TestCase):
def test_global_lesson(self):
teach(ModelForLibrary, integer=1000)
Expand Down
19 changes: 19 additions & 0 deletions docs/source/ddf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ Or even mixed them up::
assert book.authors.all().count() == 4


Mask: M (New in 3.1.0)
===============================================================================

``M`` (shortcut for ``Mask``) is a feature that tell DDF to generate a random string using a custom mask.

The mask symbols are:

- ``#``: represents a number: 0-9
- ``-``: represents a upper case char: A-Z
- ``#``: represents a lower case char: a-z
- ``!``: escape mask symbols, inclusive itself

Examples::

from ddf import G, M
instance = G(Publisher, address=M(r'St. -______, ### !- -- --'))
assert instance.address == 'St. Imaiden, 164 - SP BR'


Copier: C (New in 1.6.0)
===============================================================================

Expand Down

0 comments on commit c1dfd9b

Please sign in to comment.