Skip to content

Commit

Permalink
MockOptions, get_or_create_with defaults
Browse files Browse the repository at this point in the history
get_or_create() and create() now requires MockSet to have 'cls' and 'model' attributes being not None
MockSet() now has cls=MockModel as default cls
  • Loading branch information
szykin committed Mar 3, 2017
1 parent 9736f87 commit 7f64355
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 45 deletions.
4 changes: 4 additions & 0 deletions django_mock_queries/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ class ModelNotSpecified(Exception):

class ArgumentNotSupported(Exception):
pass


class ClsNotSpecified(Exception):
pass
9 changes: 6 additions & 3 deletions django_mock_queries/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from itertools import chain
from mock import Mock, MagicMock, patch, PropertyMock

from .query import MockSet
from .query import MockSet, create_model


def monkey_patch_test_db(disabled_features=None):
Expand Down Expand Up @@ -122,7 +122,9 @@ def __get__(self, instance, owner):
old_instance = old_instance_weak()
if entry is None or old_instance is None:
related = getattr(self.original, 'related', self.original)
related_objects = MockSet(cls=related.field.model)
related_objects = MockSet(
cls=related.field.model,
model=create_model(*[f.attname for f in related.field.model._meta.concrete_fields]))
self.__set__(instance, related_objects)

return related_objects
Expand Down Expand Up @@ -217,7 +219,8 @@ def test_dataset(self):
patchers.append(patch_object(model, 'objects', new_callable=partial(
MockSet,
mock_name=model_name + '.objects',
cls=model)))
cls=model,
model=create_model(*[f.attname for f in model._meta.concrete_fields]))))
for related_object in chain(model._meta.related_objects,
model._meta.many_to_many):
name = related_object.name
Expand Down
69 changes: 49 additions & 20 deletions django_mock_queries/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from .constants import *
from .exceptions import *
from .utils import matches, merge, intersect, get_attribute
from .utils import matches, merge, intersect, get_attribute, validate_mock_set


class MockBase(MagicMock):
Expand All @@ -30,8 +30,9 @@ def MockSet(*initial_items, **kwargs):
'prefetch_related',
'select_for_update'
])
mock_set.cls = clone.cls if clone else kwargs.get('cls', empty_func)
mock_set.cls = clone.cls if clone else kwargs.get('cls', MockModel)
mock_set.count = MagicMock(side_effect=lambda: len(items))
mock_set.model = clone.model if clone else kwargs.get('model', None)
mock_set.__len__ = MagicMock(side_effect=lambda: len(items))

def add(*model):
Expand Down Expand Up @@ -174,9 +175,11 @@ def __iter__():
mock_set.__iter__ = MagicMock(side_effect=__iter__)

def create(**attrs):
validate_mock_set(mock_set)
for k in attrs.keys():
if k not in [f.attname for f in mock_set.model._meta.concrete_fields]:
raise ValueError('MockSet model has no field {}'.format(k))
obj = mock_set.cls(**attrs)
if not obj:
raise ModelNotSpecified()
obj.save(force_insert=True, using=MagicMock())
add(obj)
return obj
Expand All @@ -194,8 +197,13 @@ def get(**attrs):

mock_set.get = MagicMock(side_effect=get)

def get_or_create(**attrs):
results = filter(**attrs)
def get_or_create(defaults=None, **attrs):
if defaults is not None:
validate_mock_set(mock_set)
defaults = defaults or {}
lookup = attrs.copy()
attrs.update(defaults)
results = filter(**lookup)
if not results.exists():
return create(**attrs), True
elif results.count() > 1:
Expand Down Expand Up @@ -255,25 +263,46 @@ def values(*fields):
return mock_set


def MockModel(cls=None, mock_name=None, spec_set=None, **attrs):
mock_attrs = dict(spec=cls, name=mock_name, spec_set=spec_set)
class MockModel(dict):
def __init__(self, *args, **kwargs):
self.save = PropertyMock()
super(MockModel, self).__init__(*args, **kwargs)

def __getattr__(self, item):
return self.get(item, None)

def __setattr__(self, key, value):
self.__setitem__(key, value)

def __hash__(self):
return hash(tuple(sorted(self.items())))

_meta = type('_meta', (object,), dict(
_forward_fields_map={}, fields_map={}, parents={},
concrete_fields=[type('concrete_field', (object,), dict(attname=x)) for x in attrs.keys()]))
@property
def _meta(self):
keys_list = list(self.keys())
keys_list.remove('save')
return MockOptions(*keys_list)

