Почему не получается замокать декоратор у функции?

Ссылка скопирована
1 ответ

Всем привет я пытаюсь написать юнит тест.
У меня есть декоратор который проверяет является ли аргумент переданный в функцию действительно аргументом ожидаемого типа. @check_types_method

Его использования выглядит вот так:

class Calculator:      @check_types_method     def call(self, a: MegaNumber, b: int) -> int:         return a.value + b

class Calculator: @check_types_method def call(self, a: MegaNumber, b: int) -> int: return a.value + b

Если переменная a будет не MegaNumber то будет брошено исключение.
Это круто работает но при тестах это создает проблемы.
Например мне надо подменить MegaNumber mock объектом и если я это сделаю то в тесте получаю ошибку.

TypeError: The argument 'a' must be of type <class 'mega_number.MegaNumber'>, received: MagicMock

TypeError: The argument 'a' must be of type <class 'mega_number.MegaNumber'>, received: MagicMock

Для теста нужно замокать декоратор что бы он не чего не делал. И пропускал Mock объекты. Однако у меня не как не получается сделать это и в чем причина я не пойму.
Моя лучшая попытка выглядит так:

import unittest from unittest.mock import MagicMock, patch from calculator import Calculator   @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase):      def test_call(self):         mock_mega_number = MagicMock()         mock_mega_number.value = 10         calculator = Calculator()         result = calculator.call(mock_mega_number, 5)          self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's "                                      "value and the integer provided.")

import unittest from unittest.mock import MagicMock, patch from calculator import Calculator @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock() mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.")

но декоратор загружается раньше чем применяется patch - и я не как не могу победить это. Подскажите как быть?

код для воспроизведения:

check_types_method.py

from inspect import signature from functools import wraps from typing import get_origin, get_args, Union   def check_types_method(func):     @wraps(func)     def wrapper(*args, **kwargs):         sig = signature(func)         bound = sig.bind(*args, **kwargs)         bound.apply_defaults()          for name, value in bound.arguments.items():             expected_type = sig.parameters[name].annotation             if expected_type is not sig.empty:                 if not is_of_generic_type(value, expected_type):                     raise TypeError(                         f"The argument '{name}' must be of type {expected_type}, received: {type(value).__name__}")         return func(*args, **kwargs)      def is_of_generic_type(obj, generic_type):         origin_type = get_origin(generic_type)         arg_types = get_args(generic_type)         if origin_type is Union:             return any(is_of_generic_type(obj, arg) for arg in arg_types)         if origin_type is None:             # Non-generic types             return isinstance(obj, generic_type)         if not isinstance(obj, origin_type):             return False         if origin_type in (list, set):             element_type = arg_types[0]             return all(is_of_generic_type(item, element_type) for item in obj)         elif origin_type is dict:             key_type, value_type = arg_types             return all(is_of_generic_type(k, key_type) and is_of_generic_type(v, value_type) for k, v in obj.items())         elif origin_type is tuple:             if len(arg_types) == 2 and arg_types[1] is Ellipsis:                 return all(isinstance(item, arg_types[0]) for item in obj)             else:                 return len(obj) == len(arg_types) and all(                     is_of_generic_type(item, t) for item, t in zip(obj, arg_types))         return False      return wrapper

from inspect import signature from functools import wraps from typing import get_origin, get_args, Union def check_types_method(func): @wraps(func) def wrapper(*args, **kwargs): sig = signature(func) bound = sig.bind(*args, **kwargs) bound.apply_defaults() for name, value in bound.arguments.items(): expected_type = sig.parameters[name].annotation if expected_type is not sig.empty: if not is_of_generic_type(value, expected_type): raise TypeError( f"The argument '{name}' must be of type {expected_type}, received: {type(value).__name__}") return func(*args, **kwargs) def is_of_generic_type(obj, generic_type): origin_type = get_origin(generic_type) arg_types = get_args(generic_type) if origin_type is Union: return any(is_of_generic_type(obj, arg) for arg in arg_types) if origin_type is None: # Non-generic types return isinstance(obj, generic_type) if not isinstance(obj, origin_type): return False if origin_type in (list, set): element_type = arg_types[0] return all(is_of_generic_type(item, element_type) for item in obj) elif origin_type is dict: key_type, value_type = arg_types return all(is_of_generic_type(k, key_type) and is_of_generic_type(v, value_type) for k, v in obj.items()) elif origin_type is tuple: if len(arg_types) == 2 and arg_types[1] is Ellipsis: return all(isinstance(item, arg_types[0]) for item in obj) else: return len(obj) == len(arg_types) and all( is_of_generic_type(item, t) for item, t in zip(obj, arg_types)) return False return wrapper

mega_number.py

class MegaNumber:      def __init__(self):         self.value = 10

class MegaNumber: def __init__(self): self.value = 10

calculator.py

from check_types_method import check_types_method from mega_number import MegaNumber   class Calculator:      @check_types_method     def call(self, a: MegaNumber, b: int) -> int:         return a.value + b

