From c79da2708cadbaefb33f23ed5dfd5f8fe0733f81 Mon Sep 17 00:00:00 2001 From: Daniil Kalinin Date: Tue, 23 Jan 2024 09:50:49 +0000 Subject: [PATCH] PY-64074 Add Quick Documentation for type parameters and type alias statements Merge-request: IJ-MR-119974 Merged-by: Daniil Kalinin GitOrigin-RevId: fa0c57b3005b31d892a394b3a5f595ac10135a71 --- python/helpers/tools/python_keywords/type | 38 +++++++++++++ .../resources/messages/PyPsiBundle.properties | 4 ++ .../documentation/PyDocumentationBuilder.java | 56 +++++++++++++++++++ .../documentation/PyDocumentationLink.kt | 9 +++ .../PythonDocumentationProvider.java | 49 ++++++++++++++++ .../testData/quickdoc/TypeAliasStatement.html | 1 + .../testData/quickdoc/TypeAliasStatement.py | 3 + python/testData/quickdoc/TypeKeyword.html | 38 +++++++++++++ python/testData/quickdoc/TypeKeyword.py | 1 + python/testData/quickdoc/TypeParameter.html | 1 + python/testData/quickdoc/TypeParameter.py | 2 + .../com/jetbrains/python/Py3QuickDocTest.java | 15 +++++ 12 files changed, 217 insertions(+) create mode 100644 python/helpers/tools/python_keywords/type create mode 100644 python/testData/quickdoc/TypeAliasStatement.html create mode 100644 python/testData/quickdoc/TypeAliasStatement.py create mode 100644 python/testData/quickdoc/TypeKeyword.html create mode 100644 python/testData/quickdoc/TypeKeyword.py create mode 100644 python/testData/quickdoc/TypeParameter.html create mode 100644 python/testData/quickdoc/TypeParameter.py diff --git a/python/helpers/tools/python_keywords/type b/python/helpers/tools/python_keywords/type new file mode 100644 index 000000000000..d26579d2635d --- /dev/null +++ b/python/helpers/tools/python_keywords/type @@ -0,0 +1,38 @@ +
+

The type statement

+
+type_stmt ::=  'type' identifier [type_params] "=" expression
+
+

The type statement declares a type alias, which is an instance +of typing.TypeAliasType.

+

For example, the following statement creates a type alias:

+
type Point = tuple[float, float]
+
+
+

This code is roughly equivalent to:

+
annotation-def VALUE_OF_Point():
+    return tuple[float, float]
+Point = typing.TypeAliasType("Point", VALUE_OF_Point())
+
+
+

annotation-def indicates an annotation scope, which behaves +mostly like a function, but with several small differences.

+

The value of the +type alias is evaluated in the annotation scope. It is not evaluated when the +type alias is created, but only when the value is accessed through the type alias’s +__value__ attribute (see Lazy evaluation). +This allows the type alias to refer to names that are not yet defined.

+

Type aliases may be made generic by adding a type parameter list +after the name. See Generic type aliases for more.

+

type is a soft keyword.

+
+

New in version 3.12.

+
+
+

See also

+
+
PEP 695 - Type Parameter Syntax

Introduced the type statement and syntax for +generic classes and functions.

+
+
+
\ No newline at end of file diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 1c19271afd3f..cb109dd50bec 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -189,6 +189,10 @@ QDOC.variable.name=Variable "{0}" QDOC.parameter.of.function.name=Parameter "{0}" of function "{1}" QDOC.parameter.of.method.name=Parameter "{0}" of method "{1}" QDOC.parameter.name=Parameter "{0}" +QDOC.type.parameter.name.of.link=Type parameter {0} of {1} +QDOC.type.parameter.name=Type parameter {0} +QDOC.type.parameter.kind=kind: +QDOC.type.alias.statement.name.of.link=Type alias statement {0} of {1} ### Formatter formatter.panel.dict.alignment.do.not.align=Do not align diff --git a/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationBuilder.java b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationBuilder.java index 16bc07cf0383..1b9ebb0043a5 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationBuilder.java +++ b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationBuilder.java @@ -17,6 +17,8 @@ import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.FactoryMap; import com.jetbrains.python.*; +import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.documentation.docstrings.DocStringUtil; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; @@ -85,6 +87,12 @@ public class PyDocumentationBuilder { else if (elementDefinition instanceof PyNamedParameter) { buildFromParameter((PyNamedParameter)elementDefinition); } + else if (elementDefinition instanceof PyTypeParameter typeParameter) { + buildFromTypeParameter(typeParameter); + } + else if (elementDefinition instanceof PyTypeAliasStatement typeAliasStatement) { + buildFromTypeAliasStatement(typeAliasStatement); + } final ASTNode node = elementDefinition.getNode(); if (node != null) { @@ -230,6 +238,44 @@ public class PyDocumentationBuilder { myBody.append(PythonDocumentationProvider.describeParameter(parameter, myContext)); } + private void buildFromTypeParameter(@NotNull PyTypeParameter typeParameter) { + ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(typeParameter); + HtmlChunk link = null; + String typeParamName = typeParameter.getName(); + if (scopeOwner instanceof PyFunction pyFunction) { + link = getLinkToFunction(pyFunction, true); + } + else if (scopeOwner instanceof PyClass pyClass) { + link = getLinkToClass(pyClass, true); + } + else if (scopeOwner instanceof PyTypeAliasStatement typeAliasStatement) { + link = getLinkToTypeAliasStatement(typeAliasStatement); + } + if (link != null && typeParamName != null) { + myBody.appendRaw(PyPsiBundle.message("QDOC.type.parameter.name.of.link", HtmlChunk.text(typeParamName).bold(), link)).br(); + myBody.append(PythonDocumentationProvider.describeTypeParameter(typeParameter, true, myContext)); + } + } + + private void buildFromTypeAliasStatement(@NotNull PyTypeAliasStatement typeAliasStatement) { + ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(typeAliasStatement); + HtmlChunk link = null; + String typeParamName = typeAliasStatement.getName(); + if (scopeOwner instanceof PyFunction pyFunction) { + link = getLinkToFunction(pyFunction, true); + } + else if (scopeOwner instanceof PyClass pyClass) { + link = getLinkToClass(pyClass, true); + } + else if (scopeOwner instanceof PyFile pyFile) { + link = getLinkToModule(pyFile); + } + if (link != null && typeParamName != null) { + myBody.appendRaw(PyPsiBundle.message("QDOC.type.alias.statement.name.of.link", HtmlChunk.text(typeParamName).bold(), link)).br(); + myBody.append(PythonDocumentationProvider.describeTypeAlias(typeAliasStatement, myContext)); + } + } + private @NotNull HtmlChunk runFormatterService(@NotNull @Nls String description) { final DocstringFormatterRequest output = PyStructuredDocstringFormatter.formatDocstring(myElement, new DocstringFormatterRequest(description, Collections.emptyList()), @@ -742,6 +788,16 @@ public class PyDocumentationBuilder { return HtmlChunk.raw(linkText); } + @Nullable + private static HtmlChunk getLinkToTypeAliasStatement(@NotNull PyTypeAliasStatement typeAliasStatement) { + final String linkText = typeAliasStatement.getQualifiedName(); + final PsiFile file = typeAliasStatement.getContainingFile(); + if (linkText == null || typeAliasStatement.getName() == null || file == null) { + return null; + } + return PyDocumentationLink.toTypeAliasStatement(linkText, typeAliasStatement); + } + @Nullable static PyStringLiteralExpression getEffectiveDocStringExpression(@NotNull PyDocStringOwner owner) { final PyStringLiteralExpression expression = owner.getDocStringExpression(); diff --git a/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationLink.kt b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationLink.kt index ea7ce723ed49..b3fc10bf1cd6 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationLink.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyDocumentationLink.kt @@ -74,6 +74,15 @@ object PyDocumentationLink { } } + @JvmStatic + fun toTypeAliasStatement(@NlsSafe content: String, typeAliasStatement: PyTypeAliasStatement) : HtmlChunk { + val qualifiedName = typeAliasStatement.qualifiedName + return when { + qualifiedName != null -> HtmlChunk.link("${DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL}$LINK_TYPE_FUNC$qualifiedName", content) + else -> HtmlChunk.text(content) + } + } + @JvmStatic fun toModule(@NlsSafe content: String, qualifiedName: String): HtmlChunk { return HtmlChunk.link("${DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL}$LINK_TYPE_MODULE$qualifiedName", content) diff --git a/python/python-psi-impl/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java b/python/python-psi-impl/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java index 80c53cb6a1c7..674d77ff16cf 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java @@ -92,6 +92,12 @@ public class PythonDocumentationProvider implements DocumentationProvider { else if (element instanceof PyExpression) { return describeExpression((PyExpression)element, referenceElement, context); } + else if (element instanceof PyTypeParameter typeParameter) { + return PyPsiBundle.message("QDOC.type.parameter.name", describeTypeParameter(typeParameter, true, context)); + } + else if (element instanceof PyTypeAliasStatement typeAliasStatement) { + return describeTypeAlias(typeAliasStatement, context).toString(); + } return null; } @@ -135,6 +141,49 @@ public class PythonDocumentationProvider implements DocumentationProvider { return result.toFragment(); } + @NotNull + static HtmlChunk describeTypeParameter(@NotNull PyTypeParameter typeParameter, boolean showKind, @NotNull TypeEvalContext context) { + HtmlBuilder result = new HtmlBuilder(); + result.append(styledSpan(StringUtil.notNullize(typeParameter.getName()), PyHighlighter.PY_TYPE_PARAMETER)); + PyExpression boundExpression = typeParameter.getBoundExpression(); + if (boundExpression != null && typeParameter.getBoundExpressionText() != null) { + result.append(styledSpan(": ", PyHighlighter.PY_OPERATION_SIGN)); + result.append(highlightExpressionText(typeParameter.getBoundExpressionText(), typeParameter.getBoundExpression())); + } + if (showKind) { + result + .append(", ") + .append(PyPsiBundle.message("QDOC.type.parameter.kind")) + .append(" ") + .append(styledSpan(formatTypeWithLinks(context.getType(typeParameter), typeParameter, typeParameter, context), PyHighlighter.PY_ANNOTATION)); + } + return result.toFragment(); + } + + + @NotNull + static HtmlChunk describeTypeAlias(@NotNull PyTypeAliasStatement typeAliasStatement, @NotNull TypeEvalContext context) { + HtmlBuilder result = new HtmlBuilder(); + result.append(styledSpan("type ", PyHighlighter.PY_KEYWORD)); //NON-NLS + result.append(styledSpan(StringUtil.notNullize(typeAliasStatement.getName()), DefaultLanguageHighlighterColors.IDENTIFIER)); + if (typeAliasStatement.getTypeParameterList() != null) { + List typeParameters = typeAliasStatement.getTypeParameterList().getTypeParameters(); + result.append(styledSpan("[", PyHighlighter.PY_BRACKETS)); + result.append(StreamEx + .of(typeParameters) + .map(typeParameter -> describeTypeParameter(typeParameter, false, context)) + .collect(HtmlChunk.toFragment(styledSpan(", ", PyHighlighter.PY_COMMA)))); + result.append(styledSpan("]", PyHighlighter.PY_BRACKETS)); + } + PyExpression typeExpression = typeAliasStatement.getTypeExpression(); + if (typeExpression != null) { + result.append(styledSpan(" = ", PyHighlighter.PY_OPERATION_SIGN)); + result.append(styledSpan(formatTypeWithLinks(context.getType(typeExpression), + typeExpression, typeAliasStatement, context), PyHighlighter.PY_ANNOTATION)); + } + return result.toFragment(); + } + @NlsSafe @NotNull private static String describeFunctionWithTypes(@NotNull PyFunction function, diff --git a/python/testData/quickdoc/TypeAliasStatement.html b/python/testData/quickdoc/TypeAliasStatement.html new file mode 100644 index 000000000000..fbeef078f0a5 --- /dev/null +++ b/python/testData/quickdoc/TypeAliasStatement.html @@ -0,0 +1 @@ +
Type alias statement my_dict of TypeAliasStatement
type my_dict[T, U] = Type[dict[T, U]]
\ No newline at end of file diff --git a/python/testData/quickdoc/TypeAliasStatement.py b/python/testData/quickdoc/TypeAliasStatement.py new file mode 100644 index 000000000000..bd823897f30f --- /dev/null +++ b/python/testData/quickdoc/TypeAliasStatement.py @@ -0,0 +1,3 @@ +type my_dict[T, U] = dict[T, U] + +x: my_dict \ No newline at end of file diff --git a/python/testData/quickdoc/TypeKeyword.html b/python/testData/quickdoc/TypeKeyword.html new file mode 100644 index 000000000000..1cafd3a38194 --- /dev/null +++ b/python/testData/quickdoc/TypeKeyword.html @@ -0,0 +1,38 @@ +
+

