Skip to content

[ty] Refine Callable class-decorator fallback for unknown results#25250

Merged
charliermarsh merged 4 commits into
mainfrom
charlie/callable-decorator
May 21, 2026
Merged

[ty] Refine Callable class-decorator fallback for unknown results#25250
charliermarsh merged 4 commits into
mainfrom
charlie/callable-decorator

Conversation

@charliermarsh
Copy link
Copy Markdown
Member

@charliermarsh charliermarsh commented May 20, 2026

Summary

This PR addresses a TODO from #25091 whereby we preserved the class binding for every Callable
decorator. We now respect explicit return annotations on Callable.

For example, this now remains a replacement rather than being treated as class-preserving:

from typing import Callable, TypeVar

T = TypeVar("T")

def decorator_factory() -> Callable[[type[object]], T]:
    raise NotImplementedError

@decorator_factory()
class Decorated: ...

reveal_type(Decorated)  # Unknown

@charliermarsh charliermarsh force-pushed the charlie/callable-decorator branch from eefba0b to b66ba5b Compare May 20, 2026 13:59
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 20, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 89.36%. The percentage of expected errors that received a diagnostic held steady at 85.49%. The number of fully passing files held steady at 88/134.

@charliermarsh charliermarsh force-pushed the charlie/callable-decorator branch from b66ba5b to 312f04e Compare May 20, 2026 14:01
@charliermarsh charliermarsh marked this pull request as ready for review May 20, 2026 14:01
@astral-sh-bot astral-sh-bot Bot requested a review from dhruvmanila May 20, 2026 14:01
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 20, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 688.84MB 688.86MB +0.00% (22.93kB)
trio 113.60MB 113.60MB +0.00% (2.62kB)
sphinx 256.66MB 256.66MB +0.00% (448.00B)
flake8 47.52MB 47.52MB +0.00% (304.00B)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
CallableType 2.50MB 2.52MB +0.86% (21.95kB)
infer_definition_types 82.00MB 82.00MB +0.00% (996.00B)

trio

Name Old New Diff Outcome
CallableType 640.52kB 643.04kB +0.39% (2.52kB)
infer_definition_types 7.24MB 7.24MB +0.00% (108.00B)

sphinx

Name Old New Diff Outcome
CallableType 1.21MB 1.22MB +0.04% (448.00B)

flake8

