diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java b/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java index 0467d5546355..b7a2002cf7ae 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java @@ -61,6 +61,14 @@ public abstract class PyPsiFacade { @Nullable public abstract PyType parseTypeAnnotation(@NotNull String annotation, @NotNull PsiElement anchor); + /** + * Retrieve a top-level class by its qualified name. The name provided is supposed to be fully qualified absolute name + * of the class, neither relative to the containing file of the anchor element, nor dependent on its imports. + * The anchor element is needed only to detect the corresponding module and its SDK. + * + * @param qName qualified name of the required class + * @param anchor arbitrary element located in the same module/SDK as the required class + */ @Nullable public abstract PyClass createClassByQName(@NotNull String qName, @NotNull PsiElement anchor); diff --git a/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java b/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java index 81c26862eeaf..5c7e79311258 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java @@ -15,16 +15,21 @@ */ package com.jetbrains.python.psi.impl; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.util.QualifiedName; +import com.intellij.util.ObjectUtils; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyPsiFacade; -import com.jetbrains.python.psi.PyUtil; -import com.jetbrains.python.psi.resolve.*; +import com.jetbrains.python.psi.resolve.PyQualifiedNameResolveContext; +import com.jetbrains.python.psi.resolve.PyResolveImportUtil; +import com.jetbrains.python.psi.resolve.QualifiedNameFinder; import com.jetbrains.python.psi.stubs.PyClassNameIndex; import com.jetbrains.python.psi.types.*; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -86,8 +91,15 @@ public class PyPsiFacadeImpl extends PyPsiFacade { @Nullable @Override public final PyClass createClassByQName(@NotNull final String qName, @NotNull final PsiElement anchor) { - final PyClassType classType = PyUtil.as(parseTypeAnnotation(qName, anchor), PyClassType.class); - return (classType != null ? classType.getPyClass() : null); + final Module module = ModuleUtilCore.findModuleForPsiElement(ObjectUtils.notNull(anchor.getContainingFile(), anchor)); + if (module == null) return null; + // Don't use PyResolveImportUtil.fromFoothold here as setting foothold file is going to affect resolve results + // particularly if the anchor element happens to be in the same file as the target class. + final PyQualifiedNameResolveContext resolveContext = PyResolveImportUtil.fromModule(module).copyWithMembers(); + return StreamEx.of(resolveQualifiedName(QualifiedName.fromDottedString(qName), resolveContext)) + .select(PyClass.class) + .findFirst() + .orElse(null); } @Nullable diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/__init__.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/bar.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/bar.py new file mode 100644 index 000000000000..07d9e0efebfa --- /dev/null +++ b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/bar.py @@ -0,0 +1,2 @@ +class MyClass: + pass diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/CreateClassByQNameDoesntDependOnExistingImports.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/CreateClassByQNameDoesntDependOnExistingImports.py new file mode 100644 index 000000000000..3d606ec59207 --- /dev/null +++ b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/CreateClassByQNameDoesntDependOnExistingImports.py @@ -0,0 +1,3 @@ +import foo + +print(foo) diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/__init__.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/foo.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/foo.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java b/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java index 1f77fe43ddaa..fa80a3c4ea03 100644 --- a/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java +++ b/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java @@ -588,4 +588,13 @@ public class PyMultiFileResolveTest extends PyMultiFileResolveTestCase { public void testIncompleteFromImport() { assertUnresolved(); } + + public void testCreateClassByQNameDoesntDependOnExistingImports() { + prepareTestDirectory(); + runWithSourceRoots(Collections.singletonList(myFixture.findFileInTempDir("src")), () -> { + myFixture.configureByFile("src/" + getTestName(false) + ".py"); + final PyPsiFacade facade = PyPsiFacade.getInstance(myFixture.getProject()); + assertNotNull(facade.createClassByQName("foo.bar.MyClass", myFixture.getFile())); + }); + } } \ No newline at end of file