Narrow closed TypedDicts on a key membership check - #11620
Conversation
|
🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR. |
| // A closed TypedDict has an "extra items" of Never, so this is what allows | ||
| // a key check to discriminate between closed TypedDicts. | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
Info · Optional note
The new guard also changes positive membership checks for declared keys typed Never, not only synthesized closed-TypedDict extra-items. Please add a small regression case to pin that intended behavior.
| if "foo" in u: | ||
| reveal_type(u, expected_text="Foo") | ||
| else: | ||
| reveal_type(u, expected_text="Open") |
There was a problem hiding this comment.
Info · Optional note
Consider adding a closed-TypedDict union with an overlapping key to exercise the existing required-key retain/clone-and-mark-provided path alongside the new elimination path.
|
The negative branch implicitly relies on closed TypedDicts' synthesized |
Stella Huang (StellaHuang95)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
|
Thanks, all three were worth doing. Added the two test cases in a7b094b. On the declared- On the overlapping key: added On the comment in the negative branch: that branch is what #11610 rewrites, and it adds the note you're describing as part of the change, distinguishing the known item from the "extra items" fallback so the asymmetry is stated outright. I've left the negative branch untouched here so the two PRs don't collide on the same lines. If #11610 ends up not landing, say the word and I'll bring the comment over to this one. |
|
Please update the PR description: the committed sample contains seven cases, not four, so its stated pre-change error count is stale. |
Stella Huang (StellaHuang95)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
|
Diff from mypy_primer, showing the effect of this PR on open source code: sympy (https://github.com/sympy/sympy)
+ .../projects/sympy/sympy/simplify/powsimp.py:686:13 - error: Operator "*=" not supported for types "Unknown | Literal[1]" and "Basic | Unknown"
+ Operator "*" not supported for types "Literal[1]" and "Basic" (reportOperatorIssue)
+ .../projects/sympy/sympy/simplify/powsimp.py:688:24 - error: Argument of type "Basic | Unknown" cannot be assigned to parameter "b" of type "Expr | complex" in function "__new__"
+ Type "Basic | Unknown" is not assignable to type "Expr | complex"
+ Type "Basic" is not assignable to type "Expr | complex"
+ "Basic" is not assignable to "Expr"
+ "Basic" is not assignable to "complex" (reportArgumentType)
+ .../projects/sympy/sympy/simplify/powsimp.py:704:29 - error: Cannot access attribute "as_numer_denom" for class "GaussianRational"
+ Attribute "as_numer_denom" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/powsimp.py:704:29 - error: Cannot access attribute "as_numer_denom" for class "MPQ"
+ Attribute "as_numer_denom" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/powsimp.py:704:29 - error: Cannot access attribute "as_numer_denom" for class "GaussianInteger"
+ Attribute "as_numer_denom" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/powsimp.py:704:29 - error: Cannot access attribute "as_numer_denom" for class "MPZ"
+ Attribute "as_numer_denom" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/powsimp.py:713:24 - error: Cannot access attribute "as_coeff_Mul" for class "Basic"
+ Attribute "as_coeff_Mul" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/powsimp.py:714:46 - error: Operator "/" not supported for types "Basic | Unknown" and "Basic"
+ Operator "/" not supported for types "Basic" and "Basic" (reportOperatorIssue)
- .../projects/sympy/sympy/simplify/radsimp.py:939:20 - error: Operator "/" not supported for types "Literal[1]" and "Expr | Unknown | None"
+ .../projects/sympy/sympy/simplify/radsimp.py:939:20 - error: Operator "/" not supported for types "Literal[1]" and "Unknown | Expr | None"
- .../projects/sympy/sympy/simplify/radsimp.py:950:36 - error: Argument of type "Expr | Unknown | Basic | bool | None" cannot be assigned to parameter "expr" of type "Expr" in function "make_args"
+ .../projects/sympy/sympy/simplify/radsimp.py:950:36 - error: Argument of type "Unknown | Expr | Basic | bool | None" cannot be assigned to parameter "expr" of type "Expr" in function "make_args"
- Type "Expr | Unknown | Basic | bool | None" is not assignable to type "Expr"
+ Type "Unknown | Expr | Basic | bool | None" is not assignable to type "Expr"
- .../projects/sympy/sympy/simplify/radsimp.py:999:36 - error: Operator "/" not supported for types "Literal[1]" and "Expr | Unknown | Basic | bool | Add | None"
+ .../projects/sympy/sympy/simplify/radsimp.py:999:36 - error: Operator "/" not supported for types "Literal[1]" and "Unknown | Expr | Basic | bool | Add | None"
+ .../projects/sympy/sympy/simplify/radsimp.py:1196:14 - error: Operator "*" not supported for types "Unknown | GaussianRational | MPQ | GaussianInteger | MPZ | Expr | Rational | NaN | ComplexInfinity | One | NegativeOne | Zero | Integer | Infinity | NegativeInfinity | Float | Number | Poly | Any" and "Unknown | GaussianRational | MPQ | GaussianInteger | MPZ | Expr | Rational | NaN | ComplexInfinity | One | NegativeOne | Zero | Integer | Infinity | NegativeInfinity | Float | Number | Poly | Any"
+ Operator "*" not supported for types "GaussianRational" and "MPQ"
+ Operator "*" not supported for types "GaussianRational" and "GaussianInteger"
+ Operator "*" not supported for types "GaussianRational" and "MPZ"
+ Operator "*" not supported for types "GaussianRational" and "Expr"
+ Operator "*" not supported for types "GaussianRational" and "Rational"
+ Operator "*" not supported for types "GaussianRational" and "NaN"
+ Operator "*" not supported for types "GaussianRational" and "ComplexInfinity"
+ Operator "*" not supported for types "GaussianRational" and "Infinity"
+ ... (reportOperatorIssue)
+ .../projects/sympy/sympy/simplify/ratsimp.py:31:17 - error: Argument of type "Expr | Unknown | Poly" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
+ Type "Expr | Unknown | Poly" is not assignable to type "Expr | complex"
+ Type "Poly" is not assignable to type "Expr | complex"
+ "Poly" is not assignable to "Expr"
+ "Poly" is not assignable to "complex" (reportArgumentType)
- .../projects/sympy/sympy/simplify/simplify.py:163:20 - error: Argument of type "dict[str, Unknown] | dict[Unknown, list[Unknown]] | Expr | Abs | Unknown | None" cannot be assigned to parameter "b" of type "Expr | complex" in function "__new__"
+ .../projects/sympy/sympy/simplify/simplify.py:163:20 - error: Argument of type "dict[str, Unknown] | dict[Unknown, list[Unknown]] | Unknown | None" cannot be assigned to parameter "b" of type "Expr | complex" in function "__new__"
- Type "dict[str, Unknown] | dict[Unknown, list[Unknown]] | Expr | Abs | Unknown | None" is not assignable to type "Expr | complex"
+ Type "dict[str, Unknown] | dict[Unknown, list[Unknown]] | Unknown | None" is not assignable to type "Expr | complex"
- .../projects/sympy/sympy/simplify/sqrtdenest.py:157:19 - error: Operator "**" not supported for types "Basic" and "Literal[2]" (reportOperatorIssue)
- .../projects/sympy/sympy/simplify/sqrtdenest.py:200:48 - error: Argument of type "Basic" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
- Type "Basic" is not assignable to type "Expr | complex"
- "Basic" is not assignable to "Expr"
- "Basic" is not assignable to "complex" (reportArgumentType)
- .../projects/sympy/sympy/simplify/sqrtdenest.py:207:26 - error: Operator "**" not supported for types "Unknown | Mul | Basic" and "Literal[2]"
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:309:10 - error: Operator "+" not supported for types "Expr" and "Unknown | Expr | Mul | None"
+ Operator "+" not supported for types "Expr" and "None" (reportOperatorIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:340:19 - error: Operator "-" not supported for types "Expr | Unknown | GaussianRational | One | NegativeOne | Zero | Integer | Any | MPQ | GaussianInteger | MPZ | Poly | NotImplementedType" and "Expr | Unknown | Any | GaussianRational | One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | NotImplementedType | MPQ | GaussianInteger | MPZ | Poly"
+ Operator "-" not supported for types "Expr" and "GaussianRational"
+ Operator "-" not supported for types "Expr" and "MPQ"
+ Operator "-" not supported for types "Expr" and "GaussianInteger"
+ Operator "-" not supported for types "Expr" and "MPZ"
+ Operator "-" not supported for types "GaussianRational" and "Expr"
+ Operator "-" not supported for types "GaussianRational" and "NaN"
+ Operator "-" not supported for types "GaussianRational" and "ComplexInfinity"
+ Operator "-" not supported for types "GaussianRational" and "Rational"
+ ... (reportOperatorIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:340:26 - error: Operator "*" not supported for types "Expr | Unknown | GaussianRational | One | NegativeOne | Zero | Integer | Any | MPQ | GaussianInteger | MPZ | Poly | NotImplementedType" and "Expr | Unknown | GaussianRational | One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | Any | MPQ | GaussianInteger | MPZ | Infinity | NegativeInfinity | Float | NotImplementedType | Poly | Number"
+ Operator "*" not supported for types "Expr" and "GaussianRational"
+ Operator "*" not supported for types "Expr" and "MPQ"
+ Operator "*" not supported for types "Expr" and "GaussianInteger"
+ Operator "*" not supported for types "Expr" and "MPZ"
+ Operator "*" not supported for types "GaussianRational" and "Expr"
+ Operator "*" not supported for types "GaussianRational" and "NaN"
+ Operator "*" not supported for types "GaussianRational" and "ComplexInfinity"
+ Operator "*" not supported for types "GaussianRational" and "Rational"
+ ... (reportOperatorIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:341:11 - error: Cannot access attribute "is_Rational" for class "GaussianRational"
+ Attribute "is_Rational" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:341:11 - error: Cannot access attribute "is_Rational" for class "MPQ"
+ Attribute "is_Rational" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:341:11 - error: Cannot access attribute "is_Rational" for class "GaussianInteger"
+ Attribute "is_Rational" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:341:11 - error: Cannot access attribute "is_Rational" for class "MPZ"
+ Attribute "is_Rational" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:342:15 - error: Cannot access attribute "is_positive" for class "GaussianRational"
+ Attribute "is_positive" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:342:15 - error: Cannot access attribute "is_positive" for class "MPQ"
+ Attribute "is_positive" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:342:15 - error: Cannot access attribute "is_positive" for class "GaussianInteger"
+ Attribute "is_positive" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/simplify/sqrtdenest.py:342:15 - error: Cannot access attribute "is_positive" for class "MPZ"
... (truncated 407 lines) ...
|
|
Updated the description, thanks for catching it. It now says seven cases and five pre-change errors, the extra one being the declared- |
Fixes #11518.
A union of closed TypedDicts can't be discriminated with an
incheck today:Baris closed, so it can't hold a"foo"key and should be eliminated in the positive branch.The cause is in
narrowTypeForTypedDictKey.getTypedDictMembersForClassrepresentsclosed=Trueby synthesizing an "extra items" entry whose value type isNever, and the positive branch looks upknownItems.get(key) ?? extraItems. For a closed TypedDict and an unknown key that lookup returns the synthesizedNeverentry, which is notundefined, so the code treats it as a real entry and keeps the subtype.The check for this was already written in that function, commented out. I enabled just the
isNeverhalf. The other commented block (returningsubtypewhen there's no entry at all) stays as it is, because that's the one that would disable the deliberately-unsound narrowing of open TypedDicts discussed in #10805, and the comment above it explains why it's being kept for now.Added
typedDictClosed11.pywith seven cases: the union from the issue, the same thing throughnot in, aFoo | Bazunion whereBazdeclaresextra_items=intand so must not be eliminated, aFoo | Openunion whereOpenis an ordinary TypedDict, pinning that the existing idiomatic narrowing still happens, a TypedDict with an item declared asNever, a union of two closed TypedDicts sharing a required key, and a closed TypedDict with aNotRequiredkey that takes the mark-as-provided path. Put it in a new sample rather than extendingtypedDictClosed1.pyso it doesn't collide with #11610, which is still open against that file.The new sample reports five errors before the change and none after: the four from the issue (two failed
reveal_typenarrowings and the two key-access errors that follow) plus the declared-Nevercase. All elevenTypedDictClosedtests pass.Ran the narrowing tests across all suites (54 passing) and the full typeEvaluator5 suite (68 passing) locally on Windows.