Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9ce3230

Browse files
authored
[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 astral-sh/ty#1482
1 parent 2bc6c78 commit 9ce3230

3 files changed

Lines changed: 16 additions & 29 deletions

File tree

crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@ This file currently covers the following details:
99
- **froms are locals**: a `from..import` can only define locals, it does not have global
1010
side-effects. Specifically any submodule attribute `a` that's implicitly introduced by either
1111
`from .a import b` or `from . import a as b` (in an `__init__.py(i)`) is a local and not a
12-
global. If you do such an import at the top of a file you won't notice this. However if you do
13-
such an import in a function, that means it will only be function-scoped (so you'll need to do
14-
it in every function that wants to access it, making your code less sensitive to execution
15-
order).
12+
global. However we only introduce this symbol if the `from..import` is in global-scope. This
13+
means imports at the start of a file work as you'd expect, while imports in a function don't
14+
introduce submodule attributes.
1615

1716
- **first from first serve**: only the *first* `from..import` in an `__init__.py(i)` that imports a
1817
particular direct submodule of the current package introduces that submodule as a local.
1918
Subsequent imports of the submodule will not introduce that local. This reflects the fact that
2019
in actual python only the first import of a submodule (in the entire execution of the program)
21-
introduces it as an attribute of the package. By "first" we mean "the first time in this scope
22-
(or any parent scope)". This pairs well with the fact that we are specifically introducing a
23-
local (as long as you don't accidentally shadow or overwrite the local).
20+
introduces it as an attribute of the package. By "first" we mean "the first time in global
21+
scope".
2422

2523
- **dot re-exports**: `from . import a` in an `__init__.pyi` is considered a re-export of `a`
2624
(equivalent to `from . import a as a`). This is required to properly handle many stubs in the
@@ -949,23 +947,23 @@ def funcmod(x: int) -> int:
949947

950948
## LHS `from` Imports In Functions
951949

952-
If a `from` import occurs in a function, LHS symbols should only be visible in that function. This
953-
very blatantly is not runtime-accurate, but exists to try to force you to write "obviously
954-
deterministically correct" imports instead of relying on execution order.
950+
If a `from` import occurs in a function, we simply ignore its LHS effects to avoid modeling
951+
execution-order-specific behaviour (and to discourage people writing code that has it).
955952

956953
`mypackage/__init__.py`:
957954

958955
```py
959956
def run1():
960957
from .funcmod import other
961958

959+
# TODO: this would be nice to support
960+
# error: [unresolved-reference]
962961
funcmod.funcmod(1)
963962

964963
def run2():
965964
from .funcmod import other
966965

967-
# TODO: this is just a bug! We only register the first
968-
# import of `funcmod` in the entire file, and not per-scope!
966+
# TODO: this would be nice to support
969967
# error: [unresolved-reference]
970968
funcmod.funcmod(2)
971969

crates/ty_python_semantic/src/semantic_index/builder.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
14541454
// * `from .x.y import z` (must be relative!)
14551455
// * And we are in an `__init__.py(i)` (hereafter `thispackage`)
14561456
// * And this is the first time we've seen `from .x` in this module
1457+
// * And we're in the global scope
14571458
//
14581459
// We introduce a local definition `x = <module 'thispackage.x'>` that occurs
14591460
// before the `z = ...` declaration the import introduces. This models the fact
@@ -1466,9 +1467,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
14661467
// in one function is visible in another function.
14671468
//
14681469
// TODO: Also support `from thispackage.x.y import z`?
1469-
// TODO: `seen_submodule_imports` should be per-scope and not per-file
1470-
// (if two functions import `.x`, they both should believe `x` is defined)
1471-
if node.level == 1
1470+
if self.current_scope() == FileScopeId::global()
1471+
&& node.level == 1
14721472
&& let Some(submodule) = &node.module
14731473
&& let Some(parsed_submodule) = ModuleName::new(submodule.as_str())
14741474
&& let Some(direct_submodule) = parsed_submodule.components().next()

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5911,20 +5911,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
59115911
submodule: &Name,
59125912
definition: Definition<'db>,
59135913
) {
5914-
// Although the *actual* runtime semantic of this kind of statement is to
5915-
// introduce a variable in the global scope of this module, we want to
5916-
// encourage users to write code that doesn't have dependence on execution-order.
5917-
//
5918-
// By introducing it as a local variable in the scope the import occurs in,
5919-
// we effectively require the developer to either do the import at the start of
5920-
// the file where it belongs, or to repeat the import in every function that
5921-
// wants to use it, which "definitely" works.
5922-
//
5923-
// (It doesn't actually "definitely" work because only the first import of `thispackage.x`
5924-
// will ever set `x`, and any subsequent overwrites of it will permanently clobber it.
5925-
// Also, a local variable `x` in a function should always shadow the submodule because
5926-
// the submodule is defined at file-scope. However, both of these issues are much more
5927-
// narrow, so this approach seems to work well in practice!)
5914+
// The runtime semantic of this kind of statement is to introduce a variable in the global
5915+
// scope of this module, so we do just that. (Actually we introduce a local variable, but
5916+
// this type of Definition is only created when a `from..import` is in global scope.)
59285917

59295918
// Get this package's module by resolving `.`
59305919
let Ok(module_name) = ModuleName::from_identifier_parts(self.db(), self.file(), None, 1)

0 commit comments

Comments
 (0)