-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Use __getattr__ to mark partial stub packages #5231
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
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 implementation looks good to me! Just some minor typos to fix.
mypy/nodes.py
Outdated
@@ -219,6 +219,10 @@ class MypyFile(SymbolNode): | |||
is_stub = False | |||
# Is this loaded from the cache and thus missing the actual body of the file? | |||
is_cache_skeleton = False | |||
# Is this representns an __init__.pyi stub with a module __getattr__ |
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.
Is this representns
-> Does this represent
mypy/build.py
Outdated
# If `temporary` is True, this State is beeing created to just | ||
# quickly parse/load the tree, without an intention to further | ||
# process it. With this flag, any changes to external state as well | ||
# as error reprting should be avoided. |
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.
reprting
-> reporting
mypy/semanal_pass1.py
Outdated
ret = func.type.ret_type | ||
if isinstance(ret, UnboundType) and not ret.args: | ||
sym = self.sem.lookup_qualified(ret.name, func, suppress_errors=True) | ||
# We only interpred package as partial if __getattr__ return 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.
We only interpred
-> We only interpret a
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, if
-> if the
test-data/unit/check-modules.test
Outdated
x = b.f() | ||
[file a/__init__.pyi] | ||
from typing import Any | ||
def __getattr__(attr: str) -> Any: pass |
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.
Don't we usually use an ellipsis for stubs/tests?
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.
Could you also add some tests that verify module-specific settings of ignore_missing_imports
work correctly?
mypy/build.py
Outdated
@@ -1738,6 +1738,11 @@ def __init__(self, | |||
caller_line: int = 0, | |||
ancestor_for: 'Optional[State]' = None, | |||
root_source: bool = False, | |||
# If `temporary` is True, this State is beeing created to just |
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.
beeing -> being
mypy/build.py
Outdated
@@ -1759,9 +1764,10 @@ def __init__(self, | |||
try: | |||
path, follow_imports = find_module_and_diagnose( | |||
manager, id, self.options, caller_state, caller_line, | |||
ancestor_for, root_source) | |||
ancestor_for, root_source, temporary) |
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'd use quick=temporary
here since it's an optional flag. (Though maybe more of the args should use keyword=value then?
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.
Though maybe more of the args should use keyword=value then?
I would do this for quick
only, because it is a bit "more special" than other.
mypy/build.py
Outdated
@@ -2252,6 +2259,7 @@ def find_module_and_diagnose(manager: BuildManager, | |||
caller_line: the line number of the import | |||
ancestor_for: the child module this is an ancestor of, if applicable | |||
root_source: whether this source was specified on the command line | |||
quick: skip any error diagnosis and reporting |
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 kind of odd to have a flag that suppresses the and_diagnose
part of the function's name. :-) Also quick
reminds (me) of --quick-and-dirty
but it has nothing to do with that. Also might want to clarify that ModuleNotFound
is still raised. (Hm, the docstring should probably mention that that is raised in the first place?)
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, I will rename it to skip_diagnose
to be more explicit.
mypy/build.py
Outdated
@@ -2283,7 +2291,8 @@ def find_module_and_diagnose(manager: BuildManager, | |||
and not options.follow_imports_for_stubs) # except when they aren't | |||
or id == 'builtins'): # Builtins is always normal | |||
follow_imports = 'normal' | |||
|
|||
if quick: | |||
return (path, follow_imports) |
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 little odd -- it's really just skipping the next if-elif block (up to line 2309 below). Would it be clearer to write it as
if quick:
pass
elif follow_imports == 'silent':
...
elif ...
? Alternatively
if not quick:
if follow_imports == 'silent':
...
elif ...
mypy/build.py
Outdated
""" | ||
while '.' in id: | ||
parent, _ = id.rsplit('.', 1) | ||
parent_mod = 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.
Maybe move this into the except
block (L2345)? I was looking whether parent_mod is always defined (because it's not set in the except
clause) and this felt too far back up. A different way to structure this would be parent_mod = manager.modules.get(parent)
followed by if parent_mod is None
.
mypy/build.py
Outdated
pass | ||
else: | ||
parent_mod = parent_st.tree | ||
if parent_mod: |
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.
is not None
mypy/nodes.py
Outdated
@@ -256,6 +260,7 @@ def serialize(self) -> JsonDict: | |||
'names': self.names.serialize(self._fullname), | |||
'is_stub': self.is_stub, | |||
'path': self.path, | |||
'is_partial_stub_package': self.is_partial_stub_package |
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.
Trailing comma.
mypy/semanal_pass1.py
Outdated
ret = func.type.ret_type | ||
if isinstance(ret, UnboundType) and not ret.args: | ||
sym = self.sem.lookup_qualified(ret.name, func, suppress_errors=True) | ||
# We only interpred package as partial if __getattr__ return 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.
Also, if
-> if the
test-data/unit/check-modules.test
Outdated
def __getattr__(attr: str) -> int: pass | ||
[out] | ||
main:1: error: Cannot find module named 'a.b' | ||
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) |
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.
Would it be too much to ask to add a note pointing to the presence of __getattr__
with the wrong return 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 can create a follow-up issue after we are done with this PR, unfortunately it will require some efforts to implement (and most likely a new flag on MypyFile
, that I would rather avoid)
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.
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 opened #5276 for this.
[builtins fixtures/module.pyi] | ||
[out] | ||
main:1: error: Cannot find module named 'a.b.c' | ||
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) |
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.
FWIW when it's about a subpackage, the part of the note about setting MYPYPATH does not apply. I'm in general not a fan of this flag -- does it really help users that much? CC: @JukkaL
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.
FWIW when it's about a subpackage, the part of the note about setting MYPYPATH does not apply.
This is not something I changed, so I would make it in a separate PR. But I actually also don't like suggesting to set MYPYPATH
, I think this adds more confusion than help.
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, I filed #5241.
I am not sure what do you mean here. Could you please show an example of what you would like to test? |
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.
Could you also add some tests that verify module-specific settings of ignore_missing_imports work correctly?
I am not sure what do you mean here. Could you please show an example of what you would like to test?
I was hoping for a test that has a mypy.ini like this:
[mypy]
[mypy-a]
ignore_missing_imports = True
And then in a.py
have an import that would cause an error without the flag, and in b.py
(or main.py
) the same import should still error. But it looks like such a test would be complicated to craft and there are no tests of that feature currently, so I don't think it makes sense to require it.
[builtins fixtures/module.pyi] | ||
[out] | ||
main:1: error: Cannot find module named 'a.b.c' | ||
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) |
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, I filed #5241.
mypy/build.py
Outdated
"""Find a module by name, respecting follow_imports and producing diagnostics. | ||
|
||
If the module is not found, then the ModuleNotFound exception is raised. | ||
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.
Conventionally there's a blank line before Args:
.
test-data/unit/check-modules.test
Outdated
def __getattr__(attr: str) -> int: pass | ||
[out] | ||
main:1: error: Cannot find module named 'a.b' | ||
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) |
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.
@gvanrossum I think I implemented all the requested changes. Is there something here that needs to be done? |
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.
Thanks, looks good!
The two ancestors json.JSONMixin and werkzeug.wrappers.Request of cmk.gui.http.Request break mypy checking. For example access to not existing members is not detected anymore. This commit fixes the issue with the json.JSONMixin class. The problem here is that the typesheds distributed with mypy, that are normally available in our virtualenv (lib/python3.7/site-packages/mypy/typeshed/third_party/2and3/werkzeug/wrappers.pyi) miss the typesheds for werkzeug.wrappers.json. I tried hard to add partial typesheds for this single file to tests-py3/typeshed, but did not find a working solution. If you have a good solution, please let me know. I tried to make use of the __getattr__ mechanic added in python/mypy#5231 and the PEP 561 "partial" feature. The only working solution I came up with was copying the original werkzeug typesheds and extending them. Sorry :/. At least it's better than copying the werkzeug.wrappers.json implementation to our code ;). Change-Id: Ie2723ab286188faa67d4738c226462a162552421
There is a problem with annotating large frameworks -- they are large. Therefore it is hard to produce good stubs in a single pass. A possible better workflow would be to allow indicating that a given (sub-)package is incomplete.
I propose to use
__getattr__
for the role of such indicator. A motivation is that currently adding a__getattr__
topackage/__init__.pyi
already makesfrom package import mod
work, butimport package.mod
still fails (plus simplicity of implementation). Here are the rules that I propose:__getattr__
to its__init__.pyi
types.ModuleType
orAny
, we assume that all imports from this (sub-)package succeed.Note: these rules apply only to stubs (i.e.
.pyi
files). I add several tests to illustrate this behaviour.This PR shouldn't give any significant performance penalty because the added parsing/loading only happens when an error would be reported (for our internal workflow the penalty will be zero because of the flags we use).
This PR will allow gradually adding stub modules to a large framework package, without generating loads of false positives for user code.
Note: PEP 561 introduces the notion of a partial stub package, implemented in #5227. I think however this is a bit different use case that I don't want to mix with this one for two reasons: