I have a model Profile
which has a one-to-one field with a User
:
from django.db import models
class Profile(models.Model):
user = models.OneToOneField(User, related_name='user_profile', on_delete=models.CASCADE)
I'm trying to update the User
model with a __getattr__
method that delegates to the Profile
model, similar to http://blog.thedigitalcatonline.com/blog/2014/08/20/python-3-oop-part-3-delegation-composition-and-inheritance/#enter-the-composition:
from django.contrib.auth.models import User
def user__getattr__(self, attr):
return getattr(self.user_profile, attr)
User.add_to_class('__getattr__', user__getattr__)
This seems to work as expected. For example, one of the fields defined on the Profile
model is a timezone
, and now I can access it like
In [4]: from lucy_web.models import *
In [5]: User.objects.first().timezone
Out[5]: 'America/Los_Angeles'
The problem occurs when I try to generate users using factory_boy
. I have a UserFactory
with a RelatedFactory
referring to the ProfileFactory
:
import factory
from lucy_web.models import User
from .profile_factory import ProfileFactory
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
user_profile = factory.RelatedFactory(ProfileFactory, 'user')
@classmethod
def _create(cls, model_class, *args, **kwargs):
"""Override the default ``_create`` with create_user."""
manager = cls._get_manager(model_class)
# The default would use ``manager.create(*args, **kwargs)``
return manager.create_user(*args, **kwargs)
The ProfileFactory
is quite simple, similar to
class ProfileFactory(factory.django.DjangoModelFactory):
class Meta:
model = Profile
The problem is that if I now try to create a user using the UserFactory
, I get an infinite recursion error:
(lucy-web-CVxkrCFK) bash-3.2$ python manage.py shell
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from lucy_web.test_factories import *
In [2]: UserFactory()
(0.034) SELECT t.oid, typarray FROM pg_type t JOIN pg_namespace ns ON typnamespace = ns.oid WHERE typname = 'hstore'; args=None
(0.001) SELECT typarray FROM pg_type WHERE typname = 'citext'; args=None
(0.073) INSERT INTO "auth_user" ("password", "last_login", "is_superuser", "username", "first_name", "last_name", "email", "is_staff", "is_active", "date_joined") VALUES ('pbkdf2_sha256$100000$QuDq3QCL8zp7$Ru1O4K6I/KaMZZXDj2WVY8TV8/7rQNIL9OYL+1hWvTI=', NULL, false, '[email protected]', 'Jeffrey', 'Kim', '[email protected]', false, true, '2018-07-31T21:43:35.374902+00:00'::timestamptz) RETURNING "auth_user"."id"; args=('pbkdf2_sha256$100000$QuDq3QCL8zp7$Ru1O4K6I/KaMZZXDj2WVY8TV8/7rQNIL9OYL+1hWvTI=', None, False, '[email protected]', 'Jeffrey', 'Kim', '[email protected]', False, True, datetime.datetime(2018, 7, 31, 21, 43, 35, 374902, tzinfo=<UTC>))
(0.002) SELECT "lucy_web_profile"."id", "lucy_web_profile"."created_at", "lucy_web_profile"."updated_at", "lucy_web_profile"."user_id", "lucy_web_profile"."using_app", "lucy_web_profile"."phone", "lucy_web_profile"."phone_country", "lucy_web_profile"."street", "lucy_web_profile"."street2", "lucy_web_profile"."city", "lucy_web_profile"."state", "lucy_web_profile"."country", "lucy_web_profile"."zip_code", "lucy_web_profile"."timezone", "lucy_web_profile"."phone_type", "lucy_web_profile"."alternate_email", "lucy_web_profile"."activation_code" FROM "lucy_web_profile" WHERE "lucy_web_profile"."user_id" = 2159; args=(2159,)
---------------------------------------------------------------------------
RecursionError Traceback (most recent call last)
<ipython-input-2-96e87501585e> in <module>()
----> 1 UserFactory()
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/factory/base.py in __call__(cls, **kwargs)
44 return cls.build(**kwargs)
45 elif cls._meta.strategy == enums.CREATE_STRATEGY:
---> 46 return cls.create(**kwargs)
47 elif cls._meta.strategy == enums.STUB_STRATEGY:
48 return cls.stub(**kwargs)
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/factory/base.py in create(cls, **kwargs)
561 def create(cls, **kwargs):
562 """Create an instance of the associated class, with overriden attrs."""
--> 563 return cls._generate(enums.CREATE_STRATEGY, kwargs)
564
565 @classmethod
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/factory/base.py in _generate(cls, strategy, params)
498
499 step = builder.StepBuilder(cls._meta, params, strategy)
--> 500 return step.build()
501
502 @classmethod
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/factory/builder.py in build(self, parent_step, force_sequence)
277 step=step,
278 args=args,
--> 279 kwargs=kwargs,
280 )
281
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/factory/base.py in instantiate(self, step, args, kwargs)
312 return self.factory._build(model, *args, **kwargs)
313 elif step.builder.strategy == enums.CREATE_STRATEGY:
--> 314 return self.factory._create(model, *args, **kwargs)
315 else:
316 assert step.builder.strategy == enums.STUB_STRATEGY
~/Documents/Dev/lucy2/lucy-web/lucy_web/test_factories/user_factory.py in _create(cls, model_class, *args, **kwargs)
48 manager = cls._get_manager(model_class)
49 # The default would use ``manager.create(*args, **kwargs)``
---> 50 return manager.create_user(*args, **kwargs)
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/django/contrib/auth/models.py in create_user(self, username, email, password, **extra_fields)
148 extra_fields.setdefault('is_staff', False)
149 extra_fields.setdefault('is_superuser', False)
--> 150 return self._create_user(username, email, password, **extra_fields)
151
152 def create_superuser(self, username, email, password, **extra_fields):
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/django/contrib/auth/models.py in _create_user(self, username, email, password, **extra_fields)
142 user = self.model(username=username, email=email, **extra_fields)
143 user.set_password(password)
--> 144 user.save(using=self._db)
145 return user
146
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/django/contrib/auth/base_user.py in save(self, *args, **kwargs)
71
72 def save(self, *args, **kwargs):
---> 73 super().save(*args, **kwargs)
74 if self._password is not None:
75 password_validation.password_changed(self._password, self)
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/django/db/models/base.py in save(self, force_insert, force_update, using, update_fields)
727
728 self.save_base(using=using, force_insert=force_insert,
--> 729 force_update=force_update, update_fields=update_fields)
730 save.alters_data = True
731
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/django/db/models/base.py in save_base(self, raw, force_insert, force_update, using, update_fields)
767 post_save.send(
768 sender=origin, instance=self, created=(not updated),
--> 769 update_fields=update_fields, raw=raw, using=using,
770 )
771
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/django/dispatch/dispatcher.py in send(self, sender, **named)
176 return [
177 (receiver, receiver(signal=self, sender=sender, **named))
--> 178 for receiver in self._live_receivers(sender)
179 ]
180
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/django/dispatch/dispatcher.py in <listcomp>(.0)
176 return [
177 (receiver, receiver(signal=self, sender=sender, **named))
--> 178 for receiver in self._live_receivers(sender)
179 ]
180
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/auditlog/receivers.py in log_create(sender, instance, created, **kwargs)
14 """
15 if created:
---> 16 changes = model_instance_diff(None, instance)
17
18 log_entry = LogEntry.objects.log_create(
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/auditlog/diff.py in model_instance_diff(old, new)
133 for field in fields:
134 old_value = get_field_value(old, field)
--> 135 new_value = get_field_value(new, field)
136
137 if old_value != new_value:
~/.local/share/virtualenvs/lucy-web-CVxkrCFK/lib/python3.7/site-packages/auditlog/diff.py in get_field_value(obj, field)
76 else:
77 try:
---> 78 value = smart_text(getattr(obj, field.name, None))
79 except ObjectDoesNotExist:
80 value = field.default if field.default is not NOT_PROVIDED else None
~/Documents/Dev/lucy2/lucy-web/lucy_web/models/user.py in user__getattr__(self, attr)
86 In accordance with PEP 562, we cannot name it '__getattr__' here as that refers to the module __getattr__ method.
87 """
---> 88 return getattr(self.user_profile, attr)
89 # if self.user_profile:
90 # return self.user_profile.__getattribute__(attr)
... last 1 frames repeated, from the frame below ...
~/Documents/Dev/lucy2/lucy-web/lucy_web/models/user.py in user__getattr__(self, attr)
86 In accordance with PEP 562, we cannot name it '__getattr__' here as that refers to the module __getattr__ method.
87 """
---> 88 return getattr(self.user_profile, attr)
89 # if self.user_profile:
90 # return self.user_profile.__getattribute__(attr)
RecursionError: maximum recursion depth exceeded
I'm a bit nonplussed as to why only the UserFactory
is giving rise to this error, whereas 'normal' attribute lookups seem to work fine. Any ideas on how to fix this?
This does not seem to be related to factory_boy: with your factories, the code is strictly equivalent to:
user = User.objects.create_user()
profile = Profile.objects.create(user=user)
However, from your stacktrace, it seems that you have installed a signal handler and connected it to your post_save()
signal (in auditlog/receivers.py
).
That signal handler seems to be computing the list of updated fields; and thus tries to access some fields of the User
object.
However, since the Profile
has not been created yet, the calls to your custom __getattr__
fail.
The proper way to fix this would be to alter your user__getattr__
to check properly whether self.user_profile
is defined before attempting to read from it; the following is an example of code that could work in your case:
def user__getattr__(user, attr):
try:
profile = user.user_profile
except ObjectDoesNotExist: # Generated by Django if the lookup fails
raise
return getattr(profile, attr)
Note: although you could also "mute" the signal using factory_boy helpers, this would only hide the problem: from a database point of view, there is no guarantee that a UserProfile
exists for a given User
object — your code should be prepared to handle that case.