-
Notifications
You must be signed in to change notification settings - Fork 242
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
Proposal: signature copying for kwargs. #270
Comments
Gotta make this quick: Yes, we experience this in our own (Dropbox) code bases a lot too. And I agree this is useful for readability even without static checking. I think there are some details to be worked out -- what I often see is that the wrapping function adds some fixed args to the function in delegates too, so those should not be accepted: def raw(name='me', age=42): ...
def cooked(first_name, last_name, **extra):
raw(name=first_name + ' ' + last_name, **extra)
cooked(age=100) # OK
cooked(name='Guido') # Error Also of course the wrapper may also have some of its own arguments that are unrelated to the wrapped function. |
I've seen this pattern quite frequently as well, and it would be nice to support it somehow. Refactoring existing code to use explicitly spelled out individual arguments instead of Expanding on the decorator idea, maybe it should be possible to declare that a function accepts all positional and keyword args another function accepts, except for a set of args. Example:
Implementing this in a type checker looks a little tricky, especially when using both |
OK, to support a bit of this with data I did a small experiment: take a random sample of stdlib files until I got about 100 function definitions with open keyword args; then another random sample in django with 100 more defs, and check manually what they do with the arguments. My finding say that:
Case 1 is trivial and works well already with So "hard" cases would be reduced to about 20% of the From what I saw of case 4, actually I think that a lot of cases could be covered automatically. The example that @JukkaL posted is not very common. The common case for those (at least in my sample) are the following two: def raw(name=..., age=...): ...
@delegate_args(raw)
def cooked(name, **extra):
raw(name=name.upper(), **extra)
# here the typechecker could deduce that `**extra` never contains a `name`, because it's a named argument of "cooked"
def raw2(a, b, c, name=..., age=...): ...
@delegate_args(raw)
def cooked2(a, b, c, **extra):
raw(a+1, b, c, **extra)
# here the typechecker could deduce that `**extra` never contains `a`, `b`, or `c`, because they are named arguments of `cooked2` So the @delegate_args(raw, include=['foo'])
def cooked(**extra):
foo = extra.pop('foo', SOME_DEFAULT)
do_something_with(foo)
raw( **extra) I would have declared foo as an explicit argument and perhaps that's what we should recommend, but I'm a bit surprised on how relatively common this pattern is. |
@dmoisset Thanks for the careful analysis! Having data like this makes decisions much easier. So the conclusion seems to be that this would be useful even without support for dict-as-struct. It might be useful for us to run a similar analysis against Dropbox code to get another data point. |
I came across this issue, and wanted to present my solution to the problem: import forge
def raw(name='he', age=42):
return f'{name} is {age}'
@forge.compose(
forge.copy(raw, exclude=('name')),
forge.insert((forge.arg('first_name'), forge.arg('last_name')), index=0),
)
def cooked(first_name, last_name, **extras):
extras['name'] = first_name + ' ' + last_name
return forge.callwith(raw, extras) (and some tests that validate that it works as expected) import inspect
assert repr(inspect.signature(cooked)) == '<Signature (first_name, last_name, age=42)>'
assert raw(age=100) == 'he is 100'
assert raw('Guido', age=42) == 'Guido is 42'
assert cooked('Guido', 'VR', 42) == 'Guido VR is 42'
try:
cooked(name='Guido') # Error
except TypeError as exc:
assert exc.args[0] == "cooked() missing a required argument: 'first_name'"
I'm curious as to whether a |
This is quite the mouthful: @forge.compose(
forge.copy(raw, exclude=('name')),
forge.insert((forge.arg('first_name'), forge.arg('last_name')), index=0),
)
def cooked(first_name, last_name, **extras): How about instead I don't understand the need for this: extras['name'] = first_name + ' ' + last_name
return forge.callwith(raw, extras) Why not |
So The reason for import forge
def func(a, b, c, d=4, e=5, f=6, *args):
return (a, b, c, d, e, f, args)
@forge.sign(
forge.arg('a', default=1),
forge.arg('b', default=2),
forge.arg('c', default=3),
*forge.args,
)
def func2(*args, **kwargs):
return forge.callwith(func, kwargs, args)
assert forge.repr_callable(func2) == 'func2(a=1, b=2, c=3, *args)'
assert func2(10, 20, 30, 'a', 'b', 'c') == (10, 20, 30, 4, 5, 6, ('a', 'b', 'c')) The alternative to that is manual interpolation of argument values which is just as big of a problem: import forge
def func(a, b, c, d=4, e=5, f=6, *args):
return (a, b, c, d, e, f, args)
@forge.sign(
forge.arg('a', default=1),
forge.arg('b', default=2),
forge.arg('c', default=3),
*forge.args,
)
def func2(*args, **kwargs):
return func(
kwargs['a'],
kwargs['b'],
kwargs['c'],
4,
5,
6,
*args,
)
assert forge.repr_callable(func2) == 'func2(a=1, b=2, c=3, *args)'
assert func2(10, 20, 30, 'a', 'b', 'c') == (10, 20, 30, 4, 5, 6, ('a', 'b', 'c')) I explain that further, in the docs. |
OK, that's cool, but:
def func(a, b, c, d=4, e=5, f=6, *args): instead of def func(a, b, c, *args, d=4, e=5, f=6): deserves to be slapped, and
|
Both are correct :) |
Alright, I got a minute to draft out an import functools
import inspect
import forge
class extend(forge.Revision):
"""
Extends a function's signature...
"""
def __init__(self, callable, *, include=None, exclude=None):
# pylint: disable=W0622, redefined-builtin
self.callable = callable
self.include = include
self.exclude = exclude
def revise(self, previous):
extensions = forge.fsignature(self.callable)
if self.include:
extensions = list(forge.findparam(extensions, self.include))
elif self.exclude:
extensions = [
param for param in extensions
if param not in forge.findparam(extensions, self.exclude)
]
params = [
param for param in previous
if param.kind is not forge.FParameter.VAR_KEYWORD
] + list(extensions)
return forge.FSignature(params) Usage is straightforward: def raw(name='he', age=42):
return f'{name} is {age}'
@extend(raw, exclude=('name'))
def cooked(first_name, last_name, **extras):
return raw(name=f'{first_name} {last_name}', **extras) And the tests still pass: assert repr(inspect.signature(cooked)) == '<Signature (first_name, last_name, age=42)>'
assert raw(age=100) == 'he is 100'
assert raw('Guido', age=42) == 'Guido is 42'
assert cooked('Guido', 'VR', 42) == 'Guido VR is 42'
try:
cooked(name='Guido') # Error
except TypeError as exc:
assert exc.args[0] == "cooked() missing a required argument: 'first_name'" The caveats to this approach are that a user must remain wary of the ordering of "parameter kind" and parameters with default values (or use within a |
python/mypy#5559 has an implementation of the basic proposal with In particular, there's the question of what syntax to use. All examples above use a decorator, but @ilevkivskyi also suggested (in python/mypy#5559) using a class DoStuff(Protocol):
def __call__(a: int, b: int = ..., c: int = ...) -> None: ...
f: DoStuff
def f(*args, **kwargs):
...
g: DoStuff
def g(*args, **kwargs):
f(*args, **kwargs) This is not optimal if the original function (such as Here's another idea (which will likely be harder to implement): def f(x: int, y: str = '') -> None:
...
g: TypeOf[f]
def g(*args, **kwargs):
... We'd introduce a def default(x: int, y: str = '') -> None:
...
def do_stuff(cb: TypeOf[default] = default) -> int:
... This would only cover uses cases where the signatures of the two functions are identical. Even if we continue with the decorator proposal, we don't have an agreement on what we'd call it. The ideas above aren't quite self-explanatory enough, in my opinion. The feature is not very widely useful, so I feel like we should try to make name very clear and explicit. Here are a bunch of random ideas (using a real example from @copy_signature(Popen)
def call(*popenargs, timeout: Optional[float] = None, **kwargs)): ...
@inherit_signature(Popen)
def call(*popenargs, timeout: Optional[float] = None, **kwargs)): ...
@with_signature(Popen)
def call(*popenargs, timeout: Optional[float] = None, **kwargs)): ...
@use_signature(Popen)
def call(*popenargs, timeout: Optional[float] = None, **kwargs)): ...
@use_signature_from(Popen)
def call(*popenargs, timeout: Optional[float] = None, **kwargs)): ...
@apply_signature(Popen)
def call(*popenargs, timeout: Optional[float] = None, **kwargs)): ... Here my rationale is that the decorator actually does no delegation -- the assumption is that the function delegates to another function, but that's actually not enforced by the feature, I assume. So the effect is to take the signature of another function and apply it to the decorated function. |
Here are few more random ideas/comments:
My preference would be probably to go with |
That's great for copying a signature, but it does nothing for the just-as, if-not-more common case where you want to mutate the parameters: #270 (comment) |
A decorator allows performing actions at runtime such as setting the |
Yes, please explain your use cases and spare us further rhetoric (words like "complete dealbreaker" and "but do I need to?"). |
Sorry, it was a genuine question, I didn't want to waste my time or anyone else's preaching to the choir. Setting This goes well beyond specific obscure use cases where a programmer wants to do some clever introspection for their own application. Here are some ways in which In PyCharm (apparently the most popular editor for Python), a keyboard shortcut shows the parameters of a function for which the user is currently writing a call. Personally, I use this feature often, and it always annoys me when I get back a meaningless In the console, setting In Jupyter notebooks, the most common editor for scientific developers after PyCharm (same survey above), the signature is used for parameter autocompletion: And of course, there's the builtin
|
Note that PyCharm's editor uses static analysis (like mypy), so setting |
Yes, I did specifically mention the console. And seeing how much static analysis PyCharm has already implemented, I think it's most likely that they will implement this feature too. Personally I want this feature in the standard library precisely so that I can make use of it in PyCharm, so if they don't implement it, I might even do it myself. In any case none of this affects the decorator vs annotation question. |
There's already a clear precedent to a @wraps(f)
def wrapper(*args, **kwargs):
# some other stuff
return f(*args, **kwargs) where Based on this similarity, I think it makes sense for the two decorators to behave similarly in other ways.
Beyond that, attaching information at runtime may have uses that none of us think of, maybe even uses that none of us could think of because the relevant Python features don't exist yet. Using an annotation is a decision that would be messy to reverse and effectively rules out these possibilities. In terms of readability, another problem I have with using an annotation is that annotations are usually (if not always) an absolute declaration of the type of the annotated object, without taking the annotated object into account. If I see: cooked: TypeOf[raw]
def cooked(foo, *args, **kwargs):
raw(*args, **kwargs) the impression given by |
I just want to note that you seem to be focused primarily on runtime behavior, while others on this thread are focused primarily on static checkers (which operate without running or importing the code they are checking). |
I'm fine with static analysis being the priority. I even wrote the PR for that. We can defer actually making decisions about or implementing the runtime stuff until much later. But I think it should be possible to implement those features eventually, and it essentially won't be if we use an annotation. All of this is just an argument in favour of using a decorator instead of an annotation, in contrast to @ilevkivskyi's stated preference for an annotation. |
I think there's still a misunderstanding though. Annotations are also introspectable at runtime (typically through some |
The suggestion is to use a variable annotation, which is the only possibility I see. Given a function, finding a corresponding variable annotation is messy at best, and AFAICT impossible if the function is locally defined. And since Python versions before 3.6 have to mimic variable annotations using comments, it's definitely impossible in all cases to attach runtime information for those versions. Of course this feature won't be directly available in those versions, but it could easily be available in a backport. |
I prefer using a decorator over a variable annotation, mainly because it allows some differences in the signatures. Based on analysis by @dmoisset above, it's pretty common that some arguments are different, or Here are some potential signature differences that the decorator-based approach can support:
I think that both approaches can support runtime introspection (in 3.6 and later), so it's probably not an important factor. The similarity to
More random ideas:
|
@alexmojaki with respect to checking function bodies it would, in principle, be possible to determine what def parent(x: int, y: int) -> int: ...
def child(z: int, **kwargs: SameAs[parent]) -> int:
reveal_type(kwargs) # Revealed type is TypedDict(x=int, y=int) But again, my use cases for this are primarily concerned with having the correct signature for the wrapping function so I'd have to agree that this isn't a priority. Having more detailed type checking information in the wrapping function's body would be more of a nicety and could be added later since most of the time, |
@JukkaL and @alexmojaki even though ilevkivskyi seems to have excluded himself from the conversation I think the idea of indicating signature copying via a type hint could be aesthetically acceptable. Such a type hint might be named something like:
And would probably look a bit like this in practice: class Parent:
def method(self, y: int, y: int) -> int: ...
class Child(Parent):
def method(self, z: int, *args: SameAs[Parent]) -> SameAs: ...
# or
def method(self, z: int, **kwargs: SameAs[Parent]) -> int: ...
# or
def method(self, z: int, *args: SameAs[Parent], **kwargs: SameAs[Parent]) -> int: ... Implementation ConcernThe ability to copy def parent(x: int, *, y: int = 0) -> int: ...
def child(z: int, *args: SameAs[parent]) -> int: ... # SameAs should only copy `x` arg Edge Case Benefits?This may have some advantages when wrapping functions in slightly more complex ways. For example, this could allow you to delegate arguments to two different functions, or delegate arguments from one or more sources: def f(*args: int) -> int: ...
def g(**kwargs: int) -> int: ...
def wrapper(*args: SameAs[f], **kwargs: SameAs[g]) -> int:
return f(*args) + g(**kwargs) T = # not sure what to put here
def f(*args: int, **kwargs: int) -> int: ...
def chain_outer(*args: SameAs[f]) -> T:
def chain_inner(**kwargs: SameAs[f]) -> SameAs[f]:
return f(*args, **kwargs)
return chain_inner |
@msullivan PEP-612 appears related to this issue. |
Sorry if I intrude on the conversation. Use caseI want to type the function returned by More precisely, I'd like be able to have some form of parameter hinting from PyCharm. (from what I understood, it is very probable to be implemented if this proposal is accepted) Sample codeI have a One possible implementation of a decorator to register objects of the "model category": register_model = functools.partial( _register_decorator_factory, category="model_class") However in this way it is not easy to use such decorator, because the signature is lost. i.e. This is the signature of def _register_decorator_factory(
wrapped=None,
*,
names: Optional[Union[List[str], str]] = None,
category: str,
) -> Any: I would expect to automatically infer the def register_model(
wrapped=None,
*,
names: Optional[Union[List[str], str]] = None,
) -> Any: tl;drConsider the use case of automatically infer the signature of |
@lucmos I don't think your use case has been brought up in this thread before, so thanks for sharing! While it seems similar, to my eyes though it looks like you need a general solution for currying so PEP-612 (which will probably close this issue) won't help you out as it stands right now. Doing a bit of digging though, it doesn't seem like anyone's brought up currying before so perhaps this warrants a new issue. TypeScript appears to have some solution to currying using recursive types, so that seems like a promising angle from which to approach the problem. |
Now that PEP 612 is accepted, can we close this issue? |
👍 even if it doesn't cover all usecases (no idea if that's the case, just started reading the spec) this thread has become sufficiently long so remaining usecases should probably get new threads. Is there a ticket that tracks the implementation in mypy I can subscribe to? |
Does PEP 612 offer a solution to the common usecase of copying the signature of the superclass method when overriding? I've read through it and while it seems like the use-case of signature-preserving decorators is now well-supported, I can't see any way to indicate 'same as parent' or even 'same as x' where x is another callable. Could anyone tell me if I've just missed something glaringly obvious? I found some parts of the PEP a little hard to digest without first seeing more examples of its use out in the wild. Honestly, I really like the use of attribute-access syntax to separate a If there is not yet a way to meet this need with PEP 612 I would like to start a new thread suggesting the following (in the spirit of PEP 612):
Allowing And for the specific use-case of method overriding:
If So yeah. Are these use-cases covered? If not, has there been a successor thread to this where I could post this as a suggestion? And if not, would it be okay if I started one? Cheers |
@matthewgdv if all you want to do is copy the signature, this comment describes a way to do that right now: #270 (comment) That doesn't really help you though if you want to actually add arguments (e.g. Perhaps in a future revision the following could be made possible? P1 = ParamSpec("P1")
P2 = ParamSpec("P2")
def concatenate_functions(f1: P1, f2: P2) -> Concatenate[P1, P2]:
...
def f(x: int) -> int: ...
def g(y: int) -> int: ...
h: Callable[[int, int], int] = concatenate_functions(f, g) There might be a separate issue to track this though - a link here would be great if anyone knows of such an issue. |
The ProblemAs mentioned above, the most-used cases of Example: class A:
def __init__(self, *, param_1: int, param_2: str, param_3: bool = False):
pass
class B:
a: A
def create_a(self, *, log: bool = False, **kwargs):
self.a = A(**kwargs)
if (log): print("Log!") Here, If I am somewhere wrong, please, correct me. P.S.If anybody is interested, I am attaching my implementation of from .merge_sig import merge_signature
class B:
a: A
@merge_signature([ A.__init__ ])
def create_a(self, *, log: bool = False, **kwargs):
... file:/home/peter/projects/test/merge-sig.tar.gz I tried different tools for generating stub files, and neither of them succeeded in generating the correct signature |
I apologize in advance if this has already been solved. I wanted to share what I found with Concatenate and ParamSpec since it took me a long time to come up with a solution to copy arguments from import functools
from typing import TypeVar, Callable, Any
from typing_extensions import Concatenate, ParamSpec
I = ParamSpec("I") # internal function
P = ParamSpec("P") # public function
R = TypeVar("R") # The return type of the internal function
def copy_inputs(internal_f: Callable[I, R]) -> Callable[[Callable[I, R]], Callable[Concatenate[bool, I], R]]:
def wrap(public_f: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(public_f)
def run(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"a_bool is {args[0]}")
args = args[1:]
internal_f(*args, **kwargs)
return run
return wrap
def internal_f(a_str: str, a_float: float) -> str:
print(f"a_str is {a_str}, a_float is {a_float}")
@copy_inputs(internal_f)
def public_f(a_bool: bool, *args, **kwargs) -> str:
...
public_f(True, "a_string", 42.8) I am not entirely sure I have everything type annotated correctly, but it does result in this on VSCode: Also, I couldn't figure out how to get the argument named (i.e. rather than |
@ahuang11 It looks like keyword parameter support by Does anyone know if this work is underway or not? I am definitely interested in making use of such a feature. |
The revealed type works as expected, but Mypy still reports the outer |
@lgetin that's pretty cool, but I think there's 2 problems with your example. It's easy to forget when commenting on typing-related threads but most users probably aren't familiar enough with advanced typing constructs like The second problem is that even if everyone were to copy your I think a pretty good compromise solution would be to include either this exact recipe or an equivalent one in the EDIT: I also just realized that using a decorator like that there's no way to indicate that a method should keep the same signature as its closest parent in the MRO without explicitly referencing the parent. I think this is possibly the most common use-case for signature copying. |
@matthewgdv that example isn't even functional, so there are definitely at least 3 problems with it 😉 I would like to propose the following implementations for a import functools
from typing import Any, Callable, Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
def copy_function_signature(source: Callable[P, T]) -> Callable[[Callable], Callable[P, T]]:
def wrapper(target: Callable) -> Callable[P, T]:
@functools.wraps(source)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
return target(*args, **kwargs)
return wrapped
return wrapper
def copy_method_signature(source: Callable[Concatenate[Any, P], T]) -> Callable[[Callable], Callable[Concatenate[Any, P], T]]:
def wrapper(target: Callable) -> Callable[Concatenate[Any, P], T]:
@functools.wraps(source)
def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> T:
return target(self, *args, **kwargs)
return wrapped
return wrapper
def f(x: bool, *extra: int) -> str:
return str(...)
@copy_function_signature(f)
def test(*args, **kwargs):
return f(*args, **kwargs)
class A:
def foo(self, x: int, y: int, z: int) -> float:
return float()
class B:
@copy_method_signature(A.foo)
def bar(self, *args, **kwargs):
print(*args)
print(test(True, 1, 2, 3)) # Elipsis
B().bar(1, 2, 3) # 1, 2, 3
reveal_type(test) # Type of "test" is "(x: bool, *extra: int) -> str"
reveal_type(B.bar) # Type of "B.bar" is "(Any, x: int, y: int, z: int) -> float" Obviously we could make use of |
@matthewgdv It wasn’t my example in the first place, it’s quoted from ilevkivskyi earlier in the thread (actually from over three years ago 😄). I just replied about it since it was recommended multiple times in the thread and also on Stack Overflow, but it didn’t seem to work as expected. I’m not very well-versed in the more advanced typing aspects of Python myself. |
@ringohoffman This caused a bunch of warnings when running Mypy in strict mode (via the import functools
from collections.abc import Callable
from typing import Any, Concatenate, ParamSpec, TypeVar, reveal_type
P = ParamSpec("P")
T = TypeVar("T")
def copy_callable_signature(
source: Callable[P, T]
) -> Callable[[Callable[..., T]], Callable[P, T]]:
def wrapper(target: Callable[..., T]) -> Callable[P, T]:
@functools.wraps(source)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
return target(*args, **kwargs)
return wrapped
return wrapper
def copy_method_signature(
source: Callable[Concatenate[Any, P], T]
) -> Callable[[Callable[..., T]], Callable[Concatenate[Any, P], T]]:
def wrapper(target: Callable[..., T]) -> Callable[Concatenate[Any, P], T]:
@functools.wraps(source)
def wrapped(self: Any, /, *args: P.args, **kwargs: P.kwargs) -> T:
return target(self, *args, **kwargs)
return wrapped
return wrapper
def f(x: bool, *extra: int) -> str:
return str(...)
@copy_callable_signature(f)
def test(*args, **kwargs): # type: ignore[no-untyped-def] # copied signature
return f(*args, **kwargs)
class A:
def foo(self, x: int, y: int, z: int) -> float:
return float()
class B:
@copy_method_signature(A.foo)
def bar(self, *args, **kwargs): # type: ignore[no-untyped-def] # copied signature
print(*args)
class Person:
def __init__(self, given_name: str, surname: str):
self.full_name = given_name + " " + surname
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {self.full_name}>"
@copy_callable_signature(Person)
def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] # copied signature
return Person(*args, **kwargs)
print(test(True, 1, 2, 3)) # Ellipsis
B().bar(1, 2, 3) # 1, 2, 3
print(wrapper("John", "Doe")) # <Person: John Doe>
reveal_type(test) # Type of "test" is "(x: bool, *extra: int) -> str"
reveal_type(B.bar) # Type of "B.bar" is "(Any, x: int, y: int, z: int) -> float"
reveal_type(wrapper) # Type of "wrapper" is "(given_name: str, surname: str) -> Person" It runs without errors, and Interestingly, |
I've been experimenting with the import functools
from collections.abc import Callable
from typing import ParamSpec, Self, TypeVar, reveal_type
from pydantic import BaseModel
P = ParamSpec("P")
T = TypeVar("T")
def copy_callable_signature(
source: Callable[P, T]
) -> Callable[[Callable[..., T]], Callable[P, T]]:
def wrapper(target: Callable[..., T]) -> Callable[P, T]:
@functools.wraps(source)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
return target(*args, **kwargs)
return wrapped
return wrapper
class Message(BaseModel):
author: str
content: str
@classmethod
@copy_callable_signature(Self)
def synthesize(cls, **kwargs):
kwargs["author"] = kwargs.get("author", "Default author")
kwargs["content"] = kwargs.get("content", "Default content")
return cls(**kwargs)
message_1 = Message.synthesize(author="Moortiii") # Note that 'content' is missing
reveal_type(message_1) # Type of "message_1" is "Any"
reveal_type(message_1.synthesize) # Type of "message_1.synthesize" is "Any"
message_2 = Message.synthesize(author="Moortiii", content="Example message")
reveal_type(message_2) # Type of "message_2" is "Message"
reveal_type(message_2.synthesize) # Type of "message_2.synthesize" is "(*, author: str, content: str) -> Message"
print(message_1) # author='Moortiii' content='Default content'
print(message_2) # author='Moortiii' content='Example message' |
@Moortiii, it looks like you're not just trying to copy the signature. Instead what you need is to be able to modify the signature since some parameters in your new Also, on an unrelated note, |
Ah yes, you're right. In the example above the type hint for Thinking out loud, I guess that even if we had a way to modify the arguments such that they all became Indeed, |
Hypothetically, depending on how keyword argument concatenation worked, that could be an avenue towards allowing for this. But I am not aware of any work being done in this regard. |
There's a quite common pattern in python code which is:
(a usual subcase of this one is when
other_function
is actuallysuper().function
). This presents two problems for a static analyzer:function
toother_function
can not be type-checked properly because of the*args, **kwargs
in the call arguments.function
, so calls to it are unchecked.The problem for me also affects readability of the code (which is for me one of the main problems that annotations tries to address). James Powell from numfocus even gave a pydata talk about the difficulties it brings at https://www.youtube.com/watch?v=MQMbnhSthZQ
Even if theoretically the args/kwargs packing feature of python can be used with more or less arbitrary data, IMO this use-case is common enough to warrant some special treatment. I was thinking on a way to flag this usage, for example:
This could hint an analyzer so:
function
, the "extra" arguments are checked to match the signature ofother_function
For me, even without static analyzer, the readability benefits of seeing
and knowing that
plot_valuation
accepts any valid arguments from matplotlib's plot function, is worth it.The text was updated successfully, but these errors were encountered: