Python 3 – abusing annotations

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 is <class 'str'=""> with a value of "'123'"
</class>
@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 is <class 'str'=""> with a value of "'123'" </class>
@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 is  with 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"
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"
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 `` I used something like the magic_decorator above and a transformer `SafeStr` (ex. def do_something(name:SafeStr)` to ensure that the name parameter is ALWAYS of type str. Anecdotally Python3 blows up if you try to do something like .startswith()`.

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).