mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 15:52:01 +07:00
[python] PY-54771 Suggest fixtures in @pytest.mark.usefixtures("")
Merge-request: IJ-MR-173095 Merged-by: Morgan Bartholomew <morgan.bartholomew@jetbrains.com> (cherry picked from commit f4a8479b755d71cd2932176acce98f4d2f8aaba5) IJ-MR-173095 GitOrigin-RevId: 382ee0e32c3a819d28fda7f5ef5db575517474ff
This commit is contained in:
committed by
intellij-monorepo-bot
parent
5e07291bd5
commit
83aac7f547
@@ -1,10 +1,12 @@
|
|||||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||||
package com.jetbrains.python.testing.pyTestFixtures
|
package com.jetbrains.python.testing.pyTestFixtures
|
||||||
|
|
||||||
|
import com.intellij.openapi.module.ModuleUtilCore
|
||||||
import com.intellij.openapi.util.Ref
|
import com.intellij.openapi.util.Ref
|
||||||
import com.intellij.openapi.util.TextRange
|
import com.intellij.openapi.util.TextRange
|
||||||
import com.intellij.patterns.PlatformPatterns
|
import com.intellij.patterns.PlatformPatterns
|
||||||
import com.intellij.psi.*
|
import com.intellij.psi.*
|
||||||
|
import com.intellij.psi.util.findParentOfType
|
||||||
import com.intellij.util.ArrayUtil
|
import com.intellij.util.ArrayUtil
|
||||||
import com.intellij.util.ProcessingContext
|
import com.intellij.util.ProcessingContext
|
||||||
import com.intellij.util.containers.ContainerUtil
|
import com.intellij.util.containers.ContainerUtil
|
||||||
@@ -14,7 +16,9 @@ import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.COROUTINE
|
|||||||
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.GENERATOR
|
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.GENERATOR
|
||||||
import com.jetbrains.python.psi.*
|
import com.jetbrains.python.psi.*
|
||||||
import com.jetbrains.python.psi.resolve.ImportedResolveResult
|
import com.jetbrains.python.psi.resolve.ImportedResolveResult
|
||||||
|
import com.jetbrains.python.psi.resolve.QualifiedNameFinder
|
||||||
import com.jetbrains.python.psi.types.*
|
import com.jetbrains.python.psi.types.*
|
||||||
|
import org.jetbrains.annotations.ApiStatus
|
||||||
|
|
||||||
class PyTestFixtureReference(pyElement: PsiElement, fixture: PyTestFixture, private val importElement: PyElement? = null, range: TextRange? = null) : BaseReference(pyElement, range), PsiPolyVariantReference {
|
class PyTestFixtureReference(pyElement: PsiElement, fixture: PyTestFixture, private val importElement: PyElement? = null, range: TextRange? = null) : BaseReference(pyElement, range), PsiPolyVariantReference {
|
||||||
private val functionRef = fixture.function?.let { SmartPointerManager.createPointer(it) }
|
private val functionRef = fixture.function?.let { SmartPointerManager.createPointer(it) }
|
||||||
@@ -33,6 +37,52 @@ class PyTestFixtureReference(pyElement: PsiElement, fixture: PyTestFixture, priv
|
|||||||
return resultList.toArray(emptyArray())
|
return resultList.toArray(emptyArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
override fun getVariants(): Array<Any> {
|
||||||
|
// Provide completion variants for fixtures, especially inside pytest.mark.usefixtures strings
|
||||||
|
val element = myElement
|
||||||
|
val project = element.project
|
||||||
|
val context = TypeEvalContext.codeCompletion(project, element.containingFile)
|
||||||
|
|
||||||
|
// If we are in a parameter, variants are handled elsewhere; be conservative and return empty
|
||||||
|
if (element !is PyStringLiteralExpression) return emptyArray()
|
||||||
|
|
||||||
|
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return emptyArray()
|
||||||
|
|
||||||
|
// Try to find the function we are attached to (decorated function)
|
||||||
|
val func = element.findParentOfType<PyFunction>()
|
||||||
|
|
||||||
|
val fixtureNames: List<String> = if (func != null) {
|
||||||
|
// For function-level decorators
|
||||||
|
getFixtures(module, func, context)
|
||||||
|
.filterNot { fixture ->
|
||||||
|
// `usefixtures` is only useful if no access to the fixture object is required.
|
||||||
|
// Otherwise, fixtures are provided via function arguments.
|
||||||
|
// Fixtures from pytest usually need some interaction to be useful.
|
||||||
|
fixture.function?.isFromPytestPackage() ?: false
|
||||||
|
}
|
||||||
|
.map { it.name }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// For class-level decorators and module-level function calls
|
||||||
|
findDecoratorsByName(module, TEST_FIXTURE_DECORATOR_NAMES)
|
||||||
|
.filterNot { dec ->
|
||||||
|
// `usefixtures` is only useful if no access to the fixture object is required.
|
||||||
|
// Otherwise, fixtures are provided via function arguments.
|
||||||
|
// Fixtures from pytest usually need some interaction to be useful.
|
||||||
|
dec.target?.isFromPytestPackage() == true
|
||||||
|
}
|
||||||
|
.mapNotNull { dec -> getTestFixtureName(dec) ?: dec.target?.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out builtin/reserved pytest fixtures and the special 'request' pseudo-fixture
|
||||||
|
val filtered = fixtureNames.filterNot { name ->
|
||||||
|
name in reservedFixturesSet || name in reservedFixtureClassSet || name == REQUEST_FIXTURE
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.distinct().sorted().toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleElementRename(newElementName: String): PsiElement {
|
override fun handleElementRename(newElementName: String): PsiElement {
|
||||||
if (myElement is PyStringLiteralExpression) {
|
if (myElement is PyStringLiteralExpression) {
|
||||||
return myElement.replace(PyElementGenerator.getInstance(myElement.project).createStringLiteralFromString(newElementName))
|
return myElement.replace(PyElementGenerator.getInstance(myElement.project).createStringLiteralFromString(newElementName))
|
||||||
@@ -85,7 +135,8 @@ private abstract class PyTestReferenceProvider : PsiReferenceProvider() {
|
|||||||
private object PyTestReferenceAsParameterProvider : PyTestReferenceProvider() {
|
private object PyTestReferenceAsParameterProvider : PyTestReferenceProvider() {
|
||||||
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
|
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
|
||||||
val namedParam = element as? PyNamedParameter ?: return emptyArray()
|
val namedParam = element as? PyNamedParameter ?: return emptyArray()
|
||||||
val namedFixtureParameterLink = getFixtureLink(namedParam, TypeEvalContext.codeAnalysis(element.project, element.containingFile)) ?: return emptyArray()
|
val namedFixtureParameterLink = getFixtureLink(namedParam, TypeEvalContext.codeAnalysis(element.project, element.containingFile))
|
||||||
|
?: return emptyArray()
|
||||||
val annotationLength = namedParam.annotation?.textLength ?: 0
|
val annotationLength = namedParam.annotation?.textLength ?: 0
|
||||||
return arrayOf(PyTestFixtureReference(namedParam, namedFixtureParameterLink.fixture, namedFixtureParameterLink.importElement, TextRange(0, element.textLength - annotationLength)))
|
return arrayOf(PyTestFixtureReference(namedParam, namedFixtureParameterLink.fixture, namedFixtureParameterLink.importElement, TextRange(0, element.textLength - annotationLength)))
|
||||||
}
|
}
|
||||||
@@ -93,7 +144,7 @@ private object PyTestReferenceAsParameterProvider : PyTestReferenceProvider() {
|
|||||||
|
|
||||||
private object PyTestReferenceAsStringProvider : PyTestReferenceProvider() {
|
private object PyTestReferenceAsStringProvider : PyTestReferenceProvider() {
|
||||||
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
|
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
|
||||||
if (element !is PyExpression) return emptyArray()
|
if (element !is PyStringLiteralExpression) return emptyArray()
|
||||||
val argumentList = element.parent as? PyArgumentList ?: return emptyArray()
|
val argumentList = element.parent as? PyArgumentList ?: return emptyArray()
|
||||||
val callExpr = argumentList.parent as? PyCallExpression ?: return emptyArray()
|
val callExpr = argumentList.parent as? PyCallExpression ?: return emptyArray()
|
||||||
var shouldProvide = false
|
var shouldProvide = false
|
||||||
@@ -113,11 +164,23 @@ private object PyTestReferenceAsStringProvider : PyTestReferenceProvider() {
|
|||||||
// else
|
// else
|
||||||
if (!shouldProvide) return emptyArray()
|
if (!shouldProvide) return emptyArray()
|
||||||
|
|
||||||
val namedFixtureParameterLink = getFixtureLink(element, TypeEvalContext.codeAnalysis(element.project, element.containingFile)) ?: return emptyArray()
|
val namedFixtureParameterLink = getFixtureLink(element, TypeEvalContext.codeAnalysis(element.project, element.containingFile))
|
||||||
return arrayOf(PyTestFixtureReference(element, namedFixtureParameterLink.fixture, namedFixtureParameterLink.importElement, TextRange(1, element.textLength - 1)))
|
|
||||||
|
return arrayOf(
|
||||||
|
if (namedFixtureParameterLink != null)
|
||||||
|
PyTestFixtureReference(element, namedFixtureParameterLink.fixture, namedFixtureParameterLink.importElement, TextRange(1, element.textLength - 1))
|
||||||
|
else
|
||||||
|
// Provide a soft reference to enable completion even when nothing is resolvable yet (e.g., empty string)
|
||||||
|
PyTestFixtureReference(element, PyTestFixture(null, null, element.stringValue), null, TextRange(1, element.textLength - 1))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun PyElement.isFromPytestPackage(): Boolean =
|
||||||
|
QualifiedNameFinder.findShortestImportableQName(containingFile)?.firstComponent.let {
|
||||||
|
it == "pytest" || it == "_pytest"
|
||||||
|
}
|
||||||
|
|
||||||
class PyTestFixtureReferenceContributor : PsiReferenceContributor() {
|
class PyTestFixtureReferenceContributor : PsiReferenceContributor() {
|
||||||
|
|
||||||
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
|
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def my_fixture():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("<caret>")
|
||||||
|
def test_abc():
|
||||||
|
...
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def my_fixture():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("<caret>")
|
||||||
|
class TestSuite:
|
||||||
|
def test_abc(self):
|
||||||
|
...
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def my_fixture():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Module-level usefixtures via pytestmark
|
||||||
|
pytestmark = pytest.mark.usefixtures("<caret>")
|
||||||
|
|
||||||
|
def test_abc():
|
||||||
|
...
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// 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.
|
// 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.testing
|
package com.jetbrains.python.testing
|
||||||
|
|
||||||
|
import com.intellij.idea.TestFor
|
||||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
||||||
import com.jetbrains.python.fixtures.PyTestCase
|
import com.jetbrains.python.fixtures.PyTestCase
|
||||||
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection
|
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection
|
||||||
@@ -45,6 +46,42 @@ class PyTestFixtureAndParametrizedTest : PyTestCase() {
|
|||||||
myFixture.checkResultByFile("after_test_test.txt")
|
myFixture.checkResultByFile("after_test_test.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestFor(issues = ["PY-54771"])
|
||||||
|
fun `test usefixtures completion suggests fix on function`() {
|
||||||
|
myFixture.copyDirectoryToProject(".", ".")
|
||||||
|
myFixture.configureByFile("test_usefixtures_completion.py")
|
||||||
|
myFixture.completeBasic()
|
||||||
|
val variants = myFixture.lookupElementStrings
|
||||||
|
assertNotNull(variants)
|
||||||
|
assertTrue(variants!!.contains("my_fixture"))
|
||||||
|
// Built-in pytest fixtures should not be suggested
|
||||||
|
assertFalse(variants.contains("tmp_path"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestFor(issues = ["PY-54771"])
|
||||||
|
fun `test usefixtures completion suggests fix on class`() {
|
||||||
|
myFixture.copyDirectoryToProject(".", ".")
|
||||||
|
myFixture.configureByFile("test_usefixtures_completion_class.py")
|
||||||
|
myFixture.completeBasic()
|
||||||
|
val variants = myFixture.lookupElementStrings
|
||||||
|
assertNotNull(variants)
|
||||||
|
assertTrue(variants!!.contains("my_fixture"))
|
||||||
|
// Built-in pytest fixtures should not be suggested
|
||||||
|
assertFalse(variants.contains("tmp_path"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestFor(issues = ["PY-54771"])
|
||||||
|
fun `test usefixtures completion suggests fix on module`() {
|
||||||
|
myFixture.copyDirectoryToProject(".", ".")
|
||||||
|
myFixture.configureByFile("test_usefixtures_completion_module.py")
|
||||||
|
myFixture.completeBasic()
|
||||||
|
val variants = myFixture.lookupElementStrings
|
||||||
|
assertNotNull(variants)
|
||||||
|
assertTrue(variants!!.contains("my_fixture"))
|
||||||
|
// Built-in pytest fixtures should not be suggested
|
||||||
|
assertFalse(variants.contains("tmp_path"))
|
||||||
|
}
|
||||||
|
|
||||||
fun testRename() {
|
fun testRename() {
|
||||||
myFixture.configureByFile("test_for_rename.py")
|
myFixture.configureByFile("test_for_rename.py")
|
||||||
myFixture.renameElementAtCaret("spam")
|
myFixture.renameElementAtCaret("spam")
|
||||||
|
|||||||
Reference in New Issue
Block a user