from check_types_method import check_types_method from mega_number import MegaNumber class Calculator: @check_types_method def call(self, a: MegaNumber, b: int) -> int: return a.value + b

test_calculator.py

import unittest from unittest.mock import MagicMock, patch from calculator import Calculator   @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase):      def test_call(self):         mock_mega_number = MagicMock()         mock_mega_number.value = 10         calculator = Calculator()         result = calculator.call(mock_mega_number, 5)          self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's "                                      "value and the integer provided.")

import unittest from unittest.mock import MagicMock, patch from calculator import Calculator @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock() mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.")

Дополнительно:

Ответы:

Мне кажется, ты просто используешь неверный инструмент, пытаясь совместить несовместимое.
Вариант А
Ты проверяешь, что у тебя объект нужного типа. Тогда твой декоратор в принципе работает, но вообще-то это задача для статического анализатора кода типа mypy или встроенного в pycharm, а не проверки в рантайме. И тогда нужно забыть про моки.

Вариант Б
Ты проверяешь, что у тебя объект имеет нужные поля и методы (duck typing).
Тогда тебе нужен typing.Protocol в комбинации с typing.runtime_checkable, с помощью которого ты сможешь описать, что должен иметь объект. Затем этот протокол можно будет подсунуть в isinstance() для проверки, и мок, по идее, её пройдёт. Но опять-таки, задача скорее для статического анализатора кода, чем для рантайм-проверки. Если у тебя в принципе неведомо что может быть передано в метод - это простыми тестами не решается.

  • Ну если чувак написал динамическую проверку типов - то это не от хорошей жизни верно?
    Я просто ловил разные приколы с конвертацией (не явной) типов данных питоне которые не палятся твоими статическими анализаторами. А когда софт считает бабло (тем более твое) как то хочется перестраховаться.
  • MasterCopipaster, ну так используй готовый инструмент, тот же pydantic, а то что ты сейчас делаешь, больше похоже на переусложнение на ровном месте.

А ларчик то просто открывался

import unittest from unittest.mock import MagicMock, patch from calculator import Calculator from mega_number import MegaNumber   class TestCalculator(unittest.TestCase):      def test_call(self):         mock_mega_number = MagicMock(MegaNumber)         mock_mega_number.value = 10         calculator = Calculator()         result = calculator.call(mock_mega_number, 5)          self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's "                                      "value and the integer provided.")

import unittest from unittest.mock import MagicMock, patch from calculator import Calculator from mega_number import MegaNumber class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock(MegaNumber) mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.")

Нужно решить такую задачу?

Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.

Заказать помощь
Лучший ответ
1
Артём Dev Ответ

Для начала давайте разберемся, что такое декораторы в программировании. Декоратор - это паттерн проектирования, который позволяет добавлять новое поведение или функциональность существующему объекту, не изменяя его структуру. Это достигается путем обертывания объекта в другой объект, который добавляет нужное поведение.

Теперь вернемся к вашей проблеме с замоканием декоратора у функции. Возможно, у вас есть функция, которую вы хотите декорировать, чтобы добавить ей новое поведение. Давайте посмотрим на пример на языке PHP:

function myFunction() {
    echo "Original function";
}
 
function myDecorator($func) {
    return function() use ($func) {
        echo "Before function call";
        $func();
        echo "After function call";
    };
}
 
$decoratedFunction = myDecorator('myFunction');
$decoratedFunction();

function myFunction() { echo "Original function"; } function myDecorator($func) { return function() use ($func) { echo "Before function call"; $func(); echo "After function call"; }; } $decoratedFunction = myDecorator('myFunction'); $decoratedFunction();

В данном примере у нас есть функция `myFunction`, которую мы хотим декорировать. Мы создаем функцию `myDecorator`, которая принимает функцию в качестве аргумента и возвращает новую функцию, которая добавляет нужное нам поведение.

Однако, возможно у вас возникает проблема с передачей аргументов в декоратор. Если вам нужно передать аргументы в декоратор, то вы можете использовать замыкание (closure) для этого. Например:

function myFunction($name) {
    echo "Hello, $name";
}
 
function myDecorator($func) {
    return function($name) use ($func) {
        echo "Before function call";
        $func($name);
        echo "After function call";
    };
}
 
$decoratedFunction = myDecorator('myFunction');
$decoratedFunction('Alice');

function myFunction($name) { echo "Hello, $name"; } function myDecorator($func) { return function($name) use ($func) { echo "Before function call"; $func($name); echo "After function call"; }; } $decoratedFunction = myDecorator('myFunction'); $decoratedFunction('Alice');

Таким образом, вы можете использовать замыкания для передачи аргументов в декораторы функций. Надеюсь, это поможет вам решить проблему с замоканием декоратора у функции.

Другие ответы (0)

Пока нет других ответов. Будьте первым, кто поможет автору.

Ответить на вопрос

комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Вам также может быть интересно