From 2d4d890e2a5eff141ec8b5e7dd5d852a11a22ac4 Mon Sep 17 00:00:00 2001 From: Andrey Vlasovskikh Date: Wed, 5 Nov 2014 22:57:31 +0300 Subject: [PATCH] Extract setuptools namespace package names from *-nspkg.pth files (PY-12738, PY-13262, PY-13259) --- python/src/META-INF/python-core.xml | 1 + .../src/com/jetbrains/python/psi/PyUtil.java | 25 ++--- .../psi/stubs/PySetuptoolsNamespaceIndex.java | 104 ++++++++++++++++++ .../a.py | 5 + .../foo/bar/__init__.py | 0 .../foo/bar/baz.py | 2 + .../foobar-0.1-py2.7-nspkg.pth | 1 + .../google/protobuf/__init__.py | 0 .../google/protobuf/service.py | 2 + .../protobuf-2.6.1-py2.7-nspkg.pth | 1 + .../NestedNamespacePackageName/a.py | 4 + .../linkedin/utils/file.py | 2 + .../zzz-lipy_utils-0.1.67-py2.6-nspkg.pth | 2 + .../PyUnresolvedReferencesInspectionTest.java | 10 ++ 14 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 python/src/com/jetbrains/python/psi/stubs/PySetuptoolsNamespaceIndex.java create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/a.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foo/bar/__init__.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foo/bar/baz.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foobar-0.1-py2.7-nspkg.pth create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/google/protobuf/__init__.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/google/protobuf/service.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/protobuf-2.6.1-py2.7-nspkg.pth create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/a.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/linkedin/utils/file.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/zzz-lipy_utils-0.1.67-py2.6-nspkg.pth diff --git a/python/src/META-INF/python-core.xml b/python/src/META-INF/python-core.xml index d943f2319f0a..f6ef8f8287d4 100644 --- a/python/src/META-INF/python-core.xml +++ b/python/src/META-INF/python-core.xml @@ -96,6 +96,7 @@ + diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index f1edf4f93ce1..47e66d75db5a 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -69,7 +69,9 @@ import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.QualifiedNameFinder; import com.jetbrains.python.psi.resolve.RatedResolveResult; +import com.jetbrains.python.psi.stubs.PySetuptoolsNamespaceIndex; import com.jetbrains.python.psi.types.*; import com.jetbrains.python.refactoring.classes.PyDependenciesComparator; import com.jetbrains.python.refactoring.classes.extractSuperclass.PyExtractSuperclassHelper; @@ -1008,7 +1010,7 @@ public class PyUtil { if (level.isAtLeast(LanguageLevel.PYTHON33)) { return true; } - return hasNamespacePackageFile(directory); + return isSetuptoolsNamespacePackage(directory); } public static boolean isPackage(@NotNull PsiFile file) { @@ -1027,18 +1029,15 @@ public class PyUtil { return null; } - private static boolean hasNamespacePackageFile(@NotNull PsiDirectory directory) { - final String name = directory.getName().toLowerCase(); - final PsiDirectory parent = directory.getParent(); - if (parent != null) { - for (PsiFile file : parent.getFiles()) { - final String filename = file.getName().toLowerCase(); - if (filename.startsWith(name) && filename.endsWith("-nspkg.pth")) { - return true; - } - } - } - return false; + private static boolean isSetuptoolsNamespacePackage(@NotNull PsiDirectory directory) { + final String packagePath = getPackagePath(directory); + return packagePath != null && !PySetuptoolsNamespaceIndex.find(packagePath, directory.getProject()).isEmpty(); + } + + @Nullable + private static String getPackagePath(@NotNull PsiDirectory directory) { + final QualifiedName name = QualifiedNameFinder.findShortestImportableQName(directory); + return name != null ? name.toString() : null; } /** diff --git a/python/src/com/jetbrains/python/psi/stubs/PySetuptoolsNamespaceIndex.java b/python/src/com/jetbrains/python/psi/stubs/PySetuptoolsNamespaceIndex.java new file mode 100644 index 000000000000..2988922415d2 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/stubs/PySetuptoolsNamespaceIndex.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.psi.stubs; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.indexing.*; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import com.jetbrains.python.psi.search.PyProjectScopeBuilder; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author vlan + */ +public class PySetuptoolsNamespaceIndex extends ScalarIndexExtension { + public static final ID NAME = ID.create("Py.setuptools.namespace"); + private static final Pattern RE_NAMESPACE = Pattern.compile("sys\\.modules\\.setdefault\\('([^']*)'"); + private static final String NAMESPACE_FILE_SUFFIX = "-nspkg.pth"; + + private final EnumeratorStringDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + + private final DataIndexer myDataIndexer = new DataIndexer() { + @NotNull + @Override + public Map map(@NotNull FileContent inputData) { + final CharSequence content = inputData.getContentAsText(); + final Matcher matcher = RE_NAMESPACE.matcher(content); + final Map results = new HashMap(); + while (matcher.find()) { + final String packageName = matcher.group(1); + results.put(packageName, null); + } + return results; + } + }; + + private FileBasedIndex.InputFilter myInputFilter = new FileBasedIndex.InputFilter() { + @Override + public boolean acceptInput(@NotNull VirtualFile file) { + return file.getName().endsWith(NAMESPACE_FILE_SUFFIX); + } + }; + + @NotNull + @Override + public ID getName() { + return NAME; + } + + @NotNull + @Override + public DataIndexer getIndexer() { + return myDataIndexer; + } + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return myKeyDescriptor; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return myInputFilter; + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return 0; + } + + @NotNull + public static Collection find(@NotNull String name, @NotNull Project project) { + final GlobalSearchScope scope = PyProjectScopeBuilder.excludeSdkTestsScope(project); + return FileBasedIndex.getInstance().getContainingFiles(NAME, name, scope); + } +} diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/a.py new file mode 100644 index 000000000000..4786a02c9c6a --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/a.py @@ -0,0 +1,5 @@ +from google.protobuf import service +from foo.bar import baz + +print(service.Service) +print(baz.Baz) diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foo/bar/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foo/bar/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foo/bar/baz.py b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foo/bar/baz.py new file mode 100644 index 000000000000..78292a636e5a --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foo/bar/baz.py @@ -0,0 +1,2 @@ +class Baz(object): + pass diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foobar-0.1-py2.7-nspkg.pth b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foobar-0.1-py2.7-nspkg.pth new file mode 100644 index 000000000000..90778b0afd58 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/foobar-0.1-py2.7-nspkg.pth @@ -0,0 +1 @@ +import sys, types, os;p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('spam',));ie = os.path.exists(os.path.join(p,'__init__.py'));m = not ie and sys.modules.setdefault('spam', types.ModuleType('spam'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p) diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/google/protobuf/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/google/protobuf/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/google/protobuf/service.py b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/google/protobuf/service.py new file mode 100644 index 000000000000..cfca8c4ad5c1 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/google/protobuf/service.py @@ -0,0 +1,2 @@ +class Service(object): + pass diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/protobuf-2.6.1-py2.7-nspkg.pth b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/protobuf-2.6.1-py2.7-nspkg.pth new file mode 100644 index 000000000000..6b67a7627412 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NamespacePackageNameDoesntMatchFileName/protobuf-2.6.1-py2.7-nspkg.pth @@ -0,0 +1 @@ +import sys, types, os;p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('google',));ie = os.path.exists(os.path.join(p,'__init__.py'));m = not ie and sys.modules.setdefault('google', types.ModuleType('google'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p) diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/a.py new file mode 100644 index 000000000000..656f189a62c5 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/a.py @@ -0,0 +1,4 @@ +from linkedin.utils.file import temp_directory + + +temp_directory() diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/linkedin/utils/file.py b/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/linkedin/utils/file.py new file mode 100644 index 000000000000..886432431626 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/linkedin/utils/file.py @@ -0,0 +1,2 @@ +def temp_directory(): + pass diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/zzz-lipy_utils-0.1.67-py2.6-nspkg.pth b/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/zzz-lipy_utils-0.1.67-py2.6-nspkg.pth new file mode 100644 index 000000000000..3353cab47921 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/NestedNamespacePackageName/zzz-lipy_utils-0.1.67-py2.6-nspkg.pth @@ -0,0 +1,2 @@ +import sys,types,os,pkgutil; p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('linkedin',)); ie = os.path.exists(os.path.join(p,'__init__.py')); m = not ie and sys.modules.setdefault('linkedin',types.ModuleType('linkedin')); mp = (m or []) and m.__dict__.setdefault('__path__',[]); (p not in mp) and mp.append(p); mp[:] = m and pkgutil.extend_path(mp, 'linkedin') or mp +import sys,types,os,pkgutil; p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('linkedin', 'utils')); ie = os.path.exists(os.path.join(p,'__init__.py')); m = not ie and sys.modules.setdefault('linkedin.utils',types.ModuleType('linkedin.utils')); mp = (m or []) and m.__dict__.setdefault('__path__',[]); (p not in mp) and mp.append(p); mp[:] = m and pkgutil.extend_path(mp, 'linkedin.utils') or mp; m and __import__('linkedin') and setattr(sys.modules['linkedin'], 'utils', m) diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java index 3451712f6bcd..9440d666c07f 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java @@ -386,6 +386,16 @@ public class PyUnresolvedReferencesInspectionTest extends PyInspectionTestCase { doMultiFileTest(); } + // PY-12738 + public void testNamespacePackageNameDoesntMatchFileName() { + doMultiFileTest(); + } + + // PY-13259 + public void testNestedNamespacePackageName() { + doMultiFileTest(); + } + @NotNull @Override protected Class getInspectionClass() {