I'm trying to override a custom Django model property via factory_boy for testing purposes. But it seems like it is simply taking the default behavior of the model. is factory boy not able to change the default behaviour of custom attributes?
Here is a basic test I wrote:
models.py:
class Session(models.Model):
name = models.CharField(max_length=100)
#property
def foo(self):
return method_not_callable_in_testing()
def method_not_callable_in_testing():
return 42
SessionFactory.py:
class SessionFactory(factory.django.DjangoModelFactory):
class Meta:
model = Session
name = "session"
foo = 1337
tests.py:
class TestSession(TestCase):
def test_custom_attribute_overwritten_by_factoryboy(self):
session = SessionFactory.create()
self.assertEquals(session.foo, 1337)
When running the tests I get the following error:
F
======================================================================
FAIL: test_custom_attribute_overwritten_by_factoryboy (bar.tests.TestSession)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/sh4ke/repos/foo/bar/tests.py", line 10, in test_custom_attribute_overwritten_by_factoryboy
self.assertEquals(session.foo, 1337)
AssertionError: 42 != 1337
tl;dr
factory_boy can change "custom attributes", (which I assume you mean ones that are not django Fields), but it can't change read-only attributes, which is what you are trying to do.
More info
Your problem is that you are trying to use the factory to set an attribute for a read-only property. Roughly, the combination of factory_boy and django means that the call to SessionFactory() is doing:
session = Session()
session.foo = 1337
But I wouldn't expect this to work because unless you declare the #foo.setter method #property assumes you wanted a read-only property, and so raises attribute error when you attempt to set it.
The weird bit is why you don't see that error, and it turns out that django quietly suppresses it! It sees that your model has the foo attribute by doing getattr(session, 'foo'), but it has wrapped the whole thing including the setattr inside a try: except AttributeError:, so while it detects and raises an attempt to set an attribute that doesn't exist at all, it also catches and quietly ignores the failed case of an attempt to set a read-only property.
I suspect this is a bug... so will probably try and raise it with the django community.
Related
I want to separate some logic from model and group it in a property, just like Django does with model managers (object property). I fact, something like ForeignKey but without database representation.
I have something like this:
class Remote(object):
def __init(self, *args, **kwargs)
self.post = ... # how to get to post instance?
def synchronize(self):
# this function requires Post object access
print(self.post.name)
class Post(models.Model):
name = models.CharField(max_length=100)
remote = Remote()
...
for post in Post.objects.all():
post.remote.synchronize()
Question
How to modify above code to get access to Post object in Remote object?
Additional question
Is it possible to determine if Remote object has been called from Post instance (post.remote... – like above) or Post class (Post.remote...)?
What you want here can be achieved with descriptors.
In order for it to work, you need to define a __get__ method in your class that you want to be accessible as an attribute of another class.
A simple example for your case will look like this:
class Remote:
def __init__(self, post)
self.post = post
def synchronize(self):
print(self.post.name)
class RemoteDescriptor:
def __get__(self, obj):
if not obj:
return self
remote = getattr(obj, '_remote', None)
if not remote:
remote = Remote(obj)
obj._remote = remote
return remote
class Post(models.Model):
name = models.CharField(max_length=100)
remote = RemoteDescriptor()
Explanation:
In the above code, every time you call an attribute remote of your Post model, __get__ method of the RemoteDescriptor will be invoked. First check for obj is to make sure that descriptor is called from other object, not directly. Two classes Remote and RemoteDescriptor are needed here in order for you to be able to add custom methods inside your descriptor accessible using dot (e.g. post.remote.calculate())
Note also that I am placing the instance of Remote to the dict of Post on first invokation and on all subsequent calls, object will be returned from there.
You should also check a great article on descriptors on RealPython.
I'm building a factory with factory_boy that generates a django model. I would like to see what arguments the user inputs inline. My factory itself looks like this
class SomeFactory(factory.django.DjangoModelFactory):
name = factory.Sequence(lambda n: 'Instance #{}'.format(n))
some_other_thing = factory.SubFactory(SomeOtherFactory)
class Meta:
model = SomeModel
Now the user could say s = SomeFactory() and it would work fine, but I want to detect if the user input their own argument. For instance, to tell if the user passed in their own name, as in s = SomeFactory(name='Matt')
What I've tried so far is
Writing my own __init__ function in the SomeFactory class
This gets mysteriously overwritten and is neither called when I call s = SomeFactory(), nor when I call s.__init__()
Same goes for overwriting the __new__ method
Overwriting the poorly named _adjust_kwargs
This gives me all fields as kwargs, not just the ones the user defined. For instance, calling s = SomeFactory(name='Matt'), I would get a kwargs dict with keys for name and some_other_thing, which makes it impossible to tell input their own argument or not
Overwriting _create
Still encounter the same problem with overwriting _adjust_kwargs, in that kwargs doesn't contain the original kwargs, but rather all of the arguments
I think a lot of the functionality I'm after is black-boxed inside of factory_boy's StepBuilder (I suspect it's in the instantiate method) but I have no idea how to modify it to do what I want.
Does anyone have any thoughts on how to figure out which kwargs were set originally in the call to s = SomeFactory()? I.e. determine that if I said s = SomeFactory(name='Matt'), that the user manually set the name?
Thanks!
Update: I'm running django version 1.11.2, factory_boy version 2.8.1, and python version 3.5.2
You can override the create method to only get the user kwargs.
A full example would be something like this:
from django.contrib.auth.models import User
import factory
class UserFactory(factory.DjangoModelFactory):
username = factory.Sequence(
lambda n: 'test'
)
email = factory.Sequence(lambda n: 'user{0}#example.com'.format(n))
class Meta:
model = User
#classmethod
def create(cls, **kwargs):
# here you'll only have the kwargs that were entered manually
print(str(kwargs))
return super(UserFactory, cls).create(**kwargs)
So when I call it:
In [2]: UserFactory(username='foobar')
{'username': 'foobar'}
Out[2]: <User: foobar>
If you want to catch kwargs for other build strategies than create, you would also need to do this for the stub and build method.
I'm working on a Django 1.8.2 project.
This project has multiple Django applications.
Application app_a has class MyClassA as follows:
class MyClassA(models.Model):
name = models.CharField(max_length=50, null=True, blank=True)
#staticmethod
def my_static_method():
ret_val = MyClassA.objects.filter()
return "World"
Application app_b has class MyClassB as follows:
class MyClassB(models.Model):
name = models.CharField(max_length=50, null=False, blank=False)
def my_method(self, arg1=MyClassA.my_static_method()):
return "Hello"
When I run manage.py test, it works fine.
However, then I change MyClassA.my_static_method() to the following:
#staticmethod
def my_static_method():
ret_val = MyClassA.objects.filter(name=None)
return "World"
When I do that, and then run manage.py test, it fails with the following error:
File "my-virtual-env/lib/python2.7/site-packages/django/apps/registry.py", line 131, in check_models_ready
raise AppRegistryNotReady("Models aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.
Why is this happening? How do I fix this?
The only change that I made is adding the filter value name=None.
In Django you must not run queries at import time. (See here for details.)
Because default argument values in Python are evaluated when the function is defined (as opposed to when it is called), your MyClassB.my_method() definition is calling MyClassA.my_static_method(), which attempts to run a query.
Why did you see an error in one version of your code and not the other? One of them is evaluating the query (i.e. trying to access the database) and one isn't, for whatever reason. It's your responsibility to make sure that nothing you do at import time tries to access the database.
If your goal is for that default argument to be evaluated on each call, the standard idiom in Python is:
def my_method(self, arg1=None):
if arg1 is None:
arg1 = MyClassA.my_static_method()
In this case you will get an error only if you make a mistake in the manager function. I tried the same and it worked for me. but I got the same error when I made an error in the filter() .like MyClassA.objects.filter(a_field_not_in_the_model=None).
I think you have to re check your original code. check if your model is ok.
I created the empty abstract class AbstractStorage and inherited the Storage class from it:
import abc
import pymongo as mongo
host = mongo.MongoClient()
print(host.alive()) # True
class AbstractStorage(metaclass=abc.ABCMeta):
pass
class Storage(AbstractStorage):
dbh = host
def __init__(self):
print('__init__')
Storage()
I expected the output to be
True
__init__
however, the one I'm getting is
True
Traceback (most recent call last):
File "/home/vaultah/run.py", line 16, in <module>
Storage()
TypeError: Can't instantiate abstract class Storage with abstract methods dbh
The problem (apparently) goes away if I remove metaclass=abc.ABCMeta (so that AbstractStorage becomes an ordinary class) and/or if I set dbh to some other value.
What's going on here?
This isn't really a problem with ABCs, it's a problem with PyMongo. There is an issue about it here. It seems that pymongo overrides __getattr__ to return some sort of database class. This means that host.__isabstractmethod__ returns a Database object, which is true in a boolean context. This cause ABCMeta to believe that host is an abstract method:
>>> bool(host.__isabstractmethod__)
True
The workaround described in the issue report is to manually set host.__isabstractmethod__ = False on your object. The last comment on the issue suggests a fix has been put in for pymongo 3.0.
Short Version
mongo.MongoClient returns an object that appears to be (is?) an abstract method, which you then assign to the dbh field in Storage. This makes Storage an abstract class, so instantiating it raises a TypeError.
Note that I don't have pymongo, so I can't tell you anything more about MongoClient than how it gets treated by ABCMeta.
Long Version
The ABCMeta.__new__ method looks inside each field of the new class it's creating. Any field that itself has a True (or "true-like") __isabstractmethod__ field is considered an abstract method. If a class has any non-overridden abstract methods, the whole class is considered abstract, so any attempt to instantiate it is an error.
From an earlier version of the standard library's abc.py:
def __new__(mcls, name, bases, namespace):
cls = super().__new__(mcls, name, bases, namespace)
# Compute set of abstract method names
abstracts = {name
for name, value in namespace.items()
if getattr(value, "__isabstractmethod__", False)}
# ...
cls.__abstractmethods__ = frozenset(abstracts)
# ...
This is not mentioned in the abc.ABCMeta class docs, but a bit lower, under the #abc.abstractmethod decorator:
In order to correctly interoperate with the abstract base class machinery, the descriptor must identify itself as abstract using __isabstractmethod__. In general, this attribute should be True if any of the methods used to compose the descriptor are abstract.
Example
I created a bogus "abstract-looking" class with an __isabstractmethod__ attribute, and two supposedly-concrete subclasses of AbstractStorage. You'll see that one produces the exact error you're getting:
#!/usr/bin/env python3
import abc
# I don't have pymongo, so I have to fake it. See CounterfeitAbstractMethod.
#import pymongo as mongo
class CounterfeitAbstractMethod():
"""
This class appears to be an abstract method to the abc.ABCMeta.__new__
method.
Normally, finding an abstract method in a class's namespace means
that class is also abstract, so instantiating that class is an
error.
If a class derived from abc.ABCMeta has an instance of
CounterfeitAbstractMethod as a value anywhere in its namespace
dictionary, any attempt to instantiate that class will raise a
TypeError: Can't instantiate abstract class <classname> with
abstract method <fieldname>.
"""
__isabstractmethod__ = True
class AbstractStorage(metaclass=abc.ABCMeta):
def __init__(self):
"""
Do-nothing initializer that prints the name of the (sub)class
being initialized.
"""
print(self.__class__.__name__ + ".__init__ executing.")
return
class ConcreteStorage(AbstractStorage):
"""
A concrete class that also _appears_ concrete to abc.ABCMeta. This
class can be instantiated normally.
"""
whatever = "Anything that doesn't appear to be an abstract method will do."
class BogusStorage(AbstractStorage):
"""
This is (supposedly) a concrete class, but its whatever field appears
to be an abstract method, making this whole class abstract ---
abc.ABCMeta will refuse to construct any this class.
"""
#whatever = mongo.MongoClient('localhost', 27017)
whatever = CounterfeitAbstractMethod()
def main():
"""
Print details of the ConcreteStorage and BogusStorage classes.
"""
for cls in ConcreteStorage, BogusStorage:
print(cls.__name__ + ":")
print(" whatever field: " + str(cls.whatever))
print(" abstract methods: " + str(cls.__abstractmethods__))
print(" Instantiating...")
print(" ", end="")
# KABOOM! Instantiating BogusStorage will raise a TypeError,
# because it appears to be an _abstract_ class.
instance = cls()
print(" instance: " + str(instance))
print()
return
if "__main__" == __name__:
main()
Running this produces:
$ ./storage.py
ConcreteStorage:
whatever field: Anything that doesn't appear to be an abstract method will do.
abstract methods: frozenset()
Instantiating...
ConcreteStorage.__init__ executing.
instance: <__main__.ConcreteStorage object at 0x253afd0>
BogusStorage:
whatever field: <__main__.CounterfeitAbstractMethod object at 0x253ad50>
abstract methods: frozenset({'whatever'})
Instantiating...
Traceback (most recent call last):
File "./storage.py", line 75, in <module>
main()
File "./storage.py", line 68, in main
instance = cls()
TypeError: Can't instantiate abstract class BogusStorage with abstract methods whatever
I am trying to mock a chained call on the Djangos model.Manager() class. For now I want to mock the values() and filter() method.
To test that I created a little test project:
Create a virtual environment
Run pip install django mock mock-django nose django-nose
Create a project django-admin.py startproject mocktest
Create an app manage.py startapp mockme
Add django_nose and mocktest.mockme to INSTALLED_APPS (settings.py)
Add TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' to settings.py
To verfiy that everything is setup correctly I ran manage.py test. One test is run, the standard test Django creates when you create an app.
Next thing I did was to create a very simple model.
mockme/models.py
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=50)
Next thing I did was to create a simple function that uses MyModel. That's the function I want to test later.
mockme/functions.py
from models import MyModel
def chained_query():
return MyModel.objects.values('name').filter(name='Frank')
Nothing special is happening here. The function is filtering the MyModel objects to find all instances where name='Frank'. The call to values() will return a ValuesQuerySet which will only contain the name field of all found MyModel instances.
mockme/tests.py
import mock
from django.test import TestCase
from mocktest.mockme.models import MyModel
from mocktest.mockme.functions import chained_query
from mock_django.query import QuerySetMock
class SimpleTest(TestCase):
def test_chained_query(self):
# without mocked queryset the result should be 0
result = chained_query()
self.assertEquals(result.count(), 0)
# now try to mock values().filter() and reeturn
# one 'Frank'.
qsm = QuerySetMock(MyModel, MyModel(name='Frank'))
with mock.patch('django.db.models.Manager.filter', qsm):
result = chained_query()
self.assertEquals(result.count(), 1)
The first assertEquals will evaluate as successful. No instances are returned since the model Manager is not mocked yet. When the second assertEquals is called I expect result to contain the MyModel instance I added as return value to the QuerySetMock:
qsm = QuerySetMock(MyModel, MyModel(name='Frank'))
I mocked the filter() method and not the values() method since I found it'll be the last evaluated call, though I am not sure.
The test will fail because the second result variable won't contain any MyModel instances.
To be sure that the filter() method is really mocked I added a "debug print" statement:
from django.db import models
print models.Manager.filter
which returned:
<SharedMock name='mock.iterator' id='4514208912'>
What am I doing wrong?
Try this:
import mock
from mocktest.mockme.models import MyModel
class SimpleTest(TestCase):
def test_chained_query(self):
my_model_value_mock = mock.patch(MyModel.objects, 'value')
my_model_value_mock.return_value.filter.return_value.count.return_value = 10000
self.assertTrue(my_model_value_mock.return_value.filter.return_value.count.called)
#Gin's answer got me most of the way there, but in my case I'm patching MyModel.objects, and the query I'm mocking looks like this:
MyModel.objects.filter(arg1=user, arg2=something_else).order_by('-something').first()
so this worked for me:
#patch('MyModel.objects')
def test_a_function(mock, a_fixture):
mock.filter.return_value.order_by.return_value.first.return_value = a_fixture
result = the_func_im_testing(arg1, arg2)
assert result == 'value'
Also, the order of the patched attributes matters, and must match the order you're calling them within the tested function.