diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
index f3a5392c542a..4b53abe84d31 100644
--- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml
+++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
@@ -93,6 +93,10 @@
+
+
+
diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
index 1825ca92b96d..5d5d523e75ea 100644
--- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties
+++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
@@ -516,6 +516,7 @@ ACT.from.some.module.import=Import from\u2026
filetype.python.docstring.description=Python docstring
filetype.python.function.type.annotation.description=Python PEP-484 function type comment
+filetype.python.type.representation.description=Type representation language
filetype.python.type.hint.description=Python PEP-484 type hint
python.docstring.format=Docstring format:
python.docstring.select.type=Select Docstring Type
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationDialect.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationDialect.kt
new file mode 100644
index 000000000000..8b71aff2a999
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationDialect.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2000-2025 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.codeInsight.typeRepresentation
+
+import com.intellij.lang.DependentLanguage
+import com.intellij.lang.Language
+import com.jetbrains.python.PythonLanguage
+
+/**
+ * Used to represent types that are not possible to express in the language like callable types
+ *
+ * This is used to serialize types, and for type engine communication
+ */
+object PyTypeRepresentationDialect : Language(PythonLanguage.getInstance(), "PyTypeRepresentation"), DependentLanguage
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationElementTypes.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationElementTypes.kt
new file mode 100644
index 000000000000..12fee0b8459e
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationElementTypes.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2000-2025 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.codeInsight.typeRepresentation
+
+import com.intellij.psi.tree.IElementType
+import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyFunctionTypeRepresentation
+import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyNamedParameterTypeRepresentation
+import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyParameterListRepresentation
+import com.jetbrains.python.psi.PyElementType
+import com.jetbrains.python.psi.impl.PyElementImpl
+
+object PyTypeRepresentationElementTypes {
+ val FUNCTION_SIGNATURE: PyElementType = PyElementType("FUNCTION_SIGNATURE") { node -> PyFunctionTypeRepresentation(node) }
+ val PARAMETER_TYPE_LIST: PyElementType = PyElementType("PARAMETER_TYPE_LIST") { node -> PyParameterListRepresentation(node) }
+ val NAMED_PARAMETER_TYPE: PyElementType = PyElementType("NAMED_PARAMETER_TYPE") { node -> PyNamedParameterTypeRepresentation(node) }
+ val PLACEHOLDER: IElementType = PyElementType("PLACEHOLDER") { node -> PyElementImpl(node) }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileElementType.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileElementType.kt
new file mode 100644
index 000000000000..94f8d7f4b13f
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileElementType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2000-2025 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.codeInsight.typeRepresentation
+
+import com.jetbrains.python.psi.PyFileElementType
+
+object PyTypeRepresentationFileElementType : PyFileElementType(PyTypeRepresentationDialect) {
+ override fun getExternalId(): String = "PyTypeRepresentation.ID"
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileType.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileType.kt
new file mode 100644
index 000000000000..29b1511690e4
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileType.kt
@@ -0,0 +1,19 @@
+package com.jetbrains.python.codeInsight.typeRepresentation
+
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.PythonFileType
+import org.jetbrains.annotations.NonNls
+
+object PyTypeRepresentationFileType : PythonFileType(PyTypeRepresentationDialect) {
+ override fun getName(): @NonNls String {
+ return "PythonTypeRepresentation"
+ }
+
+ override fun getDescription(): String {
+ return PyPsiBundle.message("filetype.python.type.representation.description")
+ }
+
+ override fun getDefaultExtension(): String {
+ return "pythonTypeRepresentation"
+ }
+}
\ No newline at end of file
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParser.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParser.kt
new file mode 100644
index 000000000000..a5a67b534011
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParser.kt
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2000-2025 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.codeInsight.typeRepresentation
+
+import com.intellij.lang.SyntaxTreeBuilder
+import com.intellij.openapi.util.NlsContexts
+import com.intellij.psi.tree.IElementType
+import com.jetbrains.python.PyElementTypes
+import com.jetbrains.python.PyParsingBundle
+import com.jetbrains.python.PyTokenTypes
+import com.jetbrains.python.parsing.ExpressionParsing
+import com.jetbrains.python.parsing.ParsingContext
+import com.jetbrains.python.parsing.PyParser
+import com.jetbrains.python.parsing.StatementParsing
+import com.jetbrains.python.psi.LanguageLevel
+
+class PyTypeRepresentationParser : PyParser() {
+ override fun createParsingContext(builder: SyntaxTreeBuilder?, languageLevel: LanguageLevel?): ParsingContext {
+ return object : ParsingContext(builder, languageLevel) {
+ private val myExpressionParsing = TypeRepresentationParser(this)
+ private val myStatementParsing = object : StatementParsing(this) {
+ override fun parseStatement() {
+ expressionParser.parseExpression()
+ }
+ }
+
+ override fun getExpressionParser(): ExpressionParsing = myExpressionParsing
+ override fun getStatementParser(): StatementParsing = myStatementParsing
+ }
+ }
+
+ private class TypeRepresentationParser(context: ParsingContext?) : ExpressionParsing(context) {
+ override fun parseExpression() {
+ if (myBuilder.eof()) {
+ return
+ }
+ // Skip statement breaks - they're not part of type expressions
+ if (atToken(PyTokenTypes.STATEMENT_BREAK)) {
+ myBuilder.advanceLexer()
+ return
+ }
+
+ val startOffset = myBuilder.currentOffset
+
+ // Handle @Todo(...) annotations for unsupported type constructs
+ if (atToken(PyTokenTypes.AT)) {
+ parseTodoAnnotation()
+ return
+ }
+
+ // Try to parse function type (callable syntax) first
+ if (!parseFunctionType()) {
+ // Not a function type, parse as regular expression
+ super.parseSingleExpression(false)
+ // Ensure we made progress
+ if (myBuilder.currentOffset == startOffset && !myBuilder.eof()) {
+ // We didn't consume anything, advance to avoid infinite loop
+ myBuilder.error("Unexpected token") // NON-NLS
+ myBuilder.advanceLexer()
+ }
+ }
+ }
+
+ /**
+ * Parses @Todo(...) style annotations used to mark unsupported type constructs.
+ * These are opaque markers and their content is not interpreted.
+ */
+ private fun parseTodoAnnotation() {
+ assert(atToken(PyTokenTypes.AT))
+ val annotationMarker = myBuilder.mark()
+ myBuilder.advanceLexer() // consume @
+
+ // Consume tokens until we hit a structural boundary
+ var depth = 0
+ while (!myBuilder.eof()) {
+ val token = myBuilder.getTokenType()
+ when {
+ token == PyTokenTypes.LPAR -> {
+ depth++
+ myBuilder.advanceLexer()
+ }
+ token == PyTokenTypes.RPAR -> {
+ if (depth == 0) break // Structural boundary (parameter list close, etc.)
+ depth--
+ myBuilder.advanceLexer()
+ }
+ token == PyTokenTypes.COMMA && depth == 0 -> break // Structural boundary
+ token == PyTokenTypes.RARROW && depth == 0 -> break // Structural boundary (->)
+ token == PyTokenTypes.STATEMENT_BREAK -> break // Structural boundary
+ else -> myBuilder.advanceLexer()
+ }
+ }
+ annotationMarker.done(PyTypeRepresentationElementTypes.PLACEHOLDER)
+ }
+
+ fun parseFunctionType(): Boolean {
+ if (atToken(PyTokenTypes.LPAR)) {
+ val mark = myBuilder.mark()
+ parseParameterTypeList()
+ // Check if this is a function type (has ->) or just a parenthesized expression/tuple
+ if (atToken(PyTokenTypes.RARROW)) {
+ myBuilder.advanceLexer() // consume ->
+ // Use parseExpression to handle nested function types in return position
+ parseExpression()
+ mark.done(PyTypeRepresentationElementTypes.FUNCTION_SIGNATURE)
+ return true
+ }
+ else {
+ // Not a function type, rollback
+ mark.rollbackTo()
+ return false
+ }
+ }
+ return false
+ }
+
+ fun parseParameterTypeList() {
+ assert(atToken(PyTokenTypes.LPAR))
+ val listMark = myBuilder.mark()
+ myBuilder.advanceLexer()
+
+ var paramCount = 0
+ while (!(atAnyOfTokens(PyTokenTypes.RPAR, PyTokenTypes.RARROW, PyTokenTypes.STATEMENT_BREAK) || myBuilder.eof())) {
+ val currentOffset = myBuilder.currentOffset
+
+ if (paramCount > 0) {
+ if (!checkMatches(PyTokenTypes.COMMA, PyParsingBundle.message("PARSE.expected.comma"))) {
+ // No comma found, break out of parameter parsing
+ break
+ }
+ }
+ val parsed: Boolean
+ if (atToken(PyTokenTypes.DIV)) {
+ // Handle positional-only separator /
+ val slashMarker = myBuilder.mark()
+ myBuilder.advanceLexer()
+ slashMarker.done(PyElementTypes.SLASH_PARAMETER)
+ parsed = true
+ }
+ else if (atToken(PyTokenTypes.MULT)) {
+ val starMarker = myBuilder.mark()
+ myBuilder.advanceLexer()
+ // Check if this is a named *args parameter (*name: type)
+ if (atToken(PyTokenTypes.IDENTIFIER) && myBuilder.lookAhead(1) == PyTokenTypes.COLON) {
+ val namedVarargMarker = myBuilder.mark()
+ myBuilder.advanceLexer() // consume identifier
+ myBuilder.advanceLexer() // consume colon
+ // Parse the type expression, handling * prefix
+ if (atToken(PyTokenTypes.MULT)) {
+ val innerStarMarker = myBuilder.mark()
+ myBuilder.advanceLexer()
+ parseExpression()
+ innerStarMarker.done(PyElementTypes.STAR_EXPRESSION)
+ }
+ else {
+ parseExpression()
+ }
+ namedVarargMarker.done(PyTypeRepresentationElementTypes.NAMED_PARAMETER_TYPE)
+ }
+ else {
+ // Unnamed *args, just parse the type
+ parseExpression()
+ }
+ parsed = true
+ starMarker.done(PyElementTypes.STAR_EXPRESSION)
+ }
+ else if (atToken(PyTokenTypes.EXP)) {
+ val doubleStarMarker = myBuilder.mark()
+ myBuilder.advanceLexer()
+ // Check if this is a named **kwargs parameter (**name: type)
+ if (atToken(PyTokenTypes.IDENTIFIER) && myBuilder.lookAhead(1) == PyTokenTypes.COLON) {
+ val namedKwargMarker = myBuilder.mark()
+ myBuilder.advanceLexer() // consume identifier
+ myBuilder.advanceLexer() // consume colon
+ // Parse the type expression, handling ** prefix
+ if (atToken(PyTokenTypes.EXP)) {
+ val innerDoubleStarMarker = myBuilder.mark()
+ myBuilder.advanceLexer()
+ parseExpression()
+ innerDoubleStarMarker.done(PyElementTypes.DOUBLE_STAR_EXPRESSION)
+ }
+ else {
+ parseExpression()
+ }
+ namedKwargMarker.done(PyTypeRepresentationElementTypes.NAMED_PARAMETER_TYPE)
+ }
+ else {
+ // Unnamed **kwargs, just parse the type
+ parseExpression()
+ }
+ parsed = true
+ doubleStarMarker.done(PyElementTypes.DOUBLE_STAR_EXPRESSION)
+ }
+ else {
+ if (atToken(PyTokenTypes.AT)) {
+ parseTodoAnnotation()
+ parsed = true
+ }
+ // Check if this is a named parameter (name: type) or (name: type = default)
+ else if (atToken(PyTokenTypes.IDENTIFIER) && myBuilder.lookAhead(1) == PyTokenTypes.COLON) {
+ val namedParamMarker = myBuilder.mark()
+ myBuilder.advanceLexer() // consume identifier
+ myBuilder.advanceLexer() // consume colon
+ // Parse the type expression
+ parseExpression()
+ // Check for default value
+ if (atToken(PyTokenTypes.EQ)) {
+ myBuilder.advanceLexer() // consume =
+ parseExpression() // parse default value
+ }
+ namedParamMarker.done(PyTypeRepresentationElementTypes.NAMED_PARAMETER_TYPE)
+ parsed = myBuilder.currentOffset > currentOffset
+ }
+ else {
+ // Use parseExpression to handle nested function types and positional types
+ parseExpression()
+ parsed = myBuilder.currentOffset > currentOffset // Check we made progress
+ }
+ }
+ if (!parsed) {
+ myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"))
+ recoverUntilMatches(PyParsingBundle.message("PARSE.expected.expression"), PyTokenTypes.COMMA, PyTokenTypes.RPAR,
+ PyTokenTypes.RARROW, PyTokenTypes.STATEMENT_BREAK)
+ }
+ paramCount++
+
+ }
+ checkMatches(PyTokenTypes.RPAR, PyParsingBundle.message("PARSE.expected.rpar"))
+ listMark.done(PyTypeRepresentationElementTypes.PARAMETER_TYPE_LIST)
+ }
+
+ fun recoverUntilMatches(errorMessage: @NlsContexts.ParsingError String, vararg types: IElementType) {
+ val errorMarker = myBuilder.mark()
+ var hasNonWhitespaceTokens = false
+ while (!(atAnyOfTokens(*types) || myBuilder.eof())) {
+ // Regular whitespace tokens are already skipped by advancedLexer()
+ if (!atToken(PyTokenTypes.STATEMENT_BREAK)) {
+ hasNonWhitespaceTokens = true
+ }
+ myBuilder.advanceLexer()
+ }
+ if (hasNonWhitespaceTokens) {
+ errorMarker.error(errorMessage)
+ }
+ else {
+ errorMarker.drop()
+ }
+ }
+ }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParserDefinition.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParserDefinition.kt
new file mode 100644
index 000000000000..4ba7934ec8d0
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParserDefinition.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2025 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.codeInsight.typeRepresentation
+
+import com.intellij.lang.PsiParser
+import com.intellij.openapi.project.Project
+import com.intellij.psi.FileViewProvider
+import com.intellij.psi.PsiFile
+import com.intellij.psi.tree.IFileElementType
+import com.intellij.psi.tree.TokenSet
+import com.jetbrains.python.PythonParserDefinition
+import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyTypeRepresentationFile
+
+class PyTypeRepresentationParserDefinition : PythonParserDefinition() {
+ override fun getCommentTokens(): TokenSet = TokenSet.EMPTY
+
+ override fun createFile(viewProvider: FileViewProvider): PsiFile = PyTypeRepresentationFile(viewProvider)
+
+ override fun getFileNodeType(): IFileElementType = PyTypeRepresentationFileElementType
+
+ override fun createParser(project: Project?): PsiParser = PyTypeRepresentationParser()
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyFunctionTypeRepresentation.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyFunctionTypeRepresentation.kt
new file mode 100644
index 000000000000..00e4c4d56016
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyFunctionTypeRepresentation.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2000-2025 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.codeInsight.typeRepresentation.psi
+
+import com.intellij.lang.ASTNode
+import com.jetbrains.python.ast.findChildByClass
+import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
+import com.jetbrains.python.psi.PyDoubleStarExpression
+import com.jetbrains.python.psi.PyExpression
+import com.jetbrains.python.psi.PySlashParameter
+import com.jetbrains.python.psi.PyStarExpression
+import com.jetbrains.python.psi.impl.PyBuiltinCache
+import com.jetbrains.python.psi.impl.PyElementImpl
+import com.jetbrains.python.psi.types.*
+
+class PyFunctionTypeRepresentation(astNode: ASTNode) : PyElementImpl(astNode), PyExpression {
+ val parameterList: PyParameterListRepresentation
+ get() = findNotNullChildByClass(PyParameterListRepresentation::class.java)
+
+ val returnType: PyExpression?
+ get() = findChildByClass(PyExpression::class.java)
+
+ override fun getType(context: TypeEvalContext, key: TypeEvalContext.Key): PyType? {
+ val returnType = returnType ?: return null
+ val params = parameterList.parameters
+ val callableParams = params.map { param ->
+ when (param) {
+ is PySlashParameter -> {
+ // Positional-only separator
+ PyCallableParameterImpl.psi(param)
+ }
+ is PyNamedParameterTypeRepresentation -> {
+ val paramName = param.parameterName
+ val paramType = param.typeExpression?.let { resolveTypeExpression(it, context) }
+ PyCallableParameterImpl.nonPsi(paramName, paramType, param.defaultValue)
+ }
+ is PyStarExpression -> {
+ // *args parameter
+ // Check if it contains a named parameter
+ val namedParam = param.findChildByClass(PyNamedParameterTypeRepresentation::class.java)
+ if (namedParam != null) {
+ // *args: type
+ val paramName = namedParam.parameterName
+ val paramType = namedParam.typeExpression?.let { resolveTypeExpression(it, context) }
+ PyCallableParameterImpl.positionalNonPsi(paramName, paramType)
+ }
+ else {
+ // Unnamed *args: *type
+ val innerExpr = param.expression
+ val paramType = innerExpr?.let { resolveTypeExpression(it, context) }
+ PyCallableParameterImpl.positionalNonPsi(null, paramType)
+ }
+ }
+ is PyDoubleStarExpression -> {
+ // **kwargs parameter
+ // Check if it contains a named parameter
+ val namedParam = param.findChildByClass(PyNamedParameterTypeRepresentation::class.java)
+ if (namedParam != null) {
+ val paramName = namedParam.parameterName
+ val paramType = namedParam.typeExpression?.let {
+ if (it is PyDoubleStarExpression)
+ // Named kwargs unpacked: `**name: **type`
+ resolveTypeExpression(it.expression!!, context)
+ else {
+ // Named kwargs: `**name: type`, adapt to `dict`
+ val builtins = PyBuiltinCache.getInstance(it)
+ PyCollectionTypeImpl(
+ builtins.dictType!!.pyClass, false, listOf(builtins.strType, resolveTypeExpression(it, context))
+ )
+ }
+ }
+ PyCallableParameterImpl.keywordNonPsi(paramName, paramType)
+ }
+ else {
+ // Unnamed kwargs: `**type`
+ val innerExpr = param.expression
+ val paramType = innerExpr?.let { resolveTypeExpression(it, context) }
+ PyCallableParameterImpl.keywordNonPsi(null, paramType)
+ }
+ }
+ is PyExpression -> {
+ val paramType = resolveTypeExpression(param, context)
+ PyCallableParameterImpl.nonPsi(paramType)
+ }
+ else -> {
+ PyCallableParameterImpl.nonPsi(null)
+ }
+ }
+ }
+ val retType = resolveTypeExpression(returnType, context)
+ return PyCallableTypeImpl(callableParams, retType)
+ }
+
+ private fun resolveTypeExpression(expr: PyExpression, context: TypeEvalContext): PyType? = when (expr) {
+ is PyDoubleStarExpression -> PyTypingTypeProvider.getType(expr.expression!!, context)?.get()
+ else -> PyTypingTypeProvider.getType(expr, context)?.get()
+ }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyNamedParameterTypeRepresentation.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyNamedParameterTypeRepresentation.kt
new file mode 100644
index 000000000000..06a79b3a2c2e
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyNamedParameterTypeRepresentation.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2025 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.codeInsight.typeRepresentation.psi
+
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.jetbrains.python.PyTokenTypes
+import com.jetbrains.python.psi.PyExpression
+import com.jetbrains.python.psi.impl.PyElementImpl
+
+class PyNamedParameterTypeRepresentation(astNode: ASTNode) : PyElementImpl(astNode), PsiElement {
+ val parameterName: String?
+ get() = node.findChildByType(PyTokenTypes.IDENTIFIER)?.text
+
+ val typeExpression: PyExpression?
+ get() = findChildByClass(PyExpression::class.java)
+
+ val defaultValue: PyExpression?
+ get() {
+ // Find the expression after the = token
+ val children = node.getChildren(null)
+ var foundEquals = false
+ for (child in children) {
+ if (foundEquals && child.psi is PyExpression) {
+ return child.psi as PyExpression
+ }
+ if (child.elementType == PyTokenTypes.EQ) {
+ foundEquals = true
+ }
+ }
+ return null
+ }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyParameterListRepresentation.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyParameterListRepresentation.kt
new file mode 100644
index 000000000000..24d3c3665770
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyParameterListRepresentation.kt
@@ -0,0 +1,14 @@
+package com.jetbrains.python.codeInsight.typeRepresentation.psi
+
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.util.elementType
+import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationElementTypes
+import com.jetbrains.python.psi.PyExpression
+import com.jetbrains.python.psi.PySlashParameter
+import com.jetbrains.python.psi.impl.PyElementImpl
+
+class PyParameterListRepresentation(astNode: ASTNode) : PyElementImpl(astNode) {
+ val parameters: List
+ get() = children.filter { it is PyExpression || it is PySlashParameter || it is PyNamedParameterTypeRepresentation || it.elementType == PyTypeRepresentationElementTypes.PLACEHOLDER }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyTypeRepresentationFile.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyTypeRepresentationFile.kt
new file mode 100644
index 000000000000..5d6d80438c1f
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyTypeRepresentationFile.kt
@@ -0,0 +1,50 @@
+package com.jetbrains.python.codeInsight.typeRepresentation.psi
+
+import com.intellij.openapi.fileTypes.FileType
+import com.intellij.psi.FileViewProvider
+import com.intellij.psi.PsiElement
+import com.intellij.psi.impl.PsiManagerEx
+import com.intellij.testFramework.LightVirtualFile
+import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationDialect
+import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationFileType
+import com.jetbrains.python.psi.LanguageLevel
+import com.jetbrains.python.psi.PyExpression
+import com.jetbrains.python.psi.PyExpressionCodeFragment
+import com.jetbrains.python.psi.impl.PyFileImpl
+
+class PyTypeRepresentationFile : PyFileImpl, PyExpressionCodeFragment {
+
+ constructor(text: String, context: PsiElement) : super(
+ PsiManagerEx.getInstanceEx(context.project)
+ .fileManager
+ .createFileViewProvider(
+ LightVirtualFile(
+ "foo.bar",
+ PyTypeRepresentationFileType,
+ // workaround for PY-86754: strip def types
+ text.replace(Regex("(?:^|[^\"'])def [\\w.]+"), "")
+ ),
+ false)
+ ) {
+ myContext = context
+ }
+
+ constructor(viewProvider: FileViewProvider) : super(viewProvider, PyTypeRepresentationDialect) {
+ myContext = null
+ }
+
+ private val myContext: PsiElement?
+
+ override fun getFileType(): FileType = PyTypeRepresentationFileType
+
+ override fun toString(): String = "TypeRepresentation:$name"
+
+ override fun getLanguageLevel(): LanguageLevel =
+ // The same as for .pyi files
+ LanguageLevel.getLatest()
+
+ override fun getContext(): PsiElement? = if (myContext?.isValid == true) myContext else super.getContext()
+
+ val type: PyExpression?
+ get() = findChildByClass(PyExpression::class.java)
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java
index 7cbd653f05c0..7e7242d33523 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java
@@ -29,6 +29,7 @@ import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeAnnotation;
import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeAnnotationFile;
import com.jetbrains.python.codeInsight.typeHints.PyTypeHintFile;
+import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyFunctionTypeRepresentation;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyEvaluator;
@@ -1018,8 +1019,16 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
if (classType != null) {
return classType;
}
- if (context.myUseFqn && resolved.getText().equals("Unknown")) {
- return Ref.create();
+ if (context.myUseFqn) {
+ if (resolved.getText().equals("Unknown")) {
+ return Ref.create();
+ }
+ if (resolved instanceof PyFunctionTypeRepresentation function) {
+ var result = context.myContext.getType(function);
+ if (result != null) {
+ return Ref.create(result);
+ }
+ }
}
return null;
}
diff --git a/python/testData/typeRepresentation/parsing/ callable complex varargs.txt b/python/testData/typeRepresentation/parsing/ callable complex varargs.txt
new file mode 100644
index 000000000000..8d85bb849d7d
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable complex varargs.txt
@@ -0,0 +1,42 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyStarExpression
+ PsiElement(Py:MULT)('*')
+ PyNamedParameterTypeRepresentation
+ PsiElement(Py:IDENTIFIER)('a')
+ PsiElement(Py:COLON)(':')
+ PsiWhiteSpace(' ')
+ PyStarExpression
+ PsiElement(Py:MULT)('*')
+ PySubscriptionExpression
+ PyReferenceExpression: tuple
+ PsiElement(Py:IDENTIFIER)('tuple')
+ PsiElement(Py:LBRACKET)('[')
+ PyTupleExpression
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:COMMA)(',')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: str
+ PsiElement(Py:IDENTIFIER)('str')
+ PsiElement(Py:RBRACKET)(']')
+ PsiElement(Py:COMMA)(',')
+ PsiWhiteSpace(' ')
+ PyDoubleStarExpression
+ PsiElement(Py:EXP)('**')
+ PyNamedParameterTypeRepresentation
+ PsiElement(Py:IDENTIFIER)('b')
+ PsiElement(Py:COLON)(':')
+ PsiWhiteSpace(' ')
+ PyDoubleStarExpression
+ PsiElement(Py:EXP)('**')
+ PyReferenceExpression: B
+ PsiElement(Py:IDENTIFIER)('B')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable dangling comma.txt b/python/testData/typeRepresentation/parsing/ callable dangling comma.txt
new file mode 100644
index 000000000000..1f53dcfa25d8
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable dangling comma.txt
@@ -0,0 +1,17 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:COMMA)(',')
+ PsiErrorElement:Unexpected token
+
+ PsiElement(Py:RPAR)(')')
+ PsiErrorElement:')' expected
+
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable default parameter.txt b/python/testData/typeRepresentation/parsing/ callable default parameter.txt
new file mode 100644
index 000000000000..ab66bb97961a
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable default parameter.txt
@@ -0,0 +1,23 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyNamedParameterTypeRepresentation
+ PsiElement(Py:IDENTIFIER)('a')
+ PsiElement(Py:COLON)(':')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:EQ)('=')
+ PsiWhiteSpace(' ')
+ PyEllipsisLiteralExpression
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable ellipsis.txt b/python/testData/typeRepresentation/parsing/ callable ellipsis.txt
new file mode 100644
index 000000000000..3c308974d0c3
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable ellipsis.txt
@@ -0,0 +1,14 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyEllipsisLiteralExpression
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable empty function type.txt b/python/testData/typeRepresentation/parsing/ callable empty function type.txt
new file mode 100644
index 000000000000..4023f7ca065f
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable empty function type.txt
@@ -0,0 +1,2 @@
+TypeRepresentation:a.pythonTypeRepresentation
+
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable empty.txt b/python/testData/typeRepresentation/parsing/ callable empty.txt
new file mode 100644
index 000000000000..1be1186b369e
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable empty.txt
@@ -0,0 +1,10 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable named parameter.txt b/python/testData/typeRepresentation/parsing/ callable named parameter.txt
new file mode 100644
index 000000000000..9dadbb74e9dc
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable named parameter.txt
@@ -0,0 +1,16 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyNamedParameterTypeRepresentation
+ PsiElement(Py:IDENTIFIER)('a')
+ PsiElement(Py:COLON)(':')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable nested.txt b/python/testData/typeRepresentation/parsing/ callable nested.txt
new file mode 100644
index 000000000000..e02ad5b98b5b
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable nested.txt
@@ -0,0 +1,38 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:COMMA)(',')
+ PsiWhiteSpace(' ')
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: str
+ PsiElement(Py:IDENTIFIER)('str')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
+ PsiElement(Py:COMMA)(',')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: str
+ PsiElement(Py:IDENTIFIER)('str')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable no return type.txt b/python/testData/typeRepresentation/parsing/ callable no return type.txt
new file mode 100644
index 000000000000..1c3e033c23dc
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable no return type.txt
@@ -0,0 +1,10 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable no type after star.txt b/python/testData/typeRepresentation/parsing/ callable no type after star.txt
new file mode 100644
index 000000000000..b250a148d189
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable no type after star.txt
@@ -0,0 +1,16 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyStarExpression
+ PsiElement(Py:MULT)('*')
+ PsiErrorElement:Unexpected token
+
+ PsiElement(Py:RPAR)(')')
+ PsiErrorElement:')' expected
+
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable simple.txt b/python/testData/typeRepresentation/parsing/ callable simple.txt
new file mode 100644
index 000000000000..55cb459437ec
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable simple.txt
@@ -0,0 +1,16 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:COMMA)(',')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: str
+ PsiElement(Py:IDENTIFIER)('str')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ callable varargs.txt b/python/testData/typeRepresentation/parsing/ callable varargs.txt
new file mode 100644
index 000000000000..d617936f637b
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ callable varargs.txt
@@ -0,0 +1,20 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyStarExpression
+ PsiElement(Py:MULT)('*')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:COMMA)(',')
+ PsiWhiteSpace(' ')
+ PyDoubleStarExpression
+ PsiElement(Py:EXP)('**')
+ PyReferenceExpression: str
+ PsiElement(Py:IDENTIFIER)('str')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ todo in callable.txt b/python/testData/typeRepresentation/parsing/ todo in callable.txt
new file mode 100644
index 000000000000..720f38b9eec3
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ todo in callable.txt
@@ -0,0 +1,24 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyElement
+ PsiElement(Py:AT)('@')
+ PsiElement(Py:IDENTIFIER)('Todo')
+ PsiElement(Py:LPAR)('(')
+ PsiElement(Py:TICK)('`')
+ PsiElement(Py:IDENTIFIER)('Unpack')
+ PsiElement(Py:LBRACKET)('[')
+ PsiElement(Py:RBRACKET)(']')
+ PsiElement(Py:TICK)('`')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:IDENTIFIER)('special')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:IDENTIFIER)('form')
+ PsiElement(Py:RPAR)(')')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/ todo.txt b/python/testData/typeRepresentation/parsing/ todo.txt
new file mode 100644
index 000000000000..f41a0d1de441
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/ todo.txt
@@ -0,0 +1,15 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyElement
+ PsiElement(Py:AT)('@')
+ PsiElement(Py:IDENTIFIER)('Todo')
+ PsiElement(Py:LPAR)('(')
+ PsiElement(Py:TICK)('`')
+ PsiElement(Py:IDENTIFIER)('Unpack')
+ PsiElement(Py:LBRACKET)('[')
+ PsiElement(Py:RBRACKET)(']')
+ PsiElement(Py:TICK)('`')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:IDENTIFIER)('special')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:IDENTIFIER)('form')
+ PsiElement(Py:RPAR)(')')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableDanglingComma.txt b/python/testData/typeRepresentation/parsing/CallableDanglingComma.txt
new file mode 100644
index 000000000000..cdaee9445fe0
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableDanglingComma.txt
@@ -0,0 +1,17 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterTypeListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:COMMA)(',')
+ PsiErrorElement:Unexpected token
+
+ PsiElement(Py:RPAR)(')')
+ PsiErrorElement:')' expected
+
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableDefAsFirstType.txt b/python/testData/typeRepresentation/parsing/CallableDefAsFirstType.txt
new file mode 100644
index 000000000000..62b1686fb7f9
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableDefAsFirstType.txt
@@ -0,0 +1,14 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyParenthesizedExpression
+ PsiElement(Py:LPAR)('(')
+ PsiErrorElement:')' expected
+
+ PsiErrorElement:Unexpected token
+
+ PsiElement(Py:DEF_KEYWORD)('def')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: foo
+ PsiElement(Py:IDENTIFIER)('foo')
+ PsiErrorElement:Unexpected token
+
+ PsiElement(Py:RPAR)(')')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableEllipsis.txt b/python/testData/typeRepresentation/parsing/CallableEllipsis.txt
new file mode 100644
index 000000000000..a4be21b9e563
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableEllipsis.txt
@@ -0,0 +1,14 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterTypeListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyEllipsisLiteralExpression
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:DOT)('.')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyNoneLiteralExpression
+ PsiElement(Py:NONE_KEYWORD)('None')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableEmpty.txt b/python/testData/typeRepresentation/parsing/CallableEmpty.txt
new file mode 100644
index 000000000000..d0377053e877
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableEmpty.txt
@@ -0,0 +1,2 @@
+TypeRepresentation:a.typeRepresentation
+
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableEmptyFunctionType.txt b/python/testData/typeRepresentation/parsing/CallableEmptyFunctionType.txt
new file mode 100644
index 000000000000..4023f7ca065f
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableEmptyFunctionType.txt
@@ -0,0 +1,2 @@
+TypeRepresentation:a.pythonTypeRepresentation
+
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableLambdaAsFirstType.txt b/python/testData/typeRepresentation/parsing/CallableLambdaAsFirstType.txt
new file mode 100644
index 000000000000..6ddd1e6f0be2
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableLambdaAsFirstType.txt
@@ -0,0 +1,13 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyParenthesizedExpression
+ PsiElement(Py:LPAR)('(')
+ PyLambdaExpression
+ PsiElement(Py:LAMBDA_KEYWORD)('lambda')
+ PyParameterList
+
+ PsiElement(Py:COLON)(':')
+ PsiWhiteSpace(' ')
+ PyNumericLiteralExpression
+ PsiElement(Py:INTEGER_LITERAL)('42')
+ PsiErrorElement:')' expected
+
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableNoArrowAndReturnType.txt b/python/testData/typeRepresentation/parsing/CallableNoArrowAndReturnType.txt
new file mode 100644
index 000000000000..c722c09d25e6
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableNoArrowAndReturnType.txt
@@ -0,0 +1,6 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyParenthesizedExpression
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:RPAR)(')')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableNoClosingParenthesis.txt b/python/testData/typeRepresentation/parsing/CallableNoClosingParenthesis.txt
new file mode 100644
index 000000000000..433750e788da
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableNoClosingParenthesis.txt
@@ -0,0 +1,7 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyParenthesizedExpression
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiErrorElement:')' expected
+
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableNoReturnType.txt b/python/testData/typeRepresentation/parsing/CallableNoReturnType.txt
new file mode 100644
index 000000000000..053290075166
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableNoReturnType.txt
@@ -0,0 +1,10 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterTypeListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
+ PsiElement(Py:RPAR)(')')
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
\ No newline at end of file
diff --git a/python/testData/typeRepresentation/parsing/CallableNoTypeAfterStar.txt b/python/testData/typeRepresentation/parsing/CallableNoTypeAfterStar.txt
new file mode 100644
index 000000000000..daae37a6b760
--- /dev/null
+++ b/python/testData/typeRepresentation/parsing/CallableNoTypeAfterStar.txt
@@ -0,0 +1,16 @@
+TypeRepresentation:a.pythonTypeRepresentation
+ PyFunctionTypeRepresentation
+ PyParameterTypeListRepresentation
+ PsiElement(Py:LPAR)('(')
+ PyStarExpression
+ PsiElement(Py:MULT)('*')
+ PsiErrorElement:Unexpected token
+
+ PsiElement(Py:RPAR)(')')
+ PsiErrorElement:')' expected
+
+ PsiWhiteSpace(' ')
+ PsiElement(Py:RARROW)('->')
+ PsiWhiteSpace(' ')
+ PyReferenceExpression: int
+ PsiElement(Py:IDENTIFIER)('int')
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java b/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java
index f6b11af7a58e..ea6809f6443e 100644
--- a/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java
+++ b/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java
@@ -10,7 +10,6 @@ import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeA
import com.jetbrains.python.documentation.doctest.PyDocstringTokenSetContributor;
import com.jetbrains.python.psi.PyEllipsisLiteralExpression;
import com.jetbrains.python.psi.PyExpression;
-import com.jetbrains.python.psi.PyNoneLiteralExpression;
import com.jetbrains.python.psi.PythonVisitorFilter;
import com.jetbrains.python.psi.impl.PythonASTFactory;
import org.jetbrains.annotations.NotNull;
@@ -30,6 +29,7 @@ public class PyFunctionTypeAnnotationParsingTest extends ParsingTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
+ getApplication().registerService(PyElementTypesFacade.class, PyElementTypesFacadeImpl.class);
registerExtensionPoint(PythonDialectsTokenSetContributor.EP_NAME, PythonDialectsTokenSetContributor.class);
registerExtension(PythonDialectsTokenSetContributor.EP_NAME, new PythonTokenSetContributor());
registerExtension(PythonDialectsTokenSetContributor.EP_NAME, new PyDocstringTokenSetContributor());
diff --git a/python/testSrc/com/jetbrains/python/PyTypeRepresentationParsingTest.kt b/python/testSrc/com/jetbrains/python/PyTypeRepresentationParsingTest.kt
new file mode 100644
index 000000000000..d01bb482442d
--- /dev/null
+++ b/python/testSrc/com/jetbrains/python/PyTypeRepresentationParsingTest.kt
@@ -0,0 +1,156 @@
+// 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.python
+
+import com.intellij.lang.LanguageASTFactory
+import com.intellij.testFramework.ParsingTestCase
+import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationDialect
+import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationParserDefinition
+import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyFunctionTypeRepresentation
+import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyTypeRepresentationFile
+import com.jetbrains.python.documentation.doctest.PyDocstringTokenSetContributor
+import com.jetbrains.python.psi.PyEllipsisLiteralExpression
+import com.jetbrains.python.psi.PyExpression
+import com.jetbrains.python.psi.PythonVisitorFilter
+import com.jetbrains.python.psi.impl.PythonASTFactory
+import junit.framework.TestCase
+import java.io.IOException
+import org.junit.jupiter.api.assertInstanceOf as assertInstanceOfJunit5
+
+class PyTypeRepresentationParsingTest : ParsingTestCase("typeRepresentation/parsing", "pythonTypeRepresentation",
+ PyTypeRepresentationParserDefinition(), PythonParserDefinition()) {
+ @Throws(Exception::class)
+ override fun setUp() {
+ super.setUp()
+ application.registerService(PyElementTypesFacade::class.java, PyElementTypesFacadeImpl::class.java)
+ registerExtensionPoint(PythonDialectsTokenSetContributor.EP_NAME,
+ PythonDialectsTokenSetContributor::class.java)
+ registerExtension(PythonDialectsTokenSetContributor.EP_NAME, PythonTokenSetContributor())
+ registerExtension(PythonDialectsTokenSetContributor.EP_NAME, PyDocstringTokenSetContributor())
+ addExplicitExtension(LanguageASTFactory.INSTANCE, PythonLanguage.getInstance(), PythonASTFactory())
+ }
+
+ @Throws(Exception::class)
+ override fun tearDown() {
+ // clear cached extensions
+ try {
+ PythonVisitorFilter.INSTANCE.removeExplicitExtension(PythonLanguage.INSTANCE,
+ PythonVisitorFilter { _, _ -> false })
+ PythonVisitorFilter.INSTANCE.removeExplicitExtension(PyTypeRepresentationDialect,
+ PythonVisitorFilter { _, _ -> false })
+ }
+ catch (e: Throwable) {
+ addSuppressedException(e)
+ }
+ finally {
+ super.tearDown()
+ }
+ }
+
+ override fun doCodeTest(code: String) {
+ try {
+ super.doCodeTest(code)
+ }
+ catch (e: IOException) {
+ throw AssertionError(e)
+ }
+ }
+
+ fun parseCallable(code: String): PyFunctionTypeRepresentation {
+ doCodeTest(code)
+ return assertInstanceOfJunit5(parsedType)
+ }
+
+ private val parsedType: PyExpression? get() = assertInstanceOfJunit5(myFile).type
+
+ fun `test todo`() {
+ doCodeTest("@Todo(`Unpack[]` special form)")
+ }
+
+ fun `test todo in callable`() {
+ val callable = parseCallable("(@Todo(`Unpack[]` special form)) -> None")
+ assertNotNull(callable)
+ assertSize(1, callable.parameterList.parameters)
+ }
+
+ fun `test callable empty`() {
+ val callable = parseCallable("() -> None")
+ assertEmpty(callable.parameterList.parameters)
+ val returnType = callable.returnType
+ assertNotNull(returnType)
+ assertEquals("None", returnType!!.text)
+ }
+
+ fun `test callable simple`() {
+ parseCallable("(int, str) -> None")
+ }
+
+ fun `test callable named parameter`() {
+ parseCallable("(a: int) -> None")
+ }
+
+ fun `test callable default parameter`() {
+ parseCallable("(a: int = ...) -> None")
+ }
+
+ fun `test callable varargs`() {
+ parseCallable("(*int, **str) -> None")
+ }
+
+ fun `test callable complex varargs`() {
+ parseCallable("(*a: *tuple[int, str], **b: **B) -> None")
+ }
+
+ fun `test callable ellipsis`() {
+ val callable = parseCallable("(...) -> None")
+ val paramTypes = callable.parameterList.parameters
+ assertSize(1, paramTypes)
+ assertInstanceOfJunit5(paramTypes[0])
+ val returnType = callable.returnType
+ assertNotNull(returnType)
+ }
+
+ fun `test callable nested`() {
+ val callable = parseCallable("(int, (str) -> None, int) -> (str) -> None")
+ val paramTypes = callable.parameterList.parameters
+ assertSize(3, paramTypes)
+ val nestedCallable = assertInstanceOfJunit5(paramTypes[1])
+ assertSize(1, nestedCallable.parameterList.parameters)
+ val nestedReturnType = nestedCallable.returnType
+ assertNotNull(nestedReturnType)
+ TestCase.assertEquals(
+ "None", nestedReturnType!!.text
+ )
+
+ val returnType = assertInstanceOfJunit5(paramTypes[1])
+ assertSize(1, returnType.parameterList.parameters)
+ val returnTypeNestedReturnType = returnType.returnType
+ assertNotNull(returnTypeNestedReturnType)
+ TestCase.assertEquals(
+ "None", nestedReturnType.text
+ )
+ }
+
+ fun `test callable no return type`() {
+ val callable = parseCallable("(int) -> ")
+ val paramTypes = callable.parameterList.parameters
+ assertSize(1, paramTypes)
+ assertNull(callable.returnType)
+ }
+
+ fun `test callable dangling comma`() {
+ parseCallable("(int,) -> None")
+ }
+
+ fun `test callable empty function type`() {
+ doCodeTest("")
+ assertNull(parsedType)
+ }
+
+ fun `test callable no type after star`() {
+ parseCallable("(*) -> int")
+ }
+
+ override fun getTestDataPath(): String {
+ return PythonTestUtil.getTestDataPath()
+ }
+}