-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[needs tests] TypedDict: Recognize declaration of TypedDict('Point', {'x': int, 'y': int}). #2206
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
I haven't looked at your code yet, but (1) indeed, having an "experimental
typing.py" that contains a superset of the regular one makes a lot of
sense. You can actually do this already using --typing-module, but it's a
bit inconvenient, and it would be great of there was a designated name that
we can distribute separately from the Python/typing repo. Also, (2) you're
probably in for a bit of a merge conflict due to some recent commits by
@elazarg.
|
Yes, sorry about that. I think there should be a dedicated module for parsing "special constructs" such as NewType, NamedTuple and TypedDict. They need similar machinery (e.g. AST matcher). |
node.kind = GDEF # TODO locally defined TypedDict | ||
node.node = typed_dict | ||
|
||
def check_typeddict(self, node: Node, var_name: str = None) -> TypeInfo: |
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.
Return should be Optional.
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.
Fixed.
Revised to no longer modify the |
@gvanrossum I think we may have different purposes in mind when talking about "an experimental typing module". Therefore I suggest we break off that likely-to-be-involved discussion. I've created a thread here: #2210 |
Review checklist[x] Comments from one reviewer. |
[case testCanDefineTypedDictType] | ||
from mypy.typing import TypedDict | ||
Point = TypedDict('Point', {'x': int, 'y': int}) | ||
[out] |
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 think you'll need to add test-data/unit/lib-stub/mypy/__init__.pyi
and test-data/unit/lib-stub/mypy/typing.pyi
, as otherwise mypy won't find the mypy.typing
module. Test cases look for stubs here and they don't use typeshed stubs. You can write a minimal stub that lets the tests pass.
Also, the default builtins stubs used in test cases don't include dict
. You should be able to add this line to the test case for dict
to work (just before [out]
):
[builtins fixtures/dict.pyi]
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.
@JukkaL Hey thanks for the tips. This may be enough for me to get the tests to work.
I'll probably take another look Wednesday evening.
Hopefully my suggestions regarding stubs in test cases will help you get your test case running. If you still have trouble and I can debug it myself. The diff looks pretty good to me! Once you have some tests working, just write test cases for more error conditions and this will likely be close to ready. |
Excited for this! Is there documentation about the usage/exact semantics anywhere? |
So, could I get some help in debugging the test failure that I mentioned above? It's crashing with an internal error but it's not clear to me how to specify |
Whoops I missed your comment @JukkaL. I know that there's such a thing as test stubs now, but I'm still not 100% sure on how they work still. Knowing how to narrow down runtests.py to run exactly the tests I care about should help. |
@ddfisher Take a look at python/typing#28 for discussions around spelling and semantics. Take a look at #985 for the larger implementation plan. |
@gvanrossum @ddfisher Can you tell why @davidfstr isn't seeing a traceback? I think that tests should always show a traceback by default. |
@JukkaL IIRC tracebacks from tests stopped happening when I added the
`--show-traceback` flag, default off. We added it back in a few places but
probably not in others. I will try to research where.
@davidfstr There are some instructions in README.md on running tests
selectively using py.test (for some subset of tests) or scripts/myunit (for
the rest).
|
I hacked errors.py to always show the traceback and got the following. Maybe it's helpful.
|
Next time you rebase, you'll get #2216 which should show tracebacks whenever a test crashes! |
I've gotten a bit further with @JukkaL's magic line to add to the tests. However it appears that the test output contains the AST of not just the test case but also the The remaining test issue is probably simple enough for someone familar with the test instructure to just look at it and fix it directly. That would give me more time to actually work on the next implementation phase. When writing more tests for later phases, is there documentation floating around anywhere for how to really use the test infrstructure? The tests at minimum seem to be using a DSL that I am not familar with. |
Some modules are filtered out from the output in There's some documentation about the test infrastructure in the wiki, but it looks a little out of date: https://github.com/python/mypy/wiki/Implementation-Overview |
(I made some updates to the implementation overview.) |
2fffdb2
to
3c8e5ac
Compare
Rebased to tip of master. All tests pass. Only outstanding issue I see is the desire to move mypy/typing.py to a better location, as per #2210 . However I'd recommend we get this change initially committed as-is to avoid further merge conflicts, as this is a somewhat large change. |
Understood, but we're waiting until after 0.4.5 is released anyway.
|
Also, I would actually like to see a separate PR for the extensions module
before going ahead with this. I really don't like the idea of checking in
mypy/typing.py and then moving it. (I do like the idea of separating the
work in two PRs.)
|
Okay. I'll consider this PR paused until a separate PR for an empty |
I'd really like to see way more unittests for this; the only unittest you've added just checks the parsing (even there I'd like to see more test coverage of the various error cases). Understood that you may want to wait until you've got mypy_extensions squared away. But even so you'll have to add (unfortunately) some "mock stubs" (if that's the term) to test-data/unit/lib-deps and/or test-data/unit/fixtures. |
I've added tests that cover all error scenarios that have error messages. |
Hmm... Normally I'd prefer that mypy_extensions distribute its own .pyi file directly rather than requiring a synchronized checkin to typeshed. But it sounds like that wouldn't work since if sys.path is not consulted then mypy presumably wouldn't find any .pyi files installed in it. Is there generally a way for a package that is natively typing-aware to distribute its own .pyi files in such a way that mypy would find it without special configuration? Or is typeshed the only supported search location for .pyi files at the present time? |
It's complicated (and I can't even find an issue about this, though I think
there should be one). The short version is that using typeshed is the only
solution that works today.
|
Alrighty. Typeshed it is then. I'll probably create a separate issue as a placeholder for how libraries that wish to distribute their own .pyi files (or have inline annotations) should go about doing that in a way that typecheckers like mypy can locate. Probably a discussion for https://github.com/python/typing . |
Yeah, there are a number of related discussions (maybe there is already an
issue there -- I forgot to look in that tracker).
|
I agree this is getting close, but I would still like to see more tests, in particular tests for the type checker (see test-data/unit/check-*.test). The tests you've added are all semanal tests which means they don't even exercise the type checker at all -- they basically just check the syntactic processing. |
@@ -0,0 +1,6 @@ | |||
from typing import Dict, Type, TypeVar |
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.
We moved this to typeshed.
This PR makes no substantive changes to the type checker (mypy/checker.py), leaving that work to the next PR in the proposed series, so I'm not sure what kind of tests I'd be able to add. In particular it's not actually possible yet to create an instance of a type instantiated by TypedDict. In reading test-data/check-namedtuple.test, which is likely analogous to a future check-typeddict.test, most of what is checked there is not implemented yet. The current tests test the main success case and every error case (that outputs a distinct error message) of mypy/semanal.py@process_typeddict_definition, which is the only major code added in this PR. Are there other cases in the implementation that I'm not seeing that you'd like me to cover? |
Oh. Wait. You were proposing to get this merged and then work on the next We don't usually merge features until they are fully implemented, so I If you want some assurance that you've done the right things so far, I'd |
Sure. I could attack the whole beast. :-) I had the entire feature split up into a larger series of PRs based on a earlier recommendation (below) to use several smaller PRs rather than one big PR. The full implementation series is here: #985 (comment)
|
OK, Jukka is the boss. I will check this over once more and then On Mon, Oct 17, 2016 at 10:06 PM, David Foster notifications@github.com
--Guido van Rossum (python.org/~guido) |
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.
OK, this is almost ready to merge.
call = node | ||
if not isinstance(call.callee, RefExpr): | ||
return None | ||
callee = call.callee |
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.
Micro-optimization: you can move this before the isinstance() check -- it's still the callee even if we don't like it. :-)
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.
Applied globally.
items, types, ok = self.parse_typeddict_args(call, fullname) | ||
if not ok: | ||
# Error. Construct dummy return value. | ||
return self.build_typeddict_typeinfo('TypedDict', [], []) |
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 wonder if this isn't going to cause a cascade of spurious errors. Might be better to just return AnyType().
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.
When I alter this line's twin in check_namedtuple() to return None, which means that something other than a NamedTuple was found (as opposed to an invalid NamedTuple), the error detected in the test suite (semanal-namedtuple.test:136) seems less helpful.
Old error: Tuple expected as NamedTuple() field
New error: Invalid base class
Honestly I don't think either error message is particularly good but at least the first one mentions that is has something to do with a NamedTuple.
Based on this regression in error message quality I've left this line as-is.
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 might be possible to return an AnyType with a more-invasive change, as I'd need to alter the signature of check_namedtuple and its callers. I'd prefer to tread lightly.
# Error. Construct dummy return value. | ||
return self.build_typeddict_typeinfo('TypedDict', [], []) | ||
else: | ||
# Give it a unique name derived from the line number. |
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 comment belongs inside the following 'if'.
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.
Done in both locations.
if name != var_name: | ||
name += '@' + str(call.line) | ||
info = self.build_typeddict_typeinfo(name, items, types) | ||
# Store it as a global just in case it would remain anonymous. |
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, this sounds risky. Consider:
foo = 12
class C:
foo = TypedDict('foo', {...})
Now it looks like the global foo will be overwritten for no reason. Maybe only do that if it's anonymous?
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 don't fully understand the implications of changing this line. I will however note that if I remove the line entirely from check_namedtuple() and check_typeddict() then the test suite continues to pass.
Agreed that it seems weird that a type declaration performed anywhere would want to be injected into the global scope. I can remove this line if someone else can confirm that I'm not going to break some subtle preexisting behavior.
|
||
def parse_typeddict_args(self, call: CallExpr, | ||
fullname: str) -> Tuple[List[str], List[Type], bool]: | ||
# TODO Share code with check_argument_count in checkexpr.py? |
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.
Add TODO: support keyword args.
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.
Done.
types: List[Type]) -> TypeInfo: | ||
strtype = self.named_type('__builtins__.str') # type: Type | ||
dictype = (self.named_type_or_none('builtins.dict', [strtype, AnyType()]) | ||
or self.object_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.
I'm guessing this 'or' branch is when the test fixtures don't define dict? It's a fine hack but might deserve a comment.
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'm guessing this 'or' branch is when the test fixtures don't define dict?
Perhaps. It's the same pattern that build_namedtuple_typeinfo() uses.
I'm not qualified to leave a comment explaining the pattern.
|
||
def build_typeddict_typeinfo(self, name: str, items: List[str], | ||
types: List[Type]) -> TypeInfo: | ||
strtype = self.named_type('__builtins__.str') # type: 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.
(Not your fault, but don't you find the difference between named_type() and named_type_or_none() jarring? I have to carefully read the implementation every time I see this... :-( )
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 don't really know what the difference is. Presumably named_type_or_none() can fail but there's no documentation explaining what None means in that context.
The underlying lookup_fully_qualified() and lookup_fully_qualified_or_none() have identical documentation even though the latter can presumably fail in some way.
Also named_type_or_none() and lookup_fully_qualified_or_none() can return Optional[...] but aren't marked as such.
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.
We're missing many Optional annotations, so not to worry about those. The difference that was jarring to me is that one starts the lookup in the local namespace (hence __builtins__
, which is a secret dict with all the builtins) while the other ("fully qualified") always starts in the module table (hence "builtins"). This is the difference between lookup_qualified() and lookup_fully_qualified_or_none() -- note, "fully".
@@ -127,9 +127,11 @@ N = namedtuple('N', 1) # E: List literal expected as the second argument to name | |||
from collections import namedtuple | |||
N = namedtuple('N', ['x', 1]) # E: String literal expected as namedtuple() item | |||
|
|||
[case testNamedTupleWithInvalidArgs] | |||
-- NOTE: The following code works at runtime but is not yet supported by mypy. |
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.
Honestly, in Python 3.6 I'd love NamedTuple to take keyword args so you can say
Point = typing.NamedTuple('Point', x=int, y=int)
It requires Python 3.6+ because that promises to preserve keyword arg order.
But this would require support in typing too. (@ilevkivskyi?) Not for this PR obviously...
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.
Yes, it would be easy to implement in typing.py
I will open an issue there now.
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.
Agreed that keyword args would be a nice syntax to use for NamedTuple
.
…: int}). Also: * Create mypy.typing module for experimental typing additions.
28c625c
to
f995b45
Compare
Revised. Rebased to tip of master. |
Thanks! You can now move on to the next step. (How many steps were you planning?) |
The next step is probably the biggest of the initial steps, as it involves making the type checker aware of TypedDict. Here are the next few steps, as taken from #985 (comment) : If (2) and (3) are turn out to be highly coupled, I may opt to do them together. |
I think (2) and (3) are probably best combined in a single PR. I think we
should hold off with (4) until we have more confidence that what we have is
useful in a pre-3.6 context (there's a lot of demand for this at Dropbox
but it's all Python 2.7). I am not sure about (5), I'd prefer not to go
there unless demand is proven.
There's another branch that I'd like to see; I really much prefer writing
TypedDict('P', x=int, y=int) rather than TypedDict('P', {'x': int, 'y':
int}).
|
Yes I was thinking of doing that (1b) in parallel with (2)+: |
That actually seems nicely separable from the type checking, and I'd like
to see the latter first.
|
As it could be seen from #2245 it is quite easy to implement the 3.6+ syntax when previous steps are done. |
The automated tests in this PR still need work but the feature implementation should be good.
How to test locally
That single error above is expected since this PR only supports declaring the TypedDict type
Point = TypedDict('Point', {'x': int, 'y': int})
but not the TypedDict instance. If you remove the last line (point = dict(x=42, y=1337) # type: Point
) then you should get no errors.Notes
Move typing.TypedDict -> mypy.typing.TypedDict?
During the incremental adding of support of TypedDict over several PRs, it would be a lot easier ifTypedDict
was not defined in the officialtyping
module but was instead defined in a mypy-specific typing module (likemypy.typing
). Then after TypedDict was stabilizied, then TypedDict could be added to the officialtyping
module. Let me know if you this is a good idea.Done. It's a lot cleaner now. No more conditional imports.
More work required on tests
I have tried to implement tests for TypedDict but am getting a failure I do not understand.
I am not sure how to pass
--show-traceback
to the test suite, as the error message recommends.Edit: Simplify instructions now that the new
mypy.typing
module to holdTypedDict
is used in lieu of the standardtyping
module.