[isort] Check full module path against project root(s) when categorizing first-party (#16565)

When attempting to determine whether `import foo.bar.baz` is a known
first-party import relative to [user-provided source
paths](https://docs.astral.sh/ruff/settings/#src), when `preview` is
enabled we now check that `SRC/foo/bar/baz` is a directory or
`SRC/foo/bar/baz.py` or `SRC/foo/bar/baz.pyi` exist.

Previously, we just checked the analogous thing for `SRC/foo`, but this
can be misleading in situations with disjoint namespace packages that
share a common base name (e.g. we may be working inside the namespace
package `foo.buzz` and importing `foo.bar` from elsewhere).

Supersedes #12987 
Closes #12984
This commit is contained in:
Dylan 2025-05-05 11:40:01 -05:00 committed by GitHub
parent 5e2c818417
commit 965a4dd731
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 652 additions and 21 deletions

View file

@ -714,6 +714,15 @@ pub trait Imported<'a> {
/// Returns the member name of the imported symbol. For a straight import, this is equivalent
/// to the qualified name; for a `from` import, this is the name of the imported symbol.
fn member_name(&self) -> Cow<'a, str>;
/// Returns the source module of the imported symbol.
///
/// For example:
///
/// - `import foo` returns `["foo"]`
/// - `import foo.bar` returns `["foo","bar"]`
/// - `from foo import bar` returns `["foo"]`
fn source_name(&self) -> &[&'a str];
}
impl<'a> Imported<'a> for Import<'a> {
@ -731,6 +740,10 @@ impl<'a> Imported<'a> for Import<'a> {
fn member_name(&self) -> Cow<'a, str> {
Cow::Owned(self.qualified_name().to_string())
}
fn source_name(&self) -> &[&'a str] {
self.qualified_name.segments()
}
}
impl<'a> Imported<'a> for SubmoduleImport<'a> {
@ -748,6 +761,10 @@ impl<'a> Imported<'a> for SubmoduleImport<'a> {
fn member_name(&self) -> Cow<'a, str> {
Cow::Owned(self.qualified_name().to_string())
}
fn source_name(&self) -> &[&'a str] {
self.qualified_name.segments()
}
}
impl<'a> Imported<'a> for FromImport<'a> {
@ -765,6 +782,10 @@ impl<'a> Imported<'a> for FromImport<'a> {
fn member_name(&self) -> Cow<'a, str> {
Cow::Borrowed(self.qualified_name.segments()[self.qualified_name.segments().len() - 1])
}
fn source_name(&self) -> &[&'a str] {
self.module_name()
}
}
/// A wrapper around an import [`BindingKind`] that can be any of the three types of imports.
@ -799,6 +820,14 @@ impl<'ast> Imported<'ast> for AnyImport<'_, 'ast> {
Self::FromImport(import) => import.member_name(),
}
}
fn source_name(&self) -> &[&'ast str] {
match self {
Self::Import(import) => import.source_name(),
Self::SubmoduleImport(import) => import.source_name(),
Self::FromImport(import) => import.source_name(),
}
}
}
#[cfg(test)]