-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Support TypedDicts with missing keys (total=False) #3558
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 1 commit
ccfc4ad
44f53a9
6bb872e
4df39fc
39f0d38
7e042b8
981023a
6223d2a
5ecafb2
0d16271
1c068e0
4429ce1
1c2d327
5359a98
a09c5b9
059bc21
b47857e
6b922b5
31b6696
d32fd68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Only the functional syntax is supported.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -292,8 +292,9 @@ def check_typeddict_call_with_dict(self, callee: TypedDictType, | |
def check_typeddict_call_with_kwargs(self, callee: TypedDictType, | ||
kwargs: 'OrderedDict[str, Expression]', | ||
context: Context) -> Type: | ||
if callee.items.keys() != kwargs.keys(): | ||
callee_item_names = callee.items.keys() | ||
if not (callee.required_keys <= kwargs.keys() <= callee.items.keys()): | ||
callee_item_names = [key for key in callee.items.keys() | ||
if key in callee.required_keys or key in kwargs.keys()] | ||
kwargs_item_names = kwargs.keys() | ||
|
||
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. This blank line irks me. 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. Removed. |
||
self.msg.typeddict_instantiated_with_unexpected_items( | ||
|
@@ -316,7 +317,7 @@ def check_typeddict_call_with_kwargs(self, callee: TypedDictType, | |
mapping_value_type = join.join_type_list(list(items.values())) | ||
fallback = self.chk.named_generic_type('typing.Mapping', | ||
[self.chk.str_type(), mapping_value_type]) | ||
return TypedDictType(items, fallback) | ||
return TypedDictType(items, set(callee.required_keys), fallback) | ||
|
||
# Types and methods that can be used to infer partial types. | ||
item_args = {'builtins.list': ['append'], | ||
|
@@ -1656,6 +1657,8 @@ def visit_typeddict_index_expr(self, td_type: TypedDictType, index: Expression) | |
if item_type is None: | ||
self.msg.typeddict_item_name_not_found(td_type, item_name, index) | ||
return AnyType() | ||
if item_name not in td_type.required_keys: | ||
self.msg.typeddict_item_may_be_undefined(item_name, index) | ||
return item_type | ||
|
||
def visit_enum_index_expr(self, enum_type: TypeInfo, index: Expression, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -921,12 +921,14 @@ class TypedDictType(Type): | |
whose TypeInfo has a typeddict_type that is anonymous. | ||
""" | ||
|
||
items = None # type: OrderedDict[str, Type] # (item_name, item_type) | ||
items = None # type: OrderedDict[str, Type] # item_name -> item_type | ||
required_keys = None # type: Set[str] | ||
fallback = None # type: Instance | ||
|
||
def __init__(self, items: 'OrderedDict[str, Type]', fallback: Instance, | ||
line: int = -1, column: int = -1) -> None: | ||
def __init__(self, items: 'OrderedDict[str, Type]', required_keys: Set[str], | ||
fallback: Instance, line: int = -1, column: int = -1) -> None: | ||
self.items = items | ||
self.required_keys = required_keys | ||
self.fallback = fallback | ||
self.can_be_true = len(self.items) > 0 | ||
self.can_be_false = len(self.items) == 0 | ||
|
@@ -938,6 +940,7 @@ def accept(self, visitor: 'TypeVisitor[T]') -> T: | |
def serialize(self) -> JsonDict: | ||
return {'.class': 'TypedDictType', | ||
'items': [[n, t.serialize()] for (n, t) in self.items.items()], | ||
'required_keys': sorted(self.required_keys), | ||
'fallback': self.fallback.serialize(), | ||
} | ||
|
||
|
@@ -946,6 +949,7 @@ def deserialize(cls, data: JsonDict) -> 'TypedDictType': | |
assert data['.class'] == 'TypedDictType' | ||
return TypedDictType(OrderedDict([(n, deserialize_type(t)) | ||
for (n, t) in data['items']]), | ||
set(data['required_keys']), | ||
Instance.deserialize(data['fallback'])) | ||
|
||
def as_anonymous(self) -> 'TypedDictType': | ||
|
@@ -955,14 +959,15 @@ def as_anonymous(self) -> 'TypedDictType': | |
return self.fallback.type.typeddict_type.as_anonymous() | ||
|
||
def copy_modified(self, *, fallback: Instance = None, | ||
item_types: List[Type] = None) -> 'TypedDictType': | ||
item_types: List[Type] = None, | ||
required_keys: Set[str] = None) -> 'TypedDictType': | ||
if fallback is None: | ||
fallback = self.fallback | ||
if item_types is None: | ||
items = self.items | ||
else: | ||
items = OrderedDict(zip(self.items, item_types)) | ||
return TypedDictType(items, fallback, self.line, self.column) | ||
return TypedDictType(items, self.required_keys, fallback, self.line, self.column) | ||
|
||
def create_anonymous_fallback(self, *, value_type: Type) -> Instance: | ||
anonymous = self.as_anonymous() | ||
|
@@ -1371,6 +1376,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: | |
for (item_name, item_type) in t.items.items() | ||
]) | ||
return TypedDictType(items, | ||
t.required_keys, | ||
# TODO: This appears to be unsafe. | ||
cast(Any, t.fallback.accept(self)), | ||
t.line, t.column) | ||
|
@@ -1516,11 +1522,17 @@ def visit_tuple_type(self, t: TupleType) -> str: | |
|
||
def visit_typeddict_type(self, t: TypedDictType) -> str: | ||
s = self.keywords_str(t.items.items()) | ||
if t.required_keys == set(t.items): | ||
keys_str = '' | ||
elif t.required_keys == set(): | ||
keys_str = ', _total=False' | ||
else: | ||
keys_str = ', _required_keys=[{}]'.format(', '.join(sorted(t.required_keys))) | ||
if t.fallback and t.fallback.type: | ||
if s == '': | ||
return 'TypedDict(_fallback={})'.format(t.fallback.accept(self)) | ||
return 'TypedDict(_fallback={}{})'.format(t.fallback.accept(self), keys_str) | ||
else: | ||
return 'TypedDict({}, _fallback={})'.format(s, t.fallback.accept(self)) | ||
return 'TypedDict({}, _fallback={}{})'.format(s, t.fallback.accept(self), keys_str) | ||
return 'TypedDict({})'.format(s) | ||
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. It's a bit disturbing that the repr of a TypedDict is so different from the actual syntax used to create one, but let's deal with that some other time. 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. Created #3590 to track this. |
||
|
||
def visit_star_type(self, t: StarType) -> str: | ||
|
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.
Naming these two variables expected_xxx and actual_xxx (matching the error message call below) would help in understanding what they mean.
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.
A good idea -- done.