Name Old New Diff Outcome
CallableType 178.55kB 178.84kB +0.17% (304.00B)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 20, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-type-form 0 18 0
unused-type-ignore-comment 11 0 0
unknown-argument 0 6 0
invalid-argument-type 0 4 0
missing-argument 0 4 0
unresolved-attribute 0 4 0
invalid-assignment 0 0 1
no-matching-overload 0 1 0
Total 11 37 1
Raw diff (49 changes)
pandera (https://github.com/pandera-dev/pandera)
- pandera/typing/pandas.py:108:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:109:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:110:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:111:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:112:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:113:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:114:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:115:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- pandera/typing/pandas.py:116:9 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
+ pandera/typing/pyspark_sql.py:35:29 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:36:26 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:37:30 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:38:31 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:39:30 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:40:29 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:41:28 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:43:27 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:44:32 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pandera/typing/pyspark_sql.py:45:29 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- tests/geopandas/test_engine.py:119:20 error[no-matching-overload] No overload of function `any` matches arguments
+ tests/pandas/test_dtypes.py:158:74 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- tests/pandas/test_dtypes.py:176:43 error[invalid-assignment] Object of type `list[tuple[dict[Unknown, Unknown], list[Unknown]] | tuple[dict[<class 'datetime'> | <class 'datetime64'> | type[Unknown] | ... omitted 3 union elements, str], Series[Timestamp]] | tuple[dict[PeriodDtype, str], Series[Period]] | tuple[dict[<class 'SparseDtype'> | SparseDtype, SparseDtype], Series[Any]] | tuple[dict[IntervalDtype, str], Series[Interval[int | float]]]]` is not assignable to `list[tuple[dict[Unknown, Unknown], list[Unknown]]]`
+ tests/pandas/test_dtypes.py:176:43 error[invalid-assignment] Object of type `list[tuple[dict[Unknown, Unknown], list[Unknown]] | tuple[dict[<class 'datetime'> | <class 'datetime64'> | type[Unknown] | DatetimeTZDtype | Unknown, str], Series[Timestamp]] | tuple[dict[PeriodDtype, str], Series[Period]] | tuple[dict[<class 'SparseDtype'> | SparseDtype, SparseDtype], Series[Any]] | tuple[dict[IntervalDtype, str], Series[Interval[int | float]]]]` is not assignable to `list[tuple[dict[Unknown, Unknown], list[Unknown]]]`
- tests/pandas/test_logical_dtypes.py:212:12 error[unresolved-attribute] Object of type `bool | Iterable[bool]` has no attribute `all`
- tests/pandas/test_logical_dtypes.py:235:16 error[unresolved-attribute] Object of type `bool | Iterable[bool]` has no attribute `any`
- tests/pandas/test_logical_dtypes.py:243:31 error[invalid-argument-type] Argument to bound method `Decimal.check` is incorrect: Expected `DataType`, found `dtype[generic[Any]] | ExtensionDtype`
- tests/pandas/test_logical_dtypes.py:245:12 error[unresolved-attribute] Object of type `bool | Iterable[bool]` has no attribute `dtype`
- tests/pandas/test_logical_dtypes.py:269:9 error[invalid-argument-type] Argument to bound method `Decimal.check` is incorrect: Expected `pandera.engines.pandas_engine.DataType`, found `pandera.dtypes.DataType`
- tests/pandas/test_logical_dtypes.py:269:52 error[invalid-argument-type] Argument to bound method `Decimal.check` is incorrect: Expected `Series[Any] | None`, found `Series[Any] | DataFrame`
- tests/pandas/test_logical_dtypes.py:272:12 error[unresolved-attribute] Object of type `bool | Iterable[bool]` has no attribute `all`
- tests/pandas/test_pandas_engine.py:129:24 error[invalid-argument-type] Argument to bound method `DataType.check` is incorrect: Expected `DataType`, found `dtype[generic[Any]] | ExtensionDtype | Series[Any]`
- tests/pandas/test_pandas_engine.py:212:9 error[missing-argument] No argument provided for required parameter `dtype` of `DataType.__init__`
- tests/pandas/test_pandas_engine.py:213:13 error[unknown-argument] Argument `tz` does not match any known parameter of `DataType.__init__`
- tests/pandas/test_pandas_engine.py:213:26 error[unknown-argument] Argument `tz_localize_kwargs` does not match any known parameter of `DataType.__init__`
- tests/pandas/test_pandas_engine.py:376:13 error[unknown-argument] Argument `time_zone_agnostic` does not match any known parameter of `DataType.__init__`
- tests/pandas/test_pandas_engine.py:376:38 error[unknown-argument] Argument `tz` does not match any known parameter of `DataType.__init__`
- tests/pandas/test_pandas_engine.py:375:26 error[missing-argument] No argument provided for required parameter `dtype` of `DataType.__init__`
- tests/pandas/test_typing.py:86:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:98:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:102:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:106:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:110:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:114:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:118:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:122:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pandas/test_typing.py:126:17 error[invalid-type-form] Variable of type `type[Unknown]` is not allowed in a type expression
- tests/pyspark/test_schemas_on_pyspark_pandas.py:280:13 error[missing-argument] No argument provided for required parameter `dtype` of `DataType.__init__`
- tests/pyspark/test_schemas_on_pyspark_pandas.py:280:13 error[missing-argument] No argument provided for required parameter `dtype` of `DataType.__init__`
- tests/pyspark/test_schemas_on_pyspark_pandas.py:280:36 error[unknown-argument] Argument `tz` does not match any known parameter of `DataType.__init__`
- tests/pyspark/test_schemas_on_pyspark_pandas.py:280:36 error[unknown-argument] Argument `tz` does not match any known parameter of `DataType.__init__`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh marked this pull request as draft May 20, 2026 14:06
@charliermarsh charliermarsh force-pushed the charlie/callable-decorator branch 2 times, most recently from 48c7c74 to c276337 Compare May 20, 2026 14:21
@charliermarsh charliermarsh marked this pull request as ready for review May 20, 2026 14:26

pub(super) kind: CallableTypeKind,

pub(crate) provenance: Option<CallableFunctionProvenance>,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Most of the changes in this diff are mechanically threading this through so that we don't lose this information at decorator-application time.

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.

Can you say what does it mean when this field is None?

Is it that if it's Some(...) then this callable is coming from either a function literal, bound method literal or a lambda expression? And, if it's None then it's coming from Callable annotation or a ParamSpec specialization?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, that's roughly right, but there are other cases where it could be None, e.g., we also use callable when we synthesize methods, and those are also marked as None here since there's no upstream function.

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.

Understood, thanks

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 20, 2026

Merging this PR will not alter performance

✅ 57 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing charlie/callable-decorator (5300e19) with main (4ff86c7)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@ntBre ntBre added the ty Multi-file analysis & type inference label May 20, 2026
Comment on lines +331 to +334
/// Whether this callable is known to come from a function and how that function's return type was
/// declared.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
pub enum CallableFunctionProvenance {
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 unable to follow where does the "whether this callable is known to come from a function" information is present because the enum members suggests only whether the return type is implicit or explicit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I moved the None in here and tried to clarify in the docs.


pub(super) kind: CallableTypeKind,

pub(crate) provenance: Option<CallableFunctionProvenance>,
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.

Can you say what does it mean when this field is None?

Is it that if it's Some(...) then this callable is coming from either a function literal, bound method literal or a lambda expression? And, if it's None then it's coming from Callable annotation or a ParamSpec specialization?

@charliermarsh charliermarsh force-pushed the charlie/callable-decorator branch from c276337 to 5300e19 Compare May 21, 2026 08:23
@charliermarsh charliermarsh merged commit 25a3191 into main May 21, 2026
59 checks passed
@charliermarsh charliermarsh deleted the charlie/callable-decorator branch May 21, 2026 08:36
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
…tral-sh#25250)

## Summary

This PR addresses a TODO from astral-sh#25091 whereby we preserved the class
binding for every `Callable`
decorator. We now respect explicit return annotations on `Callable`.

For example, this now remains a replacement rather than being treated as
class-preserving:

```python
from typing import Callable, TypeVar

T = TypeVar("T")

def decorator_factory() -> Callable[[type[object]], T]:
    raise NotImplementedError

@decorator_factory()
class Decorated: ...

reveal_type(Decorated)  # Unknown
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants