ruff icon indicating copy to clipboard operation
ruff copied to clipboard

[ty] Fix `@staticmethod` combined with other decorators incorrectly binding `self`

Open charliermarsh opened this issue 2 weeks ago • 8 comments

Summary

We already had CallableTypeKind::ClassMethodLike to track callables that behave like classmethods (always bind the first argument). This PR adds the symmetric CallableTypeKind::StaticMethodLike for callables that behave like staticmethods (never bind self).

Closes https://github.com/astral-sh/ty/issues/2114.

charliermarsh avatar Dec 21 '25 14:12 charliermarsh

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

astral-sh-bot[bot] avatar Dec 21 '25 14:12 astral-sh-bot[bot]

mypy_primer results

Changes were detected when running on open source projects
cki-lib (https://gitlab.com/cki-project/cki-lib)
+ tests/test_yaml.py:255:78: error[invalid-argument-type] Argument is incorrect: Expected `str`, found `None | Literal["a: {pooh: bear}", "1", "a: {crow: bar, pooh: !reference [a, crow]}"]`
- tests/test_yaml.py:255:78: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
- tests/test_yaml.py:285:37: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
- tests/test_yaml.py:300:37: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
- Found 242 diagnostics
+ Found 240 diagnostics

pydantic (https://github.com/pydantic/pydantic)
- pydantic/fields.py:943: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:943: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:983: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:983: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:1026: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:1026: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:1066: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:1066: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:1109: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:1109: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:1148: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:1148: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:1188: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:1188: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:1567: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:1567: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`

mypy (https://github.com/python/mypy)
+ mypy/typeshed/stdlib/_frozen_importlib.pyi:76:13: error[invalid-method-override] Invalid override of method `module_repr`: Definition is incompatible with `Loader.module_repr`
+ mypy/typeshed/stdlib/_frozen_importlib.pyi:81:13: error[invalid-method-override] Invalid override of method `exec_module`: Definition is incompatible with `InspectLoader.exec_module`
+ mypy/typeshed/stdlib/_frozen_importlib.pyi:124:9: error[invalid-method-override] Invalid override of method `exec_module`: Definition is incompatible with `InspectLoader.exec_module`
+ mypy/typeshed/stdlib/_frozen_importlib_external.pyi:57:13: error[invalid-method-override] Invalid override of method `invalidate_caches`: Definition is incompatible with `MetaPathFinder.invalidate_caches`
- Found 1780 diagnostics
+ Found 1784 diagnostics

Tanjun (https://github.com/FasterSpeeding/Tanjun)
- tanjun/dependencies/data.py:347:12: error[invalid-return-type] Return type does not match returned value: expected `_T@cached_inject`, found `Coroutine[Any, Any, _T@cached_inject | Coroutine[Any, Any, _T@cached_inject]] | _T@cached_inject`
+ tanjun/dependencies/data.py:347:12: error[invalid-return-type] Return type does not match returned value: expected `_T@cached_inject`, found `_T@cached_inject | Coroutine[Any, Any, _T@cached_inject | Coroutine[Any, Any, _T@cached_inject]]`

meson (https://github.com/mesonbuild/meson)
+ mesonbuild/compilers/objc.py:47:9: error[invalid-method-override] Invalid override of method `get_display_language`: Definition is incompatible with `Compiler.get_display_language`
+ mesonbuild/compilers/objcpp.py:48:9: error[invalid-method-override] Invalid override of method `get_display_language`: Definition is incompatible with `Compiler.get_display_language`
- Found 1947 diagnostics
+ Found 1949 diagnostics

cloud-init (https://github.com/canonical/cloud-init)
+ cloudinit/net/activators.py:237:9: error[invalid-method-override] Invalid override of method `bring_up_interfaces`: Definition is incompatible with `NetworkActivator.bring_up_interfaces`
+ cloudinit/net/activators.py:251:9: error[invalid-method-override] Invalid override of method `bring_up_all_interfaces`: Definition is incompatible with `NetworkActivator.bring_up_all_interfaces`
+ cloudinit/net/activators.py:298:9: error[invalid-method-override] Invalid override of method `bring_up_all_interfaces`: Definition is incompatible with `NetworkActivator.bring_up_all_interfaces`
+ cloudinit/sources/DataSourceCloudCIX.py:93:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceCloudSigma.py:34:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceCloudStack.py:194:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceExoscale.py:136:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceLXD.py:197:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceNWCS.py:124:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceOracle.py:175:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceScaleway.py:238:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
+ cloudinit/sources/DataSourceVultr.py:48:9: error[invalid-method-override] Invalid override of method `ds_detect`: Definition is incompatible with `DataSource.ds_detect`
- Found 1184 diagnostics
+ Found 1196 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
+ tests/contrib/aws_lambda/handlers.py:39:9: error[invalid-method-override] Invalid override of method `__call__`: Definition is incompatible with `type.__call__`
+ tests/tracer/test_writer.py:718:9: error[invalid-method-override] Invalid override of method `log_message`: Definition is incompatible with `BaseHTTPRequestHandler.log_message`
- Found 8401 diagnostics
+ Found 8403 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:98:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 43 diagnostics
+ Found 42 diagnostics

django-stubs (https://github.com/typeddjango/django-stubs)
+ django-stubs/contrib/gis/geos/prototypes/coordseq.pyi:20:9: error[invalid-method-override] Invalid override of method `errcheck`: Definition is incompatible with `GEOSFuncFactory.errcheck`
- Found 443 diagnostics
+ Found 444 diagnostics

ibis (https://github.com/ibis-project/ibis)
+ ibis/backends/duckdb/converter.py:19:13: error[invalid-method-override] Invalid override of method `convert_Array`: Definition is incompatible with `PandasData.convert_Array`
- Found 4608 diagnostics
+ Found 4609 diagnostics

scikit-learn (https://github.com/scikit-learn/scikit-learn)
- sklearn/externals/array_api_extra/_lib/_at.py:300:17: warning[possibly-missing-attribute] Attribute `dtype` may be missing on object of type `int | Array | tuple[int | slice[Any, Any, Any] | EllipsisType | Array, ...] | slice[Any, Any, Any] | EllipsisType`
+ sklearn/externals/array_api_extra/_lib/_at.py:300:17: warning[possibly-missing-attribute] Attribute `dtype` may be missing on object of type `int | slice[Any, Any, Any] | EllipsisType | Array | tuple[int | slice[Any, Any, Any] | EllipsisType | Array, ...]`
- sklearn/externals/array_api_extra/_lib/_at.py:301:17: warning[possibly-missing-attribute] Attribute `shape` may be missing on object of type `int | Array | tuple[int | slice[Any, Any, Any] | EllipsisType | Array, ...] | slice[Any, Any, Any] | EllipsisType`
+ sklearn/externals/array_api_extra/_lib/_at.py:301:17: warning[possibly-missing-attribute] Attribute `shape` may be missing on object of type `int | slice[Any, Any, Any] | EllipsisType | Array | tuple[int | slice[Any, Any, Any] | EllipsisType | Array, ...]`
- sklearn/externals/array_api_extra/_lib/_at.py:308:25: error[invalid-argument-type] Argument to function `apply_where` is incorrect: Expected `Array`, found `int | Array | tuple[int | slice[Any, Any, Any] | EllipsisType | Array, ...] | slice[Any, Any, Any] | EllipsisType`
+ sklearn/externals/array_api_extra/_lib/_at.py:308:25: error[invalid-argument-type] Argument to function `apply_where` is incorrect: Expected `Array`, found `int | slice[Any, Any, Any] | EllipsisType | Array | tuple[int | slice[Any, Any, Any] | EllipsisType | Array, ...]`

jax (https://github.com/google/jax)
+ jax/_src/tree_util.py:302:31: error[invalid-argument-type] Argument to bound method `register_node` is incorrect: Expected `(Hashable, Iterable[object], /) -> T@register_pytree_node`, found `(_AuxData@register_pytree_node, _Children@register_pytree_node, /) -> T@register_pytree_node`
+ jax/_src/tree_util.py:305:31: error[invalid-argument-type] Argument to bound method `register_node` is incorrect: Expected `(Hashable, Iterable[object], /) -> T@register_pytree_node`, found `(_AuxData@register_pytree_node, _Children@register_pytree_node, /) -> T@register_pytree_node`
+ jax/_src/tree_util.py:308:31: error[invalid-argument-type] Argument to bound method `register_node` is incorrect: Expected `(Hashable, Iterable[object], /) -> T@register_pytree_node`, found `(_AuxData@register_pytree_node, _Children@register_pytree_node, /) -> T@register_pytree_node`
- Found 2803 diagnostics
+ Found 2806 diagnostics

hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/models/hland/hland_masks.py:15:9: error[invalid-method-override] Invalid override of method `get_refindices`: Definition is incompatible with `IndexMask.get_refindices`
+ hydpy/models/lland/lland_masks.py:48:9: error[invalid-method-override] Invalid override of method `get_refindices`: Definition is incompatible with `IndexMask.get_refindices`
+ hydpy/models/whmod/whmod_masks.py:21:9: error[invalid-method-override] Invalid override of method `get_refindices`: Definition is incompatible with `IndexMask.get_refindices`
+ hydpy/models/whmod/whmod_masks.py:108:9: error[invalid-method-override] Invalid override of method `get_refindices`: Definition is incompatible with `IndexMask.get_refindices`
+ hydpy/models/wland/wland_masks.py:22:9: error[invalid-method-override] Invalid override of method `get_refindices`: Definition is incompatible with `IndexMask.get_refindices`
- Found 680 diagnostics
+ Found 685 diagnostics

sympy (https://github.com/sympy/sympy)
+ sympy/core/numbers.py:2845:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Integer.__abs__`
+ sympy/core/numbers.py:2849:9: error[invalid-method-override] Invalid override of method `__neg__`: Definition is incompatible with `Integer.__neg__`
+ sympy/core/numbers.py:2908:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Integer.__abs__`
+ sympy/core/numbers.py:2912:9: error[invalid-method-override] Invalid override of method `__neg__`: Definition is incompatible with `Integer.__neg__`
+ sympy/core/numbers.py:2922:9: error[invalid-method-override] Invalid override of method `factors`: Definition is incompatible with `Rational.factors`
+ sympy/core/numbers.py:2964:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Integer.__abs__`
+ sympy/core/numbers.py:2968:9: error[invalid-method-override] Invalid override of method `__neg__`: Definition is incompatible with `Integer.__neg__`
+ sympy/core/numbers.py:3022:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Rational.__abs__`
+ sympy/core/numbers.py:3555:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Expr.__abs__`
+ sympy/core/numbers.py:3565:9: error[invalid-method-override] Invalid override of method `__neg__`: Definition is incompatible with `Expr.__neg__`
+ sympy/core/numbers.py:3684:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Expr.__abs__`
+ sympy/core/numbers.py:3847:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Expr.__abs__`
+ sympy/core/numbers.py:4161:9: error[invalid-method-override] Invalid override of method `__abs__`: Definition is incompatible with `Expr.__abs__`
- Found 15345 diagnostics
+ Found 15358 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Series[Any, Any] | TypeBlocks | Batch | ... omitted 6 union elements, generic[object]]`
+ static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Top[Index[Any]] | TypeBlocks | Top[Bus[Any]] | ... omitted 6 union elements, generic[object]]`
- static_frame/core/series.py:4072:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[SeriesHE[Any, Any], TVDtype@SeriesHE]`, found `InterGetItemILocReduces[Bottom[Series[Any, Any]] | TypeBlocks | Batch | ... omitted 7 union elements, generic[object]]`
+ static_frame/core/series.py:4072:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[SeriesHE[Any, Any], TVDtype@SeriesHE]`, found `InterGetItemILocReduces[Self@iloc | SeriesHE[Any, Any], generic[object]]`
- static_frame/core/yarn.py:418:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Yarn[Any], object_]`, found `InterGetItemILocReduces[Top[Yarn[Any]] | TypeBlocks | Batch | ... omitted 6 union elements, generic[object]]`
+ static_frame/core/yarn.py:418:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Yarn[Any], object_]`, found `InterGetItemILocReduces[Top[Index[Any]] | TypeBlocks | Top[Bus[Any]] | ... omitted 6 union elements, generic[object]]`

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/db/solanatx.py:99:9: error[invalid-method-override] Invalid override of method `get_transactions`: Definition is incompatible with `DBCommonTx.get_transactions`
- Found 2095 diagnostics
+ Found 2096 diagnostics

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ pandas-stubs/_typing.pyi:1232:16: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ tests/frame/test_groupby.py:228:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
+ tests/frame/test_groupby.py:624:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- Found 5104 diagnostics
+ Found 5107 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, _R@ignore_variance | int | float | datetime, _P@ignore_variance, _R@ignore_variance | int | float | datetime]`
- Found 14412 diagnostics
+ Found 14413 diagnostics


No memory usage changes detected ✅

astral-sh-bot[bot] avatar Dec 21 '25 14:12 astral-sh-bot[bot]

A lot of new diagnostics but AFAICT they are correct based on how we intend to enforce Liskov per liskov.md? For example, in SymPy, class Integer defines __abs__ as an instance method:

https://github.com/sympy/sympy/blob/221edeeada0fa2b20e89d093e1f833efec4dce30/sympy/core/numbers.py#L1890

But the subclass Zero defines it as a static method:

https://github.com/sympy/sympy/blob/221edeeada0fa2b20e89d093e1f833efec4dce30/sympy/core/numbers.py#L2844C1-L2846C22

charliermarsh avatar Dec 21 '25 15:12 charliermarsh

You need to update an assertion in the benchmarks:

thread 'main' (4942) panicked at crates/ruff_benchmark/benches/ty_walltime.rs:78:5: Expected between 1 and 13100 diagnostics on project 'sympy' but got 13106

AlexWaygood avatar Dec 21 '25 15:12 AlexWaygood

Thank you!

charliermarsh avatar Dec 21 '25 15:12 charliermarsh

It would be great if that was a snapshot or something, but I think then it would be hard for it to serve its intended purpose of checking that the benchmark itself was working correctly :/

AlexWaygood avatar Dec 21 '25 15:12 AlexWaygood

(I think I noticed it last time when I clicked in, but I must not have looked closely enough this time, I thought it was spurious.)

charliermarsh avatar Dec 21 '25 15:12 charliermarsh

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

astral-sh-bot[bot] avatar Dec 21 '25 16:12 astral-sh-bot[bot]

class Integer defines __abs__ as an instance method

Pyright errors on this, Pyrefly and mypy don't. It's a bit of a gray area -- when called on an instance, that's a perfectly compatible override, but if called on a type, it's not.

The rationale for not erroring on this is that calling normal methods on types is totally Liskov-broken anyway, because self is always overridden in a Liskov-incompatible way by default. So arguably we should error on calling normal methods on types (passing in self manually), but not error on this override.

But either way, this is something we can handle separately in the Liskov work.

carljm avatar Dec 24 '25 01:12 carljm