Skip to content

Commit 45ac30a

Browse files
authored
[ty] Teach ty the meaning of desperation (try ancestor pyproject.tomls as search-paths if module resolution fails) (#21745)
## Summary This makes an importing file a required argument to module resolution, and if the fast-path cached query fails to resolve the module, take the slow-path uncached (could be cached if we want) `desperately_resolve_module` which will walk up from the importing file until it finds a `pyproject.toml` (arbitrary decision, we could try every ancestor directory), at which point it takes one last desperate attempt to use that directory as a search-path. We do not continue walking up once we've found a `pyproject.toml` (arbitrary decision, we could keep going up). Running locally, this fixes every broken-for-workspace-reasons import in pyx's workspace! * Fixes astral-sh/ty#1539 * Improves astral-sh/ty#839 ## Test Plan The workspace tests see a huge improvement on most absolute imports.
1 parent 0280949 commit 45ac30a

21 files changed

Lines changed: 608 additions & 322 deletions

File tree

crates/ruff_graph/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl ModuleImports {
4949
// Resolve the imports.
5050
let mut resolved_imports = ModuleImports::default();
5151
for import in imports {
52-
for resolved in Resolver::new(db).resolve(import) {
52+
for resolved in Resolver::new(db, path).resolve(import) {
5353
if let Some(path) = resolved.as_system_path() {
5454
resolved_imports.insert(path.to_path_buf());
5555
}

crates/ruff_graph/src/resolver.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
use ruff_db::files::FilePath;
2-
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
1+
use ruff_db::files::{File, FilePath, system_path_to_file};
2+
use ruff_db::system::SystemPath;
3+
use ty_python_semantic::{
4+
ModuleName, resolve_module, resolve_module_confident, resolve_real_module,
5+
resolve_real_module_confident,
6+
};
37

48
use crate::ModuleDb;
59
use crate::collector::CollectedImport;
610

711
/// Collect all imports for a given Python file.
812
pub(crate) struct Resolver<'a> {
913
db: &'a ModuleDb,
14+
file: Option<File>,
1015
}
1116

1217
impl<'a> Resolver<'a> {
1318
/// Initialize a [`Resolver`] with a given [`ModuleDb`].
14-
pub(crate) fn new(db: &'a ModuleDb) -> Self {
15-
Self { db }
19+
pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath) -> Self {
20+
// If we know the importing file we can potentially resolve more imports
21+
let file = system_path_to_file(db, path).ok();
22+
Self { db, file }
1623
}
1724

1825
/// Resolve the [`CollectedImport`] into a [`FilePath`].
@@ -70,13 +77,21 @@ impl<'a> Resolver<'a> {
7077

7178
/// Resolves a module name to a module.
7279
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
73-
let module = resolve_module(self.db, module_name)?;
80+
let module = if let Some(file) = self.file {
81+
resolve_module(self.db, file, module_name)?
82+
} else {
83+
resolve_module_confident(self.db, module_name)?
84+
};
7485
Some(module.file(self.db)?.path(self.db))
7586
}
7687

7788
/// Resolves a module name to a module (stubs not allowed).
7889
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
79-
let module = resolve_real_module(self.db, module_name)?;
90+
let module = if let Some(file) = self.file {
91+
resolve_real_module(self.db, file, module_name)?
92+
} else {
93+
resolve_real_module_confident(self.db, module_name)?
94+
};
8095
Some(module.file(self.db)?.path(self.db))
8196
}
8297
}

crates/ty/tests/file_watching.rs

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use ty_project::metadata::pyproject::{PyProject, Tool};
1515
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
1616
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
1717
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
18-
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module};
18+
use ty_python_semantic::{Module, ModuleName, PythonPlatform, resolve_module_confident};
1919

2020
struct TestCase {
2121
db: ProjectDatabase,
@@ -232,7 +232,8 @@ impl TestCase {
232232
}
233233

234234
fn module<'c>(&'c self, name: &str) -> Module<'c> {
235-
resolve_module(self.db(), &ModuleName::new(name).unwrap()).expect("module to be present")
235+
resolve_module_confident(self.db(), &ModuleName::new(name).unwrap())
236+
.expect("module to be present")
236237
}
237238

238239
fn sorted_submodule_names(&self, parent_module_name: &str) -> Vec<String> {
@@ -811,7 +812,8 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
811812
.with_context(|| "Failed to create __init__.py")?;
812813
std::fs::write(a_original_path.as_std_path(), "").with_context(|| "Failed to create a.py")?;
813814

814-
let sub_a_module = resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap());
815+
let sub_a_module =
816+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap());
815817

