Skip to content

Commit b228b70

Browse files
committed
Support import namespace
1 parent 9875a5a commit b228b70

File tree

16 files changed

+350
-158
lines changed

16 files changed

+350
-158
lines changed

crates/ruff_graph/src/resolver.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,20 @@ impl<'a> Resolver<'a> {
1919
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
2020
match import {
2121
CollectedImport::Import(import) => {
22-
resolve_module(self.db, &import).map(|module| module.file().path(self.db))
22+
let module = resolve_module(self.db, &import)?;
23+
Some(module.file()?.path(self.db))
2324
}
2425
CollectedImport::ImportFrom(import) => {
2526
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
2627
let parent = import.parent();
2728

28-
resolve_module(self.db, &import)
29-
.map(|module| module.file().path(self.db))
30-
.or_else(|| {
31-
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
29+
let module = resolve_module(self.db, &import).or_else(|| {
30+
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
3231

33-
resolve_module(self.db, &parent?).map(|module| module.file().path(self.db))
34-
})
32+
resolve_module(self.db, &parent?)
33+
})?;
34+
35+
Some(module.file()?.path(self.db))
3536
}
3637
}
3738
}

crates/ty/tests/file_watching.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,13 +1444,11 @@ mod unix {
14441444
)
14451445
.expect("Expected bar.baz to exist in site-packages.");
14461446
let baz_project = case.project_path("bar/baz.py");
1447+
let baz_file = baz.file().unwrap();
14471448

1449+
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
14481450
assert_eq!(
1449-
source_text(case.db(), baz.file()).as_str(),
1450-
"def baz(): ..."
1451-
);
1452-
assert_eq!(
1453-
baz.file().path(case.db()).as_system_path(),
1451+
baz_file.path(case.db()).as_system_path(),
14541452
Some(&*baz_project)
14551453
);
14561454

