Я пишу декоратор, чтобы мои функции могли принимать экземпляр одной из моих моделей Django или ее первичный ключ, а сама функция всегда будет получать экземпляр независимо от того, какой из них был передан. Во время тестирования Я получаю странную ошибку, что иногда целое число (обычный тип для первичного ключа) считается допустимым экземпляром моей модели. Например:
log.debug((value, type(value), model, isinstance(value, model)))
# Outputs: (1, <class 'int'>, <class 'myapp.models.MyModel'>, True)
Это поведение согласуется с моей функцией декоратора, но я не могу воспроизвести его из пустой оболочки Django; Я попытался использовать 1, действительные первичные ключи для MyModel
и другие значения, и он всегда возвращает False
правильно. Эта проверка также работает должным образом в некоторых других тестовых случаях с использованием других моделей, возвращая False
при передаче ключа, а не экземпляра объекта.
MyModel
имеет пару дополнительных наследований, в частности переопределяет __str__
и __repr__
и наследуется от natural_keys.NaturalKeyModel
, который не переопределяет ничего, имеющего отношение к этой проблеме. Иначе, это просто типичная Django модель, без специального определения __eq__
или что-то в этом роде.
Есть идеи, почему моя проверка isinstance
ведет себя таким образом? Для полноты, весь мой декоратор приведен ниже.
def with_obj(name, model,
required=True, key_name='pk', select_for_update=False, select_related=tuple(), prefetch_related=tuple(),
_default=None):
"""Retrieves an object of the given model, given either an instance of that model or a primary key.
Args:
name (str): The argument name to find and modify
model (models.Model): The model type to expect or retrieve
required (bool): If False and no value is provided for this argument, None will be passed. If True, an exception will be thrown if not value is provided. True by default.
key_name (str): The name of the key field on the model to use if looking up an instance from a primitive value. "pk" by default.
select_for_update (bool): Whether or not to mark this object as being selected for updating. If True, this also wraps the function call in an ``atomic`` block. An object selected for updating cannot be fetched by another process (using a database lock) until this function returns.
select_related (tuple(str)): An optional tuple of fields to be pre-fetched using ``select_related``.
prefetch_related (tuple(str)): An optional tuple of many-to-* fields to be pre-fetched using ``prefetch_related``.
Returns:
((function) -> function): A decorator that guarantees that an argument will be provided to the wrapped function as defined here.
"""
import inspect
import functools
_maybe_atomic = atomic if select_for_update else lambda x: x
def _decorator(f):
@functools.wraps(f)
@_maybe_atomic
def _inner(*args, **kwargs):
# Get whatever was passed for _name_
signature = inspect.signature(f).bind(*args, **kwargs)
signature.apply_defaults()
value = signature.arguments[name]
# Replace that value with the corresponding instance from _model_
# Note, this WILL cause a re-fetch even if the object was already passed in
# This is intentional behavior to prevent objects from being cached in the tasks queue and saved with old
# values
log.debug((value, type(value), model, isinstance(value, model))) # <-- This prints the issue
value = model.objects.filter(**{key_name: getattr(value, key_name) if isinstance(value, model) else value}) # <-- This line fails, due to retrieving "pk" from type "int"
if select_for_update:
value = value.select_for_update()
if select_related:
value = value.select_related(*select_related)
if prefetch_related:
value = value.prefetch_related(*prefetch_related)
if required and _default is None:
# This will throw an exception if no object is found, so only call if we want to fail in that case
value = value.get()
else:
value = value.first()
if value is None and _default is not None:
value = _default()
# Modify the call signature to use this new value
signature.arguments[name] = value
return f(*signature.args, **signature.kwargs)
return _inner
return _decorator
РЕДАКТИРОВАТЬ: Я пытался проверить по MyModel.__instancecheck__(value)
напрямую, и это всегда возвращает правильное, ожидаемое значение, в данном случае False
. В частности:
log.debug((value, type(value), model, isinstance(value, model), model.__instancecheck__(value)))
# Outputs: (1, <class 'int'>, <class 'myapp.models.MyModel'>, True, False)
Что может заставить isinstance
возвращать результаты, отличные от __instancecheck__
?