From 859db023202776888ebe662585b7d326cdae5ae1 Mon Sep 17 00:00:00 2001 From: Marcus Mews Date: Mon, 2 Mar 2026 16:22:36 +0000 Subject: [PATCH] PY-87890 pandas.DataFrame is considered Any with pandas >= 3 and pycharm 2026.1 (cherry picked from commit 7bab57dfde79fc86f672f7cb14193d7e1cbcdd6a) IJ-MR-193139 GitOrigin-RevId: dc4a5bd0a1f4deb2865525b7db9841ab57ac42dd --- .../psi/types/PyTypeInferenceCspFactory.kt | 20 +++--- .../psi/types/PyTypeInferenceCspSolver.kt | 61 +++++++++------- .../inspections/PyTypeInferenceCspTest.kt | 69 ++++++++++++++++++- 3 files changed, 115 insertions(+), 35 deletions(-) diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspFactory.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspFactory.kt index 6c58063fe846..e8ab47ad0271 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspFactory.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspFactory.kt @@ -175,17 +175,17 @@ object PyTypeInferenceCspFactory { throw NotSupportedException() } - for (paramType in generics.typeVars) { - if (builder.hasInferenceVariable(paramType)) continue - builder.addInferenceVariable(paramType) + for (typeParam in generics.typeVars) { + if (builder.hasInferenceVariable(typeParam)) continue + builder.addInferenceVariable(typeParam) // bounds - if (paramType.getBound() != null) { - val typeVarBound_selfBounded = substituteSelfTypes(paramType.getBound(), receiverType, context) + if (typeParam.getBound() != null) { + val typeVarBound_selfBounded = substituteSelfTypes(typeParam.getBound(), receiverType, context) // semantics: TV <: Bound - builder.addConstraint(paramType, typeVarBound_selfBounded, Variance.COVARIANT, ConstraintPriority.HIGH) + builder.addConstraint(typeParam, typeVarBound_selfBounded, Variance.COVARIANT, ConstraintPriority.HIGH) } - else if (paramType.getConstraints().isNotEmpty()) { + else if (typeParam.getConstraints().isNotEmpty()) { // Note: The Python type variable constraint(s) cannot be fully modeled without a specific CSP constraint that would model a strict logical OR. // A logical OR does unfortunately come with a performance impact since it makes backtracking during the solving process inevitable. // As a solution, Python type variable constraints will be modeled using an approximation that ensures that the type variable is both @@ -194,13 +194,13 @@ object PyTypeInferenceCspFactory { // Only at the very end, during instantiation, an actual set of remaining tv-constraints is chosen. // Note that both of these bounds are necessary to ensure that the TV will be instantiated as exactly one of the given tv-constraints // and not as a subtype of one of the given tv-constraints. - val paramTypeConstraints = paramType.getConstraints().map { substituteSelfTypes(it, receiverType, context) } + val paramTypeConstraints = typeParam.getConstraints().map { substituteSelfTypes(it, receiverType, context) } val intersectionOfConstraints = PyIntersectionType.intersection(paramTypeConstraints) val unionOfConstraints = PyUnionType.union(paramTypeConstraints) // semantics: TV approximates CV_1 ⊕ CV_2 ⊕ ... ⊕ CV_n by // CV_1 & CV_2 & ... & CV_n <: TV <: CV_1 | CV_2 | ... | CV_n - builder.addConstraint(paramType, intersectionOfConstraints, Variance.CONTRAVARIANT, ConstraintPriority.HIGH) - builder.addConstraint(paramType, unionOfConstraints, Variance.COVARIANT, ConstraintPriority.HIGH) + builder.addConstraint(typeParam, intersectionOfConstraints, Variance.CONTRAVARIANT, ConstraintPriority.HIGH) + builder.addConstraint(typeParam, unionOfConstraints, Variance.COVARIANT, ConstraintPriority.HIGH) } } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspSolver.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspSolver.kt index 85c28040d1ad..792498540d52 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspSolver.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeInferenceCspSolver.kt @@ -138,33 +138,42 @@ class CspBuilder(val context: TypeEvalContext) { fun getSolution(keepUnconstrained: Boolean): Solution { val instantiations = if (cp.failed) cp.instantiations else cp.solution val typeVars2TypeRefs: MutableMap> = LinkedHashMap() - for (entry in instantiations) { - val instantiatedType = entry.value - val instantiatedTypeOrTypeVar: PyType? - if (instantiatedType is PyUnconstrainedTypeVariable) { - val originalTypeVar = instantiatedType.typeVariable - instantiatedTypeOrTypeVar = if (keepUnconstrained) originalTypeVar else originalTypeVar.defaultType?.get() - } - else if (instantiatedType is PyTypeVarType) { - // if the solution is another PyTypeVarType, check the declared default types - if (instantiatedType.defaultType?.get() != null) { - instantiatedTypeOrTypeVar = instantiatedType.defaultType?.get() - } - else if (entry.key.typeVariable.defaultType?.get() != null) { - instantiatedTypeOrTypeVar = entry.key.typeVariable.defaultType?.get() - } - else { - instantiatedTypeOrTypeVar = instantiatedType - } - } - else { - instantiatedTypeOrTypeVar = instantiatedType - } - typeVars2TypeRefs[entry.key.typeVariable] = Ref.create(instantiatedTypeOrTypeVar) + for ((inferenceVariable, instantiatedType) in instantiations) { + val instantiatedTypeOrTypeVar = getPostComputedSolution(instantiatedType, inferenceVariable, keepUnconstrained) + typeVars2TypeRefs[inferenceVariable.typeVariable] = Ref.create(instantiatedTypeOrTypeVar) } val complete = instantiations.keys.containsAll(cp.inferenceVars.values()) return Solution(cp.failed, complete, typeVars2TypeRefs) } + + private fun getPostComputedSolution( + instantiatedType: PyType?, + inferenceVariable: InferenceVariable, + keepUnconstrained: Boolean, + ): PyType? { + when (instantiatedType) { + is PyUnconstrainedTypeVariable -> { + val originalTypeVar = instantiatedType.typeVariable + return when { + keepUnconstrained -> originalTypeVar + originalTypeVar.defaultType != null -> originalTypeVar.defaultType?.get() + originalTypeVar.bound != null -> originalTypeVar.bound + else -> null + } + } + is PyTypeVarType -> { + // if the solution is another PyTypeVarType, check the declared default types + return when { + instantiatedType.defaultType?.get() != null -> instantiatedType.defaultType?.get() + inferenceVariable.typeVariable.defaultType?.get() != null -> inferenceVariable.typeVariable.defaultType?.get() + else -> instantiatedType + } + } + else -> { + return instantiatedType + } + } + } } enum class ConstraintPriority { @@ -1371,6 +1380,12 @@ private object TypeBoundResolver { return PyUnionType.union(lowerBoundsWidened) } else if (lowerBounds.isEmpty() && upperBounds.isNotEmpty()) { + if (upperBounds.size == 1 && upperBounds[0] == infVar.typeVariable.bound) { + // special case: the type variable is constrained only by its bound (i.e., `[T : int]`). + // Therefore, we treat this type variable as unconstrained and use its bound when necessary based on `#keepUnconstrained`. + return PyUnconstrainedTypeVariable(infVar.typeVariable) + } + // It is debatable whether we should just return PyIntersectionType.intersection(*upperBounds) // Note however that intersection types are not part of Python (as of 2026). // Hence, the following logic makes it mandatory that the user declares a common subtype at some point. diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeInferenceCspTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyTypeInferenceCspTest.kt index cdbc6514fe2c..0f90f64743f5 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeInferenceCspTest.kt +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeInferenceCspTest.kt @@ -386,7 +386,7 @@ class PyTypeInferenceCspTest : PyInspectionTestCase() { fun `test Handle inferred intersections 2`() { doTestByText(""" - from typing import Callable, TypeVar, assert_type, Never + from typing import Callable, TypeVar, assert_type, Never, Any class A: ... class B: ... @@ -437,7 +437,6 @@ class PyTypeInferenceCspTest : PyInspectionTestCase() { """) } - @TestFor(issues = ["PY-86098"]) fun `test PY-86098`() { doTestByText(""" @@ -448,4 +447,70 @@ class PyTypeInferenceCspTest : PyInspectionTestCase() { assert_type(a1, A[int]) """) } + + @TestFor(issues = ["PY-87890"]) + fun `test Nested csp with type parameter bound`() { + doTestByText(""" + from typing import Any, Callable, assert_type + + def f[F: Callable[..., Any]]() -> Callable[[F], F]: + return lambda x: x + + assert_type(f()(lambda x: 1)(1), int) + """) + } + + @TestFor(issues = ["PY-87890"]) + fun `test Nested csp with type parameter default`() { + doTestByText(""" + from typing import Callable, assert_type, Optional + + def f[T = str]() -> Callable[[Optional[T]], T]: ... + + assert_type(f()(2), int) + assert_type(f()(), str) + """) + } + + fun `test Nested csp with type parameter default Any`() { + doTestByText(""" + from typing import Callable, assert_type, Any, Optional + + def f[T = Any]() -> Callable[[Optional[T]], T]: ... + + assert_type(f()(2), int) + assert_type(f()(), Any) + """) + } + + fun `test Nested csp with type parameter constraint`() { + fixme("Support for combined CSPs necessary", AssertionError::class.java) { + doTestByText(""" + from typing import Callable, assert_type, Any + + def f[T : (str, int)]() -> Callable[[T], T]: ... + + assert_type(f()(2), int) + assert_type(f()("s"), str) + """) + } + } + + fun `test Keep unconstrained type parameters for type return`() { + doTestByText(""" + from typing import Generic, TypeVar + + T = TypeVar("T", infer_variance=False) + + class Box(Generic[T]): + ... + + def box_class() -> type[Box[T]]: + return Box + + C = box_class() + box_int : Box[int] = C() + assert_type(box_int, Box[int]) + """) + } } \ No newline at end of file