Skip to content

[ty] Fix broken property tests for disjointness#18384

Merged
AlexWaygood merged 1 commit intomainfrom
alex/property-test-failure
May 30, 2025
Merged

[ty] Fix broken property tests for disjointness#18384
AlexWaygood merged 1 commit intomainfrom
alex/property-test-failure

Conversation

@AlexWaygood
Copy link
Member

Summary

Fixes astral-sh/ty#549

This assertion passes on main:

static_assert(is_disjoint_from(bool, Callable[..., Any]))

But this one does not:

static_assert(is_disjoint_from(Callable[..., Any], bool))

https://play.ty.dev/d0a6b6c2-e492-4330-a866-2c4b0d51085a

The reason is that the self.member_lookup_with_policy() call here is incorrect. self might not be the NominalInstance type in the match; it might be the Callable type:

(
Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_),
Type::NominalInstance(instance),
)
| (
Type::NominalInstance(instance),
Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_),
) if instance.class.is_final(db) => {
let member = self.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
);

I also removed an invalid TODO comment, as per the conversation in #18368 (comment)

Test Plan

  • Added mdtests
  • QUICKCHECK_TESTS=100000 cargo test --release -p ty_python_semantic -- --ignored types::property_tests::stable passes again

@AlexWaygood AlexWaygood added internal An internal refactor or improvement ty Multi-file analysis & type inference labels May 30, 2025
@AlexWaygood
Copy link
Member Author

(I added the "internal" label because the bug being fixed here hasn't appeared in any release, so this doesn't deserve an inclusion in the release notes.)

@github-actions
Copy link
Contributor

github-actions bot commented May 30, 2025

mypy_primer results

