Skip to content

Conversation

@dcreager
Copy link
Member

@dcreager dcreager commented Jun 23, 2025

Add property test generators for the new variable-length tuples. This covers homogeneous tuples as well.

The property tests did their job! This identified several fixes we needed to make to various type property methods.

cf #18600 (comment)

@dcreager dcreager added internal An internal refactor or improvement ty Multi-file analysis & type inference labels Jun 23, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jun 23, 2025

mypy_primer results

Changes were detected when running on open source projects
paasta (https://github.com/yelp/paasta)
+ error[unresolved-reference] paasta_tools/paastaapi/model/instance_status_adhoc.py:152:30: Name `_path_to_item` used when not defined
+ error[unresolved-reference] paasta_tools/paastaapi/model/instance_tasks.py:147:30: Name `_path_to_item` used when not defined
+ error[unresolved-reference] paasta_tools/paastaapi/model/resource.py:152:30: Name `_path_to_item` used when not defined
- Found 934 diagnostics
+ Found 937 diagnostics

strawberry (https://github.com/strawberry-graphql/strawberry)
- error[unresolved-attribute] strawberry/tools/merge_types.py:26:11: Type `type` has no attribute `__strawberry_definition__`
- Found 372 diagnostics
+ Found 371 diagnostics

hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
+ warning[possibly-unbound-attribute] src/hydra_zen/structured_configs/_implementations.py:2146:28: Attribute `get` on type `DataclassOptions | None` is possibly unbound
+ warning[possibly-unbound-attribute] src/hydra_zen/structured_configs/_implementations.py:3153:21: Attribute `get` on type `DataclassOptions | None` is possibly unbound
- Found 595 diagnostics
+ Found 597 diagnostics

jinja (https://github.com/pallets/jinja)
+ warning[possibly-unresolved-reference] src/jinja2/filters.py:1740:17: Name `name` used when possibly not defined
- Found 205 diagnostics
+ Found 206 diagnostics

vision (https://github.com/pytorch/vision)
+ error[invalid-assignment] test/datasets_utils.py:1043:9: Object of type `LiteralString` is not assignable to `tuple[str, ...]`
- Found 1510 diagnostics
+ Found 1511 diagnostics

pytest (https://github.com/pytest-dev/pytest)
+ error[no-matching-overload] src/_pytest/raises.py:283:20: No overload of bound method `__init__` matches arguments
- Found 640 diagnostics
+ Found 641 diagnostics

aiohttp (https://github.com/aio-libs/aiohttp)
+ warning[possibly-unbound-attribute] aiohttp/resolver.py:102:26: Attribute `get_resolver` on type `_DNSResolverManager | None` is possibly unbound
- Found 136 diagnostics
+ Found 137 diagnostics

static-frame (https://github.com/static-frame/static-frame)
+ error[invalid-argument-type] static_frame/test/unit/test_type_clinic.py:2100:29: Argument to bound method `__init__` is incorrect: Expected `Sequence[Unknown]`, found `EllipsisType`
+ error[invalid-argument-type] static_frame/test/unit/test_type_clinic.py:2103:29: Argument to bound method `__init__` is incorrect: Expected `Sequence[Unknown]`, found `EllipsisType`
+ error[invalid-argument-type] static_frame/test/unit/test_type_clinic.py:2106:43: Argument to bound method `__init__` is incorrect: Expected `Sequence[Unknown]`, found `EllipsisType`
+ error[invalid-argument-type] static_frame/test/unit/test_type_clinic.py:2109:29: Argument to bound method `__init__` is incorrect: Expected `Sequence[Unknown]`, found `EllipsisType`
+ error[invalid-argument-type] static_frame/test/unit/test_type_clinic.py:2131:29: Argument to bound method `__init__` is incorrect: Expected `Sequence[Unknown]`, found `EllipsisType`
- Found 1833 diagnostics
+ Found 1838 diagnostics

sympy (https://github.com/sympy/sympy)
+ warning[possibly-unbound-attribute] sympy/calculus/euler.py:84:16: Attribute `args` on type `@Todo(Subscript expressions on intersections) | tuple[()]` is possibly unbound
+ warning[possibly-unbound-attribute] sympy/calculus/euler.py:92:24: Attribute `args` on type `@Todo(map_with_boundness: intersections with negative contributions) | tuple[()]` is possibly unbound
+ error[index-out-of-bounds] sympy/polys/polymatrix.py:79:42: Index 0 is out of bounds for tuple `tuple[()]` with length 0
+ error[index-out-of-bounds] sympy/polys/polymatrix.py:80:20: Index 0 is out of bounds for tuple `tuple[()]` with length 0
- error[not-iterable] sympy/simplify/radsimp.py:707:28: Object of type `Basic` is not iterable
+ error[not-iterable] sympy/simplify/radsimp.py:707:28: Object of type `set[Unknown] | Basic` may not be iterable
- error[invalid-argument-type] sympy/solvers/solveset.py:3128:25: Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `tuple[Unknown, ...] | (Unknown & <Protocol with members '__iter__'>)`
+ error[invalid-argument-type] sympy/solvers/solveset.py:3128:25: Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `tuple[Unknown, ...] | (@Todo(Subscript expressions on intersections) & <Protocol with members '__iter__'>)`
- Found 17915 diagnostics
+ Found 17919 diagnostics

@MichaReiser MichaReiser added testing Related to testing Ruff itself and removed internal An internal refactor or improvement labels Jun 23, 2025
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.

Looks good! But I am seeing some property test failures here (see below). We need to either fix the underlying problems, or create issues for them and move those property tests into the "flaky" section (but that would be less preferable). I think some of these are not OK to move into "flaky" (subtyping transitivity in particular.)

Repro: QUICKCHECK_TESTS=1000000 cargo test -p ty_python_semantic -- --ignored types::property_tests::stable

(Running with a million iterations like this is pretty slow, but is the standard we need to hit in order to not get flaky CI failures on the property tests.)

Failures:

failures:

---- types::property_tests::stable::all_type_pairs_are_assignable_to_their_union stdout ----

thread 'types::property_tests::stable::all_type_pairs_are_assignable_to_their_union' panicked at /Users/carlmeyer/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quickcheck-1.0.3/src/tester.rs:165:28:
[quickcheck] TEST FAILED. Arguments: (VariableLengthTuple([], Intersection { pos: [None, SubclassOfAbcClass("ABC")], neg: [] }, []), Union([FixedLengthTuple([]), VariableLengthTuple([AlwaysTruthy], KnownClassInstance(Str), [])]))
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- types::property_tests::stable::all_fully_static_type_pairs_are_subtype_of_their_union stdout ----

thread 'types::property_tests::stable::all_fully_static_type_pairs_are_subtype_of_their_union' panicked at /Users/carlmeyer/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quickcheck-1.0.3/src/tester.rs:165:28:
[quickcheck] TEST FAILED. Arguments: (VariableLengthTuple([], BytesLiteral("\0"), []), Union([FixedLengthTuple([]), AlwaysTruthy]))

---- types::property_tests::stable::subtype_of_is_transitive stdout ----

thread 'types::property_tests::stable::subtype_of_is_transitive' panicked at /Users/carlmeyer/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quickcheck-1.0.3/src/tester.rs:165:28:
[quickcheck] TEST FAILED. Arguments: (FixedLengthTuple([]), VariableLengthTuple([], BuiltinClassLiteral("bool"), []), VariableLengthTuple([BuiltinInstance("type")], Intersection { pos: [], neg: [] }, []))

---- types::property_tests::stable::subtype_of_implies_not_disjoint_from stdout ----

thread 'types::property_tests::stable::subtype_of_implies_not_disjoint_from' panicked at /Users/carlmeyer/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quickcheck-1.0.3/src/tester.rs:165:28:
[quickcheck] TEST FAILED. Arguments: (Intersection { pos: [FixedLengthTuple([])], neg: [VariableLengthTuple([], BytesLiteral(""), [SubclassOfAbcClass("ABC")])] }, VariableLengthTuple([], VariableLengthTuple([], KnownClassInstance(Int), [Never]), []))

---- types::property_tests::stable::subtype_of_is_antisymmetric stdout ----

thread 'types::property_tests::stable::subtype_of_is_antisymmetric' panicked at /Users/carlmeyer/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quickcheck-1.0.3/src/tester.rs:165:28:
[quickcheck] TEST FAILED. Arguments: (VariableLengthTuple([], FixedLengthTuple([]), [FixedLengthTuple([])]), VariableLengthTuple([], FixedLengthTuple([]), []))


failures:
    types::property_tests::stable::all_fully_static_type_pairs_are_subtype_of_their_union
    types::property_tests::stable::all_type_pairs_are_assignable_to_their_union
    types::property_tests::stable::subtype_of_implies_not_disjoint_from
    types::property_tests::stable::subtype_of_is_antisymmetric
    types::property_tests::stable::subtype_of_is_transitive

@carljm
Copy link
Contributor

carljm commented Jun 23, 2025

(Based on my experience debugging these property test failures recently, I recommend starting with the subtype_of_is_transitive failure, because failures of subtyping transitivity can lead to all kinds of other cascading problems where union simplification depends on the order elements are added, making other property test failures sensitive to ordering in ways that make them slippery to reproduce.)

@dcreager
Copy link
Member Author

Running with a million iterations like this is pretty slow, but is the standard we need to hit in order to not get flaky CI failures on the property tests

Ha! I ran it yesterday with around 500,000 runs and didn't see any failures. I ran it this morning to reproduce your results and got a failure with the default of 100 😂

dcreager added 7 commits June 24, 2025 09:15
* main:
  [ty] Fix false positives when subscripting an object inferred as having an `Intersection` type (#18920)
  [`flake8-use-pathlib`] Add autofix for `PTH202` (#18763)
  [ty] Add relative import completion tests
  [ty] Clarify what "cursor" means
  [ty] Add a cursor test builder
  [ty] Enforce sort order of completions (#18917)
  [formatter] Fix missing blank lines before decorated classes in .pyi files (#18888)
  Apply fix availability and applicability when adding to `DiagnosticGuard` and remove `NoqaCode::rule` (#18834)
  py-fuzzer: allow relative executable paths (#18915)
  [ty] Change `environment.root` to accept multiple paths (#18913)
  [ty] Rename `src.root` setting to `environment.root` (#18760)
  Use file path for detecting package root (#18914)
  Consider virtual path for various server actions (#18910)
  [ty] Introduce `UnionType::try_from_elements` and `UnionType::try_map` (#18911)
  [ty] Support narrowing on `isinstance()`/`issubclass()` if the second argument is a dynamic, intersection, union or typevar type (#18900)
  [ty] Add decorator check for implicit attribute assignments (#18587)
  [`ruff`] Trigger `RUF037` for empty string and byte strings (#18862)
  [ty] Avoid duplicate diagnostic in unpacking (#18897)
  [`pyupgrade`] Extend version detection to include `sys.version_info.major` (`UP036`) (#18633)
  [`ruff`] Frozen Dataclass default should be valid (`RUF009`) (#18735)
@dcreager
Copy link
Member Author

Okay it's been running on a loop for around 15 minutes now and I'm not seeing any more proptest failures.

@codspeed-hq
Copy link

codspeed-hq bot commented Jun 24, 2025

CodSpeed WallTime Performance Report

Merging #18901 will not alter performance

Comparing dcreager/proptest-tuple (35a675f) with main (919af96)

Summary

✅ 8 untouched benchmarks

@AlexWaygood
Copy link
Member

Are the new primer diagnostics here expected?

@dcreager
Copy link
Member Author

Are the new primer diagnostics here expected?

My hope is they're related to the test failure that I just fixed

@AlexWaygood
Copy link
Member

Are the new primer diagnostics here expected?

My hope is they're related to the test failure that I just fixed

looks like we still have a few new ones

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Very cool work

Comment on lines +360 to +366
// If the variable-length portion is Never, it can only be instantiated with zero elements.
// That means this isn't a variable-length tuple after all!
if variable.is_never() {
return TupleSpec::Fixed(FixedLengthTupleSpec::from_elements(
prefix.into_iter().chain(suffix),
));
}
Copy link
Member

Choose a reason for hiding this comment

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

this is a great observation! 😃


fn suffix_elements(
&self,
) -> impl DoubleEndedIterator<Item = Type<'db>> + ExactSizeIterator + '_ {
Copy link
Member

Choose a reason for hiding this comment

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

nit: this is quite a complicated type to repeat twice in the same file. Maybe introduce a type alias?

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately it's not a type, it's an impl trait(s), so I don't think I can make an alias for it.

Copy link
Member

Choose a reason for hiding this comment

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

you could maybe just use the concrete type instead of the trait, and then you can alias it? This seems to compile fine:

diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs
index d19666fcd2..5d264a35f5 100644
--- a/crates/ty_python_semantic/src/types/tuple.rs
+++ b/crates/ty_python_semantic/src/types/tuple.rs
@@ -154,6 +154,8 @@ impl<'db> TupleType<'db> {
     }
 }
 
+type TupleElementIterator<'a, 'db> = std::iter::Copied<std::slice::Iter<'a, Type<'db>>>;
+
 /// A fixed-length tuple spec.
 ///
 /// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
@@ -179,9 +181,7 @@ impl<'db> FixedLengthTupleSpec<'db> {
         &self.0
     }
 
-    pub(crate) fn elements(
-        &self,
-    ) -> impl DoubleEndedIterator<Item = Type<'db>> + ExactSizeIterator + '_ {
+    pub(crate) fn elements(&self) -> TupleElementIterator<'_, 'db> {
         self.0.iter().copied()
     }
 
@@ -372,9 +372,7 @@ impl<'db> VariableLengthTupleSpec<'db> {
         })
     }
 
-    fn prefix_elements(
-        &self,
-    ) -> impl DoubleEndedIterator<Item = Type<'db>> + ExactSizeIterator + '_ {
+    fn prefix_elements(&self) -> TupleElementIterator<'_, 'db> {
         self.prefix.iter().copied()
     }
 
@@ -409,9 +407,7 @@ impl<'db> VariableLengthTupleSpec<'db> {
         )
     }
 
-    fn suffix_elements(
-        &self,
-    ) -> impl DoubleEndedIterator<Item = Type<'db>> + ExactSizeIterator + '_ {
+    fn suffix_elements(&self) -> TupleElementIterator<'_, 'db> {
         self.suffix.iter().copied()
     }

Copy link
Member Author

Choose a reason for hiding this comment

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

I mildly prefer the impl trait, since I don't like encoding concrete types into the function signature if we don't need to name them for some other reason. But I can make this change if you feel strongly.

Copy link
Member

Choose a reason for hiding this comment

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

I guess you could do something ✨fancy✨ like this, but it probably isn't worth it just to make some type signatures less complicated 😆

diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs
index d19666fcd2..5c1c7b09e1 100644
--- a/crates/ty_python_semantic/src/types/tuple.rs
+++ b/crates/ty_python_semantic/src/types/tuple.rs
@@ -154,6 +154,16 @@ impl<'db> TupleType<'db> {
     }
 }
 
+pub(crate) trait TupleElementIterator<'db>:
+    ExactSizeIterator<Item = Type<'db>> + DoubleEndedIterator
+{
+}
+
+impl<'db, T> TupleElementIterator<'db> for T where
+    T: ExactSizeIterator<Item = Type<'db>> + DoubleEndedIterator
+{
+}
+
 /// A fixed-length tuple spec.
 ///
 /// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
@@ -179,9 +189,7 @@ impl<'db> FixedLengthTupleSpec<'db> {
         &self.0
     }
 
-    pub(crate) fn elements(
-        &self,
-    ) -> impl DoubleEndedIterator<Item = Type<'db>> + ExactSizeIterator + '_ {
+    pub(crate) fn elements(&self) -> impl TupleElementIterator<'db> + '_ {
         self.0.iter().copied()
     }
 
@@ -372,9 +380,7 @@ impl<'db> VariableLengthTupleSpec<'db> {
         })
     }
 
-    fn prefix_elements(
-        &self,
-    ) -> impl DoubleEndedIterator<Item = Type<'db>> + ExactSizeIterator + '_ {
+    fn prefix_elements(&self) -> impl TupleElementIterator<'db> + '_ {
         self.prefix.iter().copied()
     }
 
@@ -409,9 +415,7 @@ impl<'db> VariableLengthTupleSpec<'db> {
         )
     }
 
-    fn suffix_elements(
-        &self,
-    ) -> impl DoubleEndedIterator<Item = Type<'db>> + ExactSizeIterator + '_ {
+    fn suffix_elements(&self) -> impl TupleElementIterator<'db> + '_ {
         self.suffix.iter().copied()
     }

Feel free to leave things as they are, I don't feel strongly!

@dcreager
Copy link
Member Author

looks like we still have a few new ones

Most of them look like new true positives!

This one is a false positive:

error[index-out-of-bounds] sympy/polys/polymatrix.py:79:42: Index 0 is out of bounds for tuple `tuple[()]` with length 0

There are a bunch of unannotated assignments to a variable in an if/elif/... chain. The first branch assigns it (), but other branches assign it a list slice, so I'm not sure why only the empty tuple is making it to this point. (There's also a length guard which is something we don't support yet, astral-sh/ty#560)

dcreager and others added 2 commits June 24, 2025 17:22
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
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.

Nice!

## Truthiness

The truthiness of the empty tuple is `False`:
The truthiness of the empty tuple is `False`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be worth a note here like this:

Suggested change
The truthiness of the empty tuple is `False`.
The truthiness of the empty tuple is `False`.
(TODO: this is only true as long as we assume that a subclass of `tuple` doesn't override `__bool__` or `__len__` -- we should enforce this.)

Copy link
Member Author

Choose a reason for hiding this comment

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

There is a note at the bottom of the section mentioning this (along with a test that shows we currently allow a subclass that overrides __bool__). Is that sufficient, or do you think it needs a note here too?

Copy link
Member

Choose a reason for hiding this comment

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

(I'm also planning on tackling this this week or next as part of my tuple subclasses/NamedTuples work)

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh that's probably sufficient if we have it mentioned somewhere

@AlexWaygood AlexWaygood removed the testing Related to testing Ruff itself label Jun 24, 2025
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

nice!

@dcreager dcreager merged commit 66f50fb into main Jun 24, 2025
36 checks passed
@dcreager dcreager deleted the dcreager/proptest-tuple branch June 24, 2025 22:13
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.

5 participants