816818
assert_eq!(sub_a_module, None);
817819
case.assert_indexed_project_files([bar]);
@@ -832,7 +834,9 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
832834
.expect("a.py to exist");
833835

834836
// `import sub.a` should now resolve
835-
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
837+
assert!(
838+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
839+
);
836840

837841
case.assert_indexed_project_files([bar, init_file, a_file]);
838842

@@ -848,7 +852,9 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
848852
])?;
849853
let bar = case.system_file(case.project_path("bar.py")).unwrap();
850854

851-
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
855+
assert!(
856+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
857+
);
852858

853859
let sub_path = case.project_path("sub");
854860
let init_file = case
@@ -870,7 +876,9 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
870876
case.apply_changes(changes, None);
871877

872878
// `import sub.a` should no longer resolve
873-
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
879+
assert!(
880+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
881+
);
874882

875883
assert!(!init_file.exists(case.db()));
876884
assert!(!a_file.exists(case.db()));
@@ -890,8 +898,12 @@ fn directory_renamed() -> anyhow::Result<()> {
890898

891899
let bar = case.system_file(case.project_path("bar.py")).unwrap();
892900

893-
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
894-
assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none());
901+
assert!(
902+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
903+
);
904+
assert!(
905+
resolve_module_confident(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none()
906+
);
895907

896908
let sub_path = case.project_path("sub");
897909
let sub_init = case
@@ -915,9 +927,13 @@ fn directory_renamed() -> anyhow::Result<()> {
915927
case.apply_changes(changes, None);
916928

917929
// `import sub.a` should no longer resolve
918-
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
930+
assert!(
931+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
932+
);
919933
// `import foo.baz` should now resolve
920-
assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some());
934+
assert!(
935+
resolve_module_confident(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some()
936+
);
921937

922938
// The old paths are no longer tracked
923939
assert!(!sub_init.exists(case.db()));
@@ -950,7 +966,9 @@ fn directory_deleted() -> anyhow::Result<()> {
950966

951967
let bar = case.system_file(case.project_path("bar.py")).unwrap();
952968

953-
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
969+
assert!(
970+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()
971+
);
954972

955973
let sub_path = case.project_path("sub");
956974

@@ -970,7 +988,9 @@ fn directory_deleted() -> anyhow::Result<()> {
970988
case.apply_changes(changes, None);
971989

972990
// `import sub.a` should no longer resolve
973-
assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
991+
assert!(
992+
resolve_module_confident(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()
993+
);
974994

975995
assert!(!init_file.exists(case.db()));
976996
assert!(!a_file.exists(case.db()));
@@ -999,7 +1019,7 @@ fn search_path() -> anyhow::Result<()> {
9991019
let site_packages = case.root_path().join("site_packages");
10001020

10011021
assert_eq!(
1002-
resolve_module(case.db(), &ModuleName::new("a").unwrap()),
1022+
resolve_module_confident(case.db(), &ModuleName::new("a").unwrap()),
10031023
None
10041024
);
10051025

@@ -1009,7 +1029,7 @@ fn search_path() -> anyhow::Result<()> {
10091029

10101030
case.apply_changes(changes, None);
10111031

1012-
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
1032+
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
10131033
case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
10141034

10151035
Ok(())
@@ -1022,7 +1042,7 @@ fn add_search_path() -> anyhow::Result<()> {
10221042
let site_packages = case.project_path("site_packages");
10231043
std::fs::create_dir_all(site_packages.as_std_path())?;
10241044

1025-
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
1045+
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
10261046

10271047
// Register site-packages as a search path.
10281048
case.update_options(Options {
@@ -1040,7 +1060,7 @@ fn add_search_path() -> anyhow::Result<()> {
10401060

10411061
case.apply_changes(changes, None);
10421062

1043-
assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
1063+
assert!(resolve_module_confident(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
10441064

10451065
Ok(())
10461066
}
@@ -1172,7 +1192,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
11721192

11731193
// Unset the custom typeshed directory.
11741194
assert_eq!(
1175-
resolve_module(case.db(), &ModuleName::new("os").unwrap()),
1195+
resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()),
11761196
None
11771197
);
11781198

@@ -1187,7 +1207,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
11871207

11881208
case.apply_changes(changes, None);
11891209

1190-
assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
1210+
assert!(resolve_module_confident(case.db(), &ModuleName::new("os").unwrap()).is_some());
11911211

11921212
Ok(())
11931213
}
@@ -1410,7 +1430,7 @@ mod unix {
14101430
Ok(())
14111431
})?;
14121432

1413-
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
1433+
let baz = resolve_module_confident(case.db(), &ModuleName::new_static("bar.baz").unwrap())
14141434
.expect("Expected bar.baz to exist in site-packages.");
14151435
let baz_project = case.project_path("bar/baz.py");
14161436
let baz_file = baz.file(case.db()).unwrap();
@@ -1486,7 +1506,7 @@ mod unix {
14861506
Ok(())
14871507
})?;
14881508

1489-
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
1509+
let baz = resolve_module_confident(case.db(), &ModuleName::new_static("bar.baz").unwrap())
14901510
.expect("Expected bar.baz to exist in site-packages.");
14911511
let baz_file = baz.file(case.db()).unwrap();
14921512
let bar_baz = case.project_path("bar/baz.py");
@@ -1591,7 +1611,7 @@ mod unix {
15911611
Ok(())
15921612
})?;
15931613

1594-
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
1614+
let baz = resolve_module_confident(case.db(), &ModuleName::new_static("bar.baz").unwrap())
15951615
.expect("Expected bar.baz to exist in site-packages.");
15961616
let baz_site_packages_path =
15971617
case.project_path(".venv/lib/python3.12/site-packages/bar/baz.py");
@@ -1854,11 +1874,11 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
18541874
let mut case = setup([("lib.py", "class Foo: ...")])?;
18551875

18561876
assert!(
1857-
resolve_module(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
1877+
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
18581878
"Expected `lib` module to exist."
18591879
);
18601880
assert_eq!(
1861-
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()),
1881+
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()),
18621882
None,
18631883
"Expected `Lib` module not to exist"
18641884
);
@@ -1891,13 +1911,13 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
18911911

18921912
// Resolving `lib` should now fail but `Lib` should now succeed
18931913
assert_eq!(
1894-
resolve_module(case.db(), &ModuleName::new("lib").unwrap()),
1914+
resolve_module_confident(case.db(), &ModuleName::new("lib").unwrap()),
18951915
None,
18961916
"Expected `lib` module to no longer exist."
18971917
);
18981918

18991919
assert!(
1900-
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
1920+
resolve_module_confident(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
19011921
"Expected `Lib` module to exist"
19021922
);
19031923

crates/ty_ide/src/all_symbols.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub fn all_symbols<'db>(
2020

2121
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
2222
let is_typing_extensions_available = importing_from.is_stub(db)
23-
|| resolve_real_shadowable_module(db, &typing_extensions).is_some();
23+
|| resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some();
2424

2525
let results = std::sync::Mutex::new(Vec::new());
2626
{

0 commit comments

Comments
 (0)