[ty] Fix ParamSpec defaults and alias variance#24479
Conversation
Typing conformance results improved 🎉The percentage of diagnostics emitted that were expected errors increased from 88.70% to 88.79%. The percentage of expected errors that received a diagnostic held steady at 84.63%. The number of fully passing files held steady at 84/134. SummaryHow are test cases classified?Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (
Test file breakdown1 file altered
False positives removed (1)1 diagnostic
|
Memory usage reportSummary
Significant changesClick to expand detailed breakdownprefect
|
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-assignment |
0 | 0 | 10 |
unresolved-attribute |
0 | 0 | 3 |
invalid-argument-type |
2 | 0 | 0 |
missing-argument |
1 | 0 | 0 |
| Total | 3 | 0 | 13 |
Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.
Raw diff (16 changes)
core (https://github.com/home-assistant/core)
+ homeassistant/components/rflink/__init__.py:270:56 error[invalid-argument-type] Argument to function `async_call_later` is incorrect: Expected `HassJob[(datetime, /), Coroutine[Any, Any, None] | None] | ((datetime, /) -> Coroutine[Any, Any, None] | None)`, found `HassJob[(_: Exception | None = None), None]`
discord.py (https://github.com/Rapptz/discord.py)
- discord/ext/commands/core.py:1942:17 error[invalid-assignment] Object of type `list[Unknown]` is not assignable to attribute `__commands_checks__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]] & ~<Protocol with members '__commands_checks__'>`
+ discord/ext/commands/core.py:1942:17 error[invalid-assignment] Object of type `list[Unknown]` is not assignable to attribute `__commands_checks__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]] & ~<Protocol with members '__commands_checks__'>`
- discord/ext/commands/core.py:1944:13 error[unresolved-attribute] Object of type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]` has no attribute `__commands_checks__`
+ discord/ext/commands/core.py:1944:13 error[unresolved-attribute] Object of type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]` has no attribute `__commands_checks__`
- discord/ext/commands/core.py:2365:17 error[invalid-assignment] Object of type `list[Unknown]` is not assignable to attribute `__commands_checks__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]] & ~<Protocol with members '__commands_checks__'>`
+ discord/ext/commands/core.py:2365:17 error[invalid-assignment] Object of type `list[Unknown]` is not assignable to attribute `__commands_checks__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]] & ~<Protocol with members '__commands_checks__'>`
- discord/ext/commands/core.py:2367:13 error[unresolved-attribute] Object of type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]` has no attribute `__commands_checks__`
+ discord/ext/commands/core.py:2367:13 error[unresolved-attribute] Object of type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]` has no attribute `__commands_checks__`
- discord/ext/commands/core.py:2368:13 error[invalid-assignment] Object of type `Literal[True]` is not assignable to attribute `__discord_app_commands_guild_only__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]`
+ discord/ext/commands/core.py:2368:13 error[invalid-assignment] Object of type `Literal[True]` is not assignable to attribute `__discord_app_commands_guild_only__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]`
- discord/ext/commands/core.py:2440:17 error[invalid-assignment] Object of type `list[Unknown]` is not assignable to attribute `__commands_checks__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]] & ~<Protocol with members '__commands_checks__'>`
+ discord/ext/commands/core.py:2440:17 error[invalid-assignment] Object of type `list[Unknown]` is not assignable to attribute `__commands_checks__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]] & ~<Protocol with members '__commands_checks__'>`
- discord/ext/commands/core.py:2442:13 error[unresolved-attribute] Object of type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]` has no attribute `__commands_checks__`
+ discord/ext/commands/core.py:2442:13 error[unresolved-attribute] Object of type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]` has no attribute `__commands_checks__`
- discord/ext/commands/core.py:2443:13 error[invalid-assignment] Object of type `Literal[True]` is not assignable to attribute `__discord_app_commands_is_nsfw__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]`
+ discord/ext/commands/core.py:2443:13 error[invalid-assignment] Object of type `Literal[True]` is not assignable to attribute `__discord_app_commands_is_nsfw__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]`
- discord/ext/commands/core.py:2499:13 error[invalid-assignment] Object of type `CooldownMapping[Context[Any]]` is not assignable to attribute `__commands_cooldown__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]`
+ discord/ext/commands/core.py:2499:13 error[invalid-assignment] Object of type `CooldownMapping[Context[Any]]` is not assignable to attribute `__commands_cooldown__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]`
- discord/ext/commands/core.py:2547:13 error[invalid-assignment] Object of type `DynamicCooldownMapping[Context[Any]]` is not assignable to attribute `__commands_cooldown__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]`
+ discord/ext/commands/core.py:2547:13 error[invalid-assignment] Object of type `DynamicCooldownMapping[Context[Any]]` is not assignable to attribute `__commands_cooldown__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]`
- discord/ext/commands/core.py:2582:13 error[invalid-assignment] Object of type `MaxConcurrency` is not assignable to attribute `__commands_max_concurrency__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]`
+ discord/ext/commands/core.py:2582:13 error[invalid-assignment] Object of type `MaxConcurrency` is not assignable to attribute `__commands_max_concurrency__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]`
- discord/ext/commands/core.py:2634:13 error[invalid-assignment] Object of type `((CogT@before_invoke, ContextT@before_invoke, /) -> Coroutine[Any, Any, Any]) | ((ContextT@before_invoke, /) -> Coroutine[Any, Any, Any])` is not assignable to attribute `__before_invoke__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]`
+ discord/ext/commands/core.py:2634:13 error[invalid-assignment] Object of type `((CogT@before_invoke, ContextT@before_invoke, /) -> Coroutine[Any, Any, Any]) | ((ContextT@before_invoke, /) -> Coroutine[Any, Any, Any])` is not assignable to attribute `__before_invoke__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]`
- discord/ext/commands/core.py:2657:13 error[invalid-assignment] Object of type `((CogT@after_invoke, ContextT@after_invoke, /) -> Coroutine[Any, Any, Any]) | ((ContextT@after_invoke, /) -> Coroutine[Any, Any, Any])` is not assignable to attribute `__after_invoke__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, Top[(...)], Unknown]]`
+ discord/ext/commands/core.py:2657:13 error[invalid-assignment] Object of type `((CogT@after_invoke, ContextT@after_invoke, /) -> Coroutine[Any, Any, Any]) | ((ContextT@after_invoke, /) -> Coroutine[Any, Any, Any])` is not assignable to attribute `__after_invoke__` on type `((...) -> Coroutine[Any, Any, Any]) & ~Top[Command[Unknown, (...), Unknown]]`
starlette (https://github.com/encode/starlette)
+ tests/middleware/test_base.py:247:33 error[missing-argument] No argument provided for required parameter 1 of `Middleware.__init__`
streamlit (https://github.com/streamlit/streamlit)
+ lib/streamlit/runtime/caching/cache_utils.py:265:57 error[invalid-argument-type] Argument to `BoundCachedFunc.__init__` is incorrect: Expected `CachedFunc[_PWrapper@update_wrapper, Unknown]`, found `Self@__get__`16045ff to
9248f41
Compare
86d80d7 to
f994992
Compare
94eb63e to
9a410a1
Compare
| && Self::is_gradual_paramspec_value(db, other) => | ||
| { | ||
| self.always() | ||
| } |
There was a problem hiding this comment.
You can see the ecosystem diagnostics addressed by this change here: #24903
630cd23 to
8f9fbfd
Compare
| } else { | ||
| variance | ||
| } | ||
| } |
There was a problem hiding this comment.
You can see the ecosystem diagnostics fixed by this change here: #24910
| && Self::is_top_paramspec_value(db, other) => | ||
| { | ||
| self.always() | ||
| } |
There was a problem hiding this comment.
You can see the ecosystem diagnostics addressed by this change here: #24911
42f8eb5 to
b5ec99c
Compare
carljm
left a comment
There was a problem hiding this comment.
Nice work, thank you!!
BTW there's a typing-spec PR in the works, with solid typing council support, that will fully support variance for ParamSpec, including enabling the covariant=True and contravariant=True arguments to legacy typing.ParamSpec. But that should probably be a separate PR, once the typing spec PR lands.
| ``` | ||
|
|
||
| `...` has the same gradual behavior when used as a `ParamSpec` argument in a generic class, | ||
| regardless of the inferred variance of the `ParamSpec`. |
There was a problem hiding this comment.
This test doesn't use inferred variance (it uses a legacy ParamSpec), so it's odd to specifically reference inferred variance here.
| regardless of the inferred variance of the `ParamSpec`. | |
| regardless of the variance of the `ParamSpec`. |
| ```py | ||
| from typing import Callable, Generic, ParamSpec | ||
|
|
||
| P = ParamSpec("P") |
There was a problem hiding this comment.
For clarity maybe
| P = ParamSpec("P") | |
| # legacy ParamSpec with no variance specified is invariant | |
| P = ParamSpec("P") |
| def _(concrete: Command[[str]], gradual: Command[...]) -> None: | ||
| a: Command[...] = concrete | ||
| b: Command[[str]] = gradual |
There was a problem hiding this comment.
I think it's useful to also add assertions demonstrating that we are definitely dealing with an invariant paramspec here -- that makes the two gradual tests more meaningful.
| def _(concrete: Command[[str]], gradual: Command[...]) -> None: | |
| a: Command[...] = concrete | |
| b: Command[[str]] = gradual | |
| # confirm that Command is invariant in P | |
| def _(of_int: Command[int], of_bool: Command[bool]) -> None: | |
| a: Command[int] = of_bool # error: [invalid-assignment] | |
| b: Command[bool] = of_int # error: [invalid-assignment] | |
| # but gradual signature is still assignable in both directions | |
| def _(concrete: Command[[str]], gradual: Command[...]) -> None: | |
| a: Command[...] = concrete | |
| b: Command[[str]] = gradual |
| ``` | ||
|
|
||
| The gradual `...` form of a `ParamSpec` argument is assignable to and from a concrete `ParamSpec` | ||
| value when it appears in a generic class. This is assignability consistency, not subtyping. |
There was a problem hiding this comment.
| value when it appears in a generic class. This is assignability consistency, not subtyping. | |
| value when it appears in a generic class. This is assignability, not subtyping. |
| static_assert(is_assignable_to(TypeOf[named_job], Job[[int]])) | ||
| static_assert(is_assignable_to(TypeOf[defaulted_job], Job[[int]])) | ||
| static_assert(not is_assignable_to(TypeOf[wrong_job], Job[[int]])) |
There was a problem hiding this comment.
This test feels out of place in this file, because it's less of a "type properties" test and more a "real use case" test, and it has to kind of stretch to use is_assignable_to. It would be significantly simpler and shorter just to have a function that takes an argument of type Job[[int]] and try calling that function with all three of named_job, defaulted_job and wrong_job. I would make that change, and move this test over to paramspec.md.
| reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int | ||
| ``` | ||
|
|
||
| `...` has the same gradual behavior when used as a `ParamSpec` argument in a generic class, |
There was a problem hiding this comment.
Typically if we are asserting "core" behavior of a generic, we try to add the same tests in both legacy/paramspec.md and pep695/paramspec.md (using the two different syntaxes) to make sure we keep the behavior in sync.
| from ty_extensions import static_assert, is_assignable_to, is_subtype_of | ||
| from typing import Callable, Generic, ParamSpec | ||
|
|
||
| P = ParamSpec("P") |
There was a problem hiding this comment.
This feels like the same test as in legacy/paramspec.md?
| if known_class == Some(KnownClass::ParamSpec) { | ||
| if matches!( | ||
| known_class, | ||
| Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) |
There was a problem hiding this comment.
Can we add a test to lock this in?
| let Type::Callable(callable) = ty else { | ||
| return false; | ||
| }; | ||
|
|
There was a problem hiding this comment.
It seems like we could simplify by having this method take a CallableType (or be a method on CallableType, for that matter), and avoid needing this unwrapping. The relation-checking arm above that calls this method could just as easily match on Type::Callable(other) instead of plain other.
| let Type::Callable(callable) = ty else { | ||
| return false; | ||
| }; |
b5ec99c to
86c9db5
Compare
|
Cool I can own following up on that spec/conformance PR. |
## Summary This PR adds `covariant`, `contravariant`, and `infer_variance` support to `ParamSpec`. See: python/typing#2215. See: #24479 (review).
## Summary This PR fixes several ParamSpec variance and gradual-specialization edge cases that fell out of astral-sh#24319. We also now treat `typing_extensions.ParamSpec` defaults like `typing.ParamSpec` defaults, which I think was an oversight.
## Summary This PR adds `covariant`, `contravariant`, and `infer_variance` support to `ParamSpec`. See: python/typing#2215. See: astral-sh#24479 (review).
Summary
This PR fixes several ParamSpec variance and gradual-specialization edge cases that fell out of #24319.
We also now treat
typing_extensions.ParamSpecdefaults liketyping.ParamSpecdefaults, which I think was an oversight.