Обработка аргумента функции с помощью декоратора
По сути, я пытаюсь взять ряд функций, которые выглядят как эта недекорированная функция проверки:
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. Более сложные решения, возможно, возможны, но излишний ответ.