The type statement

+
+type_stmt ::=  'type' identifier [type_params] "=" expression
+
+

The type statement declares a type alias, which is an instance +of typing.TypeAliasType.

+

For example, the following statement creates a type alias:

+
type Point = tuple[float, float]
+
+
+

This code is roughly equivalent to:

+
annotation-def VALUE_OF_Point():
+    return tuple[float, float]
+Point = typing.TypeAliasType("Point", VALUE_OF_Point())
+
+
+

annotation-def indicates an annotation scope, which behaves +mostly like a function, but with several small differences.

+

The value of the +type alias is evaluated in the annotation scope. It is not evaluated when the +type alias is created, but only when the value is accessed through the type alias’s +__value__ attribute (see Lazy evaluation). +This allows the type alias to refer to names that are not yet defined.

+

Type aliases may be made generic by adding a type parameter list +after the name. See Generic type aliases for more.

+

type is a soft keyword.

+
+

New in version 3.12.

+
+
+

See also

+
+
PEP 695 - Type Parameter Syntax

Introduced the type statement and syntax for +generic classes and functions.

+
+
+
\ No newline at end of file diff --git a/python/testData/quickdoc/TypeKeyword.py b/python/testData/quickdoc/TypeKeyword.py new file mode 100644 index 000000000000..af818d69e6c5 --- /dev/null +++ b/python/testData/quickdoc/TypeKeyword.py @@ -0,0 +1 @@ +type my_type[T: str] = list[T] \ No newline at end of file diff --git a/python/testData/quickdoc/TypeParameter.html b/python/testData/quickdoc/TypeParameter.html new file mode 100644 index 000000000000..700e9acc80f1 --- /dev/null +++ b/python/testData/quickdoc/TypeParameter.html @@ -0,0 +1 @@ +
Type parameter T of TypeParameter.foo
T: (str, bytes), kind: TypeVar
\ No newline at end of file diff --git a/python/testData/quickdoc/TypeParameter.py b/python/testData/quickdoc/TypeParameter.py new file mode 100644 index 000000000000..1977020d0a9b --- /dev/null +++ b/python/testData/quickdoc/TypeParameter.py @@ -0,0 +1,2 @@ +def foo[T: (str, bytes)](x: T) -> T: + return x \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java b/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java index 208e98586119..f7d91e585bc3 100644 --- a/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java +++ b/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java @@ -856,6 +856,21 @@ public class Py3QuickDocTest extends LightMarkedTestCase { checkHTMLOnly(); } + // PY-64074 + public void testTypeParameter() { + checkHTMLOnly(); + } + + // PY-64074 + public void testTypeKeyword() { + checkHTMLOnly(); + } + + // PY-64074 + public void testTypeAliasStatement() { + checkHTMLOnly(); + } + @Override protected String getTestDataPath() { return super.getTestDataPath() + "/quickdoc/";