Enhance quick doc generation; copy inherited docs for overridden methods. Add module quickdoc (PY-166). Add tests.

This commit is contained in:
Dmitry Cheryasov
2009-06-08 01:33:35 +04:00
parent eeb16ad701
commit 801ea7b5c4
19 changed files with 248 additions and 42 deletions

View File

@@ -12,6 +12,7 @@ ACT.NAME.add.import=Add import
ACT.NAME.use.import=Find in imported modules
ACT.CMD.use.import=Use an imported module
ACT.qualify.with.module=Qualify with an imported module
### Quick fixes ###
QFIX.add.parameter.self=Add parameter 'self'
@@ -106,6 +107,10 @@ PARSE.expected.statement.break=Statement break expected
PARSE.expected.@.or.def='@' or 'def' expected
PARSE.expected.formal.param.name=formal parameter name expected
### qiuck doc generator
QDOC.copied.from.$0.$1=<i>Documentation is missing.</i> The following if copied from <code>{0}.{1}</code>.
QDOC.copied.from.builtin=<small>(copied from built-in description)</small>
### unittest run configuration
runcfg.unittest.display_name=Python's unittest
runcfg.unittest.description=Python's unittest run configuration
@@ -138,4 +143,3 @@ runcfg.labels.interpreter_options=Interpreter &options:
runcfg.labels.working_directory=&Working directory:
runcfg.captions.script_parameters_dialog=Enter script parameters
runcfg.captions.interpreter_options_dialog=Enter interpreter options
ACT.qualify.with.module=Qualify with an imported module

View File

