Visitar URL original
[ty] Infer `ParamSpec` from class constructors for callable protocols by charliermarsh · Pull Request #22853 · astral-sh/ruff · GitHub
Skip to content

[ty] Infer ParamSpec from class constructors for callable protocols - #22853

Merged
charliermarsh merged 1 commit into
mainfrom
charlie/callable
Feb 2, 2026
Merged

charliermarsh merged 1 commit into
mainfrom
charlie/callable

Conversation

@charliermarsh

Copy link
Copy Markdown
Member

Summary

Closes astral-sh/ty#2587.

@charliermarsh charliermarsh added the ty The ty type checker label Jan 25, 2026
@charliermarsh
charliermarsh marked this pull request as ready for review January 25, 2026 19:34
@charliermarsh
charliermarsh marked this pull request as draft January 25, 2026 19:35
@astral-sh-bot

astral-sh-bot Bot commented Jan 25, 2026 •

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

@astral-sh-bot

astral-sh-bot Bot commented Jan 25, 2026 •

Copy link
Copy Markdown

mypy_primer results

Changes were detected when running on open source projects
pydantic (https://github.com/pydantic/pydantic)
- pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | ((dict[str, int | float | str | ... omitted 3 union elements], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
+ pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | ((dict[str, Divergent], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
- pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
+ pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, Divergent], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`

psycopg (https://github.com/psycopg/psycopg)
- psycopg/psycopg/_typeinfo.py:102:51: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `RowFactory[tuple[Any, ...]]`, found `def dict_row(cursor: BaseCursor[Any, Any]) -> RowMaker[dict[str, Any]]`
- psycopg/psycopg/_typeinfo.py:110:40: error[invalid-argument-type] Argument to bound method `_from_records` is incorrect: Expected `Sequence[dict[str, Any]]`, found `list[tuple[Any, ...]]`
- psycopg/psycopg/_typeinfo.py:120:46: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `AsyncRowFactory[tuple[Any, ...]]`, found `def dict_row(cursor: BaseCursor[Any, Any]) -> RowMaker[dict[str, Any]]`
- psycopg/psycopg/_typeinfo.py:128:40: error[invalid-argument-type] Argument to bound method `_from_records` is incorrect: Expected `Sequence[dict[str, Any]]`, found `list[tuple[Any, ...]]`
- Found 653 diagnostics
+ Found 649 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/prefect/deployments/runner.py:997:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
+ src/prefect/deployments/runner.py:997:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | ((...) -> Any)`
+ src/prefect/flow_engine.py:989:32: error[invalid-await] `Unknown | R@FlowRunEngine | Coroutine[Any, Any, R@FlowRunEngine]` is not awaitable
+ src/prefect/flow_engine.py:1580:24: error[invalid-await] `Unknown | R@AsyncFlowRunEngine | Coroutine[Any, Any, R@AsyncFlowRunEngine]` is not awaitable
+ src/prefect/flow_engine.py:1661:43: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Unknown | R@run_generator_flow_sync`
+ src/prefect/flow_engine.py:1669:21: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_sync`
+ src/prefect/flow_engine.py:1703:44: warning[possibly-missing-attribute] Attribute `__anext__` may be missing on object of type `Unknown | R@run_generator_flow_async`
+ src/prefect/flow_engine.py:1710:25: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flows.py:285:34: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:285:34: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
- src/prefect/flows.py:403:68: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:403:68: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
- src/prefect/flows.py:1877:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ src/prefect/flows.py:1937:21: error[no-matching-overload] No overload of function `run_coro_as_sync` matches arguments
- Found 5359 diagnostics
+ Found 5365 diagnostics

hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/models/sw1d_network.py:1168:75: error[invalid-argument-type] Argument to bound method `append_submodel` is incorrect: Expected `RoutingModel_V1 | RoutingModel_V2`, found `RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3`
+ hydpy/models/sw1d_network.py:1169:78: error[invalid-argument-type] Argument to bound method `append_submodel` is incorrect: Expected `RoutingModel_V2 | RoutingModel_V3`, found `RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3`
- Found 660 diagnostics
+ Found 662 diagnostics

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/chain/decoding/tools.py:96:44: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- rotkehlchen/chain/decoding/tools.py:97:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress`, found `A@BaseDecoderTools`
+ rotkehlchen/chain/decoding/tools.py:99:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `Sequence[A@BaseDecoderTools]`, found `Unknown | tuple[BTCAddress, ...] | tuple[ChecksumAddress, ...] | tuple[SubstrateAddress, ...] | tuple[SolanaAddress, ...]`
- rotkehlchen/chain/decoding/tools.py:98:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress | None`, found `A@BaseDecoderTools | None`
+ rotkehlchen/chain/decoding/tools.py:100:62: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 2050 diagnostics
+ Found 2051 diagnostics

core (https://github.com/home-assistant/core)
- homeassistant/util/variance.py:47:12: error[invalid-return-type] Return type does not match returned value: expected `(**_P@ignore_variance) -> _R@ignore_variance`, found `_Wrapped[_P@ignore_variance, int | _R@ignore_variance | float | datetime, _P@ignore_variance, _R@ignore_variance | int | float | datetime]`
- Found 14481 diagnostics
+ Found 14480 diagnostics

No memory usage changes detected ✅

@charliermarsh

Copy link
Copy Markdown
Member Author

Unless I'm mistaken, I think the second comment (astral-sh/ty#2587 (comment)) is a separate issue.

@charliermarsh

Copy link
Copy Markdown
Member Author

I believe the psycopg change is correctly removing false positives.

@charliermarsh
charliermarsh marked this pull request as ready for review January 25, 2026 20:03

@AlexWaygood AlexWaygood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit hesitant to make too many changes to how generic protocols work while #21902 is still in progress (it should land this week, hopefully). Having said that, the tests you've added don't appear to be fixed by Doug's branch there, and it's true that __call__ members on protocols need to be quite heavily special-cased (sadly)

Comment on lines +973 to +974
# error: [invalid-argument-type]
create(MyType, "wrong type")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# error: [invalid-argument-type]
create(MyType, "wrong type")
# error: [too-many-positional-arguments]
create(MyType, 1, 2)
# error: [missing-argument]
# error: [unknown-argument]
create(MyType, y=2)
# error: [unknown-argument]
create(MyType, x=1, y=2)
# error: [invalid-argument-type]
create(MyType, "wrong type")

class NoArgs(ParentClass):
pass

create(NoArgs) # OK - P is inferred as []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
create(NoArgs) # OK - P is inferred as []
create(NoArgs) # OK - P is inferred as []
create(NoArgs, 1) # error: [too-many-positional-arguments]

@AlexWaygood

Copy link
Copy Markdown
Member

(I'd like @dcreager's review on this one!)

@carljm
carljm removed their request for review January 27, 2026 01:27

@dcreager dcreager left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit hesitant to make too many changes to how generic protocols work while #21902 is still in progress (it should land this week, hopefully). Having said that, the tests you've added don't appear to be fixed by Doug's branch there, and it's true that __call__ members on protocols need to be quite heavily special-cased (sadly)

👍 Code looks good! This does not look to conflict much with #21902.

}

/// Infer type mappings by comparing formal callable signatures against actual callables.
fn infer_from_callable_signature(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to this refactoring

@sharkdp
sharkdp removed their request for review February 2, 2026 15:29
@charliermarsh
charliermarsh merged commit 8d78aa6 into main Feb 2, 2026
49 checks passed
@charliermarsh
charliermarsh deleted the charlie/callable branch February 2, 2026 15:55
carljm added a commit that referenced this pull request Feb 2, 2026
* main: (48 commits)
  add info for non_octal permissions (#22972)
  Fix empty body rule rendering (#23039)
  [ty] Infer `ParamSpec` from class constructors for callable protocols (#22853)
  Update NPM Development dependencies (#23030)
  Update CodSpeedHQ/action action to v4.8.2 (#23029)
  [ty] remove special handling for `Any()` in match class patterns (#23011)
  Update Rust crate get-size2 to v0.7.4 (#23022)
  Update Rust crate insta to v1.46.1 (#23023)
  Update taiki-e/install-action action to v2.67.11 (#23033)
  Update Rust crate colored to v3.1.1 (#23031)
  Update cargo-bins/cargo-binstall action to v1.17.3 (#23028)
  Update Rust crate uuid to v1.20.0 (#23032)
  [ty] Avoid using `.node()` for detecting `Self` (#23000)
  Update Rust crate proc-macro2 to v1.0.106 (#23024)
  Update actions/setup-python action to v6.2.0 (#23027)
  [ty] fix query cycles in decorated function with parameter defaults (#23014)
  Update Rust crate quote to v1.0.44 (#23025)
  Update Rust crate thiserror to v2.0.18 (#23026)
  Update Rust crate filetime to v0.2.27 (#23021)
  Update Rust crate clearscreen to v4.0.3 (#23020)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty The ty type checker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False negative when inferring paramspec for a class constructor

3 participants