Changes were detected when running on open source projects
beartype (https://github.com/beartype/beartype)
+ error[unresolved-attribute] beartype/_util/hint/pep/proposal/pep612.py:596:9: Type `(...) -> Unknown` has no attribute `__name__`
- Found 575 diagnostics
+ Found 576 diagnostics

starlette (https://github.com/encode/starlette)
+ error[unresolved-attribute] starlette/config.py:139:81: Type `(Any, /) -> Any` has no attribute `__name__`
- Found 177 diagnostics
+ Found 178 diagnostics

ignite (https://github.com/pytorch/ignite)
+ error[not-iterable] ignite/handlers/base_logger.py:52:29: Object of type `list[Unknown] | ((str, Unknown, /) -> bool)` may not be iterable
- Found 2222 diagnostics
+ Found 2223 diagnostics

werkzeug (https://github.com/pallets/werkzeug)
- error[unsupported-operator] src/werkzeug/utils.py:508:12: Operator `>` is not supported for types `(str | None, /) -> int | None` and `Literal[0]`, in comparing `int | (((str | None, /) -> int | None) & ~None) | (Unknown & ~None)` with `Literal[0]`
+ error[unsupported-operator] src/werkzeug/utils.py:508:12: Operator `>` is not supported for types `(str | None, /) -> int | None` and `Literal[0]`, in comparing `int | ((str | None, /) -> int | None) | (Unknown & ~None)` with `Literal[0]`
- error[invalid-assignment] src/werkzeug/utils.py:512:9: Object of type `int | (((str | None, /) -> int | None) & ~None) | (Unknown & ~None)` is not assignable to attribute `max_age` of type `int | None`
+ error[invalid-assignment] src/werkzeug/utils.py:512:9: Object of type `int | ((str | None, /) -> int | None) | (Unknown & ~None)` is not assignable to attribute `max_age` of type `int | None`
- warning[unused-ignore-comment] src/werkzeug/utils.py:513:45: Unused blanket `type: ignore` directive
- Found 452 diagnostics
+ Found 451 diagnostics

nox (https://github.com/wntrblm/nox)
- warning[possibly-unbound-attribute] nox/registry.py:107:26: Attribute `__name__` on type `(((...) -> Any) & ~None) | Func` is possibly unbound
- warning[possibly-unbound-attribute] nox/registry.py:120:23: Attribute `__name__` on type `(((...) -> Any) & ~None) | Func` is possibly unbound
+ error[unresolved-attribute] nox/registry.py:107:26: Type `((...) -> Any) | Func` has no attribute `__name__`
+ error[unresolved-attribute] nox/registry.py:120:23: Type `((...) -> Any) | Func` has no attribute `__name__`

websockets (https://github.com/aaugustin/websockets)
+ error[unresolved-attribute] src/websockets/server.py:110:17: Type `(ServerProtocol, Sequence[Unknown], /) -> Unknown | None` has no attribute `__get__`
- Found 109 diagnostics
+ Found 110 diagnostics

vision (https://github.com/pytorch/vision)
- error[invalid-argument-type] test/datasets_utils.py:835:52: Argument to function `create_image_file` is incorrect: Expected `Sequence[int] | int`, found `Unknown | Sequence[int] | int | (((int, /) -> Sequence[int] | int) & ~None)`
+ error[invalid-argument-type] test/datasets_utils.py:835:52: Argument to function `create_image_file` is incorrect: Expected `Sequence[int] | int`, found `Unknown | Sequence[int] | int | ((int, /) -> Sequence[int] | int)`

hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
+ error[invalid-assignment] src/hydra_zen/third_party/beartype.py:125:9: Implicit shadowing of function `__init__`
- Found 639 diagnostics
+ Found 640 diagnostics

pytest (https://github.com/pytest-dev/pytest)
- error[invalid-argument-type] src/_pytest/fixtures.py:1022:42: Argument to function `_eval_scope_callable` is incorrect: Expected `(str, Config, /) -> Unknown`, found `Scope | (Unknown & ~None) | (((str, Config, /) -> Unknown) & ~None)`
+ error[invalid-argument-type] src/_pytest/fixtures.py:1022:42: Argument to function `_eval_scope_callable` is incorrect: Expected `(str, Config, /) -> Unknown`, found `Scope | (Unknown & ~None) | ((str, Config, /) -> Unknown)`
- error[invalid-argument-type] src/_pytest/fixtures.py:1385:9: Argument is incorrect: Expected `tuple[object, ...] | ((Any, /) -> object) | None`, found `None | Sequence[object] | (((Any, /) -> object) & ~None) | tuple[Unknown, ...]`
+ error[invalid-argument-type] src/_pytest/fixtures.py:1385:9: Argument is incorrect: Expected `tuple[object, ...] | ((Any, /) -> object) | None`, found `None | Sequence[object] | ((Any, /) -> object) | tuple[Unknown, ...]`
- error[invalid-argument-type] src/_pytest/fixtures.py:1385:70: Argument to function `__new__` is incorrect: Expected `Iterable[Unknown]`, found `Sequence[object] | (((Any, /) -> object) & ~None)`
+ error[invalid-argument-type] src/_pytest/fixtures.py:1385:70: Argument to function `__new__` is incorrect: Expected `Iterable[Unknown]`, found `Sequence[object] | ((Any, /) -> object)`
- error[invalid-argument-type] src/_pytest/python.py:1380:13: Argument is incorrect: Expected `((Any, /) -> object) | None`, found `None | Iterable[object] | (((Any, /) -> object) & ~None)`
+ error[invalid-argument-type] src/_pytest/python.py:1380:13: Argument is incorrect: Expected `((Any, /) -> object) | None`, found `None | Iterable[object] | ((Any, /) -> object)`

streamlit (https://github.com/streamlit/streamlit)
+ warning[possibly-unbound-attribute] lib/streamlit/runtime/fragment.py:171:47: Attribute `__qualname__` on type `Unknown | F` is possibly unbound
+ error[call-non-callable] lib/streamlit/runtime/fragment.py:245:38: Object of type `F` is not callable
+ error[call-non-callable] lib/streamlit/runtime/metrics_util.py:443:22: Object of type `F` is not callable
- Found 3271 diagnostics
+ Found 3274 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- warning[possibly-unbound-attribute] ddtrace/debugging/_signal/utils.py:227:38: Attribute `__name__` on type `(((Any, /) -> bool) & ~None) | ((_) -> Unknown)` is possibly unbound
+ error[unresolved-attribute] ddtrace/debugging/_signal/utils.py:227:38: Type `((Any, /) -> bool) | ((_) -> Unknown)` has no attribute `__name__`
- warning[possibly-unbound-attribute] ddtrace/debugging/_signal/utils.py:257:38: Attribute `__name__` on type `(((Any, /) -> bool) & ~None) | ((_) -> Unknown)` is possibly unbound
+ error[unresolved-attribute] ddtrace/debugging/_signal/utils.py:257:38: Type `((Any, /) -> bool) | ((_) -> Unknown)` has no attribute `__name__`
- warning[possibly-unbound-attribute] ddtrace/debugging/_signal/utils.py:313:41: Attribute `__name__` on type `(((Any, /) -> bool) & ~None) | ((_) -> Unknown)` is possibly unbound
+ error[unresolved-attribute] ddtrace/debugging/_signal/utils.py:313:41: Type `((Any, /) -> bool) | ((_) -> Unknown)` has no attribute `__name__`
- warning[possibly-unbound-attribute] ddtrace/debugging/_signal/utils.py:332:34: Attribute `__name__` on type `(((Any, /) -> bool) & ~None) | ((_) -> Unknown)` is possibly unbound
+ error[unresolved-attribute] ddtrace/debugging/_signal/utils.py:332:34: Type `((Any, /) -> bool) | ((_) -> Unknown)` has no attribute `__name__`
- warning[possibly-unbound-attribute] ddtrace/debugging/_signal/utils.py:358:37: Attribute `__name__` on type `(((Any, /) -> bool) & ~None) | ((_) -> Unknown)` is possibly unbound
+ error[unresolved-attribute] ddtrace/debugging/_signal/utils.py:358:37: Type `((Any, /) -> bool) | ((_) -> Unknown)` has no attribute `__name__`

sympy (https://github.com/sympy/sympy)
+ error[invalid-argument-type] sympy/assumptions/refine.py:63:30: Argument is incorrect: Expected `Boolean`, found `Unknown | Literal[True]`
- Found 18553 diagnostics
+ Found 18554 diagnostics

@AlexWaygood AlexWaygood force-pushed the alex/property-test-failure branch from 59ef9e1 to d72d3cb Compare May 30, 2025 12:36
@AlexWaygood
Copy link
Member Author

AlexWaygood commented May 30, 2025

Pretty interesting that there's a fair amount of mypy_primer fallout here, given that there was none on #18368! I'll go through it in a moment

@AlexWaygood
Copy link
Member Author

AlexWaygood commented May 30, 2025

+ error[unresolved-attribute] beartype/_util/hint/pep/proposal/pep612.py:596:9: Type `(...) -> Unknown` has no attribute `__name__`

Here's a minimal repro of this hit. On main our behaviour is:

from typing import Callable, reveal_type

def f(func: Callable | None):
    x = reveal_type(func).__name__ if func is not None else 'bar'  # revealed: `((...) -> Unknown) & ~None`
    reveal_type(x)  # revealed: `@Todo(map_with_boundness: intersections with negative contributions) | Literal["bar"]`

This is because we do not infer that Callable is disjoint from None. With this PR branch, we instead have this behaviour, which seems much more correct:

from typing import Callable, reveal_type

def f(func: Callable | None):
    # error: Type `(...) -> Unknown` has no attribute `__name__`
    x = reveal_type(func).__name__ if func is not None else 'bar'  # revealed: `((...) -> Unknown)`
    reveal_type(x)  # revealed: `Unknown | Literal["bar"]`

There's an open question about whether we should infer all Callable types as having the same attributes as types.FunctionType instances, the same as mypy/pyright, but that should definitely not be solved in this PR.

The starlette and websockets hits appear to be the same thing as beartype: we didn't understand that Callable types were disjoint from None; now we do.

@AlexWaygood
Copy link
Member Author

+ error[not-iterable] ignite/handlers/base_logger.py:52:29: Object of type `list[Unknown] | ((str, Unknown, /) -> bool)` may not be iterable

This one appears to be due to missing support for TypeIs -- once we support TypeIs, we'll be able to do ~Callable narrowing in the else branch here: https://github.com/pytorch/ignite/blob/adbb260bfb1756c106fed2e793e1b645918ece27/ignite/handlers/base_logger.py#L44-L54

@AlexWaygood
Copy link
Member Author

The sympy one looks correct to me. It's yet another case where we didn't see Callable types and None as being disjoint, but now we do. The function checks that the handler variable is not None, which narrows the type of handler (returned from the handlers_dict.get() call to Callable[[Expr, Boolean], Expr]. But the function then uses the assumptions variable as the second argument when it calls the handler callback, and the assumptions variable could still be Literal[True].

So I think all the mypy_primer hits are correct, and show that this PR improves our inference overall!

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!

@AlexWaygood AlexWaygood merged commit 77c8ddf into main May 30, 2025
35 checks passed
@AlexWaygood AlexWaygood deleted the alex/property-test-failure branch May 30, 2025 15:49
dcreager added a commit that referenced this pull request May 30, 2025
* main:
  [ty] support callability of bound/constrained typevars (#18389)
  [ty] Minor tweaks to "list all members" docs and tests (#18388)
  [ty] Fix broken property tests for disjointness (#18384)
  [ty] List available members for a given type (#18251)
  [`airflow`] Add unsafe fix for module moved cases (`AIR312`) (#18363)
  Add a `SourceFile` to `OldDiagnostic` (#18356)
  Update salsa past generational id change (#18362)
  [`airflow`] Add unsafe fix for module moved cases (`AIR311`) (#18366)
  [`airflow`] Add unsafe fix for module moved cases (`AIR301`) (#18367)
  [ty] Improve tests for `site-packages` discovery (#18374)
  [ty] _typeshed.Self is not a special form (#18377)
  [ty] Callable types are disjoint from non-callable `@final` nominal instance types (#18368)
  [ty] Add diagnosis for function with no return statement but with return type annotation (#18359)
  [`airflow`] Add unsafe fix module moved cases (`AIR302`) (#18093)
  Rename `ruff_linter::Diagnostic` to `OldDiagnostic` (#18355)
  [`refurb`] Add coverage of `set` and `frozenset` calls (`FURB171`) (#18035)
@AlexWaygood AlexWaygood restored the alex/property-test-failure branch July 25, 2025 18:27
@AlexWaygood AlexWaygood deleted the alex/property-test-failure branch July 25, 2025 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal An internal refactor or improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Daily property test run failed on Fri May 30 2025

2 participants