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.
+
+
+
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 @@
+
\ 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.
+
+
+
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 @@
+ty
pe 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 @@
+
\ 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/";