Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MockOptions, get_or_create_with defaults #36

Merged
merged 2 commits into from
Mar 5, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we remove .cls now that we have . model that covers everything? It has a _meta with all the fields required for create and get_or_create etc.

Copy link
Contributor Author

@szykin szykin Mar 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it, but had to do some tweaks with MockModel class.

    def __call__(self, *args, **kwargs):
        return MockModel(*args, **kwargs)

Also create() now fills fields that are not specified in kwargs with None.

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))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we raise in this case a TypeError: 'foo' is an invalid keyword argument for this function, like Django, see django/db/models/base.py - line 555?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we can!

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