Skip to content

Commit

Permalink
Merge pull request #99 from paulocheque/teach-lessons
Browse files Browse the repository at this point in the history
New Teach/Lessons feature
  • Loading branch information
paulocheque committed Jan 3, 2020
2 parents c4f4414 + 35f66e7 commit 4d60cd5
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 258 deletions.
3 changes: 2 additions & 1 deletion ddf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Short alias to use: # `from ddf import *` instead of `from django_dynamic_fixture import *`
from django_dynamic_fixture import *
from django_dynamic_fixture import N, G, F, C, P, PRE_SAVE, POST_SAVE
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
54 changes: 39 additions & 15 deletions django_dynamic_fixture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django_dynamic_fixture.fixture_algorithms.sequential_fixture import SequentialDataFixture, \
StaticSequentialDataFixture
from django_dynamic_fixture.global_settings import DDF_DEFAULT_DATA_FIXTURE, DDF_FILL_NULLABLE_FIELDS, DDF_NUMBER_OF_LAPS, \
DDF_IGNORE_FIELDS, DDF_VALIDATE_MODELS, DDF_VALIDATE_ARGS, DDF_USE_LIBRARY, \
DDF_IGNORE_FIELDS, DDF_VALIDATE_MODELS, DDF_VALIDATE_ARGS, \
DDF_DEBUG_MODE, DDF_FIELD_FIXTURES


Expand Down Expand Up @@ -48,7 +48,6 @@ def fixture(**kwargs):
fill_nullable_fields=kwargs.pop('fill_nullable_fields', DDF_FILL_NULLABLE_FIELDS),
ignore_fields=kwargs.pop('ignore_fields', []),
number_of_laps=kwargs.pop('number_of_laps', DDF_NUMBER_OF_LAPS),
use_library=kwargs.pop('use_library', DDF_USE_LIBRARY),
validate_models=kwargs.pop('validate_models', DDF_VALIDATE_MODELS),
validate_args=kwargs.pop('validate_args', DDF_VALIDATE_ARGS),
print_errors=kwargs.pop('print_errors', True),
Expand All @@ -58,47 +57,49 @@ def fixture(**kwargs):


# Wrappers
def new(model, shelve=False, n=1, persist_dependencies=True, **kwargs):
def new(model, n=1, lesson=None, persist_dependencies=True, **kwargs):
"""
Return one or many valid instances of Django Models with fields filled with auto generated or customized data.
All instances will NOT be persisted in the database, except its dependencies, in case @persist_dependencies is True.
@model: The class of the Django model.
@n: number of instances to be created with the given configuration. Default is 1.
@lesson: use a custom lesson to build the model object.
@persist_dependencies: If True, save internal dependencies, otherwise just instantiate them. Default is True.
@data_fixture: override DDF_DEFAULT_DATA_FIXTURE configuration. Default is SequentialDataFixture().
@fill_nullable_fields: override DDF_FILL_NULLABLE_FIELDS global configuration. Default is True.
@ignore_fields: List of fields that will be ignored by DDF. It will be concatenated with the global list DDF_IGNORE_FIELDS. Default is [].
@number_of_laps: override DDF_NUMBER_OF_LAPS global configuration. Default 1.
@shelve: If it is True or a string, the used configuration will be stored in memory. It must be a True, False or a string (named shelve). Default False.
@use_library: Use a previously saved (by @shelve attribute) configuration. It must be a boolean or a string (name used in @shelve). override DDF_USE_LIBRARY global configuration. Default is False.
@n: number of instances to be created with the given configuration. Default is 1.
@validate_models: override DDF_VALIDATE_MODELS global configuration. Default is False.
@validate_args: override DDF_VALIDATE_ARGS global configuration. Default is False.
@print_errors: print on console all instance values if DDF can not generate a valid object with the given configuration.
@persist_dependencies: If True, save internal dependencies, otherwise just instantiate them. Default is True.
Wrapper for the method DynamicFixture.new
"""
kwargs = look_up_alias(**kwargs)
d = fixture(**kwargs)
if n == 1:
return d.new(model, shelve=shelve, persist_dependencies=persist_dependencies, **kwargs)
return d.new(model, lesson=lesson, persist_dependencies=persist_dependencies, **kwargs)
instances = []
for _ in range(n):
instances.append(d.new(model, persist_dependencies=persist_dependencies, **kwargs))
instances.append(d.new(model, lesson=lesson, persist_dependencies=persist_dependencies, **kwargs))
return instances


def get(model, shelve=False, n=1, **kwargs):
def get(model, n=1, lesson=None, **kwargs):
"""
Return one or many valid instances of Django Models with fields filled with auto generated or customized data.
All instances will be persisted in the database.
@model: The class of the Django model.
@n: number of instances to be created with the given configuration. Default is 1.
@lesson: use a custom lesson to build the model object.
@data_fixture: override DDF_DEFAULT_DATA_FIXTURE configuration. Default is SequentialDataFixture().
@fill_nullable_fields: override DDF_FILL_NULLABLE_FIELDS global configuration. Default is True.
@ignore_fields: List of fields that will be ignored by DDF. It will be concatenated with the global list DDF_IGNORE_FIELDS. Default is [].
@number_of_laps: override DDF_NUMBER_OF_LAPS global configuration. Default 1.
@shelve: If it is True or a string, the used configuration will be stored in memory. It must be a True, False or a string (named shelve). Default False.
@use_library: Use a previously saved (by @shelve attribute) configuration. It must be a boolean or a string (name used in @shelve). override DDF_USE_LIBRARY global configuration. Default is False.
@n: number of instances to be created with the given configuration. Default is 1.
@validate_models: override DDF_VALIDATE_MODELS global configuration. Default is False.
@validate_args: override DDF_VALIDATE_ARGS global configuration. Default is False.
@print_errors: print on console all instance values if DDF can not generate a valid object with the given configuration.
Expand All @@ -108,13 +109,36 @@ def get(model, shelve=False, n=1, **kwargs):
kwargs = look_up_alias(**kwargs)
d = fixture(**kwargs)
if n == 1:
return d.get(model, shelve=shelve, **kwargs)
return d.get(model, lesson=lesson, **kwargs)
instances = []
for _ in range(n):
instances.append(d.get(model, **kwargs))
instances.append(d.get(model, lesson=lesson, **kwargs))
return instances


def teach(model, lesson=None, **kwargs):
'''
@model: The class of the Django model.
@lesson: Name of custom lesson to be created.
@raise an CantOverrideLesson error if the same model/lesson were called twice.
Sometimes DDF can't create an model instance because the particularities of the model.
The workaround for this is to teach DDF how to create it.
Basically, we use the same object creation approach that will be saved as a template
for the next DDF calls.
Use this method to teach DDF how to create an instance.
New metaphor for the shelve/library feature.
`Shelve` becomes `Teach`
`Library` becomes `Lessons`
'''
kwargs = look_up_alias(**kwargs)
d = fixture(**kwargs)
return d.teach(model, lesson=lesson, **kwargs)


# Shortcuts
N = new
G = get
Expand Down
98 changes: 54 additions & 44 deletions django_dynamic_fixture/ddf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ class PendingField(Exception):
"Internal exception to control pending fields when using Copier."


class CantOverrideLesson(Exception):
"Override a lesson is an anti-pattern and will turn your test suite very hard to understand."


def _validate_model(model_class):
if not is_model_class(model_class):
raise InvalidReceiverError(model_class, 'Invalid model')
Expand Down Expand Up @@ -203,6 +207,8 @@ def get_instance(cls):
def add_configuration(self, model_class, kwargs, name=None):
if name in [None, True]:
name = self.DEFAULT_KEY
if model_class in self.configs and name in self.configs[model_class]:
raise CantOverrideLesson('A lesson {} has already been saved for the model {}'.format(name, model_class))
model_class_config = self.configs.setdefault(model_class, {})
model_class_config[name] = kwargs

Expand All @@ -212,11 +218,11 @@ def get_configuration(self, model_class, name=None):
# copy is important because this dict will be updated every time in the algorithm.
config = self.configs.get(model_class, {})
if name != self.DEFAULT_KEY and name not in config.keys():
raise InvalidConfigurationError('There is no shelved configuration for model %s with the name "%s"' % (get_unique_model_name(model_class), name))
raise InvalidConfigurationError('There is no lesson for model %s with the name "%s"' % (get_unique_model_name(model_class), name))
return config.get(name, {}).copy() # default configuration never raises an error

def clear(self):
"Remove all shelved configurations of the library."
"Remove all lessons of the library. Util for the DDF tests."
self.configs = {}

def clear_configuration(self, model_class):
Expand All @@ -230,17 +236,16 @@ class DynamicFixture(object):
Responsibility: create a valid model instance according to the given configuration.
"""