@@ -77,12 +77,16 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
public String generateDoc(final PsiElement element, final PsiElement originalElement) {
StringBuffer cat = new StringBuffer("<html><body><code>");
final StringBuffer cat = new StringBuffer("<html><body><code>");
if (element instanceof PyDocStringOwner) {
String docString = ((PyDocStringOwner) element).getDocString();
String docString = null;
boolean prepended_something = false;
PyStringLiteralExpression doc_expr = ((PyDocStringOwner) element).getDocStringExpression();
if (doc_expr != null) docString = doc_expr.getStringValue();
if (element instanceof PyClass) {
PyClass cls = (PyClass)element;
describeClass(cls, cat);
prepended_something = true;
}
else if (element instanceof PyFunction) {
PyFunction fun = (PyFunction)element;
@@ -91,25 +95,56 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
cat.append("<small>class ").append(cls.getName()).append("</small>").append(BR);
}
describeFunction(fun, cat);
prepended_something = true;
boolean not_found = true;
if (docString == null) {
// for well-known methods, copy built-in doc string
// TODO: also handle predefined __xxx__ that are not part of 'object'.
String meth_name = fun.getName();
if (cls != null && meth_name != null && PyNames.UnderscoredNames.contains(meth_name)) {
PyClassType objtype = PyBuiltinCache.getInstance(fun.getProject()).getObjectType(); // old- and new-style classes share the __xxx__ stuff
if (objtype != null) {
PyClass objcls = objtype.getPyClass();
if (objcls != null) {
PyFunction obj_underscored = objcls.findMethodByName(meth_name);
if (obj_underscored != null) {
String predefined_doc = obj_underscored.getDocString();
if (predefined_doc != null && predefined_doc.length() > 1) { // only a real-looking doc string counts
if (cls != null && meth_name != null ) {
// look for inherited
for (PyClass ancestor : cls.iterateAncestors()) {
PyFunction inherited = ancestor.findMethodByName(meth_name);
if (inherited != null) {
PyStringLiteralExpression doc_elt = inherited.getDocStringExpression();
if (doc_elt != null) {
String inherited_doc = doc_elt.getStringValue();
if (inherited_doc.length() > 1) {
cat
.append(BR).append(BR).append("</code>")
.append(PyBundle.message("QDOC.copied.from.$0.$1", ancestor.getName(), meth_name))
.append(BR).append(BR)
.append(predefined_doc)
.append(BR)
.append("</code><small>(copied from built-in description)</small><code>")
.append(inherited_doc)
.append("<code>")
;
not_found = false;
break;
}
}
}
}
if (not_found) {
// above could have not worked because inheritance is not searched down to 'object'.
// for well-known methods, copy built-in doc string.
// TODO: also handle predefined __xxx__ that are not part of 'object'.
if (PyNames.UnderscoredNames.contains(meth_name)) {
PyClassType objtype = PyBuiltinCache.getInstance(fun.getProject()).getObjectType(); // old- and new-style classes share the __xxx__ stuff
if (objtype != null) {
PyClass objcls = objtype.getPyClass();
if (objcls != null) {
PyFunction obj_underscored = objcls.findMethodByName(meth_name);
if (obj_underscored != null) {
PyStringLiteralExpression predefined_doc_expr = obj_underscored.getDocStringExpression();
String predefined_doc = predefined_doc_expr != null? predefined_doc_expr.getStringValue() : null;
if (predefined_doc != null && predefined_doc.length() > 1) { // only a real-looking doc string counts
cat
.append(BR).append(BR).append("</code>")
.append(predefined_doc)
.append(BR)
.append(PyBundle.message("QDOC.copied.from.builtin"))
.append("<code>")
;
}
}
}
}
}
@@ -117,12 +152,17 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
}
}
}
else if (element instanceof PyFile) {
// what to prepend to a module description??
}
else { // not a func, not a class
cat.append(combUp(PyUtil.getReadableRepr(element, false)));
prepended_something = true;
}
cat.append("</code>");
if (docString != null) {
cat.append(BR).append(BR).append(combUp(docString));
if (prepended_something) cat.append(BR).append(BR);
cat.append(combUp(docString.trim()));
}
return cat.append("</body></html>").toString();
}

View File

@@ -0,0 +1,33 @@
package com.jetbrains.python;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.psi.PyElement;
import com.jetbrains.python.psi.PyExpressionStatement;
import com.jetbrains.python.psi.PyStringLiteralExpression;
import com.jetbrains.python.psi.PyUtil;
import org.jetbrains.annotations.Nullable;
// TODO: find a better place for this.
/**
* Utility class for finding an expression which would fit as a doc string.
* User: dcheryasov
* Date: Jun 7, 2009 5:06:12 AM
*/
public class PythonDosStringFinder {
private PythonDosStringFinder() {}
/**
* Looks for a doc string under given parent.
* @param parent where to look. For classes and functions, this would be PyStatementList, for modules, PyFile.
* @return the defining expression, or null.
*/
@Nullable
public static PyStringLiteralExpression find(PyElement parent) {
if (parent != null) {
PsiElement seeker = PyUtil.getFirstNonCommentAfter(parent.getFirstChild());
if (seeker instanceof PyExpressionStatement) seeker = PyUtil.getFirstNonCommentAfter(seeker.getFirstChild());
if (seeker instanceof PyStringLiteralExpression) return (PyStringLiteralExpression)seeker;
}
return null;
}
}

View File

@@ -2,10 +2,7 @@ package com.jetbrains.python.psi;
import org.jetbrains.annotations.Nullable;
/**
* @author yole
*/
public interface PyDocStringOwner {
public interface PyDocStringOwner extends PyElement {
@Nullable
String getDocString();
PyStringLiteralExpression getDocStringExpression();
}

View File

@@ -11,7 +11,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
public interface PyFile extends PyElement, PsiFile {
public interface PyFile extends PyElement, PsiFile, PyDocStringOwner {
Key<Boolean> KEY_IS_DIRECTORY = Key.create("Dir impersonated by __init__.py");
Key<Boolean> KEY_EXCLUDE_BUILTINS = Key.create("Don't include builtins to processDeclaration results");

View File

@@ -26,12 +26,12 @@ import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.VariantsProcessor;
import com.jetbrains.python.psi.stubs.PyClassStub;
import com.jetbrains.python.psi.stubs.PyFunctionStub;
import com.jetbrains.python.validation.DocStringAnnotator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -172,7 +172,7 @@ public class PyClassImpl extends PyPresentableElementImpl<PyClassStub> implement
}
public void remove() {
//To change body of implemented methods use File | Settings | File Templates.
throw new UnsupportedOperationException();
}
};
}
@@ -186,7 +186,7 @@ public class PyClassImpl extends PyPresentableElementImpl<PyClassStub> implement
// maybe a bare old-style class?
// TODO: depend on language version: py3k does not do old style classes
PsiElement paren = PsiTreeUtil.getChildOfType(this, PyParenthesizedExpression.class).getFirstChild(); // no NPE, we always have the par expr
if (paren != null && "(".equals(paren.getText())) { // no, we have "()" after class name, it's new style
if (paren != null && "(".equals(paren.getText())) { // "()" after class name, it's new style
for(PsiElement element: superClassElements) {
if (element instanceof PyClass) {
result.add((PyClass) element);
@@ -309,16 +309,6 @@ public class PyClassImpl extends PyPresentableElementImpl<PyClassStub> implement
PsiElement lastParent,
@NotNull PsiElement place)
{
/*
for(PyFunction func: getMethods()) {
if (func == lastParent) continue;
if (!processor.execute(func, substitutor)) return false;
}
for(PyTargetExpression expr: getClassAttributes()) {
if (expr == lastParent) continue;
if (!processor.execute(expr, substitutor)) return false;
}
*/
// class level
final PsiElement the_psi = getNode().getPsi();
PyResolveUtil.treeCrawlUp(processor, true, the_psi, the_psi);
@@ -339,8 +329,8 @@ public class PyClassImpl extends PyPresentableElementImpl<PyClassStub> implement
return name != null ? name.getStartOffset() : super.getTextOffset();
}
public String getDocString() {
return DocStringAnnotator.findDocString(getStatementList());
public PyStringLiteralExpression getDocStringExpression() {
return PythonDosStringFinder.find(getStatementList());
}
public String toString() {

View File

@@ -30,6 +30,7 @@ import com.intellij.psi.tree.TokenSet;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.ResolveProcessor;
@@ -235,4 +236,8 @@ public class PyFileImpl extends PsiFileBase implements PyFile, PyExpression {
}
return myType;
}
public PyStringLiteralExpression getDocStringExpression() {
return PythonDosStringFinder.find(this);
}
}

View File

@@ -25,10 +25,10 @@ import com.intellij.util.Icons;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.stubs.PyClassStub;
import com.jetbrains.python.psi.stubs.PyFunctionStub;
import com.jetbrains.python.validation.DocStringAnnotator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -146,8 +146,8 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp
node.getTreeParent().removeChild(node);
}
public String getDocString() {
return DocStringAnnotator.findDocString(getStatementList());
public PyStringLiteralExpression getDocStringExpression() {
return PythonDosStringFinder.find(getStatementList());
}
protected String getElementLocation() {

View File

@@ -0,0 +1,2 @@
<html><body><code>class <b>Foo</b>(object)</code><br><br>Doc&nbsp;of&nbsp;Foo.</body></html>

View File

@@ -0,0 +1,6 @@
# direct class doc
class Foo(object):
"<the_doc>Doc of Foo."
pass
<the_ref>Foo

View File

@@ -0,0 +1,2 @@
<html><body><code>def <b>foo</b>()</code><br><br>Doc&nbsp;of&nbsp;foo.</body></html>

View File

@@ -0,0 +1,6 @@
# directly in function
def foo():
"<the_doc>Doc of foo."
pass
<the_ref>foo

View File

@@ -0,0 +1 @@
<html><body><code><small>class B</small><br>def <b>foo</b>(self)<br><br></code><i>Documentation is missing.</i> The following if copied from <code>A.foo</code>.<br><br>Doc from A.foo.<code></code></body></html>

View File

@@ -0,0 +1,12 @@
# copied doc of inherited method
class A:
def foo(self):
"<the_doc>Doc from A.foo."
pass
class B(A):
def foo(self):
return None
b = B()
b.<the_ref>foo()

View File

@@ -0,0 +1 @@
<html><body><code><small>class Foo</small><br>@<i>deco</i>()<br>def <b>meth</b>(self)</code><br><br>Doc&nbsp;of&nbsp;meth.</body></html>

View File

@@ -0,0 +1,10 @@
# just method
class Foo:
@deco
def meth(self):
"""<the_doc>
Doc of meth.
"""
f = Foo()
f.<the_ref>meth

View File

@@ -0,0 +1 @@
<html><body><code></code>Module's&nbsp;doc.</body></html>

View File

@@ -0,0 +1,7 @@
# module. reuse ourselves
"""
<the_doc>Module's doc.
"""
import <the_ref>Module

View File

@@ -0,0 +1,89 @@
package com.jetbrains.python;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.psi.*;
import java.io.File;
import java.util.Map;
/**
* TODO: Add description
* User: dcheryasov
* Date: Jun 7, 2009 12:31:07 PM
*/
public class PyQuickDocTest extends MarkedTestCase {
private PythonDocumentationProvider myProvider;
@Override
protected void setUp() throws Exception {
super.setUp();
// the provider is stateless, can be reused, as in real life
myProvider = new PythonDocumentationProvider();
}
protected String getTestDataPath() {
return PathManager.getHomePath() + "/plugins/python/testData/quickdoc/";
}
private void checkByHTML(String text) throws Exception {
assertNotNull(text);
String filePath = getTestName(false) + ".html";
final String fullPath = getTestDataPath() + filePath;
final VirtualFile vFile = LocalFileSystem.getInstance().findFileByPath(fullPath.replace(File.separatorChar, '/'));
assertNotNull("file " + filePath + " not found", vFile);
String fileText = StringUtil.convertLineSeparators(VfsUtil.loadText(vFile), "\n");
assertEquals(fileText.trim(), text.trim());
}
private void processRefDocPair() throws Exception {
Map<String, PsiElement> marks = loadTest();
assertEquals(2, marks.size());
PsiElement doc_elt = marks.get("<the_doc>").getParent(); // ident -> expr
assertTrue(doc_elt instanceof PyStringLiteralExpression);
String doc_text = ((PyStringLiteralExpression)doc_elt).getStringValue();
assertNotNull(doc_text);
PsiElement ref_elt = marks.get("<the_ref>").getParent(); // ident -> expr
final PyDocStringOwner doc_owner = (PyDocStringOwner)((PyReferenceExpression)ref_elt).resolve();
assertEquals(doc_owner.getDocStringExpression(), doc_elt);
checkByHTML(myProvider.generateDoc(doc_owner, null));
}
public void testDirectFunc() throws Exception {
processRefDocPair();
}
public void testDirectClass() throws Exception {
processRefDocPair();
}
public void testModule() throws Exception {
processRefDocPair();
}
public void testMethod() throws Exception {
processRefDocPair();
}
public void testInheritedMethod() throws Exception {
Map<String, PsiElement> marks = loadTest();
assertEquals(2, marks.size());
PsiElement doc_elt = marks.get("<the_doc>").getParent(); // ident -> expr
assertTrue(doc_elt instanceof PyStringLiteralExpression);
String doc_text = ((PyStringLiteralExpression)doc_elt).getStringValue();
assertNotNull(doc_text);
PsiElement ref_elt = marks.get("<the_ref>").getParent(); // ident -> expr
final PyDocStringOwner doc_owner = (PyDocStringOwner)((PyReferenceExpression)ref_elt).resolve();
assertNull(doc_owner.getDocStringExpression()); // no direct doc!
checkByHTML(myProvider.generateDoc(doc_owner, null));
}
}