-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Fix #11472: TypeVarTuple escapes method with very nested recursive tuple aliases #11497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # This sample tests the case where a TypeVarTuple is used in a method | ||
| # called on an instance whose type arguments contain deeply nested | ||
| # recursive tuple type aliases. The deep nesting previously caused the | ||
| # solved TypeVar to "escape" because type var transformation bailed out | ||
| # early on deeply nested tuples. | ||
|
|
||
| from typing import final, override | ||
| from collections.abc import Callable | ||
|
|
||
|
|
||
| @final | ||
| class Ok[T]: | ||
| __match_args__ = ("_value",) | ||
|
|
||
| def __init__(self, value: T): | ||
| self._value: T = value | ||
|
|
||
|
|
||
| @final | ||
| class Err[E]: | ||
| def __init__(self, value: E): | ||
| self._value: E = value | ||
|
|
||
|
|
||
| type Result[T, E] = Ok[T] | Err[E] | ||
|
|
||
| type ParserResult[O, E] = Result[tuple[int, O], E] | ||
| type ParserFunc[O, E] = Callable[[str, int], ParserResult[O, E]] | ||
|
|
||
|
|
||
| class Parser[O, E]: | ||
| def __init__(self, func: ParserFunc[O, E]): | ||
| self._func: ParserFunc[O, E] = func | ||
|
|
||
| def then[OO, OE](self, _other: "Parser[OO, OE]") -> "Parser[tuple[O, OO], E | OE]": | ||
| raise NotImplementedError | ||
|
|
||
| def unpack_then[*TS, OO, OE]( | ||
| self: "Parser[tuple[*TS], E]", _other: "Parser[OO, OE]" | ||
| ) -> "Parser[tuple[*TS, OO], E | OE]": | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| def produce[T](_: T | None = None) -> T: | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| class ForwardRefParser[O, E](Parser[O, E]): | ||
| @override | ||
| def __init__(self, func: Callable[[], Parser[O, E]]): | ||
| self._meta_func: Callable[[], Parser[O, E]] = func | ||
| super().__init__(produce()) | ||
|
|
||
|
|
||
| type Whitespace = tuple[tuple[tuple[tuple[Whitespace]]]] | ||
| type Implementations = tuple[tuple[tuple[Whitespace], Implementations]] | ||
| type BlockItem = tuple[tuple[Implementations]] | tuple[BlockItem] | ||
|
|
||
| ws: ForwardRefParser[Whitespace, None] = produce() | ||
| implementations: ForwardRefParser[Implementations, None] = produce() | ||
| block: Parser[tuple[BlockItem], None] = produce() | ||
|
|
||
| # This should not generate an error. Previously the "OO" TypeVar from | ||
| # "unpack_then" escaped into the inferred type of this assignment. | ||
| forloop: ForwardRefParser[ | ||
| tuple[ | ||
| Whitespace, | ||
| Whitespace, | ||
| tuple[BlockItem], | ||
| Whitespace, | ||
| ], | ||
| None, | ||
| ] = ForwardRefParser(lambda: ws.then(ws).unpack_then(block).unpack_then(ws)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -431,6 +431,14 @@ test('TypeVarTuple30', () => { | |
| TestUtils.validateResults(analysisResults, 0); | ||
| }); | ||
|
|
||
| test('TypeVarTuple31', () => { | ||
| const configOptions = new ConfigOptions(Uri.empty()); | ||
|
|
||
| configOptions.defaultPythonVersion = pythonVersion3_12; | ||
| const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarTuple31.py'], configOptions); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| TestUtils.validateResults(analysisResults, 0); | ||
| }); | ||
|
|
||
| test('Match1', () => { | ||
| const configOptions = new ConfigOptions(Uri.empty()); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing this early-return fixes the escaping TypeVar (1→0 errors), but it also deletes a documented performance safeguard whose comment explicitly claimed the recursion-count limit is insufficient ("will effectively hang the analyzer"). Consider a narrower, safe-bailout variant: keep a depth bound but make the bailout safe (e.g. return
Unknownor still solve top-level TypeVars) so nothing escapes at any depth while preserving the perf guard. Since this is vendored, upstream-owned Pyright core (keep diffs minimal, coordinate upstream), prefer the safe-bailout variant or coordinate the bound removal upstream against the original motivating repro, and confirm that repro still terminates without the guard.