首页 > 解决方案 > Mixer.blend() 模块无法正确创建引用自定义用户对象的实例,该对象使用 UUID 作为 PK

问题描述

我正在尝试使用 pytest 对我的模型进行测试。我在“:memory:”数据库中的 SQLite3 上使用混音器时遇到问题。

我有一个名为 Category 的模型,它有一个名为“created_by”的外键属性,它引用了我的自定义用户模型。

将 User 的引用模型分配给 mixer.blend() 中的类别变量时,我收到一个异常,指出 User 实例不是 UUID 的实例。

请参阅下面的测试:

import pytest

from mixer.backend.django import mixer
from apps.API import models

pytestmark = pytest.mark.django_db


class TestCategory():
    def test_model(self):
        category = mixer.blend(models.Category)
        assert category.pk == 1, 'Should create a Category instance'

这很奇怪,因为它以前(大约一周前)有效。为什么混音器不能从我的用户对象的主键中强制 UUID?

如果有人有任何想法,我会全力以赴。下面我定义了我的模型并显示了一些堆栈跟踪。希望我们能找到一些见解。

这是pytest中的堆栈跟踪:

self = <django.db.models.fields.UUIDField: id>, value = <User: doliver@smith.com>

    def to_python(self, value):
        if value is not None and not isinstance(value, uuid.UUID):
            try:
>               return uuid.UUID(value)

venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2325:


self = <[AttributeError("'UUID' object has no attribute 'int'") raised in repr()] UUID object at 0x7feeebf18f98>, hex = <User: doliver@smith.com>, bytes = None, bytes_le = None
 fields = None, int = None, version = None

def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None,
                   int=None, version=None):
    r"""Create a UUID from either a string of 32 hexadecimal digits,
        a string of 16 bytes as the 'bytes' argument, a string of 16 bytes
        in little-endian order as the 'bytes_le' argument, a tuple of six
        integers (32-bit time_low, 16-bit time_mid, 16-bit time_hi_version,
        8-bit clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as
        the 'fields' argument, or a single 128-bit integer as the 'int'
        argument.  When a string of hex digits is given, curly braces,
        hyphens, and a URN prefix are all optional.  For example, these
        expressions all yield the same UUID:

        UUID('{12345678-1234-5678-1234-567812345678}')
        UUID('12345678123456781234567812345678')
        UUID('urn:uuid:12345678-1234-5678-1234-567812345678')
        UUID(bytes='\x12\x34\x56\x78'*4)
        UUID(bytes_le='\x78\x56\x34\x12\x34\x12\x78\x56' +
                      '\x12\x34\x56\x78\x12\x34\x56\x78')
        UUID(fields=(0x12345678, 0x1234, 0x5678, 0x12, 0x34, 0x567812345678))
        UUID(int=0x12345678123456781234567812345678)

        Exactly one of 'hex', 'bytes', 'bytes_le', 'fields', or 'int' must
        be given.  The 'version' argument is optional; if given, the resulting
        UUID will have its variant and version set according to RFC 4122,
        overriding the given 'hex', 'bytes', 'bytes_le', 'fields', or 'int'.
        """

    if [hex, bytes, bytes_le, fields, int].count(None) != 4:
        raise TypeError('one of the hex, bytes, bytes_le, fields, '
                        'or int arguments must be given')
    if hex is not None:
>       hex = hex.replace('urn:', '').replace('uuid:', '')
E       AttributeError: 'User' object has no attribute 'replace'

/usr/lib/python3.6/uuid.py:137: AttributeError

During handling of the above exception, another exception occurred:

self = <apps.API.tests.test_models.TestCategory object at 0x7feeef113a58>

def test_model(self):
>       category = mixer.blend(models.Category)

apps/API/tests/test_models.py:13:

venv/lib/python3.6/site-packages/mixer/main.py:568: in blend
    return type_mixer.blend(**values)
venv/lib/python3.6/site-packages/mixer/main.py:116: in blend
    for name, value in defaults.items()
venv/lib/python3.6/site-packages/mixer/main.py:116: in <genexpr>
    for name, value in defaults.items()
venv/lib/python3.6/site-packages/mixer/mix_types.py:222: in gen_value
    return type_mixer.gen_field(field)
venv/lib/python3.6/site-packages/mixer/backend/django.py:270: in gen_field
    return super(TypeMixer, self).gen_field(field)
venv/lib/python3.6/site-packages/mixer/main.py:193: in gen_field
    return self.gen_value(field.name, field, unique=unique)
venv/lib/python3.6/site-packages/mixer/main.py:261: in gen_value
    return self.get_value(field_name, value)
venv/lib/python3.6/site-packages/mixer/backend/django.py:218: in get_value
    return self._get_value(name, value, field)
venv/lib/python3.6/site-packages/mixer/backend/django.py:233: in _get_value
    value = field.scheme.to_python(value)
venv/lib/python3.6/site-packages/django/db/models/fields/related.py:874: in to_python
    return self.target_field.to_python(value)


