Refactor numpy/scipy documentation link provider; add tests.

This commit is contained in:
Dmitry Trofimov
2018-11-05 23:06:57 +01:00
parent 27fe2ff3f8
commit 14d9810174
6 changed files with 125 additions and 49 deletions
@@ -785,6 +785,7 @@
<typeProvider implementation="com.jetbrains.numpy.codeInsight.NumpyDocStringTypeProvider"/>
<resolveResultRater implementation="com.jetbrains.numpy.codeInsight.NumpyResolveRater"/>
<pyClassMembersProvider implementation="com.jetbrains.numpy.codeInsight.NumpyClassMembersProvider"/>
<documentationLinkProvider implementation="com.jetbrains.numpy.codeInsight.SciPyDocumentationLinkProvider"/>
<!-- PyDocstring -->
<typeProvider implementation="com.jetbrains.python.documentation.docstrings.PyDocStringTypeProvider"/>
@@ -0,0 +1,53 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.numpy.codeInsight
import com.google.common.collect.ImmutableMap
import com.intellij.psi.PsiElement
import com.jetbrains.python.documentation.PythonDocumentationLinkProvider
import com.jetbrains.python.documentation.PythonDocumentationProvider
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
class SciPyDocumentationLinkProvider : PythonDocumentationLinkProvider {
private val nameToWebpageName: Map<String, String> by lazy {
val b = ImmutableMap.builder<String, String>()
try {
BufferedReader(
InputStreamReader(
SciPyDocumentationLinkProvider::class.java
.getResourceAsStream("/com/jetbrains/numpy/codeInsight/scipyNameMapping.tsv"), // generated by scipy_doc_mapping.py
Charsets.UTF_8
)
).use { inputStream ->
inputStream.lines().forEach { line ->
val kv = line.split("\t".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
b.put(kv[0], kv[1])
}
}
}
catch (e: IOException) {
throw RuntimeException(e)
}
b.build()
}
override fun getExternalDocumentationUrl(element: PsiElement?, originalElement: PsiElement?): String? {
val qname = PythonDocumentationProvider.getFullQualifiedName(element)
return if (qname != null && qname.firstComponent in listOf("numpy", "scipy")) {
val webPage = nameToWebpageName.get(qname.toString())
if (webPage != null) {
"https://docs.scipy.org/doc/${qname.firstComponent}/reference/generated/$webPage.html"
} else {
"https://docs.scipy.org/doc/${qname.firstComponent}/reference/"
}
}
else null
}
}
@@ -2,7 +2,7 @@
package com.jetbrains.python.documentation;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.ServiceManager;
@@ -17,17 +17,11 @@ import com.jetbrains.python.psi.PyFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author yole
@@ -46,28 +40,6 @@ public class PythonDocumentationMap implements PersistentStateComponent<PythonDo
return ServiceManager.getService(PythonDocumentationMap.class);
}
private static class ScipyExternalDocumentationMap {
private static final Map<String, String> nameToWebpageName = ((Supplier<Map<String, String>>)() -> {
final ImmutableMap.Builder<String, String> b = ImmutableMap.builder();
try (final BufferedReader inputStream = new BufferedReader(
new InputStreamReader(
ScipyExternalDocumentationMap.class
.getResourceAsStream("/com/jetbrains/python/documentation/scipyNameMapping.tsv"),
StandardCharsets.US_ASCII
)
)) {
inputStream.lines().forEach(line -> {
final String[] kv = line.split("\t");
b.put(kv[0], kv[1]);
});
}
catch (final IOException e) {
throw new RuntimeException(e);
}
return b.build();
}).get();
}
public static class Entry {
private String myPrefix;
private String myUrlPattern;
@@ -124,10 +96,9 @@ public class PythonDocumentationMap implements PersistentStateComponent<PythonDo
addEntry(PyQt4, PYQT4_DOC_URL);
addEntry("PyQt5", "http://doc.qt.io/qt-5/{class.name.lower}.html#{functionOrProp.name}");
addEntry("PySide", "http://pyside.github.io/docs/pyside/{module.name.slashes}/{class.name}.html#{module.name}.{element.qname}");
addEntry("gtk", "http://library.gnome.org/devel/pygtk/stable/class-gtk{class.name.lower}.html#method-gtk{class.name.lower}--{function.name.dashes}");
addEntry("gtk",
"http://library.gnome.org/devel/pygtk/stable/class-gtk{class.name.lower}.html#method-gtk{class.name.lower}--{function.name.dashes}");
addEntry("wx", "http://www.wxpython.org/docs/api/{module.name}.{class.name}-class.html#{function.name}");
addEntry("numpy", "https://docs.scipy.org/doc/numpy/reference/generated/{module.name}.{element.name}.html");
addEntry("scipy", "https://docs.scipy.org/doc/scipy/reference/generated/{module.name}.{element.name}.html");
addEntry("kivy", "http://kivy.org/docs/api-{module.name}.html");
addEntry("matplotlib", "http://matplotlib.org/api/{module.basename}_api.html#{element.qname}");
addEntry("pyramid", "http://docs.pylonsproject.org/projects/pyramid/en/latest/api/{module.basename}.html#{element.qname}");
@@ -171,18 +142,25 @@ public class PythonDocumentationMap implements PersistentStateComponent<PythonDo
@Override
public void loadState(@NotNull State state) {
myState = state;
for (Entry e: myState.getEntries()) {
for (Entry e : myState.getEntries()) {
if (PyQt4.equals(e.myPrefix) && PYQT4_DOC_URL_OLD.equals(e.myUrlPattern)) {
// old URL is broken, switch to new one
e.setUrlPattern(PYQT4_DOC_URL);
}
}
addAbsentEntriesFromDefaultState(myState);
removeEntriesThatHandledSpecially(myState);
}
private static void removeEntriesThatHandledSpecially(@NotNull State state) {
ArrayList<String> strings = Lists.newArrayList("django", "numpy", "scipy");
// those packages are handled by implementations of PythonDocumentationLinkProvider
state.setEntries(state.getEntries().stream().filter((entry -> !strings.contains(entry.myPrefix))).collect(Collectors.toList()));
}
private static void addAbsentEntriesFromDefaultState(@NotNull State state) {
State defaultState = new State();
for (Entry e: defaultState.myEntries) {
for (Entry e : defaultState.myEntries) {
if (state.myEntries.stream().noneMatch(entry -> entry.myPrefix.equals(e.myPrefix))) {
state.addEntry(e.getPrefix(), e.getUrlPattern());
}
@@ -239,10 +217,6 @@ public class PythonDocumentationMap implements PersistentStateComponent<PythonDo
if (pattern == null) {
return rootForPattern(urlPattern);
}
final Matcher match = Pattern.compile("\\A(.+/)((?:numpy|scipy)\\..+)\\.html\\Z").matcher(pattern);
if (match.find()) {
return match.group(1) + ScipyExternalDocumentationMap.nameToWebpageName.getOrDefault(match.group(2), match.group(2)) + ".html";
}
return pattern;
}
@@ -268,7 +242,6 @@ public class PythonDocumentationMap implements PersistentStateComponent<PythonDo
.replace("{" + entry.getKey() + ".lower}", entry.getValue().toLowerCase())
.replace("{" + entry.getKey() + ".slashes}", entry.getValue().replace(".", "/"))
.replace("{" + entry.getKey() + ".dashes}", entry.getValue().replace("_", "-"));
}
return urlPattern.replace("{}", "");
}
@@ -25,6 +25,8 @@ import com.intellij.psi.util.QualifiedName;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDialectsTokenSetProvider;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.console.PydevConsoleRunner;
import com.jetbrains.python.console.PydevDocumentationProvider;
@@ -604,12 +606,7 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i
if (file == null) return null;
if (file instanceof PyiFile) {
return null;
}
final QualifiedName moduleQName = QualifiedNameFinder.findCanonicalImportPath(element, element);
if (moduleQName == null) {
if (file instanceof PyiFile) { //TODO: why???
return null;
}
@@ -651,15 +648,16 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i
}
@Nullable
private static PsiNamedElement getNamedElement(PsiElement element) {
public static PsiNamedElement getNamedElement(@Nullable PsiElement element) {
PsiNamedElement namedElement = (element instanceof PsiNamedElement) ? (PsiNamedElement)element : null;
if (namedElement instanceof PyFunction && PyNames.INIT.equals(namedElement.getName())) {
final PyClass containingClass = ((PyFunction)namedElement).getContainingClass();
if (containingClass != null) {
namedElement = containingClass;
}
} else {
namedElement = (PsiNamedElement) PyUtil.turnInitIntoDir(namedElement);
}
else {
namedElement = (PsiNamedElement)PyUtil.turnInitIntoDir(namedElement);
}
return namedElement;
}
@@ -756,4 +754,47 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i
describeTypeWithLinks(type, context, anchor, holder);
return holder.toString();
}
@Nullable
public static QualifiedName getFullQualifiedName(@Nullable final PsiElement element) {
final String name =
(element instanceof PsiNamedElement) ? ((PsiNamedElement)element).getName() : element != null ? element.getText() : null;
if (name != null) {
final ScopeOwner owner = ScopeUtil.getScopeOwner(element);
final PyBuiltinCache builtinCache = PyBuiltinCache.getInstance(element);
if (owner instanceof PyClass) {
final QualifiedName importQName = QualifiedNameFinder.findCanonicalImportPath(element, element);
if (importQName != null) {
return QualifiedName.fromDottedString(importQName.toString() + "." + owner.getName() + "." + name);
}
}
else if (owner instanceof PyFunction && PyNames.INIT.equals(owner.getName()) && ((PyFunction)owner).getContainingClass() != null) {
final QualifiedName importQName = QualifiedNameFinder.findCanonicalImportPath(owner, element);
if (importQName != null) {
return QualifiedName
.fromDottedString(importQName.toString() + "." + ((PyFunction)owner).getContainingClass().getName() + "." + name);
}
}
else if (owner instanceof PyFile) {
if (builtinCache.isBuiltin(element)) {
return QualifiedName.fromDottedString(name);
}
else {
final VirtualFile virtualFile = ((PyFile)owner).getVirtualFile();
if (virtualFile != null) {
final QualifiedName fileQName = QualifiedNameFinder.findCanonicalImportPath(element, element);
if (fileQName != null) {
return QualifiedName.fromDottedString(fileQName.toString() + "." + name);
}
}
}
}
else {
if (element instanceof PyFile) {
return QualifiedNameFinder.findCanonicalImportPath(element, element);
}
}
}
return null;
}
}
@@ -117,6 +117,14 @@ print(os.path.isf<caret>ile)
""".trimIndent(), """<dt id="os.path.isfile">""".trimIndent())
}
fun testNumpyNdarray() {
myFixture.copyDirectoryToProject("/inspections/PyNumpyType/numpy", "numpy")
doTestDocumentationUrl("""import numpy
a = numpy.multiarray.ndarr<caret>ay()
""".trimMargin(), "https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html", myFixture)
}
}