Home > Back-end >  python: recreating a function's typed parameter/defaults from strings
python: recreating a function's typed parameter/defaults from strings

Time:10-19

i have the following code:

import inspect, importlib
from pydoc import locate

def func(var:str = 'test', var2: int = 1, var3:list[int] = [1,2,3]):
    pass

fspec = inspect.getfullargspec(func)
dflts = fspec.defaults
# fspec
spec_dict = {}
for i, (a) in enumerate(fspec.annotations.items()):
    type_name = a[1].__name__ if type(a[1]) is type else str(a[1])
    spec_dict[a[0]] =(str(dflts[i]), str(type_name))
print(spec_dict)

this produces a dictionary that stores the function parameter name, and a tuple with the value and type, all as strings.

{'var': ('test', 'str'), 'var2': ('1', 'int'), 'var3': ('[1, 2, 3]', 'list[int]')}

Now, from the string values, i'd like to convert the values to their proper type:

for v in spec_dict.values():
    val_as_str = v[0]
    param_type = locate(v[1])
    print(param_type(val_as_str))

This however fails when it gets to 'list[int]', as the call for locate with that string returns None.
What is the proper way to handle this situation, if possible? (fwiw...reason i am doing this is that the functions, their types and defaults are serialized and deserialized as json strings - so i need to be able to convert back to the function signature)

any adivce/help would be appreciated

CodePudding user response:

typing.get_type_hints is able to convert forward-ref strings into the actual types. i.e.:

def f(x: "int") -> "str":
   pass

print(get_type_hints(f))

Will output:

{'x': <class 'int'>, 'return': <class 'str'>}

Knowing this, you can make a function, set its function signature and annotations with the parsed string and then use get_type_hints to get the forward-ref types:

def convert_type_string(s):
    args = {"a": s}

    def tmp(**kwargs):
        pass

    params = [
        inspect.Parameter(
            param,
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
            annotation=type_
        ) for param, type_ in args.items()
    ]

    tmp.__signature__ = inspect.Signature(params)
    tmp.__annotations__ = args
    return typing.get_type_hints(tmp)["a"]

And applying this on your class (With an extra 'Path' to prove it works on non builtins)

def func(
        var: str = 'test',
        var2: int = 1,
        var3: list[int] = [1,2,3],
        var4: Path = Path("__file__")
):
    pass

fspec = inspect.getfullargspec(func)
dflts = fspec.defaults
# fspec
spec_dict = {}
for i, (a) in enumerate(fspec.annotations.items()):
    type_name = a[1].__name__ if type(a[1]) is type else str(a[1])
    spec_dict[a[0]] =(str(dflts[i]), str(type_name))

print({k: convert_type_string(spec_dict[k][1]) for k in spec_dict})

Outputs:

{'var': <class 'str'>, 'var2': <class 'int'>, 'var3': list[int], 'var4': <class 'pathlib.Path'>}
  • Related