self = <django.db.models.fields.UUIDField: id>, value = <User: doliver@smith.com>

   def to_python(self, value):
        if value is not None and not isinstance(value, uuid.UUID):
            try:
                return uuid.UUID(value)
            except (AttributeError, ValueError):
                raise exceptions.ValidationError(
                    self.error_messages['invalid'],
                    code='invalid',
>                   params={'value': value},
                )
E               django.core.exceptions.ValidationError: ["'doliver@smith.com' is not a valid UUID."]

venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2330: ValidationError

请参阅上述堆栈跟踪中的最后一段代码:

我发现如果我将 try 块中的“value”更改为“value.id”,则不会引发异常。

def to_python(self, value):
    if value is not None and not isinstance(value, uuid.UUID):
        try:
            return uuid.UUID(value)
        except (AttributeError, ValueError):
            raise exceptions.ValidationError(
                    self.error_messages['invalid'],
                    code='invalid',
>                   params={'value': value},
                )

E               django.core.exceptions.ValidationError: 
["'doliver@smith.com' is not a valid UUID."]

我的用户模型使用 UUID 作为其主键。

这是我正在使用的两个模型(类别和用户)。

我在 Category 中的抽象 BaseModel 继承仅包含 created_date、is_active 等基本信息。

类别:

# django
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
# models
from apps.API.models.BaseModel import BaseModel


class Category(BaseModel):
    CATEGORIES = (
        # choices tuples go here
    )

    category = models.CharField(_('category'),
                                choices=CATEGORIES,
                                max_length=80,
                                blank=False,
                                db_index=True
                                )

    created_by = models.ForeignKey(settings.AUTH_USER_MODEL,
                                   on_delete=models.SET(
                                       BaseModel.get_sentinel_user),
                                   blank=False,
                                   editable=False,
                                   db_index=True,
                                   related_name=(
                                       'created_categories')
                                   )

    class Meta:
        app_label = "API"
        verbose_name = _('Category')
        verbose_name_plural = _('Categories')

    def __str__(self):
        return '{}'.format(self.category)

这是我的用户模型:

import uuid
# django
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from django.utils.translation import ugettext_lazy as _
# models
from apps.API.models.BaseModel import BaseModel
from apps.API.manager import UserManager


class User(AbstractBaseUser, PermissionsMixin):

    # Use UUID to make the merging of tables much easier if need be.
    id = models.UUIDField(primary_key=True,
                          editable=False,
                          default=uuid.uuid4,
                          verbose_name='public identifier',)
    email = models.EmailField(_('email address'), unique=True)

    first_name = models.CharField(_('first name'),
                                  max_length=255,
                                  blank=True)

    last_name = models.CharField(_('last name'),
                                 max_length=255,
                                 blank=True)

    created_date = models.DateTimeField(_('created date'), 
                                        auto_now_add=True)

    is_active = models.BooleanField(_('active'), default=True)
    is_superuser = models.BooleanField(_('superuser'), default=False)
    is_staff = models.BooleanField(_('staff'), default=False)


    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        app_label = "API"
        verbose_name = _('User')
        verbose_name_plural = _('Users')

    def __str__(self):
        return '{}'.format(self.email)

我突然进入 Django shell 并且在混音器上遇到了同样的问题,所以这不是 pytest 的问题。

Traceback (most recent call last):

    File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site- 
        packages/django/db/models/fields/__init__.py", line 2325, in to_python

        return uuid.UUID(value)

    File "/usr/lib/python3.6/uuid.py", line 137, in __init__
        hex = hex.replace('urn:', '').replace('uuid:', '')

AttributeError: 'User' object has no attribute 'replace'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
     File "<console>", line 1, in <module>

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/main.py", line 568, in blend

         return type_mixer.blend(**values)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/main.py", line 116, in blend
         for name, value in defaults.items()

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/main.py", line 116, in <genexpr>
         for name, value in defaults.items()

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/mix_types.py", line 222, in gen_value
         return type_mixer.gen_field(field)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/backend/django.py", line 270, in gen_field

         return super(TypeMixer, self).gen_field(field)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/main.py", line 193, in gen_field

         return self.gen_value(field.name, field, unique=unique)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/main.py", line 261, in gen_value

         return self.get_value(field_name, value)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/backend/django.py", line 218, in get_value
         return self._get_value(name, value, field)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/mixer/backend/django.py", line 233, in _get_value
         value = field.scheme.to_python(value)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/django/db/models/fields/related.py", line 874, in to_python

         return self.target_field.to_python(value)

     File "/home/users/myname/dev/blah-django-app/venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py", line 2330, in to_python

         params={'value': value},

     django.core.exceptions.ValidationError: ["'michaela32@yahoo.com' is not a valid UUID."]

标签: pythondjangopytestuuiddjango-mixer

解决方案


这似乎是一个版本问题,因为 Mixer 还不支持 Django 2.1。

据说这个 PR 会解决这个问题。不确定何时或是否会合并,因为 Mixer 存储库已经 5 个月没有更新了。

Github 上支持 Django 2.1 的 Mixer PR


推荐阅读