Fix ParamSpec inference for generic descriptors - #4878
tobyh-canva wants to merge 3 commits into
Conversation
|
This pull request has been imported. If you are a Meta employee, you can view this in D119452786. (Because this pull request was imported automatically, there will not be any future comments.) |
This comment has been minimized.
This comment has been minimized.
|
According to ChatGPT, all the
|
|
Diff from mypy_primer, showing the effect of this PR on open source code: ============================================================
SUMMARY
============================================================
Total: +0 new errors, -20 fixed errors
By preset: +0/-20 (default), +0/-20 (strict)
Projects with changes (2):
hydra-zen: +0 -1
core: +0 -19
============================================================
FULL DIFF DETAILS
------------------------------------------------------------
hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
- ERROR src/hydra_zen/typing/_implementations.py:85:7-14: Type variable `T2` in class `Partial` is declared as invariant, but could be covariant based on its usage [variance-mismatch]
core (https://github.com/home-assistant/core)
- ERROR homeassistant/auth/__init__.py:566:13-37: Argument `HassJob[[_: datetime | None = None], None]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_track_point_in_utc_time` [bad-argument-type]
- ERROR homeassistant/components/bluetooth/__init__.py:206:13-31: Argument `HassJob[[now: datetime], Coroutine[Unknown, Unknown, None]]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/bond/entity.py:200:13-53: Argument `HassJob[[now: datetime], None]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/cloud/client.py:195:50-66: Argument `HassJob[[_: Any], Coroutine[Unknown, Unknown, None]]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/google_assistant/report_state.py:64:44-61: Argument `HassJob[[now: Unknown | None = None], Coroutine[Unknown, Unknown, Unknown]]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/google_assistant/report_state.py:143:44-61: Argument `HassJob[[now: Unknown | None = None], Coroutine[Unknown, Unknown, Unknown]]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/harmony/select.py:73:39-58: Argument `HassJob[[activity_info: tuple[Unknown, ...]], None]` is not assignable to parameter `activity_starting` with type `HassJob[[tuple[Unknown, ...]], Any] | None` in function `homeassistant.components.harmony.subscriber.HarmonyCallback.__new__` [bad-argument-type]
- ERROR homeassistant/components/harmony/select.py:74:38-57: Argument `HassJob[[activity_info: tuple[Unknown, ...]], None]` is not assignable to parameter `activity_started` with type `HassJob[[tuple[Unknown, ...]], Any] | None` in function `homeassistant.components.harmony.subscriber.HarmonyCallback.__new__` [bad-argument-type]
- ERROR homeassistant/components/hdmi_cec/__init__.py:189:51-72: Argument `HassJob[[now: Unknown | None = None], Unknown]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` [bad-argument-type]
- ERROR homeassistant/components/hdmi_cec/__init__.py:200:64-85: Argument `HassJob[[now: Unknown | None = None], Unknown]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/ld2410_ble/coordinator.py:72:46-72: Argument `HassJob[[_now: datetime], None]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/nasweb/coordinator.py:184:13-22: Argument `HassJob[[now: datetime], Coroutine[Unknown, Unknown, None]]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_at` [bad-argument-type]
- ERROR homeassistant/components/onvif/event_manager.py:515:35-58: Argument `HassJob[[_now: datetime | None = None], None]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/reolink/host.py:852:56-70: Argument `HassJob[[*_: Any], Coroutine[Unknown, Unknown, None]]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/components/ssdp/websocket_api.py:67:78-81: Argument `HassJob[[info: SsdpServiceInfo, change: SsdpChange], None]` is not assignable to parameter `callback` with type `HassJob[[SsdpServiceInfo, SsdpChange], Coroutine[Any, Any, None] | None]` in function `homeassistant.components.ssdp.scanner.Scanner.async_register_callback` [bad-argument-type]
- ERROR homeassistant/components/tts/__init__.py:700:13-29: Argument `HassJob[[_now: datetime], None]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/helpers/chat_session.py:83:13-29: Argument `HassJob[[now: datetime], None]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `homeassistant.helpers.event.async_call_later` [bad-argument-type]
- ERROR homeassistant/helpers/event.py:1447:48-57: Argument `HassJob[[utc_now: datetime], None]` is not assignable to parameter `action` with type `((datetime) -> Coroutine[Any, Any, None] | None) | HassJob[[datetime], Coroutine[Any, Any, None] | None]` in function `async_track_point_in_utc_time` [bad-argument-type]
- ERROR homeassistant/helpers/trigger.py:1583:34-50: Cannot set item in `dict[object, tuple[HassJob[[dict[str, Any], Context | None], Coroutine[Any, Any, None] | Any], dict[str, Any]]]` [unsupported-operation] |
kinto0
left a comment
There was a problem hiding this comment.
Review automatically exported from Phabricator review in Meta.
|
thanks! looks great at first glance, but it will take a sec to review it. I'll be splitting it into 3 diffs for review:
bug 2 false negative on unsafe callback replacement: one diff |
There was a problem hiding this comment.
looks great to me! sorry we missed this for 1.3, but I'm sure we'll have a pre-release out soon enough
I'll get another set of eyes from someone who understands variance better than me before merging
I've already split it so aside from anything else I tell you about I'll resolve all follow-ups on our end
| } | ||
|
|
||
| /// Whether a bound is a placeholder for deferred generic callable structure. | ||
| fn is_generic_callable_residual(ty: &Type) -> bool { |
There was a problem hiding this comment.
nit: I think we prefer this defined in crates/pyrefly_types/src/callable_residual.rs
stroxler
left a comment
There was a problem hiding this comment.
Review automatically exported from Phabricator review in Meta.
|
rebasing and merging |
Summary: ParamSpec arguments describe callable inputs, so their internal ordering is already contravariant. Applying class parameter variance directly reverses compatibility. Flip covariant and contravariant ParamSpec arguments before comparing generic class arguments. This corrects ParamSpec conformance behavior and provides the directional half required for generic descriptor binding. First of three commits split from the original change. Part of #4878 by Toby Harradine <tobyh@canva.com>. Reviewed By: stroxler Differential Revision: D119452786 fbshipit-source-id: 22377f15b5f15e4e7334008ae46f31429b6799f7
Summary: Generic callable residuals preserve deferred ParamSpec structure while inference is incomplete. Treating them as concrete bounds lets placeholders conflict with or override later concrete evidence. Keep generic residuals as fallback-only bounds during consistency checks, bound solving, and implicit-literal promotion. Concrete bounds now win once available, allowing descriptor binding to infer the remaining parameter list. Second of three commits split from the original change. Part of #4878 by Toby Harradine <tobyh@canva.com>. Reviewed By: stroxler Differential Revision: D119739484 fbshipit-source-id: 083077e2b02ba5c928b59582d39d4359ed48316f
Summary: Variance reached through a recursively referenced generic can be incomplete. Combining that fallback with direct evidence lets the recursive result override method and field uses. Writable callable fields were also treated as covariant even though both reads and writes make them invariant. Track reliable and fallback evidence separately, prefer direct evidence when available, and infer invariant variance for writable callable fields. This preserves generic descriptor binding while rejecting callback replacements that remove valid calling forms. Third of three commits split from the original change. Part of #4878 by Toby Harradine <tobyh@canva.com>. Reviewed By: stroxler Differential Revision: D119739486 fbshipit-source-id: 97f5087eda2c7a4bcd11d271a67f4783220d4c9e
|
Hmm. I think this got merged, but the PR linkage was somehow lost so the PR didn't auto-close via ShipIt Thanks @tobyh-canva! |
Summary
Pyrefly handled some
ParamSpecargument lists in the wrong direction. This caused Pyrefly to accept valid descriptor access (false-positivebad-argument-type/no-matching-overload) and accept unsafe callback replacements (false-negativebad-assignment/bad-argument-type).Fixes #4844.
Example
Before
Pyrefly rejected valid instance access for generic descriptors that use
Concatenate. It did not recognise that descriptor binding changes:to:
This affected descriptor APIs such as
@weave.op().Pyrefly also accepted unsafe callback replacements. A replacement could remove keyword support from a callback. A later valid keyword call could then fail at runtime.
After
Pyrefly removes the receiver parameter when it binds these descriptors. It keeps the remaining parameter types, names, and keyword support.
Pyrefly also rejects callback replacements that would make an existing valid call unsafe.
Typing behaviour
A
ParamSpecdescribes function inputs. Input compatibility runs in the opposite direction to return-type compatibility. Pyrefly now uses this direction when it checks aParamSpecinside a generic class.Pyrefly now infers variance correctly when a generic class refers to itself. An incomplete result from the recursive reference no longer overrides direct evidence from methods and fields.
A writable callable field supports both reads and writes. Pyrefly therefore treats its type parameters as invariant. This prevents an unsafe callback replacement.
Test Plan
I've added two regression tests covering #4844:
no-matching-overloadobserved with@weave.op().bad-argument-type.I also added a control test to ensure this fix doesn't over-correct and introduce false-negative errors. That's actually how I found the 2nd bug related to callback replacement 😅
Conformance
Resolves 13 outstanding conformance differences (reduced from 42 to 29).
I used Codex to help draft this PR :)