_DDF_CONFIGS = ['fill_nullable_fields', 'ignore_fields', 'data_fixture', 'number_of_laps', 'use_library',
_DDF_CONFIGS = ['fill_nullable_fields', 'ignore_fields', 'data_fixture', 'number_of_laps',
'validate_models', 'validate_args', 'print_errors']

def __init__(self, data_fixture, fill_nullable_fields=True, ignore_fields=[], number_of_laps=1, use_library=False,
def __init__(self, data_fixture, fill_nullable_fields=True, ignore_fields=[], number_of_laps=1,
validate_models=False, validate_args=False, print_errors=True, model_path=[], debug_mode=False, **kwargs):
"""
:data_fixture: algorithm to fill field data.
:fill_nullable_fields: flag to decide if nullable fields must be filled with data.
:ignore_fields: list of field names that must not be filled with data.
:number_of_laps: number of laps for each cyclic dependency.
:use_library: flag to decide if DDF library will be used to load default fixtures.
:validate_models: flag to decide if the model_instance.full_clean() must be called before saving the object.
:validate_args: flag to enable field name validation of custom fixtures.
:print_errors: flag to determine if the model data must be printed to console on errors. For some scripts is interesting to disable it.
Expand All @@ -255,7 +260,6 @@ def __init__(self, data_fixture, fill_nullable_fields=True, ignore_fields=[], nu
# extend ignore_fields with globally declared ignore_fields
self.ignore_fields.extend(DDF_IGNORE_FIELDS)
self.number_of_laps = number_of_laps
self.use_library = use_library
# other ddfs configs
self.validate_models = validate_models
self.validate_args = validate_args
Expand Down Expand Up @@ -347,7 +351,6 @@ def _process_foreign_key(self, model_class, field, persist_dependencies):
fill_nullable_fields=self.fill_nullable_fields,
ignore_fields=ignore_fields,
number_of_laps=self.number_of_laps,
use_library=self.use_library,
validate_models=self.validate_models,
validate_args=self.validate_args,
print_errors=self.print_errors,
Expand Down Expand Up @@ -426,61 +429,55 @@ def _validate_kwargs(self, model_class, kwargs):
if not model_has_the_field(model_class, field_name):
raise InvalidConfigurationError('Field "%s" does not exist.' % field_name)

def _configure_params(self, model_class, shelve, named_shelve, **kwargs):
def _configure_params(self, model_class, lesson, **kwargs):
"""
1) validate kwargs
2) load default fixture from DDF library. Store default fixture in DDF library.
3) Load fixtures defined in F attributes.
"""
if self.validate_args:
self._validate_kwargs(model_class, kwargs)

# load ddf_setup.py of the model application
app_name = get_app_name_of_model(model_class)
if app_name not in _LOADED_DDF_SETUP_MODULES:
full_module_name = '%s.tests.ddf_setup' % app_name
try:
_LOADED_DDF_SETUP_MODULES.append(app_name)
import_module(full_module_name)
except ImportError:
pass # ignoring if module does not exist
except Exception as e:
six.reraise(InvalidDDFSetupError, InvalidDDFSetupError(e), sys.exc_info()[2])

library = DDFLibrary.get_instance()
if shelve: # shelving before use_library property: do not twist two different configurations (anti-pattern)
for field_name in kwargs.keys():
if field_name in self._DDF_CONFIGS:
continue
field = get_field_by_name_or_raise(model_class, field_name)
fixture = kwargs[field_name]
if field.unique and not (isinstance(fixture, (DynamicFixture, Copier, DataFixture)) or callable(fixture)):
raise InvalidConfigurationError('It is not possible to store static values for fields with unique=True (%s). Try using a lambda function instead.' % get_unique_field_name(field))
library.add_configuration(model_class, kwargs, name=shelve)
if self.use_library:
# load ddf_setup.py of the model application
app_name = get_app_name_of_model(model_class)
if app_name not in _LOADED_DDF_SETUP_MODULES:
full_module_name = '%s.tests.ddf_setup' % app_name
try:
_LOADED_DDF_SETUP_MODULES.append(app_name)
import_module(full_module_name)
except ImportError:
pass # ignoring if module does not exist
except Exception as e:
six.reraise(InvalidDDFSetupError, InvalidDDFSetupError(e), sys.exc_info()[2])
configuration_default = library.get_configuration(model_class, name=DDFLibrary.DEFAULT_KEY)
configuration_custom = library.get_configuration(model_class, name=named_shelve)
configuration = {}
configuration.update(configuration_default) # always use default configuration
configuration = {}
# 1. Load the default/global lesson for the model.
configuration_default = library.get_configuration(model_class, name=DDFLibrary.DEFAULT_KEY)
configuration.update(configuration_default) # always use default configuration
# 2. Load a custom lesson for the model.
if lesson:
configuration_custom = library.get_configuration(model_class, name=lesson)
configuration.update(configuration_custom) # override default configuration
configuration.update(kwargs) # override shelved configuration with current configuration
else:
configuration = kwargs
# 3. Load the custom `kwargs` attributes.
configuration.update(kwargs) # override the configuration with current configuration
configuration.update(self.kwargs) # Used by F: kwargs are passed by constructor, not by get.

return configuration

def new(self, model_class, shelve=False, named_shelve=None, persist_dependencies=True, **kwargs):
def new(self, model_class, lesson=None, persist_dependencies=True, **kwargs):
"""
Create an instance filled with data without persist it.
1) validate all kwargs match Model.fields.
2) validate model is a model.Model class.
3) Iterate model fields: for each field, fill it with data.
:shelve: the current configuration will be stored in the DDF library. It can be True or a string (named shelve).
:named_shelve: restore configuration saved in DDF library with a name.
:lesson: the lesson that will be used to create the model instance, if exists.
:persist_dependencies: tell if internal dependencies will be saved in the database or not.
"""
if self.debug_mode:
LOGGER.debug('>>> [%s] Generating instance.' % get_unique_model_name(model_class))
configuration = self._configure_params(model_class, shelve, named_shelve, **kwargs)
configuration = self._configure_params(model_class, lesson, **kwargs)
instance = model_class()
if not is_model_class(instance):
raise InvalidModelError(get_unique_model_name(model_class))
Expand Down Expand Up @@ -574,14 +571,13 @@ def _save_the_instance(self, instance):
for field in self.fields_to_disable_auto_now_add:
enable_auto_now_add(field)

def get(self, model_class, shelve=False, named_shelve=None, **kwargs):
def get(self, model_class, lesson=None, **kwargs):
"""
Create an instance with data and persist it.
:shelve: the current configuration will be stored in the DDF library.
:named_shelve: restore configuration saved in DDF library with a name.
:lesson: a custom lesson that will be used to create the model object.
"""
instance = self.new(model_class, shelve=shelve, named_shelve=named_shelve, **kwargs)
instance = self.new(model_class, lesson=lesson, **kwargs)
if is_model_abstract(model_class):
raise InvalidModelError(get_unique_model_name(model_class))
try:
Expand Down Expand Up @@ -615,3 +611,17 @@ def get(self, model_class, shelve=False, named_shelve=None, **kwargs):
except Exception as e:
six.reraise(InvalidManyToManyConfigurationError, InvalidManyToManyConfigurationError(get_unique_field_name(field), e), sys.exc_info()[2])
return instance

def teach(self, model_class, lesson=None, **kwargs):
"""
@raise an CantOverrideLesson error if the same model/lesson were called twice.
"""
library = DDFLibrary.get_instance()
for field_name in kwargs.keys():
if field_name in self._DDF_CONFIGS:
continue
field = get_field_by_name_or_raise(model_class, field_name)
fixture = kwargs[field_name]
if field.unique and not (isinstance(fixture, (DynamicFixture, Copier, DataFixture)) or callable(fixture)):
raise InvalidConfigurationError('It is not possible to store static values for fields with unique=True (%s). Try using a lambda function instead.' % get_unique_field_name(field))
library.add_configuration(model_class, kwargs, name=lesson)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from datetime import datetime
import six

from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.test import TestCase

Expand All @@ -13,6 +12,7 @@

try:
import psycopg2
from django.contrib.postgres.fields import ArrayField

class PostgresDataFixtureTestMixin(object):
def test_arrayfield_integer_config(self):
Expand Down Expand Up @@ -60,5 +60,5 @@ class CustomFixture(UniqueRandomDataFixture, PostgresFixtureMixin):
pass
self.fixture = CustomFixture()

except ImportError:
except (ImportError, ModuleNotFoundError):
print('Skipping Postgres tests because psycopg2 has not been installed.')
1 change: 0 additions & 1 deletion django_dynamic_fixture/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,5 @@ def get_boolean_config(config_name, default=False):
DDF_FILL_NULLABLE_FIELDS = get_boolean_config('DDF_FILL_NULLABLE_FIELDS', default=False)
DDF_VALIDATE_MODELS = get_boolean_config('DDF_VALIDATE_MODELS', default=False)
DDF_VALIDATE_ARGS = get_boolean_config('DDF_VALIDATE_ARGS', default=False)
DDF_USE_LIBRARY = get_boolean_config('DDF_USE_LIBRARY', default=True)
DDF_DEBUG_MODE = get_boolean_config('DDF_DEBUG_MODE', default=False)

0 comments on commit 4d60cd5

Please sign in to comment.