使用合成模式会导致Django Factory Boy无限递归吗?

时间:2018-07-31 22:05:46

标签: python django recursion factory-boy

我有一个模型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会引起此错误,我却有些困惑,而“普通”属性查找似乎可以正常工作。有关如何解决此问题的任何想法?

1 个答案:

答案 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对象存在-您的代码应准备好处理这种情况。