我有一个模型Profile
,其中有一个User
的一对一字段:
from django.db import models
class Profile(models.Model):
user = models.OneToOneField(User, related_name='user_profile', on_delete=models.CASCADE)
我正尝试使用委托给User
模型的__getattr__
方法更新Profile
模型,类似于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__)
这似乎按预期工作。例如,在Profile
模型上定义的字段之一是timezone
,现在我可以像访问它一样
In [4]: from lucy_web.models import *
In [5]: User.objects.first().timezone
Out[5]: 'America/Los_Angeles'
当我尝试使用factory_boy
生成用户时出现问题。我有一个UserFactory
,其中一个RelatedFactory
指向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)
ProfileFactory
非常简单,类似于
class ProfileFactory(factory.django.DjangoModelFactory):
class Meta:
model = Profile
问题是,如果我现在尝试使用UserFactory
创建用户,则会收到无限递归错误:
(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, 'ypearson-0@davis.biz', 'Jeffrey', 'Kim', 'ashleypham-0@scott.info', 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, 'ypearson-0@davis.biz', 'Jeffrey', 'Kim', 'ashleypham-0@scott.info', 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
对于为什么只有UserFactory
会引起此错误,我却有些困惑,而“普通”属性查找似乎可以正常工作。有关如何解决此问题的任何想法?
答案 0 :(得分:2)
这似乎与factory_boy无关:在您的工厂中,代码严格等同于:
user = User.objects.create_user()
profile = Profile.objects.create(user=user)
但是,从堆栈跟踪看来,您似乎已经安装了信号处理程序并将其连接到post_save()
信号(在auditlog/receivers.py
中)。
那个信号处理器似乎正在计算更新字段的列表;因此尝试访问User
对象的某些字段。
但是,由于尚未创建Profile
,因此对自定义__getattr__
的调用失败。
解决此问题的正确方法是更改user__getattr__
,以在尝试读取之前self.user_profile
进行正确检查;以下是可能适合您的情况的代码示例:
def user__getattr__(user, attr):
try:
profile = user.user_profile
except ObjectDoesNotExist: # Generated by Django if the lookup fails
raise
return getattr(profile, attr)
注意:尽管您也可以使用factory_boy帮助程序“静音”信号,但这只会掩盖问题:从数据库的角度来看,不能保证UserProfile
给定的User
对象存在-您的代码应准备好处理这种情况。