-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request: check that method parameters match #35
Comments
Here is an implementation of this functionality using import inspect
from typing import Callable
import typing_utils
def is_compatible(
x: Callable,
y: Callable,
) -> None:
"""Verify that the signature of `y` is compatible with the signature of `x`.
Ensures that any call to `x` will work on `y` by checking the following criteria:
1. The return type of `y` is a subtype of the return type of `x`.
2. All parameters of `x` are present in `y`.
3. All positional parameters of `x` appear in the same order in `y`.
4. All parameters of `x` are a subtype of the corresponding parameters of `y`.
5. All parameters of `y` are present in `x`, unless `x` has a `*args` or `**kwargs` parameter.
:param x: Function to check compatibility with.
:param y: Function to check compatibility of.
"""
x_sig = inspect.signature(x)
y_sig = inspect.signature(y)
# Verify that the return type of `y` is a subtype of `x`.
if x_sig.return_annotation != inspect.Signature.empty \
and y_sig.return_annotation != inspect.Signature.empty \
and not typing_utils.issubtype(y_sig.return_annotation, x_sig.return_annotation):
raise TypeError(f"`{y_sig.return_annotation}` is not a `{x_sig.return_annotation}`.")
# Verify that all parameters in `x` are specified in `y` and that their types are compatible.
x_var_args = False
x_var_kwargs = False
for x_index, (name, x_param) in enumerate(x_sig.parameters.items()):
if x_param.kind == inspect.Parameter.VAR_POSITIONAL:
x_var_args = True
elif x_param.kind == inspect.Parameter.VAR_KEYWORD:
x_var_kwargs = True
elif name not in y_sig.parameters:
raise TypeError(f"`{name}` is not present.")
else:
y_index = list(y_sig.parameters.keys()).index(name)
y_param = y_sig.parameters[name]
if x_param.kind != y_param.kind:
raise TypeError(f"`{name}` is not `{x_param.kind.description}`")
elif x_param.kind != inspect.Parameter.KEYWORD_ONLY and x_index != y_index:
raise TypeError(f"`{name}` is not parameter `{x_index}`")
elif x_param.annotation != inspect.Parameter.empty \
and y_param.annotation != inspect.Parameter.empty \
and not typing_utils.issubtype(x_param.annotation, y_param.annotation):
raise TypeError(f"`{name} must be a supertype of `{x_param.annotation}`")
# Verify that no parameters are specified in `y` that are not specified in `x`.
for name, y_param in y_sig.parameters.items():
if name not in x_sig.parameters \
and not (y_param.kind == inspect.Parameter.KEYWORD_ONLY and x_var_kwargs) \
and not (y_param.kind == inspect.Parameter.VAR_KEYWORD and x_var_kwargs) \
and not (y_param.kind == inspect.Parameter.VAR_POSITIONAL and x_var_args) \
and not (y_param.kind == inspect.Parameter.POSITIONAL_ONLY and x_var_args) \
and not (y_param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD and x_var_args):
raise TypeError(f"`{name}` is not a valid parameter.") Here are some examples that demonstrate this functionality. def foo(x) -> int:
pass
def bar(x) -> str:
pass
is_compatible(foo, bar)
# TypeError: `<class 'str'>`` is not a `<class 'int'>`. def foo(x, y):
pass
def bar(y, x):
pass
is_compatible(foo, bar)
# TypeError: `x` is not parameter `0` def foo(x, *, y):
pass
def bar(x, y):
pass
is_compatible(foo, bar)
# TypeError: `y` is not `keyword-only` def foo(x: int):
pass
def bar(x: str):
pass
is_compatible(foo, bar)
# TypeError: `x must be a supertype of `<class 'int'>` def foo(x):
pass
def bar(x, y):
pass
is_compatible(foo, bar)
# TypeError: `y` is not a valid parameter. def foo(x):
pass
def bar(x, *args):
pass
is_compatible(foo, bar)
# TypeError: `args` is not a valid parameter. def foo(x, **kwargs):
pass
def bar(x, y):
pass
is_compatible(foo, bar)
# TypeError: `y` is not a valid parameter. def foo(x, *args):
pass
def bar(x, y):
pass
is_compatible(foo, bar)
# Ok. def foo(x, **kwargs):
pass
def bar(x, *, y):
pass
is_compatible(foo, bar)
# Ok. |
Note that this would be a breaking change to |
The |
Thanks @ashwin153 ! |
This will be in the next release. |
It could be cool to post on r/programming after the release. This library is a big win for static typing in Python IMO; any time that you use |
I think we still need this #56 to actually make a difference. It will then be a new major version again .. so 5.0.0 |
It'd be fantastic if the checks performed by @OverRide could be extended to include validation of the parameters, to ensure they are the same (with certain exceptions such as the use of *args & **kwargs) and have the same type information (where present).
The text was updated successfully, but these errors were encountered: