mirror of https://github.com/astral-sh/ruff
[ty] Make implicit submodule imports only occur in global scope (#21370)
This loses any ability to have "per-function" implicit submodule imports, to avoid the "ok but now we need per-scope imports" and "ok but this should actually introduce a global that only exists during this function" problems. A simple and clean implementation with no weird corners. Fixes https://github.com/astral-sh/ty/issues/1482
This commit is contained in:
parent
2bc6c78e26
commit
9ce3230add
|
|
@ -9,18 +9,16 @@ This file currently covers the following details:
|
|||
- **froms are locals**: a `from..import` can only define locals, it does not have global
|
||||
side-effects. Specifically any submodule attribute `a` that's implicitly introduced by either
|
||||
`from .a import b` or `from . import a as b` (in an `__init__.py(i)`) is a local and not a
|
||||
global. If you do such an import at the top of a file you won't notice this. However if you do
|
||||
such an import in a function, that means it will only be function-scoped (so you'll need to do
|
||||
it in every function that wants to access it, making your code less sensitive to execution
|
||||
order).
|
||||
global. However we only introduce this symbol if the `from..import` is in global-scope. This
|
||||
means imports at the start of a file work as you'd expect, while imports in a function don't
|
||||
introduce submodule attributes.
|
||||
|
||||
- **first from first serve**: only the *first* `from..import` in an `__init__.py(i)` that imports a
|
||||
particular direct submodule of the current package introduces that submodule as a local.
|
||||
Subsequent imports of the submodule will not introduce that local. This reflects the fact that
|
||||
in actual python only the first import of a submodule (in the entire execution of the program)
|
||||
introduces it as an attribute of the package. By "first" we mean "the first time in this scope
|
||||
(or any parent scope)". This pairs well with the fact that we are specifically introducing a
|
||||
local (as long as you don't accidentally shadow or overwrite the local).
|
||||
introduces it as an attribute of the package. By "first" we mean "the first time in global
|
||||
scope".
|
||||
|
||||
- **dot re-exports**: `from . import a` in an `__init__.pyi` is considered a re-export of `a`
|
||||
(equivalent to `from . import a as a`). This is required to properly handle many stubs in the
|
||||
|
|
@ -949,9 +947,8 @@ def funcmod(x: int) -> int:
|
|||
|
||||
## LHS `from` Imports In Functions
|
||||
|
||||
If a `from` import occurs in a function, LHS symbols should only be visible in that function. This
|
||||
very blatantly is not runtime-accurate, but exists to try to force you to write "obviously
|
||||
deterministically correct" imports instead of relying on execution order.
|
||||
If a `from` import occurs in a function, we simply ignore its LHS effects to avoid modeling
|
||||
execution-order-specific behaviour (and to discourage people writing code that has it).
|
||||
|
||||
`mypackage/__init__.py`:
|
||||
|
||||
|
|
@ -959,13 +956,14 @@ deterministically correct" imports instead of relying on execution order.
|
|||
def run1():
|
||||
from .funcmod import other
|
||||
|
||||
# TODO: this would be nice to support
|
||||
# error: [unresolved-reference]
|
||||
funcmod.funcmod(1)
|
||||
|
||||
def run2():
|
||||
from .funcmod import other
|
||||
|
||||
# TODO: this is just a bug! We only register the first
|
||||
# import of `funcmod` in the entire file, and not per-scope!
|
||||
# TODO: this would be nice to support
|
||||
# error: [unresolved-reference]
|
||||
funcmod.funcmod(2)
|
||||
|
||||
|
|
|
|||
|
|
@ -1454,6 +1454,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||
// * `from .x.y import z` (must be relative!)
|
||||
// * And we are in an `__init__.py(i)` (hereafter `thispackage`)
|
||||
// * And this is the first time we've seen `from .x` in this module
|
||||
// * And we're in the global scope
|
||||
//
|
||||
// We introduce a local definition `x = <module 'thispackage.x'>` that occurs
|
||||
// before the `z = ...` declaration the import introduces. This models the fact
|
||||
|
|
@ -1466,9 +1467,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||
// in one function is visible in another function.
|
||||
//
|
||||
// TODO: Also support `from thispackage.x.y import z`?
|
||||
// TODO: `seen_submodule_imports` should be per-scope and not per-file
|
||||
// (if two functions import `.x`, they both should believe `x` is defined)
|
||||
if node.level == 1
|
||||
if self.current_scope() == FileScopeId::global()
|
||||
&& node.level == 1
|
||||
&& let Some(submodule) = &node.module
|
||||
&& let Some(parsed_submodule) = ModuleName::new(submodule.as_str())
|
||||
&& let Some(direct_submodule) = parsed_submodule.components().next()
|
||||
|
|
|
|||
|
|
@ -5911,20 +5911,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
submodule: &Name,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// Although the *actual* runtime semantic of this kind of statement is to
|
||||
// introduce a variable in the global scope of this module, we want to
|
||||
// encourage users to write code that doesn't have dependence on execution-order.
|
||||
//
|
||||
// By introducing it as a local variable in the scope the import occurs in,
|
||||
// we effectively require the developer to either do the import at the start of
|
||||
// the file where it belongs, or to repeat the import in every function that
|
||||
// wants to use it, which "definitely" works.
|
||||
//
|
||||
// (It doesn't actually "definitely" work because only the first import of `thispackage.x`
|
||||
// will ever set `x`, and any subsequent overwrites of it will permanently clobber it.
|
||||
// Also, a local variable `x` in a function should always shadow the submodule because
|
||||
// the submodule is defined at file-scope. However, both of these issues are much more
|
||||
// narrow, so this approach seems to work well in practice!)
|
||||
// The runtime semantic of this kind of statement is to introduce a variable in the global
|
||||
// scope of this module, so we do just that. (Actually we introduce a local variable, but
|
||||
// this type of Definition is only created when a `from..import` is in global scope.)
|
||||
|
||||
// Get this package's module by resolving `.`
|
||||
let Ok(module_name) = ModuleName::from_identifier_parts(self.db(), self.file(), None, 1)
|
||||
|
|
|
|||
Loading…
Reference in New Issue