Non-consistent type-hinting for signature of func with variable amount of dynamically added params #9472
-
**EDIT: for completeness, link to the discussion for this in the Python Forum, in case anyone ever suggests a solution there 71432 Hello everyone! Using VSCode: There are different type-hinting results for logically (almost) the same ways of prepending a function signature with new parameters. Example setup (simplified as much as I could): from typing import cast, Callable
class MyBase:
pass
def my_func(p_1 = None, *, p_2 = None):
pass
class MyClass_1(MyBase):
def __init__(self, *, f_1 = None, f_2 = None):
self.f_1, self.f_2 = f_1, f_2
class MyClass_2(MyBase):
def __init__(self, *, f_1 = None, f_2 = None):
self.f_1, self.f_2 = f_1, f_2 The idea is to be able to call First option:
def my_wrapper[**ParamsT, ReturnT, MetaT: MyBase](
some_func: Callable[ParamsT, ReturnT],
new_arg_type: type[MetaT],
):
if not issubclass(new_arg_type, MyBase):
raise TypeError()
def wrapped(new_arg: MetaT | None = None, /, *args: ParamsT.args, **kwargs: ParamsT.kwargs) -> ReturnT:
f = some_func(*args, **kwargs)
if new_arg is not None:
if not isinstance(new_arg, new_arg_type):
raise ValueError(f"expected: {new_arg_type} <-> received: {type(new_arg)}")
# do something ...
return cast(ReturnT, f)
return wrapped
wrapped = my_wrapper(my_func, MyClass_1)
wrapped = my_wrapper(wrapped, MyClass_2)
wrapped(MyClass_2(), MyClass_1(), ... other args for my_func) Result:
Second option:
def my_wrapper[**ParamsT, ReturnT, MetaT: MyBase](
some_func: Callable[ParamsT, ReturnT],
new_arg_type: type[MetaT],
*new_arg_types: type[MetaT],
):
def wrapper(
some_func: Callable[ParamsT, ReturnT],
new_arg_type: type[MetaT],
):
def wrapped(new_arg: MetaT | None = None, /, *args: ParamsT.args, **kwargs: ParamsT.kwargs) -> ReturnT:
f = some_func(*args, **kwargs)
if new_arg is not None:
if not isinstance(new_arg, new_arg_type):
raise ValueError(f"expected: {new_arg_type} <-> received: {type(new_arg)}")
# do something ...
return cast(ReturnT, f)
return wrapped
res = wrapper(
some_func,
new_arg_types[-1] if new_arg_types else new_arg_type,
)
for arg_type in reversed([new_arg_type, *new_arg_types[:len(new_arg_types)-2]]):
res = wrapper(res, arg_type)
return res
wrapped = my_wrapper(my_func, MyClass_1, MyClass_2)
wrapped(MyClass_1(), MyClass_2(), ... other args for my_func) Result:
Other tested options:
All of the options produce working code with the only difference on runtime being the expected order of the new args. So, is there a way to have the dynamic way of doing it, but still preserving the correct and useful type-hinting from the First option? I guess the issue is that every time the decorator is called, it appends a new param with a duplicate name of an existing param? Tried figuring out how to do that insane thing as well (dynamic name of a function parameter) but couldn't figure out how to make it work, not to mention type it..... Any ideas, comments, suggestions how this or something similar can be achieved? BTW, just spamming n-count of decorator calls over the Thank you in advance!!! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 5 replies
-
If you have general questions about the Python typing spec or how to use type annotations for specific use cases, the Python typing forum is a good place to post. That forum is frequented by experts in the Python typing community, and they can help you with such questions. If you think that pyright is behaving incorrectly (contrary to the typing spec), I'd appreciate it if you could post a minimal, self-contained sample along with the observed and expected behavior. |
Beta Was this translation helpful? Give feedback.
When a type checker evaluates the type of a call expression, it uses the signature of the target call. It does not use the implementation of the target call in this circumstance. Often (e.g. in the case of type stubs), the implementation is not even provided to a type checker.
If you omit the return type annotation for a function, pyright will attempt to infer its return type based on the implementation, but that inferred return type is then used to define the signature.
If the implementation of a function is provided, a type checker will check the code within that function. However, the body of a function isn't used to evaluate calls to that function. That's why I stripped out the implem…