@@ -1465,7 +1463,7 @@ mod unix {
14651463
case.apply_changes(changes);
14661464

14671465
assert_eq!(
1468-
source_text(case.db(), baz.file()).as_str(),
1466+
source_text(case.db(), baz_file).as_str(),
14691467
"def baz(): print('Version 2')"
14701468
);
14711469

@@ -1478,7 +1476,7 @@ mod unix {
14781476
case.apply_changes(changes);
14791477

14801478
assert_eq!(
1481-
source_text(case.db(), baz.file()).as_str(),
1479+
source_text(case.db(), baz_file).as_str(),
14821480
"def baz(): print('Version 3')"
14831481
);
14841482

@@ -1524,6 +1522,7 @@ mod unix {
15241522
&ModuleName::new_static("bar.baz").unwrap(),
15251523
)
15261524
.expect("Expected bar.baz to exist in site-packages.");
1525+
let baz_file = baz.file().unwrap();
15271526
let bar_baz = case.project_path("bar/baz.py");
15281527

15291528
let patched_bar_baz = case.project_path("patched/bar/baz.py");
@@ -1534,11 +1533,8 @@ mod unix {
15341533
"def baz(): ..."
15351534
);
15361535

1537-
assert_eq!(
1538-
source_text(case.db(), baz.file()).as_str(),
1539-
"def baz(): ..."
1540-
);
1541-
assert_eq!(baz.file().path(case.db()).as_system_path(), Some(&*bar_baz));
1536+
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
1537+
assert_eq!(baz_file.path(case.db()).as_system_path(), Some(&*bar_baz));
15421538

15431539
case.assert_indexed_project_files([patched_bar_baz_file]);
15441540

@@ -1567,7 +1563,7 @@ mod unix {
15671563
let patched_baz_text = source_text(case.db(), patched_bar_baz_file);
15681564
let did_update_patched_baz = patched_baz_text.as_str() == "def baz(): print('Version 2')";
15691565

1570-
let bar_baz_text = source_text(case.db(), baz.file());
1566+
let bar_baz_text = source_text(case.db(), baz_file);
15711567
let did_update_bar_baz = bar_baz_text.as_str() == "def baz(): print('Version 2')";
15721568

15731569
assert!(
@@ -1650,7 +1646,7 @@ mod unix {
16501646
"def baz(): ..."
16511647
);
16521648
assert_eq!(
1653-
baz.file().path(case.db()).as_system_path(),
1649+
baz.file().unwrap().path(case.db()).as_system_path(),
16541650
Some(&*baz_original)
16551651
);
16561652

crates/ty_ide/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,10 @@ impl HasNavigationTargets for Type<'_> {
188188

189189
impl HasNavigationTargets for TypeDefinition<'_> {
190190
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
191-
let full_range = self.full_range(db.upcast());
191+
let Some(full_range) = self.full_range(db.upcast()) else {
192+
return NavigationTargets::empty();
193+
};
194+
192195
NavigationTargets::single(NavigationTarget {
193196
file: full_range.file(),
194197
focus_range: self.focus_range(db.upcast()).unwrap_or(full_range).range(),

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import parent.child.two
2929
`from.py`
3030

3131
```py
32-
# TODO: This should not be an error
33-
from parent.child import one, two # error: [unresolved-import]
32+
from parent.child import one, two
33+
34+
reveal_type(one) # revealed: <module 'parent.child.one'>
35+
reveal_type(two) # revealed: <module 'parent.child.two'>
3436
```
3537

3638
## Regular package in namespace package
@@ -103,3 +105,42 @@ from foo import x
103105

104106
reveal_type(x) # revealed: Unknown | Literal["module"]
105107
```
108+
109+
## `from` import with namespace package
110+
111+
Regression test for <https://github.com/astral-sh/ty/issues/363>
112+
113+
`google/cloud/pubsub_v1/__init__.py`:
114+
115+
```py
116+
class PublisherClient: ...
117+
```
118+
119+
```py
120+
from google.cloud import pubsub_v1
121+
122+
reveal_type(pubsub_v1.PublisherClient) # revealed: <class 'PublisherClient'>
123+
```
124+
125+
## `from` root importing sub-packages
126+
127+
Regresssion test for <https://github.com/astral-sh/ty/issues/375>
128+
129+
`opentelemetry/trace/__init__.py`:
130+
131+
```py
132+
class Trace: ...
133+
```
134+
135+
`opentelemetry/metrics/__init__.py`:
136+
137+
```py
138+
class Metric: ...
139+
```
140+
141+
```py
142+
from opentelemetry import trace, metrics
143+
144+
reveal_type(trace) # revealed: <module 'opentelemetry.trace'>
145+
reveal_type(metrics) # revealed: <module 'opentelemetry.metrics'>
146+
```

crates/ty_python_semantic/src/dunder_all.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ impl<'db> DunderAllNamesCollector<'db> {
109109
else {
110110
return false;
111111
};
112-
let Some(module_dunder_all_names) =
113-
dunder_all_names(self.db, module_literal.module(self.db).file())
112+
let Some(module_dunder_all_names) = module_literal
113+
.module(self.db)
114+
.file()
115+
.and_then(|file| dunder_all_names(self.db, file))
114116
else {
115117
// The module either does not have a `__all__` variable or it is invalid.
116118
return false;
@@ -179,7 +181,7 @@ impl<'db> DunderAllNamesCollector<'db> {
179181
let module_name =
180182
ModuleName::from_import_statement(self.db, self.file, import_from).ok()?;
181183
let module = resolve_module(self.db, &module_name)?;
182-
dunder_all_names(self.db, module.file())
184+
dunder_all_names(self.db, module.file()?)
183185
}
184186

185187
/// Infer the type of a standalone expression.

crates/ty_python_semantic/src/module_resolver/module.rs

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ pub struct Module {
1414
}
1515

1616
impl Module {
17-
pub(crate) fn new(
17+
pub(crate) fn file_module(
1818
name: ModuleName,
1919
kind: ModuleKind,
2020
search_path: SearchPath,
2121
file: File,
2222
) -> Self {
2323
let known = KnownModule::try_from_search_path_and_name(&search_path, &name);
24+
2425
Self {
25-
inner: Arc::new(ModuleInner {
26+
inner: Arc::new(ModuleInner::FileModule {
2627
name,
2728
kind,
2829
search_path,
@@ -32,19 +33,36 @@ impl Module {
3233
}
3334
}
3435

36+
pub(crate) fn namespace_package(name: ModuleName) -> Self {
37+
Self {
38+
inner: Arc::new(ModuleInner::NamespacePackage { name }),
39+
}
40+
}
41+
3542
/// The absolute name of the module (e.g. `foo.bar`)
3643
pub fn name(&self) -> &ModuleName {
37-
&self.inner.name
44+
match &*self.inner {
45+
ModuleInner::FileModule { name, .. } => name,
46+
ModuleInner::NamespacePackage { name, .. } => name,
47+
}
3848
}
3949

4050
/// The file to the source code that defines this module
41-
pub fn file(&self) -> File {
42-
self.inner.file
51+
///
52+
/// This is `None` for namespace packages.
53+
pub fn file(&self) -> Option<File> {
54+
match &*self.inner {
55+
ModuleInner::FileModule { file, .. } => Some(*file),
56+
ModuleInner::NamespacePackage { .. } => None,
57+
}
4358
}
4459

4560
/// Is this a module that we special-case somehow? If so, which one?
4661
pub fn known(&self) -> Option<KnownModule> {
47-
self.inner.known
62+
match &*self.inner {
63+
ModuleInner::FileModule { known, .. } => *known,
64+
ModuleInner::NamespacePackage { .. } => None,
65+
}
4866
}
4967

5068
/// Does this module represent the given known module?
@@ -53,13 +71,19 @@ impl Module {
5371
}
5472

5573
/// The search path from which the module was resolved.
56-
pub(crate) fn search_path(&self) -> &SearchPath {
57-
&self.inner.search_path
74+
pub(crate) fn search_path(&self) -> Option<&SearchPath> {
75+
match &*self.inner {
76+
ModuleInner::FileModule { search_path, .. } => Some(search_path),
77+
ModuleInner::NamespacePackage { .. } => None,
78+
}
5879
}
5980

6081
/// Determine whether this module is a single-file module or a package
6182
pub fn kind(&self) -> ModuleKind {
62-
self.inner.kind
83+
match &*self.inner {
84+
ModuleInner::FileModule { kind, .. } => *kind,
85+
ModuleInner::NamespacePackage { .. } => ModuleKind::Package,
86+
}
6387
}
6488
}
6589

@@ -70,17 +94,26 @@ impl std::fmt::Debug for Module {
7094
.field("kind", &self.kind())
7195
.field("file", &self.file())
7296
.field("search_path", &self.search_path())
97+
.field("known", &self.known())
7398
.finish()
7499
}
75100
}
76101

77102
#[derive(PartialEq, Eq, Hash)]
78-
struct ModuleInner {
79-
name: ModuleName,
80-
kind: ModuleKind,
81-
search_path: SearchPath,
82-
file: File,
83-
known: Option<KnownModule>,
103+
enum ModuleInner {
104+
/// A module that resolves to a file (`lib.py` or `package/__init__.py`)
105+
FileModule {
106+
name: ModuleName,
107+
kind: ModuleKind,
108+
search_path: SearchPath,
109+
file: File,
110+
known: Option<KnownModule>,
111+
},
112+
113+
/// A namespace package. Namespace packages are special because
114+
/// there are multiple possible paths and they have no corresponding
115+
/// code file.
116+
NamespacePackage { name: ModuleName },
84117
}
85118

86119
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]

crates/ty_python_semantic/src/module_resolver/path.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,23 @@ impl ModulePath {
143143
}
144144
}
145145

146+
pub(super) fn to_system_path(&self) -> Option<SystemPathBuf> {
147+
let ModulePath {
148+
search_path,
149+
relative_path,
150+
} = self;
151+
match &*search_path.0 {
152+
SearchPathInner::Extra(search_path)
153+
| SearchPathInner::FirstParty(search_path)
154+
| SearchPathInner::SitePackages(search_path)
155+
| SearchPathInner::Editable(search_path) => Some(search_path.join(relative_path)),
156+
SearchPathInner::StandardLibraryCustom(stdlib_root) => {
157+
Some(stdlib_root.join(relative_path))
158+
}
159+
SearchPathInner::StandardLibraryVendored(_) => None,
160+
}
161+
}
162+
146163
#[must_use]
147164
pub(super) fn to_file(&self, resolver: &ResolverContext) -> Option<File> {
148165
let db = resolver.db.upcast();

0 commit comments

Comments
 (0)