-
-
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
Don't error on re-exported imports in cycles. Fixes #4049. #4495
Conversation
So it turned out that just removing the blanket prohibition on re-exporting a node of kind Without the changes in
|
Er, this isn't quite done yet; I hadn't asserted the resolved type at the right spot. On my other cyclic import example it works fully (all types resolve correctly), but on this example it leaves a type as I think that bogus |
Pushed my initial attempt to get the type corrected. Not working yet, will look into it more later. Any tips welcome! |
I spent some time debugging this but could not figure it out. I'm eager to see what you come up with. |
This passes all tests, including the added one, but in internal Instagram codebases it now generates new "{name} is not defined" errors on certain inline imports. Haven't found a simple-case repro for that yet, tracking it down. |
Ok, this fixes the problem and causes no new errors (both in mypy test suite and in internal Instagram code). I think it's ready for an initial round of review. |
Anything I could do by way of testing or anything else to make this more attractive for review? Not complaining, I know everyone's got plenty to do (been on the "overloaded maintainer" side plenty often), just wondering if I can help move it along somehow. |
This is a "controversial" topic, and some refactoring of symbol table kinds will happen soon. Therefore this may wait for some time, sorry. |
@ilevkivskyi Thanks for the explanation. This fixes a clear bug, the new behavior is unequivocally better, it's been thoroughly tested on a large real-world code base, and the PR is not complex; it doesn't change any major behaviors, just does some fixups in third pass on things that were missed in the second pass. It doesn't seem likely to add significant complexity to any upcoming refactor of symbol table kinds. Is there any strong reason to let it languish rather than just merging it? |
@ilevkivskyi Can you give specific reasons why this PR should be postponed? If you are worried about type alias refactoring, it looks to me like the required change to this PR should be pretty minor and is not enough to block this from being merged before the type alias refactor. @carljm Fixing issues related to import cycles like #4049 is definitely high on our list of things we want to fix in the next few months or so. |
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 a real review, just some things I spotted when looking at this yesterday.
mypy/semanal.py
Outdated
@@ -1499,7 +1502,8 @@ def visit_import_from(self, imp: ImportFrom) -> None: | |||
symbol = SymbolTableNode(GDEF, ast_node) | |||
self.add_symbol(name, symbol, imp) | |||
return | |||
if node and node.kind != UNBOUND_IMPORTED and not node.module_hidden: | |||
|
|||
if node and not node.module_hidden and not (is_unbound and is_same_module): |
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 line is complex (many conditions), so it deserves a comment.
mypy/semanal.py
Outdated
possible_module_id = import_id + '.' + id | ||
|
||
# If the module does not contain a symbol with the name 'id', | ||
# try checking if it's a module instead. | ||
if not node or node.kind == UNBOUND_IMPORTED: | ||
if not node or is_unbound: |
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 find the original form simpler (don't need look above to is_unbound
).
test-data/unit/check-modules.test
Outdated
reveal_type(One) | ||
class Two: | ||
pass | ||
[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.
Add also tests for incremental mode.
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 can look at some existing incremental-mode tests to see how that's done.
test-data/unit/check-modules.test
Outdated
@@ -1943,3 +1943,24 @@ main:2: error: Name 'name' is not defined | |||
main:3: error: Revealed type is 'Any' | |||
|
|||
[builtins fixtures/module.pyi] | |||
|
|||
|
|||
[case testImportCycle] |
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 would prefer a more descriptive name.
test-data/unit/check-modules.test
Outdated
reveal_type(One) | ||
class Two: | ||
pass | ||
[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.
Please also add tests for type aliases (you seem to care about them in visit_import_from
).
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.
So the type-alias stuff is there just because it seemed better to be complete about transferring node info (in the same way that visit_import_from
in second pass is complete), rather than only transfer some of the node attributes. I have not had any real-world issue with type aliases and circular imports that those lines are intended to solve. I will try to see if I can repro some case involving type aliases that is fixed by those lines.
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.
As discussed the comments, this only fixes the import cycle issue in some contexts. This might still be a reasonable incremental improvement, but note that we are planning a more general fix in the near/medium term.
We have at least two options:
- Polish this PR a bit and merge, and create a new issue about the remaining problems.
- Postpone this and wait for the more general fix.
What do you think?
mypy/semanal_pass3.py
Outdated
def visit_name_expr(self, expr: NameExpr) -> None: | ||
# Fixup remaining UNBOUND_IMPORTED nodes from import cycles | ||
if expr.kind == UNBOUND_IMPORTED: | ||
n = self.sem.lookup(expr.name, expr, suppress_errors=True) |
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.
Calling lookup()
in pass 3 generally doesn't do the right thing, since not all the namespace context is set up correctly. It works for module-level attributes, though. Add a TODO comment about this since there currently really isn't a better way to do this.
test-data/unit/check-modules.test
Outdated
@@ -1943,3 +1943,24 @@ main:2: error: Name 'name' is not defined | |||
main:3: error: Revealed type is 'Any' | |||
|
|||
[builtins fixtures/module.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.
Style nit: we use one empty line between test cases.
test-data/unit/check-modules.test
Outdated
|
||
[case testImportCycle] | ||
from m import One | ||
reveal_type(One) |
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 make sense to also try doing other things with One
, just to make sure that import is resolved correctly. For example, use it as a base class and in a type annotation.
[file m/__init__.py] | ||
from .one import One | ||
from .two import Two | ||
reveal_type(One) |
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.
Again, it would be nice to test how all these references work with type aliases and named tuples, for example. (In separate tese cases.)
pass | ||
[file m/two.py] | ||
from m import One | ||
reveal_type(One) |
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.
Note that references to One
in annotations still don't work correctly:
from m import One
o: One
reveal_type(o) # Any
Similarly, if we make One
a type alias and use it in an annotation, it will be treated as Any
.
If we create a subclass of One
in m.two
, it seems to have an implicit Any
base class, which is unexpected.
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.
Good catches! I think these are probably fixable by adding more third-pass visits to different node types to propagate type info that we missed in second pass due to the cycle, but it does begin to feel a bit like a game of whack-a-mole.
Thanks for the review comments! I see that this fix is going to get uglier as I make it more complete. I'm curious what the longer-term plan is to address import cycles in a more thorough way (I guess some kind of fixed-point iteration in semantic analysis might be needed?) From a practical point of view doing gradual typing in a large codebase, spurious |
We discussed with @JukkaL and it looks like this PR is unlikely to cause any long-term harm, and it would be OK as a temporary partial solution (provided you fix some review comments). |
Here are a few ideas:
|
* master: (32 commits) Fix some fine-grained cache/fswatcher problems (python#4560) Sync typeshed (python#4559) Add _cached suffix to test cases in fine-grained tests with cache (python#4558) Add back support for simplified fine-grained logging (python#4557) Type checking of class decorators (python#4544) Sync typeshed (python#4556) When loading from a fine-grained cache, use the real path, not the cached (python#4555) Switch all of the fine-grained debug logging to use manager.log (python#4550) Caching for fine-grained incremental mode (python#4483) Fix --warn-return-any for NotImplemented (python#4545) Remove myunit (python#4369) Store line numbers of imports in the cache metadata (python#4533) README.md: Fix a typo (python#4529) Enable generation and caching of fine-grained dependencies from normal runs (python#4526) Move argument parsing for the fine-grained flag into the main arg parsing code (python#4524) Don't warn about unrecognized options starting with 'x_' (python#4522) stubgen: don't append star arg when args list already has varargs appended (python#4518) Handle TypedDict in diff and deps (python#4510) Fix Options.__repr__ to not infinite recurse (python#4514) Fix some fine-grained incremental bugs with newly imported files (python#4502) ...
* master: (27 commits) Don't call --strict-optional and --incremental experimental (python#4642) Sync typeshed (python#4641) Fix callable types with inconsistent argument counts (python#4611) Fix example (add 'class A:') Make psutil an optional dependency (python#4634) mypy and mypy_extensions aren't posix only (python#3765) Documentation for attr support (python#4632) Use read_with_python_encoding in stubgen to handle file encoding (python#3790) Sync typeshed (python#4631) Add remaining core team emails to CREDITS (python#4629) Fix issues with attr code. (python#4628) Better support for converter in attrs plugin. (python#4607) Clean up credits (python#4626) Support type aliases in fine-grained incremental mode (python#4525) Fine-grained: Fix crash caused by unreachable class (python#4613) Treat divmod like a binary operator (python#4585) Sync typeshed (python#4605) Fine-grained: Don't infer partial types from multiple targets (python#4553) Fine-grained: Compare symbol table snapshots when following dependencies (python#4598) Fix type of forward reference to a decorated class method (python#4486) ...
* master: New files shouldn't trigger a coarse-grained rebuild in fg cache mode (python#4669) Bump version to 0.580-dev Update revision history for 0.570 (python#4662) Fine-grained: Fix crashes when refreshing synthetic types (python#4667) Fine-grained: Support NewType and reset subtype caches (python#4656) Fine-grained: Detect changes in additional TypeInfo attributes (python#4659) Fine-grained: Apply semantic analyzer patch callbacks (python#4658) Optimize fine-grained update by using Graph as the cache (python#4622) Cleanup check_reverse_op_method (python#4017) Fine-grained: Fix AST merge issues (python#4652) Optionally check that we don't have duplicate nodes after AST merge (python#4647)
Ok, I think I've addressed all the feedback so far. I fixed several new cases (member expressions, use in type annotations) and added tests for type aliases, named tuples, and incremental mode. I left TODOs for a) inheriting from a re-exported-in-cycle import (it seemed too complex to fix up in third pass) and b) name expressions in scopes other than module-level variables. I certainly won't claim that this is a complete fix, but I think it does improve behavior incrementally for many common cases, and at the very least the tests and TODOs will be a useful starting point for a more complete fix. Review and further comments much appreciated. I'll be very curious to see what form the more complete fix takes, having wrestled with this for awhile. I'm probably missing something key, but it feels to me like the mentioned approaches (cross-module references, using ForwardRef) are effectively just different ways to handle the book-keeping for unresolved cycles; they don't address the root problem with this diff: that we have to go back and fix up all kinds of things in third pass that normally should be handled in second pass, by effectively copying bits of code in from second pass. That doesn't feel like a path to "complete fix", it feels like whack-a-mole, where the asymptote is that third pass becomes a more and more "complete" copy of second pass. It seems like for a thorough and sustainable fix of these kinds of issues, some kind of scheme for fixed-point iteration in second pass will be needed instead. But I'm sure I'm missing something, and I look forward to learning a lot from seeing the better fix implemented :-) Thanks @JukkaL and @ilevkivskyi for reviewing! |
@carljm Thanks for the updates! I'm currently prototyping what might become a cleaner fix, where we handle many exported imported names in semantic analyzer pass 2. If it works out, it might be at least a partial replacement of this PR, but your PR has been useful first step towards fixing the underlying issue. I'm going to try your test cases with my work-in-progress fix. I still don't have complete story for references to named tuples, type aliases and such with my approach. |
This adds supports for some cases of importing an imported name within an import cycle. Originally they could result in false positives or false negatives. The idea is to use a new node type ImportedName in semantic analysis pass 1 to represent an indirect reference to a name in another module. It will get resolved in semantic analysis pass 2. ImportedName is not yet used everywhere where it could make sense and this doesn't fix all related issues with import cycles. Also did a bit of refactoring of type semantic analysis to avoid passing multiple callback functions. Fixes #4049. Fixes #4429. Fixes #4682. Inspired by (and borrowed test cases from) #4495 by @carljm. Supersedes #4495
This is superseded by 0b7597c, so closing. |
Interesting that it didn't happen automatically. Looking at the previous type when it worked, it looks like it auto-closes superseded PRs only on comments like |
Yeah, I think github's keywords are a bit specific here. (I think closes may work, but not sure) |
|
This adds supports for some cases of importing an imported name within an import cycle. Originally they could result in false positives or false negatives. The idea is to use a new node type ImportedName in semantic analysis pass 1 to represent an indirect reference to a name in another module. It will get resolved in semantic analysis pass 2. ImportedName is not yet used everywhere where it could make sense and this doesn't fix all related issues with import cycles. Also did a bit of refactoring of type semantic analysis to avoid passing multiple callback functions. Fixes python#4049. Fixes python#4429. Fixes python#4682. Inspired by (and borrowed test cases from) python#4495 by @carljm. Supersedes python#4495
Fixes #4049.