mock_model = MagicMock(**mock_attrs)

if mock_name:
setattr(type(mock_model), '__repr__', MagicMock(return_value=mock_name))
def create_model(*fields):
if len(fields) == 0:
raise ValueError('create_model() is called without fields specified')
return MockModel(**{f: None for f in fields})

for key, value in attrs.items():
setattr(type(mock_model), key, PropertyMock(return_value=value))

setattr(type(mock_model), '_meta', PropertyMock(return_value=_meta))
class MockOptions(object):
def __init__(self, *fields):
for key in ('_forward_fields_map', 'parents', 'fields_map'):
self.__dict__[key] = {}
for key in ('local_concrete_fields', 'concrete_fields', 'fields'):
self.__dict__[key] = []

return mock_model
for field in fields:
for key in ('local_concrete_fields', 'concrete_fields', 'fields'):
self.__dict__[key].append(MockField(field))


def empty_func(*args, **kwargs):
pass
class MockField(object):
def __init__(self, field):
for key in ('name', 'attname'):
self.__dict__[key] = field
8 changes: 8 additions & 0 deletions django_mock_queries/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.exceptions import FieldError

from .constants import *
from .exceptions import *

import django_mock_queries.query

Expand Down Expand Up @@ -95,3 +96,10 @@ def matches(*source, **attrs):
for x in source:
if x not in exclude:
yield x


def validate_mock_set(mock_set):
if mock_set.model is None:
raise ModelNotSpecified()
if mock_set.cls is None:
raise ClsNotSpecified()
102 changes: 93 additions & 9 deletions tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from django.db.models import Q

from django_mock_queries.constants import *
from django_mock_queries.exceptions import ModelNotSpecified, ArgumentNotSupported
from django_mock_queries.query import MockSet, MockModel
from django_mock_queries.exceptions import ModelNotSpecified, ArgumentNotSupported, ClsNotSpecified
from django_mock_queries.query import MockSet, MockModel, create_model
from tests.mock_models import Car, Sedan, Manufacturer


Expand Down Expand Up @@ -450,22 +450,36 @@ def test_query_implements_iterator_on_items(self):
assert [x for x in MockSet(*items)] == items

def test_query_creates_new_model_and_adds_to_set(self):
qs = MockSet(cls=MockModel)

qs = MockSet(model=create_model('foo', 'bar'))
attrs = dict(foo=1, bar='a')
obj = qs.create(**attrs)

obj.save.assert_called_once_with(force_insert=True, using=ANY)
assert obj in [x for x in qs]

for k, v in attrs.items():
assert getattr(obj, k, None) == v

def test_query_create_raises_model_not_specified_when_mock_set_called_without_cls(self):
def test_query_create_raises_model_not_specified_when_mockset_model_is_none(self):
qs = MockSet()
attrs = dict(foo=1, bar='a')
self.assertRaises(ModelNotSpecified, qs.create, **attrs)

def test_query_create_raises_cls_not_specified_when_mockset_cls_is_none(self):
qs = MockSet(
cls=None,
model=create_model('foo', 'bar')
)
attrs = dict(foo=1, bar='a')
self.assertRaises(ClsNotSpecified, qs.create, **attrs)

def test_query_create_raises_value_error_when_kwarg_key_is_not_in_concrete_fields(self):
qs = MockSet(
model=create_model('first', 'second', 'third')
)
attrs = dict(first=1, second=2, third=3, fourth=4)
with self.assertRaises(ValueError):
qs.create(**attrs)

def test_query_gets_unique_match_by_attrs_from_set(self):
item_1 = MockModel(foo=1)
item_2 = MockModel(foo=2)
Expand All @@ -489,15 +503,15 @@ def test_query_get_raises_specific_exception(self):
item_2 = Car(model='pious')
item_3 = Car(model='hummus')

self.mock_set = MockSet(item_1, item_2, item_3, cls=Car)
self.mock_set = MockSet(item_1, item_2, item_3, cls=Car, model=create_model('model'))
self.assertRaises(Car.DoesNotExist, self.mock_set.get, model='clowncar')

def test_filter_keeps_class(self):
item_1 = Car(model='battle')
item_2 = Car(model='pious')
item_3 = Car(model='hummus')

self.mock_set = MockSet(item_1, item_2, item_3, cls=Car)
self.mock_set = MockSet(item_1, item_2, item_3, cls=Car, model=create_model('model'))
filtered = self.mock_set.filter(model__endswith='s')
self.assertRaises(Car.DoesNotExist, filtered.get, model='clowncar')

Expand Down Expand Up @@ -533,13 +547,79 @@ def test_query_get_or_create_creates_new_model_when_no_match(self):
item_2 = MockModel(foo=2)
item_3 = MockModel(foo=3)

qs = MockSet(cls=MockModel)
qs = MockSet(model=create_model('foo'))
qs.add(item_1, item_2, item_3)
obj, created = qs.get_or_create(foo=4)

assert hasattr(obj, 'foo') and obj.foo == 4
assert created is True

def test_query_get_or_create_gets_existing_unique_match_with_defaults(self):
qs = MockSet(
model=create_model('first', 'second', 'third')
)
item_1 = MockModel(first=1)
item_2 = MockModel(second=2)
item_3 = MockModel(third=3)
qs.add(item_1, item_2, item_3)

obj, created = qs.get_or_create(defaults={'first': 3, 'third': 1}, second=2)

assert hasattr(obj, 'second') and obj.second == 2
assert created is False

def test_query_get_or_create_raises_does_multiple_objects_returned_when_more_than_one_match_with_defaults(self):
qs = MockSet(
model=create_model('first', 'second', 'third')
)
item_1 = MockModel(first=1)
item_2 = MockModel(first=1)
item_3 = MockModel(third=3)
qs.add(item_1, item_2, item_3)

qs.add(item_1, item_2, item_3)
with self.assertRaises(MultipleObjectsReturned):
qs.get_or_create(first=1, defaults={'second': 2})

def test_query_get_or_create_creates_new_model_when_no_match_with_defaults(self):
qs = MockSet(
model=create_model('first', 'second', 'third')
)
item_1 = MockModel(first=1)
item_2 = MockModel(second=2)
item_3 = MockModel(third=3)
qs.add(item_1, item_2, item_3)

obj, created = qs.get_or_create(defaults={'first': 3, 'third': 2}, second=1)

assert hasattr(obj, 'first') and obj.first == 3
assert hasattr(obj, 'second') and obj.second == 1
assert hasattr(obj, 'third') and obj.third == 2
assert created is True

def test_query_get_or_create_raises_model_not_specified_with_defaults_when_mockset_model_is_none(self):
qs = MockSet()
item_1 = MockModel(first=1)
item_2 = MockModel(second=2)
item_3 = MockModel(third=3)
qs.add(item_1, item_2, item_3)

with self.assertRaises(ModelNotSpecified):
qs.get_or_create(defaults={'first': 3, 'third': 2}, second=1)

def test_query_get_or_create_raises_cls_not_specified_with_defaults_when_mockset_cls_is_none(self):
qs = MockSet(
cls=None,
model=create_model('first', 'second', 'third')
)
item_1 = MockModel(first=1)
item_2 = MockModel(second=2)
item_3 = MockModel(third=3)
qs.add(item_1, item_2, item_3)

with self.assertRaises(ClsNotSpecified):
qs.get_or_create(defaults={'first': 3, 'third': 2}, second=1)

def test_query_return_self_methods_accept_any_parameters_and_return_instance(self):
qs = MockSet(MockModel(foo=1), MockModel(foo=2))
assert qs == qs.all()
Expand Down Expand Up @@ -620,3 +700,7 @@ def test_length2(self):
n = len(q)

self.assertEqual(2, n)

def test_create_model_raises_value_error_with_zero_arguments(self):
with self.assertRaises(ValueError):
create_model()
14 changes: 1 addition & 13 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from mock import patch, MagicMock
from unittest import TestCase

from django_mock_queries import utils, constants, query
from django_mock_queries import utils, constants


class TestUtils(TestCase):
Expand Down Expand Up @@ -60,18 +60,6 @@ def test_get_attribute_returns_default_value_when_object_is_none(self):
assert value == default_value
assert comparison is None

def test_get_attribute_returns_value_when_spec_set_is_true(self):
obj = query.MockModel(spec_set=True, foo='foo')
value, comparison = utils.get_attribute(obj, 'foo')
assert value == 'foo'
assert comparison is None

def test_getattr_builtin_raises_exception_when_spec_set_is_true(self):
obj = query.MockModel(spec_set=True, foo='foo')
assert getattr(obj, 'bar', None) is None
with self.assertRaises(AttributeError):
getattr(obj, 'bar')

def test_is_match_equality_check_when_comparison_none(self):
result = utils.is_match(1, 1)
assert result is True
Expand Down

0 comments on commit 7f64355

Please sign in to comment.