-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
✨ Added new Error to TypedDict #14225
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
Changes from all commits
c7824be
f19a7f7
2a4220c
fcf4909
deb6543
222869d
70c0a18
eda0c8b
25f3dbc
c7c687f
a275c86
f7a7f4f
06a3866
780e10d
fb98524
708d322
e4b82b6
1c9a623
f4f0cd0
957ba17
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 |
---|---|---|
|
@@ -67,6 +67,9 @@ def __str__(self) -> str: | |
TYPEDDICT_ITEM: Final = ErrorCode( | ||
"typeddict-item", "Check items when constructing TypedDict", "General" | ||
) | ||
TYPPEDICT_UNKNOWN_KEY: Final = ErrorCode( | ||
"typeddict-unknown-key", "Check unknown keys when constructing TypedDict", "General" | ||
) | ||
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. As mentioned on the issue, for consistency this error code should be also used for errors in case like: d["unknown"]
d["unknown"] = 42 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. Indeed, that makes sense, but I'm afraid I don't like it. I'm worried about what would happen when the user would ignore this error. For example Running A = T.TypedDict("A", {"x": int})
def f(x: A) -> None:
X["obvious error"]
f({"x": 1, "y": "foo"}) # I want this to be OK Would result in an obvious error being completely ignored by mypy. So to my view we have to options: 1 - rename the unknown-key to something that better reflects the code ( Open to suggestions. 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. OK, reading an unknown key may be too much, but setting it (i.e. d: TD = {"known": 1, "unknown": 2} and d: TD = {"known": 1}
d["unknown"] = 2 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. I think that's very reasonable. Will ping you when I have that sorted. 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. Done! Not sure if the solution is pretty. Added a test for it on the typeddict file. |
||
HAS_TYPE: Final = ErrorCode( | ||
"has-type", "Check that type of reference can be determined", "General" | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1637,30 +1637,28 @@ def unexpected_typeddict_keys( | |
expected_set = set(expected_keys) | ||
if not typ.is_anonymous(): | ||
# Generate simpler messages for some common special cases. | ||
if actual_set < expected_set: | ||
# Use list comprehension instead of set operations to preserve order. | ||
missing = [key for key in expected_keys if key not in actual_set] | ||
# Use list comprehension instead of set operations to preserve order. | ||
missing = [key for key in expected_keys if key not in actual_set] | ||
if missing: | ||
self.fail( | ||
"Missing {} for TypedDict {}".format( | ||
format_key_list(missing, short=True), format_type(typ) | ||
), | ||
context, | ||
code=codes.TYPEDDICT_ITEM, | ||
) | ||
extra = [key for key in actual_keys if key not in expected_set] | ||
if extra: | ||
self.fail( | ||
"Extra {} for TypedDict {}".format( | ||
format_key_list(extra, short=True), format_type(typ) | ||
), | ||
context, | ||
code=codes.TYPPEDICT_UNKNOWN_KEY, | ||
) | ||
if missing or extra: | ||
# No need to check for further errors | ||
return | ||
else: | ||
extra = [key for key in actual_keys if key not in expected_set] | ||
if extra: | ||
# If there are both extra and missing keys, only report extra ones for | ||
# simplicity. | ||
self.fail( | ||
"Extra {} for TypedDict {}".format( | ||
format_key_list(extra, short=True), format_type(typ) | ||
), | ||
context, | ||
code=codes.TYPEDDICT_ITEM, | ||
) | ||
return | ||
found = format_key_list(actual_keys, short=True) | ||
if not expected_keys: | ||
self.fail(f"Unexpected TypedDict {found}", context) | ||
|
@@ -1680,8 +1678,15 @@ def typeddict_key_must_be_string_literal(self, typ: TypedDictType, context: Cont | |
) | ||
|
||
def typeddict_key_not_found( | ||
self, typ: TypedDictType, item_name: str, context: Context | ||
self, typ: TypedDictType, item_name: str, context: Context, setitem: bool = False | ||
) -> None: | ||
"""Handle error messages for TypedDicts that have unknown keys. | ||
|
||
Note, that we differentiate in between reading a value and setting a | ||
value. | ||
Setting a value on a TypedDict is an 'unknown-key' error, whereas | ||
reading it is the more serious/general 'item' error. | ||
""" | ||
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. Please use common convention for docstrings. It should be like this. def func() -> None:
"""Short description in form of "do something".
After empty line, long description indented with function body.
The closing quotes n a separate line.
""" 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. Fixed in: 06a3866 |
||
if typ.is_anonymous(): | ||
self.fail( | ||
'"{}" is not a valid TypedDict key; expected one of {}'.format( | ||
|
@@ -1690,17 +1695,14 @@ def typeddict_key_not_found( | |
context, | ||
) | ||
else: | ||
err_code = codes.TYPPEDICT_UNKNOWN_KEY if setitem else codes.TYPEDDICT_ITEM | ||
self.fail( | ||
f'TypedDict {format_type(typ)} has no key "{item_name}"', | ||
context, | ||
code=codes.TYPEDDICT_ITEM, | ||
f'TypedDict {format_type(typ)} has no key "{item_name}"', context, code=err_code | ||
) | ||
matches = best_matches(item_name, typ.items.keys(), n=3) | ||
if matches: | ||
self.note( | ||
"Did you mean {}?".format(pretty_seq(matches, "or")), | ||
context, | ||
code=codes.TYPEDDICT_ITEM, | ||
"Did you mean {}?".format(pretty_seq(matches, "or")), context, code=err_code | ||
) | ||
|
||
def typeddict_context_ambiguous(self, types: list[TypedDictType], context: Context) -> None: | ||
|
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.
This condition is unreachable, there is
callee.required_keys <= actual_keys
in the enclosing if statement.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.
Hello!
I believe this is correct as the
callee.required_keys <= actual_keys
has a not preceeding it.Indeed, removing this return wil fail the test
[case TestCannotCreateTypedDictInstanceWithMissingItems]
Let's imagine the following:
Will return AnyType and print out a single error message: "missing required key 3"
Whereas with
actual_keys = {1,2,3, 'unrelated'}
will print out "extra key 'unrelated' but not return AnyType; ie it will continue to check the TypedDict for any other errors (For example, the actual keys might all be accounted for but have the wrong type).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.
Oh yeah, right, I have missed the
not
.