mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 11:50:54 +07:00
PY-65385 Retain unbound ParamSpecs as-is during type parameter substitution
The original problem with @contextlib.asynccontextmanager was due to a bug in PyTypeChecker.substitute introduced in the TypeVarTuple support. Namely, we started to substitute unmatched ParamSpec types with null, effectively replacing them in a callable signature with a single parameter of type Any. Then the special logic in PyCallExpressionHelper.mapArguments that treated unmatched ParamSpecs as "catch-all" varargs stopped working, and we started to highlight all extra arguments in the substituted callable invocations. In other words, binding type parameters from decorator targets, e.g. ParamSpecs or function return types, never worked because we can't resolve functions passed as decorator arguments in "de-sugared" expression fragments in the codeAnalysis context, i.e. when we replace ``` @deco def f(x: int, y: str): ... ``` with `deco(f)` and then try to infer its type in PyDecoratedFunctionTypeProvider, but we didn't report it thanks to that special-casing of unmatched ParamSpecs (other type parameters replaced by Any don't trigger such warnings). Ideally, we should start resolving references in arguments of function calls in such virtual expression fragments in some stub-safe manner instead of relying on this fallback logic. In the general case, however, complete stub-safe inference for decorators is a hard problem because arbitrary expressions can affect types of their return type, .e.g. ``` def deco(result: T) -> Callable[[Callable[P, Any]], Callable[P, T]]: ... @deco(arbitrary_call().foo + 42) # how to handle this without unstubbing? def f(x: int, y: str): ... ``` GitOrigin-RevId: adeb625611a3ebb7d5db523df00388d619323545
This commit is contained in:
committed by
intellij-monorepo-bot
parent
07cadf2cf2
commit
7320815ad2
@@ -1106,7 +1106,10 @@ public final class PyTypeChecker {
|
||||
}
|
||||
return substitution;
|
||||
}
|
||||
else if (type instanceof PyParamSpecType paramSpecType) {
|
||||
else if (type instanceof PyParamSpecType paramSpecType && paramSpecType.getParameters() == null) {
|
||||
if (!substitutions.paramSpecs.containsKey(paramSpecType)) {
|
||||
return paramSpecType;
|
||||
}
|
||||
PyParamSpecType substitution = substitutions.paramSpecs.get(paramSpecType);
|
||||
if (substitution != null && !substitution.equals(paramSpecType) && hasGenerics(substitution, context)) {
|
||||
return substitute(substitution, substitutions, context);
|
||||
@@ -1166,9 +1169,9 @@ public final class PyTypeChecker {
|
||||
for (PyCallableParameter parameter : parameters) {
|
||||
final var parameterType = parameter.getType(context);
|
||||
if (parameters.size() == 1 && parameterType instanceof PyParamSpecType) {
|
||||
final var parameterTypeSubst = substitutions.paramSpecs.get(parameterType);
|
||||
if (parameterTypeSubst != null && parameterTypeSubst.getParameters() != null) {
|
||||
substParams = parameterTypeSubst.getParameters();
|
||||
final var substitution = substitute(parameterType, substitutions, context);
|
||||
if (substitution instanceof PyParamSpecType paramSpecSubst && paramSpecSubst.getParameters() != null) {
|
||||
substParams = paramSpecSubst.getParameters();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from mod import func
|
||||
|
||||
|
||||
async def main():
|
||||
async with func(42, "foo"):
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def func(x: int, y: str):
|
||||
yield "foo"
|
||||
@@ -4441,6 +4441,21 @@ public class PyTypingTest extends PyTestCase {
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-65385
|
||||
public void testUnboundParamSpecFromUnresolvedArgumentRetained() {
|
||||
doTest("(ParamSpec(\"P\")) -> str",
|
||||
"""
|
||||
from typing import Callable, Any, ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
def deco(fn: Callable[P, Any]) -> Callable[P, str]:
|
||||
return ...
|
||||
|
||||
expr = deco(unresolved)
|
||||
""");
|
||||
}
|
||||
|
||||
private void doTestNoInjectedText(@NotNull String text) {
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, text);
|
||||
final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. and contributors. 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.inspections;
|
||||
|
||||
import com.intellij.testFramework.LightProjectDescriptor;
|
||||
import com.jetbrains.python.fixtures.PyInspectionTestCase;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class Py3ArgumentListInspectionTest extends PyInspectionTestCase {
|
||||
@NotNull
|
||||
@@ -136,4 +134,41 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase {
|
||||
non_working_function(1.1, 2.2)
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-70484
|
||||
public void testParamSpecInDecoratorReturnTypeCannotBeBoundFromArguments() {
|
||||
doTestByText("""
|
||||
from typing import Callable, Any, ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
def deco(fn: Callable[..., Any]) -> Callable[P, Any]:
|
||||
return fn
|
||||
|
||||
@deco
|
||||
def f(x: int):
|
||||
pass
|
||||
|
||||
f("foo", 42)
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-70484
|
||||
public void testParamSpecInDecoratorReturnTypeUnboundDueToUnresolvedArgument() {
|
||||
doTestByText("""
|
||||
from typing import Callable, Any, ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
def deco(fn: Callable[P, Any]) -> Callable[P, Any]:
|
||||
return fn
|
||||
|
||||
deco(unresolved)("foo", 42)
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-65385
|
||||
public void testImportedFunctionDecoratedWithAsyncContextManager() {
|
||||
doMultiFileTest();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user