Skip to content

[ty] Use ParamSpec without the attr for inferable check#21934

Merged
dhruvmanila merged 3 commits intomainfrom
dhruv/inferrable-paramspec-check-without-attr
Dec 15, 2025
Merged

[ty] Use ParamSpec without the attr for inferable check#21934
dhruvmanila merged 3 commits intomainfrom
dhruv/inferrable-paramspec-check-without-attr

Conversation

@dhruvmanila
Copy link
Member

@dhruvmanila dhruvmanila commented Dec 12, 2025

Summary

fixes: astral-sh/ty#1820

Test Plan

Add new mdtests.

Ecosystem changes removes all false positives.

@dhruvmanila dhruvmanila added bug Something isn't working ty Multi-file analysis & type inference labels Dec 12, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 12, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 12, 2025

mypy_primer results

Changes were detected when running on open source projects
pytest-robotframework (https://github.com/detachhead/pytest-robotframework)
+ pytest_robotframework/__init__.py:358:14: warning[unused-ignore-comment] Unused `ty: ignore` directive
- Found 172 diagnostics
+ Found 173 diagnostics

async-utils (https://github.com/mikeshardmind/async-utils)
- src/async_utils/corofunc_cache.py:148:12: error[invalid-return-type] Return type does not match returned value: expected `CoroCacheDeco`, found `def wrapper[**P, R](coro: CoroLike[P@wrapper, R@wrapper], /) -> CoroFunc[P@wrapper, R@wrapper]`
- src/async_utils/corofunc_cache.py:231:12: error[invalid-return-type] Return type does not match returned value: expected `CoroCacheDeco`, found `def wrapper[**P, R](coro: CoroLike[P@wrapper, R@wrapper], /) -> CoroFunc[P@wrapper, R@wrapper]`
- src/async_utils/task_cache.py:213:12: error[invalid-return-type] Return type does not match returned value: expected `TaskCacheDeco`, found `def wrapper[**P, R](coro: TaskCoroFunc[P@wrapper, R@wrapper], /) -> TaskFunc[P@wrapper, R@wrapper]`
- src/async_utils/task_cache.py:301:12: error[invalid-return-type] Return type does not match returned value: expected `TaskCacheDeco`, found `def wrapper[**P, R](coro: TaskCoroFunc[P@wrapper, R@wrapper], /) -> TaskFunc[P@wrapper, R@wrapper]`
- Found 15 diagnostics
+ Found 11 diagnostics

antidote (https://github.com/Finistere/antidote)
- src/antidote/lib/interface_ext/__init__.py:61:24: error[invalid-assignment] Object of type `InterfaceImpl` is not assignable to `Interface`
- src/antidote/lib/lazy_ext/__init__.py:29:14: error[invalid-assignment] Object of type `LazyImpl` is not assignable to `Lazy`
- Found 275 diagnostics
+ Found 273 diagnostics

Expression (https://github.com/cognitedata/Expression)
- expression/effect/async_option.py:122:9: error[invalid-method-override] Invalid override of method `__call__`: Definition is incompatible with `AsyncBuilder.__call__`
- expression/effect/async_result.py:123:9: error[invalid-method-override] Invalid override of method `__call__`: Definition is incompatible with `AsyncBuilder.__call__`
- expression/effect/option.py:98:9: error[invalid-method-override] Invalid override of method `__call__`: Definition is incompatible with `Builder.__call__`
- expression/effect/result.py:89:9: error[invalid-method-override] Invalid override of method `__call__`: Definition is incompatible with `Builder.__call__`
- Found 214 diagnostics
+ Found 210 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 41 diagnostics
+ Found 42 diagnostics

pandas (https://github.com/pandas-dev/pandas)
- pandas/core/resample.py:265:9: error[invalid-method-override] Invalid override of method `pipe`: Definition is incompatible with `BaseGroupBy.pipe`
- pandas/core/window/expanding.py:337:9: error[invalid-method-override] Invalid override of method `pipe`: Definition is incompatible with `RollingAndExpandingMixin.pipe`
- pandas/core/window/rolling.py:2266:9: error[invalid-method-override] Invalid override of method `pipe`: Definition is incompatible with `RollingAndExpandingMixin.pipe`
- Found 3717 diagnostics
+ Found 3714 diagnostics

No memory usage changes detected ✅

@dhruvmanila dhruvmanila marked this pull request as ready for review December 12, 2025 08:27
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Thank you!

Comment on lines 2177 to 2193
Copy link
Contributor

Choose a reason for hiding this comment

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

I find it a bit suspicious that this rule is so much simpler than our existing rules for other typevars, which require six or seven different match clauses to implement. It seems like in principle the rules should be the same, the only difference being that we need to account for stripping the attributes in this case.

Is it possible that this could be instead implemented by just checking the attribute is the same, stripping the attribute, and then delegating to a recursive call to has_relation_to_impl with the base typevars instead of the attributes?

Alternatively, given the restrictions on where ParamSpec attributes can legally appear (only in a callable signature, and only together, applied to *args and **kwargs), I wonder if this wouldn't be easier/simpler if we handled it directly in callable-type has_relation_to_impl?

If none of that seems to work easily, I think it'd be OK to land this version (ecosystem impact looks good as far as it goes!), but maybe with an added TODO that this probably isn't fully correct yet?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is it possible that this could be instead implemented by just checking the attribute is the same, stripping the attribute, and then delegating to a recursive call to has_relation_to_impl with the base typevars instead of the attributes?

Yeah, I tried this but it fails in this test case that I added in this PR:

from typing import Callable

class Container[**P]:
    def method(self, f: Callable[P, None]) -> Callable[P, None]:
        return f

    def try_assign[**Q](self, f: Callable[Q, None]) -> Callable[Q, None]:
        # error: [invalid-return-type] "Return type does not match returned value: expected `(**Q@try_assign) -> None`, found `(**P@Container) -> None`"
        # error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
        return self.method(f)

where the invalid-argument-type diagnostic is not emitted.

Alternatively, given the restrictions on where ParamSpec attributes can legally appear (only in a callable signature, and only together, applied to *args and **kwargs), I wonder if this wouldn't be easier/simpler if we handled it directly in callable-type has_relation_to_impl?

Yeah, I think that makes sense. I'll move this check to the Signature::has_relation_to_inner

Some additional context here: https://discord.com/channels/1039017663004942429/1449080092515893316/1449975836055703673

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. I think perhaps this reveals a bug in our handling of non-ParamSpec typevars?

class Container[T]:
    def method(self, x: T) -> T:
        return x

    def try_assign[U](self, x: U) -> U:
        return self.method(x)

https://play.ty.dev/e477cd72-491d-4953-b77a-97ad0d47fd1f

It seems like there should be two diagnostics on the last line there as well (I wouldn't expect U to be assignable to T in the call to self.method), but we only get one (invalid return type).

Copy link
Contributor

Choose a reason for hiding this comment

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

@dhruvmanila dhruvmanila merged commit ba47349 into main Dec 15, 2025
42 checks passed
@dhruvmanila dhruvmanila deleted the dhruv/inferrable-paramspec-check-without-attr branch December 15, 2025 05:34
dcreager added a commit that referenced this pull request Dec 15, 2025
* origin/main:
  Update MSRV to 1.90 (#21987)
  [ty] Improve check enforcing that an overloaded function must have an implementation (#21978)
  Update actions/checkout digest to 8e8c483 (#21982)
  [ty] Use `ParamSpec` without the attr for inferable check (#21934)
  [ty] Emit diagnostic when a type variable with a default is followed by one without a default (#21787)
  [ty] Fix callout syntax in configuration mkdocs (#1875) (#21961)
  Update debug_assert which pointed at missing method (#21969)
  [ty] Add support for `__qualname__` and other implicit class attributes (#21966)
dcreager added a commit that referenced this pull request Dec 15, 2025
* origin/main:
  Fluent formatting of method chains (#21369)
  [ty] Avoid stack overflow when calculating inferable typevars (#21971)
  [ty] Add "qualify ..." code fix for undefined references (#21968)
  [ty] Use jemalloc on linux (#21975)
  Update MSRV to 1.90 (#21987)
  [ty] Improve check enforcing that an overloaded function must have an implementation (#21978)
  Update actions/checkout digest to 8e8c483 (#21982)
  [ty] Use `ParamSpec` without the attr for inferable check (#21934)
  [ty] Emit diagnostic when a type variable with a default is followed by one without a default (#21787)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

invalid-method-override false positive when overriding method that uses Callable with a paramspec

2 participants

Comments