-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Implement type-aware get
for TypedDict
#2620
Implement type-aware get
for TypedDict
#2620
Conversation
0de22fc
to
586c1fd
Compare
Thanks! This is probably pretty important for a lot of typed dict use cases. Here are some notes after a quick pass:
More generally, we don't have a complete story for incomplete typed dicts (i.e. ones with missing keys). This adds support for them partially, but not when explicitly creating a typed dict. You don't need to fully solve that in this PR, though. |
I'm a little bit surprised by the use case. Consider this test: TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int})
PointSet = TypedDict('PointSet', {'first_point': TaggedPoint})
p = PointSet(first_point=TaggedPoint(type='2d', x=42, y=1337))
reveal_type(p.get('first_point', {}).get('x')) # E: Revealed type is 'builtins.int' In what real-world variation of this code does the default value (whether explicit or implicit) to either [Note that my comment and Jukka's crossed each other.] |
Yeah, Let's move discussion about missing keys to #2632, since it's more complex than just implementing |
@JukkaL, what do you want to happen to this PR? |
It's still unclear to me. It would be nice to get some idea of what the complete typed dict approach might look like before deciding what still needs to be done here (see my comments in #2632). |
I'm on vacation currently, but will be back in on Friday to make any changes. @JukkaL, you're totally correct RE: Optional types. I'll make that change. For data validation, we use Marshmallow - https://marshmallow.readthedocs.io/en/latest/ . My plan was to build on top of it to generate stub files containing TypedDicts. TypedDicts have also been super useful for us when dealing with some external API like Github's where honestly it's not super well documented what's required and what isn't, and they occasionally broke us by say omitting a field instead of returning an empty object. We generally are defensive and use |
f8c9dff
to
24d2ae6
Compare
@JukkaL Took your feedback and now return an @gvanrossum Fixed up the handling of the default parameter. In most cases we construct a simplified union, but I did special case |
Previously, `get` would simply fallback to the type of the underlying dictionary which made TypedDicts hard to use with code that's parsing objects where fields may or may not be present (for example, parsing a response). This implementation _explicitly_ ignores the default parameter's type as it's quite useful to chain together get calls (Until something like PEP 505 hits 😄) ```python foo.get('a', {}).get('b', {}).get('c') ``` This fixes python#2612
24d2ae6
to
c5f7481
Compare
…ions. After poking around with this a bunch today I realized it would be much simplier to simply create a context-specific Callable as opposed to attemping to hijack the rest of the typechecking. The original implementation had problems in places, for example where a TypedDict had a List field. A default empty list was not being coerced correctly.
8c5975e
to
b1022bf
Compare
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 looks almost ready! One non-trivial issue (Mapping
type arguments) and a few minor things.
TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) | ||
PointSet = TypedDict('PointSet', {'first_point': TaggedPoint}) | ||
p = PointSet(first_point=TaggedPoint(type='2d', x=42, y=1337)) | ||
p.get('first_point', 32) # E: Argument 2 to "get" of "Mapping" has incompatible type "int"; expected "Union[TypedDict(type=str, x=int, y=int), 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.
This is a pretty minor thing, but the error message is a little confusing: it says "get" of "Mapping"
even though the signature of get
in this particular case comes from PointSet
. A better message would be ... "get" of "PointSet" ...
or `... "get" of a TypedDict ..." (if the name of the typed dict is not available).
(If this seems hard to do, we can create as a separate issue for this.)
from mypy_extensions import TypedDict | ||
Items = TypedDict('Items', {'name': str, 'values': List[str]}) | ||
def foo(i: Items) -> None: | ||
reveal_type(i.get('values', [])) # E: Revealed type is 'builtins.list[builtins.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.
Style nit: using 2 spaces for indent here.
p = Point(x=42, y=13) | ||
def invoke_method(method: Callable[[str, int], int]) -> None: | ||
pass | ||
invoke_method(p.get) |
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.
Maybe add a test case where p.get
does not have a compatible type (e.g. target function expects Callable[[int, str], int]
or something).
# concise way without having to set up exception handlers. | ||
arg_types = [callee.arg_types[0], | ||
UnionType.make_union([return_type, | ||
self.named_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.
It would better to use the type such as Mapping[str, Any]
. Not having type arguments for Mapping
may cause trouble in some cases (e.g. index errors). This still wouldn't be quite safe, but let's deal with that as a separate issue.
@rowillia Are you still interested in working on this? If you are busy, I can make the final changes and get this merged, since this would be a useful thing to have before the next mypy release. |
@rowillia I'm still hoping that you'll pick up this ball. |
This is a PR for a high-priority issue, I am thinking maybe it's time to change the owner? |
I can move this forward. I'm going to use a somewhat different approach (a method 'plugin') to implement this. |
…#3501) Implement a general-purpose way of extending type inference of methods. Also special case TypedDict get and `int.__pow__`. Implement a new plugin system that can handle both module-level functions and methods. This an alternative to #2620 by @rowillia. I borrowed some test cases from that PR. This PR has a few major differences: * Use the plugin system instead of full special casing. * Don't support `d.get('x', {})` as it's not type safe. Once we have #2632 we can add support for this idiom safely. * Code like `f = foo.get` loses the special casing for get. Fixes #2612. Work towards #1240.
I think this now can be closed in favor of recently merged #3501 |
Previously,
get
would simply fallback to the type of the underlying dictionary which madeTypedDicts hard to use with code that's parsing objects where fields may or may not be
present (for example, parsing a response).
This implementation explicitly ignores the default parameter's type as it's quite useful to
chain together get calls (Until something like PEP 505 hits 😄)
This fixes #2612