Обработка аргумента функции с помощью декоратора

По сути, я пытаюсь взять ряд функций, которые выглядят как эта недекорированная функция проверки:

def f(k: bool):
    def g(n):
        # check that n is valid
        return n
    return g

И сделать их похожими на эту декорированную функцию проверки:

@k
def f():
    def g(n):
        # check that n is valid
        return n
    return g

Идея здесь в том, что k описывает одну и ту же функциональность во всех реализующих функциях.

В частности, все эти функции возвращают функции "проверки" для использования со структурой сладострастной проверки. Так что все функции типа f() возвращают функцию, которая позже выполняется Schema(), k на самом деле allow_none то есть флаг, который определяет, является ли None значение в порядке. Очень простым примером может быть этот пример использования кода:

x = "Some input value."
y = None
input_validator = Schema(f(allow_none=True))
x = input_validator(x)  # succeeds, returning x
y = input_validator(y)  # succeeds, returning None
input_validator_no_none = Schema(f(allow_none=False))
x = input_validator(x)  # succeeds, returning x
y = input_validator(y)  # raises an Invalid

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

def valid_identifier(allow_none: bool=True):
    min_range = Range(min=1)
    validator = Any(All(int, min_range), All(Coerce(int), min_range))
    return Any(validator, None) if allow_none else validator

К этому:

@allow_none(default=True)
def valid_identifier():
    min_range = Range(min=1)
    return Any(All(int, min_range), All(Coerce(int), min_range))

Функция, возвращаемая из этих двух, должна быть эквивалентной.

То, что я пытался написать это, используя decorator библиотека:

from decorator import decorator

@decorator
def allow_none(default: bool=True):
    def decorate_validator(wrapped_validator, allow_none: bool=default):
        @wraps(wrapped_validator)
        def validator_allowing_none(*args, **kwargs):
            if allow_none:
                return Any(None, wrapped_validator)
            else:
                return wrapped_validator(*args, **kwargs)
        return validator_allowing_none
    return decorate_validator

И у меня есть unittest.TestCase чтобы проверить, работает ли это как ожидалось:

@allow_none()
def test_wrapped_func():
    return Schema(str)

class TestAllowNone(unittest.TestCase):

    def test_allow_none__success(self):
        test_string = "blah"

        validation_function = test_wrapped_func(allow_none=False)
        self.assertEqual(test_string, validation_function(test_string))
        self.assertEqual(None, validation_function(None))

Но мой тест возвращает следующую ошибку:

    def validate_callable(path, data):
        try:
>           return schema(data)
E           TypeError: test_wrapped_func() takes 0 positional arguments but 1 was given

Я попытался отладить это, но не смог заставить отладчик войти в декорацию. Я подозреваю, что из-за проблем с именами, таких как поднятые в этой (очень длинной) серии постов в блоге, что test_wrapped_func не получает должным образом установленный список аргументов, и поэтому декоратор даже не выполняется, но он также может быть полностью другим.

Я попробовал некоторые другие варианты. Удаляя скобки функции из @allow_none:

@allow_none
def test_wrapped_func():
    return Schema(str)

Я получаю другую ошибку:

>       validation_function = test_wrapped_func(allow_none=False)
E       TypeError: test_wrapped_func() got an unexpected keyword argument 'allow_none'

Сбросив @decorator не удается с:

>       validation_function = test_wrapped_func(allow_none=False)
E       TypeError: decorate_validator() missing 1 required positional argument: 'wrapped_validator'

Что имеет смысл, потому что @allow_none принимает аргумент, и поэтому логические скобки будут необходимы. Замена их дает оригинальную ошибку.

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

1 ответ

Решение

Я думаю, что вы кладете свои allow_none=default аргумент на неправильном уровне вложенности. Он должен быть на самой внутренней функции (оболочка), а не на декораторе (средний уровень).

Попробуйте что-то вроде этого:

def allow_none(default=True):    # this is the decorator factory
    def decorator(validator):    # this is the decorator
        @wraps(validator)
        def wrapper(*args, allow_none=default, **kwargs):    # this is the wrapper
            if allow_none:
                return Any(None, validator)
            else:
                return validator(*args, **kwargs)
        return wrapper
    return decorator

Если вам не нужно, чтобы значение по умолчанию было настраиваемым, вы можете избавиться от внешнего слоя вложения и просто сделать значение по умолчанию константой в функции-обертке (или опустить его, если ваши вызывающие всегда будут передавать значение). Обратите внимание, что, как я уже писал выше, allow_none аргумент обертки является аргументом только для ключевых слов. Если вы хотите передать его как позиционный параметр, вы можете переместить его вперед *args, но это требует, чтобы это был первый позиционный аргумент, который может быть нежелателен с точки зрения API. Более сложные решения, возможно, возможны, но излишний ответ.

Другие вопросы по тегам