Получение "int" принято как пример моей Django модели - PullRequest
0 голосов
/ 10 марта 2020

Я пишу декоратор, чтобы мои функции могли принимать экземпляр одной из моих моделей 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__?

1 Ответ

1 голос
/ 10 марта 2020

И вот где я признаю, какой я идиот:

from unittest.mock import patch

# ...

with patch('my_module.isinstance') as _isinstance:
    _isinstance.return_value = True
    # Call offending code

Так что я не вызывал встроенную функцию isinstance, я вызывал свою собственную поддельную версию, которая всегда возвращалась True.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...