-
-
Notifications
You must be signed in to change notification settings - Fork 74
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
Annotate depends and accept_arguments decorators #962
Conversation
129dd0d
to
b424b3d
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #962 +/- ##
==========================================
- Coverage 86.70% 86.45% -0.25%
==========================================
Files 10 10
Lines 5151 5176 +25
==========================================
+ Hits 4466 4475 +9
- Misses 685 701 +16 ☔ View full report in Codecov by Sentry. |
|
||
@accept_arguments | ||
def depends(func, *dependencies, watch=False, on_init=False, **kw): | ||
def depends( | ||
func: CallableT, /, *dependencies: Dependency, watch: bool = False, on_init: bool = False, **kw: Parameter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without the /
, Mypy complains:
param/depends.py:40: error: Argument 1 to "accept_arguments" has incompatible type "Callable[[CallableT@depends, VarArg(Parameter | str), DefaultNamedArg(bool, 'watch'), DefaultNamedArg(bool, 'on_init'), KwArg(Parameter)], Callable[[CallableT@depends], DependsFunc[CallableT@depends]]]"; expected "Callable[[CallableT, VarArg(Parameter | str), DefaultNamedArg(bool, 'watch'), DefaultNamedArg(bool, 'on_init'), KwArg(Parameter)], Callable[[CallableT], DependsFunc[CallableT]]]" [arg-type]
param/depends.py:40: note: This is likely because "depends" has named arguments: "func". Consider marking them positional-only
This looks like a Mypy bug; variadic arguments via *
should necessarily mean that func
is positional-only. However, adding in /
I believe makes no meaningful difference at runtime, so I just went ahead and did that.
@overload | ||
def depends( | ||
*dependencies: Parameter, watch: bool = ..., on_init: bool = ..., **kw: Parameter | ||
) -> Callable[[CallableT], DependsFunc[CallableT]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intention with these overloads is to prevent usage that violates the validation that depends
applies internally at runtime, for example, this fails:
@depends("test", foobar=Parameter())
def test(a: int, b: str) -> None:
print("foobar")
param/depends.py:154: error: No overload variant of "depends" matches argument types "str", "Parameter" [call-overload]
param/depends.py:154: note: Possible overload variants:
param/depends.py:154: note: def depends(*dependencies: str, watch: bool = ..., on_init: bool = ...) -> Callable[[CallableT], DependsFunc[CallableT]]
param/depends.py:154: note: def depends(*dependencies: Parameter, watch: bool = ..., on_init: bool = ..., **kw: Parameter) -> Callable[[CallableT], DependsFunc[CallableT]]
While this works:
@depends(Parameter(), foobar=Parameter())
def test(a: int, b: str) -> None:
print("foobar")
Is it intentional that this project is still on Python 3.8 while Edit: Made it Python 3.8-compatible regardless |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intentional that this project is still on Python 3.8 while panel is on python 3.10?
Yes. Panel depends on projects that have dropped support for Python 3.9. Param is more of a fundamental tool, so we don't want to drop Python versions before it is necessary. See HEP1
param/_utils.py
Outdated
if TYPE_CHECKING: | ||
from typing_extensions import Concatenate, ParamSpec | ||
|
||
_P = ParamSpec("_P") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use underscore here? Is this the norm?
It is "hidden" behind TYPE_CHECKING
, so it will be private for users. It also adds extra overhead to reading the type hinting itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is typical for typeshed
, for example: https://github.com/python/typeshed/blob/main/stdlib/urllib/request.pyi#L51
It is "hidden" behind TYPE_CHECKING, so it will be private for users.
Not if they are also using TYPE_CHECKING
:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from param._utils import P
And regardless of TYPE_CHECKING
it still becomes visible in IDE auto-complete, interspersed with symbols that are part of the public API (as opposed to being namespaced with an underscore prefix):
(Granted, underscore is just a convention, they could just as well import it anyway.)
That said, I don't really care either way, so I'll just remove the underscores
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the clarification.
Whatever the norm should be done, and if you say it is making them private, then so be it.
If you want to avoid repeating complex types, you are welcome to create a _typing.py
file, though it does not need to be in this PR.
Do you have a preferred way to test type hints? If possible, I would like your help in setting it up.
And like Philipp said, I also appreciate your work on these PRs! 💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a preferred way to test type hints?
For these PRs I generally am just running mypy path/to/file/changed.py --follow-imports=silent
and making sure that the errors after are a strict subset of the errors that existed before. I also just create some dummy code that uses the decorator to see whether it works as expected.
For something more automated - and I'm mostly thinking in the context of panel
- we could set up a Mypy run that's part of CI. We could start with some subset of the examples
folder that we can get zero issues on, then extend to all the examples, and after we reach 100% no issues, we can switch to Mypy strict
and repeat. The point would be to prioritize the public API - which examples
would encompass - over the internal library code.
I figure examples
would work a lot better than me manually creating functions in test code and applying @param.depends
etc. on it.
Only after that would I try to Mypy panel
or param
internal code as that is a much bigger lift and could require some actual code changes to be type-compatible, etc. So we'd probably want to run the examples CI check with --follow-imports=silent
so that it doesn't prematurely start checking internal code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have opened a PR for Panel here holoviz/panel#7151
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really appreciate this, thanks for all your efforts so far!
Mergeable? Build seems to be flaky, assuming there's no good reason for it to fail on 3.12 windows and pass on 3.12 mac |
It took some tinkering but I finally have this working. Addresses #958
@MarcSkovMadsen @philippjfr
Results: