Since my retirement a few years ago my habit of trying out sometimes useless or convoluted ideas has gone up a few notches.
Latest discovery is that `inspect.signature()` passes parameter annotations straight through. With a bit of function decorator hackery, you can get positional/keyword instrumented transformers.
@magic_params def some_func(always_str:str): print(f"always_str is {type(always_str)} with a value of {repr(always_str)}") >>some_func(123) always_str iswith a value of "'123'"
another example
def reversed_str(raw): if not isinstance(raw, str): raw = str(raw) return raw[::-1] @magic_decorator def goofy_func(bizarro:reversed_str): return bizarro assert goofy_func("Hello World") == "dlroW olleH"
A working proof of concept
""" | |
Command line output | |
================================= | |
Test with both arguments supplied | |
Handling call | |
Parameter name: always_int | |
Parameter name: always_str | |
Original position arguments: ('123',) | |
Original keyword arguments: {'always_str': 345} | |
Transformed arguments: [123] | |
Transformed keyword arguments: {'always_str': '345'} | |
Completed | |
Test with only one positional argument supplied | |
Handling call | |
Parameter name: always_int | |
Parameter name: always_str | |
Original position arguments: ('123',) | |
Original keyword arguments: {} | |
Transformed arguments: [123] | |
Transformed keyword arguments: {'always_str': None} | |
Completed | |
Test with custom transformer | |
Handling call | |
Parameter name: bizarro | |
Original position arguments: ('Hello World',) | |
Original keyword arguments: {} | |
Transformed arguments: ['dlroW olleH'] | |
Transformed keyword arguments: {} | |
Completed | |
""" | |
from inspect import signature | |
def magic_decorator(func): | |
""" | |
Anytime I put the word "magic" in a identifier, its my way of putting up | |
a red flag that whatever is attached is possible toxic, dangerous, or dangerously toxic. | |
In this case, what magic_decorate does is really nifty but I am confident there is a gotcha in here | |
like the threat of inspect.signature.parameter.annotation changing behavior in the future. | |
That said.... | |
""" | |
#Grab the parameters of the function passed into the decorator | |
params = signature(func).parameters | |
#actual decorator logic | |
def decorator(*args, **kwargs): | |
#constants for noting where a function argument came from, either positional or keyword | |
POSITIONED = 1 | |
KW = 2 | |
new_args = [] | |
new_kwargs = {} | |
#*args is a tuple which is inconveniant | |
arg_list = [x for x in args] | |
print("Handling call") | |
for parameter_name, parameter_signature in params.items(): | |
print(f"\tParameter name: {parameter_name}") | |
#First step is to get the argument values passed in | |
source = None | |
if arg_list: | |
source = POSITIONED | |
original_value = arg_list.pop(0) | |
else: | |
source = KW | |
original_value = kwargs.get(parameter_signature.name, parameter_signature.empty) | |
#Handles cases where a value is not provided for a keyword argument | |
if original_value == parameter_signature.empty: | |
new_value = parameter_signature.default | |
elif parameter_signature.annotation != parameter_signature.empty: | |
#Here is the magic, in vanilla_func and goofy_func, the annotations are int/str/reversed_str | |
# and are provided as is via the inspect.Parameter.annotation property | |
new_value = parameter_signature.annotation(original_value) | |
else: | |
new_value = original_value | |
#Put the transformed (or untouched) arguments back to where they came from | |
if source == POSITIONED: | |
new_args.append(new_value) | |
else: | |
new_kwargs[parameter_signature.name] = new_value | |
print(f"\tOriginal position arguments: {args} \n\tOriginal keyword arguments: {kwargs}") | |
print(f"\tTransformed arguments: {new_args} \n\tTransformed keyword arguments: {new_kwargs}") | |
return_value = func(*new_args, **new_kwargs) | |
print("Completed") | |
return return_value | |
return decorator | |
@magic_decorator | |
def vanilla_func(always_int:int=None, always_str:str=None): | |
return always_int, always_str | |
def reversed_str(raw): | |
if not isinstance(raw, str): | |
raw = str(raw) | |
return raw[::-1] | |
@magic_decorator | |
def goofy_func(bizarro:reversed_str): | |
return bizarro | |
print("Test with both arguments supplied") | |
assert vanilla_func("123", always_str=345) == (123, "345",) | |
print("Test with only one positional argument supplied") | |
assert vanilla_func("123") == (123, None,) | |
print("Test with custom transformer") | |
assert goofy_func("Hello World") == "dlroW olleH" | |
In one of my pet projects, I have a method with a signature like `def process_request(self, action:SomeClass.FromDict)` which takes a dictionary for the `action` parameter and passes that to SomeClass.FromDict which then returns a instance of `SomeClass`.
In another case, when dealing with Twisted in Python3 and that all strings are type `
Grand scheme I think this is an interesting quirk but if my comments and wording isn’t clear, I would prescribe caution if using this in revenue generating code (or code intended to make you wealthy or at least provide money for pizza & beer).