-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[in progress] TypedDict: Recognize creation of TypedDict instance. Define TypedDictType. #2342
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
Conversation
@JukkaL can probably help you most effectively. In the meantime could you rebase? |
48758ee
to
80bcb3e
Compare
Rebased. The first question under consideration is how can I trigger the TypeJoinVisitor.visit_typeddict_type() code path? Thoughts @JukkaL ? Prior investigation notes:
|
Probably the most common scenario for joins is something like For meets, it's a little trickier. Mypy calculates the meet of
Also, there are some unit tests in |
extra_item_names = [k for k in kwargs_item_names if k not in callee_item_names] | ||
|
||
self.chk.fail( | ||
'Expected items {} but found {}. Missing {}. Extra {}.'.format( |
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.
For consistency, the message logic would better live in mypy/messages.py
, as a method.
Maybe omit empty Missing
/ Extra
from the message.
item_actual_type = self.chk.check_simple_assignment( | ||
lvalue_type=item_expected_type, rvalue=item_value, context=item_value, | ||
msg=messages.INCOMPATIBLE_TYPES, | ||
lvalue_name='TypedDict item', |
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.
Include item name in lvalue_name
, as otherwise it can be hard to figure out which item has an invalid value type.
items[item_name] = item_actual_type | ||
|
||
mapping_value_type = join.join_type_list(list(items.values())) | ||
fallback = self.chk.named_generic_type('typing.Mapping', |
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.
The question about the fallback type is actually kind of tricky. It's not clear to me if Mapping
is the most practical fallback definition, though I think that it's a type-safe one.
In existing code, people likely use Dict[str, Any]
for things that would be suitable for TypedDict
. Introducing TypedDict
gradually to such a codebase would likely imply getting rid of many of the Dict[str, Any]
types or replacing them with Mapping[str, Any]
(or adding casts), so the introduction wouldn't be perfectly gradual.
Here are alternatives that may or may not be reasonable:
- If a typed dict has uniform value types, make the fallback
Dict[str, value_type]
. This wouldn't be quite safe sincedict
values support__del__
and adding new keys. - Have two fallbacks (the second one could be special cased somehow),
Mapping[str, ...]
(as currently) andDict[str, Any]
. Code dealing withAny
types won't be safe anyway, so this would still be safe for fully typed code but would also perhaps provide a smoother gradual typing story.
We don't need to decide this now -- it may well take some practical experimentation with real code to determine the best approach.
@@ -342,11 +342,26 @@ def visit_tuple_type(self, template: TupleType) -> List[Constraint]: | |||
else: | |||
return [] | |||
|
|||
def visit_typeddict_type(self, template: TypedDictType) -> List[Constraint]: | |||
actual = self.actual | |||
# TODO: Is it correct to require identical keys here? |
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.
Not quite. I think that it would be reasonable to consider the intersection of keys here, between actual and template. Compatibility will be checked elsewhere so it won't be unsafe.
(item_name, self.join(s_item_type, t_item_type)) | ||
for (item_name, s_item_type, t_item_type) in self.s.zip(t) | ||
]) | ||
fallback = join_instances(self.s.fallback, t.fallback) |
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.
It's unclear whether taking the join of the fallbacks is the right thing to do. For example, a join may have fewer items, and the fallback could actually be more specific than operand fallbacks (if it's Mapping[str, ...]
). Maybe at least add a TODO comment about this.
dictype = (self.named_type_or_none('builtins.dict', [strtype, AnyType()]) | ||
or self.object_type()) | ||
fallback = dictype | ||
mapping_value_type = join.join_type_list(types) |
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.
I suspect that we can't take joins at this stage of semantic analysis, since some of the required semantic information might not have been processed yet. Actually, calculating joins may only be possible after all semantic analysis passes have been completed for a strongly-connected component of modules.
@@ -125,6 +125,10 @@ def visit_instance(self, left: Instance) -> bool: | |||
self.right, | |||
self.check_type_parameter): | |||
return True | |||
if left.type.typeddict_type is not None and is_subtype(left.type.typeddict_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.
Hmm this likely isn't right. We must assume that a typed dict type is always represented as TypedDictType
here. The way to ensure this is probably by updating TypeAnalyser.visit_unbound_type
to perform a check on typeddict_type
and return one as needed. It already does this for tuple_type
(towards the end of the method), and this should be similar.
def visit_typeddict_type(self, left: TypedDictType) -> bool: | ||
right = self.right | ||
if isinstance(right, Instance): | ||
if right.type.typeddict_type is not 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.
Similar to above, we should assume that a typed dict type is represented as a TypedDictType
. This should be handled in a similar fashion to TypeInfo.tuple_type
/ TupleType
.
if not left.names_are_wider_than(right): | ||
return False | ||
for (_, l, r) in left.zip(right): | ||
if not is_subtype(r, l, self.check_type_parameter): |
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.
Item types should be invariant, since they are mutable. This would make them covariant -- use is_equivalent
instead of is_subtype
. Also add test case.
@@ -223,6 +224,13 @@ def visit_tuple_type(self, t: TupleType) -> Type: | |||
fallback = t.fallback if t.fallback else self.builtin_type('builtins.tuple', [AnyType()]) | |||
return TupleType(self.anal_array(t.items), fallback, t.line) | |||
|
|||
def visit_typeddict_type(self, t: TypedDictType) -> 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.
This is not sufficient, I think. It seems that we hit the visit_unbound_type
case instead (see above for a discussion of that), and it should be handled similarly to named tuples.
from mypy_extensions import TypedDict | ||
from typing import Mapping | ||
Point = TypedDict('Point', {'x': int, 'y': int}) | ||
def as_mapping(p: Point) -> Mapping[str, int]: |
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.
Also check what happens if we have an incompatible Mapping
return type.
[builtins fixtures/dict.pyi] | ||
[out] | ||
main: note: In function "convert": | ||
main:5: error: Incompatible return value type (got "Point", expected "ObjectPoint") |
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.
Use # E:
for this and elsewhere (you can leave the main: note: ...
in the [out]
section, or modify the flags to omit context information in output).
Some more ideas for test cases:
Also, we should reject a typed dict type as a base class. |
Generally looks great -- thanks for working on this! This is going to be a useful feature. |
Thanks for the solid feedback. This will take me some time to integrate. :-) |
@davidfstr Are you ready for another review? If so could you resolve the merge conflict (maybe using rebase) so the tests can run? |
@gvanrossum Not ready for another review yet. I'll make another post when I am. |
The second question under consideration is how can I trigger the ConstraintBuilderVisitor.visit_typeddict_type() code path? Thoughts @JukkaL ? From the following call hierarchy analysis, it looks like an expression involving Callable[...] might do the trick:
|
…Type. Notable visitor implementations added: * Subtype * Join * Meet * Constraint Solve Also: * Fix support for using dict(...) in TypedDict instance constructor. * Allow instantiation of empty TypedDict. * Disallow underscore prefix on TypedDict item names. * TypeAnalyser: Resolve an unbound reference to a typeddict as a named TypedDictType rather than as an Instance.
0bbcceb
to
0dd787d
Compare
I have squashed and rebased all changes to the tip of master. I think most of the major items of feedback have been integrated. There are some lingering issues, but it would probably be more efficient to address those with followup PRs rather than adding to the already 1,500+ line diff in this PR. Thanks for taking the time to review all these changes. Summary of immediate remaining issues:
|
Thanks. I need to take a break from all work, hopefully it can wait or someone else will be able to review it. |
Thanks for the updates! I'm going to merge this and create follow-up issues for further work. Now that we have the basic infrastructure in for typed dicts, it should be possible to add the remaining functionality through pretty small, specific PRs, which are going to be easier and faster to review. |
Here are some follow-up issues: https://github.com/python/mypy/issues?q=is%3Aopen+is%3Aissue+label%3ATypedDict I think #2486 and #2487 are the most urgent, as they may be enough to start experimenting with real code. |
Thanks @JukkaL ! The next few items I'm planning to help with are:
|
This PR was created in the following steps:
Remaining work:
I am creating this PR now to solicit assistance on approach for the preceding items of remaining work.
Since I haven't been able to trigger the code paths for the Join, Meet, and Constraint Solve visitors, I am not as confident in their correctness.
Out of Scope for this PR:
__getitem__
,__setitem__
,get
,isinstance