mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-90726 install Python CLI tools via uv when available
Add a pluggable PyToolManagerProvider EP (PyToolManager: uv tool install, pip fallback) and PyTool models for uv, hatch, poetry, pipenv. Route the Add Interpreter, pyproject, and External Tools flows through PyTool.performToolInstallation. Prefer uv (latest, isolated) over pip and drop the pinned Poetry 1.8.0. IJ-MR-211863 GitOrigin-RevId: 52cd9e4bd22303c1a6a7d9a27059ffca6120cd73
This commit is contained in:
committed by
intellij-monorepo-bot
parent
39aa5467fd
commit
ca1cb3145a
@@ -513,6 +513,7 @@ jvm_library(
|
||||
"//python/python-uv/backend",
|
||||
"//platform/testIntegration",
|
||||
"//platform/testIntegration-ui",
|
||||
"//python/python-poetry/backend",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -669,6 +670,7 @@ jvm_library(
|
||||
"//python/python-uv/backend:backend_test_lib",
|
||||
"//platform/testIntegration:testIntegration_test_lib",
|
||||
"//platform/testIntegration-ui:testIntegration-ui_test_lib",
|
||||
"//python/python-poetry/backend:backend_test_lib",
|
||||
],
|
||||
)
|
||||
### auto-generated section `build intellij.python.community.impl` end
|
||||
|
||||
@@ -208,5 +208,6 @@
|
||||
<orderEntry type="module" module-name="intellij.python.uv.backend" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testIntegration" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testIntegration.ui" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.impl.poetry.backend" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -28,6 +28,8 @@ jvm_library(
|
||||
"//python/python-sdk:sdk",
|
||||
"//platform/util",
|
||||
"//python/python-process-output/common",
|
||||
"//python/openapi:community",
|
||||
"//python/python-pytools:pytools",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -47,6 +49,8 @@ jvm_library(
|
||||
"//python/python-sdk:sdk_test_lib",
|
||||
"//platform/util:util_test_lib",
|
||||
"//python/python-process-output/common:common_test_lib",
|
||||
"//python/openapi:community_test_lib",
|
||||
"//python/python-pytools:pytools_test_lib",
|
||||
],
|
||||
)
|
||||
### auto-generated section `build intellij.python.community.impl.pipenv` end
|
||||
|
||||
@@ -15,5 +15,7 @@
|
||||
<orderEntry type="module" module-name="intellij.python.sdk" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util" />
|
||||
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
|
||||
<orderEntry type="module" module-name="intellij.python.community" />
|
||||
<orderEntry type="module" module-name="intellij.python.pytools" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -2,8 +2,11 @@
|
||||
<dependencies>
|
||||
<module name="intellij.python.sdk"/>
|
||||
<module name="intellij.python.processOutput.common"/>
|
||||
<module name="intellij.python.community"/>
|
||||
<module name="intellij.python.pytools"/>
|
||||
</dependencies>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<python.pytools.pyTool implementation="com.intellij.python.community.impl.pipenv.PipEnvPyTool"/>
|
||||
<python.processOutput.common.processOutputIconMapping implementation="com.intellij.python.community.impl.pipenv.PipenvIconMapping"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
python.pipenv.tool.description=A Python dependency and virtual environment manager.
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.impl.pipenv
|
||||
|
||||
import com.intellij.python.community.impl.pipenv.PyPipenvBundle.message
|
||||
import com.intellij.python.community.impl.pipenv.icons.PythonCommunityImplPipenvIcons
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* [Pipenv](https://pipenv.pypa.io/) — a Python dependency and virtual-environment manager maintained
|
||||
* under the PyPA. It combines pip and virtualenv into a single workflow, tracking declared and locked
|
||||
* dependencies in `Pipfile` and `Pipfile.lock` and creating a per-project virtual environment.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class PipEnvPyTool : PyTool {
|
||||
override val presentableName: String = "Pipenv"
|
||||
override val packageName: PyPackageName = PyPackageName.from("pipenv")
|
||||
override val description: String get() = message("python.pipenv.tool.description")
|
||||
// TODO: Provide a special icon for pipenv
|
||||
override val icon: Icon get() = PythonCommunityImplPipenvIcons.PythonClosed
|
||||
|
||||
@Suppress("CompanionObjectInExtension")
|
||||
companion object {
|
||||
fun getInstance(): PipEnvPyTool = PyTool.EP_NAME.findExtensionOrFail(PipEnvPyTool::class.java)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,6 @@ import com.intellij.python.processOutput.common.ProcessOutputIconMapping
|
||||
|
||||
internal class PipenvIconMapping : ProcessOutputIconMapping() {
|
||||
override val mapping: Map<ProcessBinaryFileName, ProcessIcon> = mapOf(
|
||||
ProcessBinaryFileName("pipenv") to ProcessIcon(PIPENV_ICON, PythonCommunityImplPipenvIcons::class.java)
|
||||
ProcessBinaryFileName("pipenv") to ProcessIcon(PythonCommunityImplPipenvIcons.PythonClosed, PythonCommunityImplPipenvIcons::class.java)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.impl.pipenv
|
||||
|
||||
import com.intellij.DynamicBundle
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
|
||||
internal object PyPipenvBundle {
|
||||
private const val BUNDLE = "messages.PyPipenvBundle"
|
||||
|
||||
private val INSTANCE = DynamicBundle(PyPipenvBundle::class.java, BUNDLE)
|
||||
|
||||
fun message(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg params: Any): @Nls String {
|
||||
return INSTANCE.getMessage(key, *params)
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,5 @@ package com.intellij.python.community.impl.pipenv
|
||||
import com.intellij.python.community.impl.pipenv.icons.PythonCommunityImplPipenvIcons
|
||||
import javax.swing.Icon
|
||||
|
||||
// TODO: Provide a special icon for pipenv
|
||||
@Deprecated("This icon is not the icon of pipenv")
|
||||
val PIPENV_ICON: Icon = PythonCommunityImplPipenvIcons.PythonClosed
|
||||
@@ -12,8 +12,8 @@
|
||||
<module name="intellij.platform.structureView"/>
|
||||
<module name="intellij.platform.testRunner"/>
|
||||
<module name="intellij.platform.ui.jcef"/>
|
||||
<module name="intellij.python.community.common"/>
|
||||
<module name="intellij.python.community"/>
|
||||
<module name="intellij.python.community.common"/>
|
||||
<module name="intellij.python.community.core.impl"/>
|
||||
<module name="intellij.python.community.execService"/>
|
||||
<module name="intellij.python.community.execService.python"/>
|
||||
@@ -21,6 +21,7 @@
|
||||
<module name="intellij.python.community.impl.conda"/>
|
||||
<module name="intellij.python.community.impl.installer"/>
|
||||
<module name="intellij.python.community.impl.pipenv"/>
|
||||
<module name="intellij.python.community.impl.poetry.backend"/>
|
||||
<module name="intellij.python.community.impl.poetry.common"/>
|
||||
<module name="intellij.python.community.interpreters"/>
|
||||
<module name="intellij.python.community.services.systemPython"/>
|
||||
@@ -49,6 +50,7 @@
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<applicationService serviceInterface="com.jetbrains.python.errorProcessing.ErrorSink"
|
||||
serviceImplementation="com.jetbrains.python.impl.ShowingMessageErrorSink"/>
|
||||
<python.pytools.pyToolManagerProvider implementation="com.jetbrains.python.sdk.configuration.SystemPythonToolManagerProvider"/>
|
||||
<statusBarWidgetFactory id="pythonDependencyFileWidget"
|
||||
implementation="com.jetbrains.python.requirements.widget.PyDependenciesFileStatusBarWidgetFactory"
|
||||
order="after pythonInterpreterWidget, before git, before hg, before Notifications"/>
|
||||
|
||||
@@ -563,7 +563,6 @@ sdk.create.binary.not.executable=Binary is not executable
|
||||
sdk.create.executable.directory.error=Path cannot be a directory
|
||||
sdk.create.tooltip.browse=Browse\u2026
|
||||
sdk.create.custom.venv.install.fix.title=Install {0}
|
||||
sdk.create.custom.venv.install.fix.title.using.pip=Install {0} using pip
|
||||
sdk.create.custom.venv.run.error.message=Error Running {0}
|
||||
sdk.create.custom.venv.progress.title.detect.executable=Detect executable
|
||||
sdk.create.custom.existing.env.title=Environment:
|
||||
|
||||
@@ -8,20 +8,27 @@ import com.intellij.openapi.util.Version
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.PyToolsState
|
||||
import com.intellij.python.pytools.icons.PythonPyToolsIcons
|
||||
import com.intellij.python.pytools.configuration.ExecutableDiscoveryMode
|
||||
import com.intellij.python.pytools.ui.PyToolDetailConfigurableProvider
|
||||
import com.intellij.python.pytools.configuration.ConfigurablePyTool
|
||||
import com.intellij.python.black.PyBlackBundle.message
|
||||
import com.intellij.python.black.configuration.BlackFormatterConfigurable
|
||||
import com.intellij.python.black.configuration.BlackFormatterConfiguration
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
import kotlin.io.path.Path
|
||||
|
||||
/**
|
||||
* [Black](https://black.readthedocs.io/) — the uncompromising Python code formatter maintained under
|
||||
* the PSF. It reformats source into a single, consistent style, leaving little to configure.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class BlackPyTool : PyTool, PyToolDetailConfigurableProvider {
|
||||
class BlackPyTool : PyTool, ConfigurablePyTool {
|
||||
override val presentableName: String = "Black"
|
||||
override val description: String get() = message("black.tool.description")
|
||||
override val packageName: PyPackageName = PyPackageName.from("black")
|
||||
override val icon: Icon get() = PythonPyToolsIcons.Logo
|
||||
|
||||
/**
|
||||
* `--line-ranges` (fragment formatting) requires Black 23.11.0; older versions cannot honour
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<pyprojectmanager implementation="com.intellij.python.hatch.impl.HatchPyProjectManager"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<python.pytools.pyTool implementation="com.intellij.python.hatch.HatchPyTool"/>
|
||||
<python.community.common.toolToIconMapper implementation="com.intellij.python.hatch.impl.HatchIdMapper"/>
|
||||
<python.processOutput.common.processOutputIconMapping implementation="com.intellij.python.hatch.impl.HatchIconMapping"/>
|
||||
</extensions>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
python.hatch.tool.description=A modern, extensible Python project manager.
|
||||
python.hatch.error.executable.is.not.found=Hatch executable is not found at: {0}
|
||||
python.hatch.error.base.python.executable.is.not.found=Base python executable is not found at: {0}
|
||||
python.hatch.error.environment.creation=Environment creation failure: {0}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.hatch
|
||||
|
||||
import com.intellij.python.hatch.PyHatchBundle.message
|
||||
import com.intellij.python.hatch.icons.PythonHatchIcons
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* [Hatch](https://hatch.pypa.io/) — a modern, extensible Python project manager maintained under the
|
||||
* PyPA. It manages isolated and matrix environments, builds distributions through its Hatchling build
|
||||
* backend, bumps and manages project versions, publishes packages to PyPI, and runs project scripts.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class HatchPyTool : PyTool {
|
||||
override val presentableName: String = "Hatch"
|
||||
override val packageName: PyPackageName = PyPackageName.from("hatch")
|
||||
override val description: String get() = message("python.hatch.tool.description")
|
||||
override val icon: Icon get() = PythonHatchIcons.Logo
|
||||
|
||||
@Suppress("CompanionObjectInExtension")
|
||||
companion object {
|
||||
fun getInstance(): HatchPyTool = PyTool.EP_NAME.findExtensionOrFail(HatchPyTool::class.java)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ jvm_library(
|
||||
"//python/python-pyproject:pyproject",
|
||||
"//platform/util/base/multiplatform",
|
||||
"//python/common",
|
||||
"//platform/util/jdom",
|
||||
"//python/python-sdk:sdk",
|
||||
"//platform/projectModel-api:projectModel",
|
||||
"//python/python-pytools:pytools",
|
||||
"//platform/core-api:core",
|
||||
"//platform/util",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -47,6 +53,12 @@ jvm_library(
|
||||
"//python/python-pyproject:pyproject_test_lib",
|
||||
"//platform/util/base/multiplatform:multiplatform_test_lib",
|
||||
"//python/common:common_test_lib",
|
||||
"//platform/util/jdom:jdom_test_lib",
|
||||
"//python/python-sdk:sdk_test_lib",
|
||||
"//platform/projectModel-api:projectModel_test_lib",
|
||||
"//python/python-pytools:pytools_test_lib",
|
||||
"//platform/core-api:core_test_lib",
|
||||
"//platform/util:util_test_lib",
|
||||
],
|
||||
)
|
||||
### auto-generated section `build intellij.python.community.impl.poetry.backend` end
|
||||
|
||||
@@ -15,5 +15,11 @@
|
||||
<orderEntry type="module" module-name="intellij.python.pyproject" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.base.multiplatform" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.common" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
|
||||
<orderEntry type="module" module-name="intellij.python.sdk" />
|
||||
<orderEntry type="module" module-name="intellij.platform.projectModel" />
|
||||
<orderEntry type="module" module-name="intellij.python.pytools" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util" />
|
||||
</component>
|
||||
</module>
|
||||
+4
@@ -7,8 +7,12 @@
|
||||
<module name="intellij.platform.backend"/>
|
||||
<module name="intellij.python.community.impl.poetry.common"/>
|
||||
<module name="intellij.python.community.common"/>
|
||||
<module name="intellij.python.pytools"/>
|
||||
</dependencies>
|
||||
<extensions defaultExtensionNs="com.intellij.python.pyproject.model">
|
||||
<pyprojectmanager implementation="com.intellij.python.community.impl.poetry.backend.PoetryPyProjectManager"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<python.pytools.pyTool implementation="com.intellij.python.community.impl.poetry.backend.PoetryPyTool"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1 @@
|
||||
python.poetry.tool.description=A tool for Python dependency management and packaging.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.impl.poetry.backend
|
||||
|
||||
import com.intellij.python.community.impl.poetry.backend.PyPoetryBundle.message
|
||||
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* [Poetry](https://python-poetry.org/) — a tool for Python dependency management and packaging. It
|
||||
* declares dependencies in `pyproject.toml`, resolves them into a `poetry.lock` file for reproducible
|
||||
* installs, manages the project's virtual environment, and builds and publishes packages.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class PoetryPyTool : PyTool {
|
||||
override val presentableName: String = "Poetry"
|
||||
override val packageName: PyPackageName = PyPackageName.from("poetry")
|
||||
override val description: String get() = message("python.poetry.tool.description")
|
||||
override val icon: Icon get() = PythonCommunityImplPoetryCommonIcons.Poetry
|
||||
|
||||
@Suppress("CompanionObjectInExtension")
|
||||
companion object {
|
||||
fun getInstance(): PoetryPyTool = PyTool.EP_NAME.findExtensionOrFail(PoetryPyTool::class.java)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.impl.poetry.backend
|
||||
|
||||
import com.intellij.DynamicBundle
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
|
||||
internal object PyPoetryBundle {
|
||||
private const val BUNDLE = "messages.PyPoetryBundle"
|
||||
|
||||
private val INSTANCE = DynamicBundle(PyPoetryBundle::class.java, BUNDLE)
|
||||
|
||||
fun message(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg params: Any): @Nls String {
|
||||
return INSTANCE.getMessage(key, *params)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ jvm_library(
|
||||
"//platform/util:util-ui",
|
||||
"//platform/core-api:core",
|
||||
"//platform/core-ui",
|
||||
"//platform/eel",
|
||||
"//platform/eel-provider",
|
||||
"//platform/ide-core",
|
||||
"//platform/platform-impl:ide-impl",
|
||||
"//platform/observable",
|
||||
@@ -64,6 +66,8 @@ jvm_library(
|
||||
"//platform/util:util-ui_test_lib",
|
||||
"//platform/core-api:core_test_lib",
|
||||
"//platform/core-ui:core-ui_test_lib",
|
||||
"//platform/eel:eel_test_lib",
|
||||
"//platform/eel-provider:eel-provider_test_lib",
|
||||
"//platform/ide-core:ide-core_test_lib",
|
||||
"//platform/platform-impl:ide-impl_test_lib",
|
||||
"//platform/observable:observable_test_lib",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.util.ui" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core.ui" />
|
||||
<orderEntry type="module" module-name="intellij.platform.eel" />
|
||||
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.observable" />
|
||||
|
||||
@@ -1,37 +1,15 @@
|
||||
settings.installation.group=Installation
|
||||
settings.installation.type.label=Type:
|
||||
settings.installation.type.binary=Installed as binary
|
||||
settings.installation.type.uv=Managed by uv
|
||||
settings.installation.type.unavailable=This installation type is stored in settings but is currently not available. Please choose another type.
|
||||
settings.installation.interpreter.label=Python interpreter:
|
||||
settings.installation.executable.auto.detected.path=Auto-detected: {0}
|
||||
|
||||
execution.mode.label=Execution mode:
|
||||
execution.mode.package=Interpreter
|
||||
execution.mode.binary=Path
|
||||
|
||||
executable.label=Executable:
|
||||
select.path.to.executable=Select Path to Executable
|
||||
tool.executable.not.found=Unable to auto-detect executable in $PATH
|
||||
|
||||
dialog.message.must.not.be.empty=Must not be empty
|
||||
dialog.message.must.be.absolute=Must be absolute
|
||||
dialog.message.not.found=Not found
|
||||
dialog.message.must.not.be.directory=Must not be a directory
|
||||
dialog.message.not.executable=Not executable
|
||||
|
||||
python.sdk.rendering.module.default=Module default
|
||||
python.packaging.list.progress=Reading installed python packages
|
||||
|
||||
button.install=Install {0}
|
||||
label.package.not.installed.in.current.interpreter={0} is not installed
|
||||
label.tool.version=Tool version:
|
||||
link.tool.update=Update
|
||||
|
||||
settings.external.tools.title=External Tools
|
||||
settings.external.tools.description=Enable and configure tools, choose a lookup method, and optionally override the path. The tool can be detected from sdk and path, or you can run it manually without installation via the 'uvx \u2039tool\u203A' command. <a href="actionsOnSave">All actions on save\u2026</a>
|
||||
settings.external.tools.column.name=Tool
|
||||
settings.external.tools.column.enabled=Enabled
|
||||
settings.external.tools.column.mode=Lookup
|
||||
settings.external.tools.column.path=Executable Path
|
||||
settings.external.tools.path.not.found=Not found in $PATH
|
||||
@@ -46,28 +24,26 @@ settings.external.tools.lookup.strategy.path=Try the tool from $PATH first, then
|
||||
settings.external.tools.lookup.strategy.uvx=Run the tool via uvx (no installation required).
|
||||
settings.external.tools.lookup.sdk.tooltip.header=Project SDKs:
|
||||
settings.external.tools.lookup.sdk.tooltip.not.installed=not installed
|
||||
settings.external.tools.install.via.uv.tooltip=Install with `uv tool install`
|
||||
settings.external.tools.install.via.uv.progress=Installing {0}\u2026
|
||||
settings.external.tools.install.via.uv.error.title=Could Not Install {0}
|
||||
settings.external.tools.install.via.uv.success.balloon={0} installed
|
||||
settings.external.tools.install.via.uv.success.to.version.balloon={0} {1} installed
|
||||
settings.external.tools.install.via.uv.error.no.uv=The uv executable could not be found.
|
||||
settings.external.tools.install.tooltip=Install the latest version
|
||||
settings.external.tools.install.progress=Installing {0}\u2026
|
||||
settings.external.tools.install.error.title=Could Not Install {0}
|
||||
settings.external.tools.install.success.balloon={0} installed
|
||||
settings.external.tools.install.success.to.version.balloon={0} {1} installed
|
||||
settings.external.tools.path.version.tooltip=Version: {0}
|
||||
settings.external.tools.path.below.minimum.tooltip=Below minimum: requires {0} {1} or later (detected {2}).
|
||||
settings.external.tools.path.upgrade.unknown.tooltip=Try to upgrade
|
||||
settings.external.tools.path.upgrade.to.version.tooltip=Upgrade to {0}
|
||||
settings.external.tools.path.edit.tooltip=Browse for executable
|
||||
settings.external.tools.path.reset.tooltip=Use auto-detection
|
||||
settings.external.tools.upgrade.via.uv.progress=Upgrading {0}\u2026
|
||||
settings.external.tools.upgrade.via.uv.error.title=Could Not Upgrade {0}
|
||||
settings.external.tools.upgrade.via.uv.success.balloon={0} upgraded
|
||||
settings.external.tools.upgrade.via.uv.success.to.version.balloon={0} upgraded to {1}
|
||||
settings.external.tools.upgrade.via.uv.up.to.date.balloon={0} is already up to date ({1})
|
||||
settings.external.tools.upgrade.progress=Upgrading {0}\u2026
|
||||
settings.external.tools.upgrade.error.title=Could Not Upgrade {0}
|
||||
settings.external.tools.upgrade.success.balloon={0} upgraded
|
||||
settings.external.tools.upgrade.success.to.version.balloon={0} upgraded to {1}
|
||||
settings.external.tools.upgrade.up.to.date.balloon={0} is already up to date ({1})
|
||||
settings.external.tools.uv.hint.not.installed=uv is not installed on your system. install uv to use uvx running mode.
|
||||
settings.external.tools.uv.hint.install.button=Install uv
|
||||
settings.external.tools.install.uv.progress=Installing uv\u2026
|
||||
settings.external.tools.install.uv.error.title=Could Not Install uv
|
||||
settings.external.tools.install.uv.error.no.python=No system Python interpreter was found.
|
||||
|
||||
label.features=Features:
|
||||
checkbox.inspections=Inspections
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ package com.intellij.python.pytools.ui
|
||||
import com.intellij.ide.ui.search.SearchableOptionContributor
|
||||
import com.intellij.ide.ui.search.SearchableOptionProcessor
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.configuration.ConfigurablePyTool
|
||||
import com.intellij.python.pytools.ui.configuration.PyExternalToolsConfigurable
|
||||
|
||||
/**
|
||||
@@ -14,7 +15,7 @@ import com.intellij.python.pytools.ui.configuration.PyExternalToolsConfigurable
|
||||
internal class PyExternalToolsSearchableOptionContributor : SearchableOptionContributor() {
|
||||
override fun processOptions(processor: SearchableOptionProcessor) {
|
||||
val displayName = PyToolsUiBundle.message("settings.external.tools.title")
|
||||
for (tool in PyTool.EP_NAME.extensionList) {
|
||||
for (tool in PyTool.EP_NAME.extensionList.filter { it is ConfigurablePyTool }) {
|
||||
processor.addOptions(
|
||||
tool.presentableName,
|
||||
null,
|
||||
|
||||
-11
@@ -1,20 +1,9 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.pytools.ui
|
||||
|
||||
import com.intellij.openapi.options.UnnamedConfigurable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.python.pytools.lsp.PyLspToolSettings
|
||||
|
||||
/**
|
||||
* Implemented by a `com.intellij.python.pytools.PyTool` that contributes a detail panel to the
|
||||
* External Tools table's Edit dialog. Kept out of `PyTool` itself so the core tool abstraction stays
|
||||
* free of UI types; the table discovers it with `tool as? PyToolDetailConfigurableProvider`.
|
||||
*/
|
||||
interface PyToolDetailConfigurableProvider {
|
||||
fun createConfigurable(project: Project): UnnamedConfigurable
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared comma-separated summary of the standard LSP feature toggles, for `PyTool.summaryFor`.
|
||||
* Tools with extra toggles (e.g. Ruff) build their own.
|
||||
+1
-2
@@ -54,7 +54,7 @@ internal interface PathCellHost {
|
||||
/**
|
||||
* Status provider used by the Lookup-column renderers to decorate each chain step (SDK, PATH,
|
||||
* uvx) with a ✓ / ✗ / ◐ glyph. Implemented by the configurable since the renderers don't have
|
||||
* direct access to the [UvController] or the project SDK list.
|
||||
* direct access to the [PyToolManagementController] or the project SDK list.
|
||||
*/
|
||||
internal interface ModeCellHost {
|
||||
fun modeStatusFor(toolRow: ToolRow, mode: ExecutableDiscoveryMode): ChainStepStatus
|
||||
@@ -66,7 +66,6 @@ internal interface ModeCellHost {
|
||||
* string when the cell value isn't a [ExecutableDiscoveryMode]. Shared between the combobox
|
||||
* and read-only renderers so both go through the same null-handling path.
|
||||
*/
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
private fun renderChainText(host: ModeCellHost?, table: JTable, value: Any?, row: Int): String {
|
||||
val mode = value as? ExecutableDiscoveryMode ?: return ""
|
||||
val toolRow = (table.model as? ListTableModel<*>)?.getRowValue(row) as? ToolRow
|
||||
|
||||
+4
-4
@@ -33,10 +33,10 @@ class PyExternalToolsConfigurable(private val project: Project) : BoundSearchabl
|
||||
|
||||
/**
|
||||
* Owns the uv-state snapshot + install/upgrade actions. Its coroutine scope is supplied later by
|
||||
* [createPanel]'s `launchOnShow` block via [UvController.onShown], so background work lives for
|
||||
* [createPanel]'s `launchOnShow` block via [PyToolManagementController.onShown], so background work lives for
|
||||
* the panel's showing-lifetime.
|
||||
*/
|
||||
private val uv: UvController = UvController(
|
||||
private val uv: PyToolManagementController = PyToolManagementController(
|
||||
project = project,
|
||||
onStateChanged = ::onUvStateChanged,
|
||||
refreshRow = ::refreshRow,
|
||||
@@ -45,12 +45,12 @@ class PyExternalToolsConfigurable(private val project: Project) : BoundSearchabl
|
||||
/** Initialized once in [createPanel]; every method that touches it is invoked while the UI is live. */
|
||||
private lateinit var toolsTable: PyExternalToolsTable
|
||||
|
||||
/** Wired in as [UvController.onStateChanged]; refresh the table on EDT. The hint footer rebinds via [UvController.uvAvailable]. */
|
||||
/** Wired in as [PyToolManagementController.onStateChanged]; refresh the table on EDT. The hint footer rebinds via [PyToolManagementController.uvAvailable]. */
|
||||
private fun onUvStateChanged() {
|
||||
toolsTable.fireAllRowsChanged()
|
||||
}
|
||||
|
||||
/** Wired in as [UvController.refreshRow]; delegate to the table. */
|
||||
/** Wired in as [PyToolManagementController.refreshRow]; delegate to the table. */
|
||||
private fun refreshRow(item: ToolRow) {
|
||||
toolsTable.refreshRow(item)
|
||||
}
|
||||
|
||||
+8
-10
@@ -22,7 +22,7 @@ import com.intellij.python.pytools.findExecutableInPath
|
||||
import com.intellij.python.pytools.findExecutableInSdk
|
||||
import com.jetbrains.python.sdk.pyInterpreterPresentation
|
||||
import com.intellij.python.pytools.ui.PyToolsUiBundle
|
||||
import com.intellij.python.pytools.ui.PyToolDetailConfigurableProvider
|
||||
import com.intellij.python.pytools.configuration.ConfigurablePyTool
|
||||
import com.intellij.python.pytools.ui.icons.PythonPytoolsUIIcons
|
||||
import com.jetbrains.python.Result
|
||||
import com.intellij.python.pytools.validateCustomPath
|
||||
@@ -94,7 +94,7 @@ internal class ToolRow(
|
||||
var sdkAvailability: SdkAvailability? = null,
|
||||
) {
|
||||
/** This tool's detail-panel provider, or `null` when the tool has no detail configurable. */
|
||||
val detailConfigurableProvider: PyToolDetailConfigurableProvider? = tool as? PyToolDetailConfigurableProvider
|
||||
val detailConfigurableProvider: ConfigurablePyTool? = tool as? ConfigurablePyTool
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,15 +148,12 @@ internal enum class PathIconKind(val icon: Icon?) {
|
||||
|
||||
/**
|
||||
* Compute the hover-only icon for a Path cell given the row's current state. The function is
|
||||
* deliberately pure: the caller supplies the live uv-availability snapshot (`null` while
|
||||
* detection is in flight) and the "is this tool uv-managed" predicate, so the renderer doesn't
|
||||
* need to know how those are sourced.
|
||||
* deliberately pure: the caller supplies the "is an upgrade available" predicate, so the renderer
|
||||
* doesn't need to know how it is sourced.
|
||||
*/
|
||||
internal fun iconKindFor(
|
||||
toolRow: ToolRow?,
|
||||
detected: PathFieldValue?,
|
||||
uvAvailable: Boolean?,
|
||||
isUvManaged: (ToolRow) -> Boolean,
|
||||
isUpgradeAvailable: (ToolRow) -> Boolean,
|
||||
): PathIconKind = when {
|
||||
toolRow == null -> PathIconKind.NONE
|
||||
@@ -164,10 +161,11 @@ internal fun iconKindFor(
|
||||
// action there is "revert to auto-detection". Skip install / upgrade / info — none of them
|
||||
// apply to a user-pointed-at executable.
|
||||
detected is PathFieldValue.Custom -> PathIconKind.RESET
|
||||
detected is PathFieldValue.NotFound && uvAvailable == true -> PathIconKind.INSTALL
|
||||
detected is PathFieldValue.NotFound -> PathIconKind.NONE
|
||||
// Offer install for any undiscovered tool; the installer uses uv when present and otherwise
|
||||
// falls back to a pip install into a system Python.
|
||||
detected is PathFieldValue.NotFound -> PathIconKind.INSTALL
|
||||
toolRow.version == null -> PathIconKind.NONE
|
||||
isUvManaged(toolRow) && isUpgradeAvailable(toolRow) -> PathIconKind.UPGRADE
|
||||
isUpgradeAvailable(toolRow) -> PathIconKind.UPGRADE
|
||||
// Otherwise no actionable icon — the path text + version tooltip already conveys the state.
|
||||
else -> PathIconKind.NONE
|
||||
}
|
||||
|
||||
+8
-6
@@ -12,6 +12,7 @@ import com.intellij.python.pytools.PyToolsState
|
||||
import com.intellij.python.pytools.configuration.ExecutableDiscoveryMode
|
||||
import com.intellij.python.pytools.statistics.PyToolUsagesCollector
|
||||
import com.intellij.python.pytools.statistics.PyToolActionSource
|
||||
import com.intellij.python.pytools.configuration.ConfigurablePyTool
|
||||
import com.intellij.python.pytools.ui.PyToolsUiBundle
|
||||
import com.intellij.python.pytools.ui.icons.PythonPytoolsUIIcons
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
@@ -57,11 +58,12 @@ import javax.swing.table.TableCellRenderer
|
||||
*/
|
||||
internal class PyExternalToolsTable(
|
||||
override val project: Project,
|
||||
private val uv: UvController,
|
||||
private val uv: PyToolManagementController,
|
||||
) : TooltipHost {
|
||||
|
||||
/** Source-of-truth row list, materialised once from the [PyTool] extension point. */
|
||||
private val rows: List<ToolRow> = PyTool.EP_NAME.extensionList
|
||||
.filter { it is ConfigurablePyTool }
|
||||
.sortedBy { it.presentableName.lowercase() }
|
||||
.map { ToolRow(it, snapshotOf(it)) }
|
||||
|
||||
@@ -77,18 +79,18 @@ internal class PyExternalToolsTable(
|
||||
override var hoveredRow: Int = -1
|
||||
private set
|
||||
|
||||
/** View row currently under the mouse for the Path column; -1 means no hover. Drives the install-via-uv icon. */
|
||||
/** View row currently under the mouse for the Path column; -1 means no hover. Drives the install/upgrade icon. */
|
||||
override var pathHoveredRow: Int = -1
|
||||
private set
|
||||
|
||||
override fun iconKindFor(toolRow: ToolRow?, pathFieldValue: PathFieldValue?): PathIconKind =
|
||||
iconKindFor(toolRow, pathFieldValue, uv.uvAvailable.get(), uv::isUvManaged, uv::isUpgradeAvailable)
|
||||
iconKindFor(toolRow, pathFieldValue, uv::isUpgradeAvailable)
|
||||
|
||||
override fun latestVersionFor(toolRow: ToolRow): String? = uv.latestVersionFor(toolRow)
|
||||
|
||||
/**
|
||||
* Per-step availability for the Lookup column glyphs. SDK comes from the row's enumerated
|
||||
* project SDKs, PATH from the row's resolved [PathFieldValue], uvx from the [UvController].
|
||||
* project SDKs, PATH from the row's resolved [PathFieldValue], uvx from the [PyToolManagementController].
|
||||
*/
|
||||
override fun modeStatusFor(toolRow: ToolRow, mode: ExecutableDiscoveryMode): ChainStepStatus = when (mode) {
|
||||
ExecutableDiscoveryMode.INTERPRETER -> toolRow.sdkAvailability.toChainStatus()
|
||||
@@ -258,8 +260,8 @@ internal class PyExternalToolsTable(
|
||||
// shouldn't trigger another install/upgrade when clicked.
|
||||
if (rows[viewRow].lastSuccessMessage != null) return
|
||||
when (pathIconAtHover(viewRow)) {
|
||||
PathIconKind.INSTALL -> uv.installViaUv(rows[viewRow], PyToolActionSource.SETTINGS_TABLE)
|
||||
PathIconKind.UPGRADE -> uv.upgradeViaUv(rows[viewRow], PyToolActionSource.SETTINGS_TABLE)
|
||||
PathIconKind.INSTALL -> uv.installTool(rows[viewRow], PyToolActionSource.SETTINGS_TABLE)
|
||||
PathIconKind.UPGRADE -> uv.upgradeTool(rows[viewRow], PyToolActionSource.SETTINGS_TABLE)
|
||||
PathIconKind.RESET -> resetPathFor(rows[viewRow])
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
+1
-1
@@ -215,7 +215,7 @@ private fun buildPathTooltip(
|
||||
*/
|
||||
private fun actionHintFor(kind: PathIconKind, latestVersion: String?): String? = when (kind) {
|
||||
PathIconKind.NONE -> null
|
||||
PathIconKind.INSTALL -> PyToolsUiBundle.message("settings.external.tools.install.via.uv.tooltip")
|
||||
PathIconKind.INSTALL -> PyToolsUiBundle.message("settings.external.tools.install.tooltip")
|
||||
PathIconKind.RESET -> PyToolsUiBundle.message("settings.external.tools.path.reset.tooltip")
|
||||
PathIconKind.UPGRADE -> if (latestVersion != null) {
|
||||
PyToolsUiBundle.message("settings.external.tools.path.upgrade.to.version.tooltip", latestVersion)
|
||||
|
||||
+77
-170
@@ -4,20 +4,21 @@ package com.intellij.python.pytools.ui.configuration
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.Version as PlatformVersion
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.toEelApi
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.pytools.statistics.PyToolUsagesCollector
|
||||
import com.intellij.python.pytools.statistics.PyToolActionSource
|
||||
import com.intellij.python.pytools.Version
|
||||
import com.intellij.python.pytools.ui.PyToolsUiBundle
|
||||
import com.intellij.python.uv.backend.cli.uv.UvTool
|
||||
import com.intellij.python.uv.backend.runtime.createUvToolRuntime
|
||||
import com.intellij.python.uv.backend.runtime.uvCli
|
||||
import com.intellij.python.uv.backend.UvPyTool
|
||||
import com.intellij.python.pytools.PyToolManager
|
||||
import com.intellij.python.pytools.PyToolManagerProvider
|
||||
import com.intellij.python.pytools.performToolInstallation
|
||||
import com.intellij.python.pytools.performToolUpgrade
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.sdk.installExecutableViaPythonScript
|
||||
import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal
|
||||
import com.jetbrains.python.packaging.PyPackageVersionComparator
|
||||
import com.jetbrains.python.sdk.uv.impl.hasUvExecutableLocal
|
||||
import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -25,24 +26,23 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Owns everything uv-related on the External Tools page: the in-memory uv state snapshot, the
|
||||
* one-shot uv-availability detection, the uv-managed-tool list refresh, and the install / upgrade
|
||||
* actions invoked from the Path column's hover icon (and the install-uv-itself action invoked
|
||||
* from the footer hint).
|
||||
* Backs the External Tools page's tool actions: the install / upgrade actions invoked from the Path
|
||||
* column's hover icon (delegated to [PyToolManager] — uv when present, pip otherwise), the aggregated
|
||||
* set of outdated tools that drives the upgrade affordance, uv-availability detection, and the
|
||||
* install-uv-itself action invoked from the footer hint.
|
||||
*
|
||||
* The configurable hands the controller a [CoroutineScope] via [onShown] — typically the scope
|
||||
* provided by `JComponent.launchOnShow`, so detection and background tasks live for the panel's
|
||||
* showing-lifetime and are cancelled automatically when the page is hidden. Two callbacks bridge
|
||||
* back to the configurable on EDT:
|
||||
* - [onStateChanged] whenever uv state changes (availability / version / managed-tool set) so
|
||||
* the configurable can refresh the table and rebuild the footer hint;
|
||||
* - [onStateChanged] whenever state changes (uv availability / the outdated-tool set) so the
|
||||
* configurable can refresh the table and rebuild the footer hint;
|
||||
* - [refreshRow] forwarded into [ToolRow.probeVersion] after a successful install/upgrade so
|
||||
* the freshly resolved version flows back into the row.
|
||||
*/
|
||||
internal class UvController(
|
||||
internal class PyToolManagementController(
|
||||
private val project: Project,
|
||||
private val onStateChanged: () -> Unit,
|
||||
private val refreshRow: (ToolRow) -> Unit,
|
||||
@@ -66,104 +66,52 @@ internal class UvController(
|
||||
*/
|
||||
val uvAvailable: AtomicProperty<Boolean?> = AtomicProperty(null)
|
||||
|
||||
/** uv version reported by `uv self version --short`; null means not yet known. */
|
||||
/**
|
||||
* Package name → latest available version for tools that have a newer release, aggregated across all
|
||||
* registered [PyToolManager]s (so it is populated even without uv). Drives the upgrade icon and its
|
||||
* tooltip. Refreshed asynchronously; a tool present here is, by definition, manageable and outdated.
|
||||
*/
|
||||
@Volatile
|
||||
var uvVersion: String? = null
|
||||
var outdatedVersions: Map<String, String> = emptyMap()
|
||||
private set
|
||||
|
||||
/**
|
||||
* True iff the cached [uvVersion] is at least [UV_OUTDATED_SUPPORTED_SINCE], i.e. `uv tool list --outdated`
|
||||
* is available. While [uvVersion] is `null` (detection in flight or failed), this stays `false` so we fall
|
||||
* back to the legacy "try to upgrade" behaviour rather than miscalling a flag that isn't there.
|
||||
*/
|
||||
val supportsOutdated: Boolean
|
||||
get() = uvVersion?.let { PlatformVersion.parseVersion(it) }?.let { it >= UV_OUTDATED_SUPPORTED_SINCE } == true
|
||||
|
||||
/**
|
||||
* Names of tools currently installed by `uv tool install` (i.e. listed by `uv tool list`).
|
||||
* Populated asynchronously after [uvAvailable] is confirmed; drives whether the Path column's
|
||||
* hover icon is the plain version-info icon or the upgrade-via-uv button.
|
||||
*/
|
||||
@Volatile
|
||||
var uvManagedNames: Set<String> = emptySet()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Names of uv-managed tools that have a newer release available, per `uv tool list --outdated`.
|
||||
* Maps the package name to the latest version uv would upgrade it to, so callers can surface
|
||||
* "Update to X.Y.Z" without re-querying. Only populated when [supportsOutdated] is true; with
|
||||
* older uv versions the legacy fallback uses [attemptedUpgrades] to decide whether the update
|
||||
* icon should still be shown.
|
||||
*/
|
||||
@Volatile
|
||||
var uvOutdatedVersions: Map<String, String> = emptyMap()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Names of uv-managed tools whose upgrade was already attempted from this settings page in the
|
||||
* current session. Used only when [supportsOutdated] is false: once we've kicked off `uv tool
|
||||
* upgrade <name>` we can't know whether anything changed (uv's exit code is the same either way),
|
||||
* so we hide the icon so the user isn't tempted to keep clicking the same "try" button.
|
||||
*/
|
||||
@Volatile
|
||||
private var attemptedUpgrades: Set<String> = emptySet()
|
||||
|
||||
/**
|
||||
* Package name uv knows this row's tool by. Each tool maps 1:1 to a single PyPI package, so every uv
|
||||
* operation (lookup, install/upgrade, attempted-upgrade tracking) targets the tool's package name.
|
||||
* Package name this row's tool is known by. Each tool maps 1:1 to a single PyPI package, so every
|
||||
* install / upgrade / outdated lookup targets the tool's package name.
|
||||
*/
|
||||
private fun ToolRow.uvPackageName(): String = tool.packageName.name
|
||||
|
||||
/** True iff the detected package is currently installed by uv (per the cached list). */
|
||||
fun isUvManaged(toolRow: ToolRow): Boolean =
|
||||
uvAvailable.get() == true && toolRow.uvPackageName() in uvManagedNames
|
||||
/** Whether the upgrade icon should be offered for [toolRow] — i.e. a newer version is known. */
|
||||
fun isUpgradeAvailable(toolRow: ToolRow): Boolean = toolRow.uvPackageName() in outdatedVersions
|
||||
|
||||
/**
|
||||
* Whether the upgrade icon should be offered for [toolRow]. Two regimes:
|
||||
* - modern uv (`supportsOutdated`) — only when the detected package is known-outdated;
|
||||
* - legacy uv — always, unless the detected package has already been upgraded, after
|
||||
* which the icon is suppressed (we have no way to confirm whether anything changed).
|
||||
*/
|
||||
fun isUpgradeAvailable(toolRow: ToolRow): Boolean {
|
||||
val name = toolRow.uvPackageName()
|
||||
return if (supportsOutdated) name in uvOutdatedVersions
|
||||
else name !in attemptedUpgrades
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest version uv would upgrade [toolRow]'s tool to, when known. Only returns non-null on
|
||||
* modern uv where `--outdated` reports versions; the legacy path has no way to know in advance.
|
||||
*/
|
||||
fun latestVersionFor(toolRow: ToolRow): String? =
|
||||
uvOutdatedVersions[toolRow.uvPackageName()]
|
||||
/** Latest version [toolRow]'s tool can be upgraded to, when known. */
|
||||
fun latestVersionFor(toolRow: ToolRow): String? = outdatedVersions[toolRow.uvPackageName()]
|
||||
|
||||
/**
|
||||
* Called by the configurable from within `launchOnShow`'s coroutine. Stores [scope] for
|
||||
* click-driven actions and kicks off the initial uv-availability detection.
|
||||
* click-driven actions, detects uv availability (for the footer/uvx), and loads outdated tools.
|
||||
*/
|
||||
fun onShown(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
scope.launch {
|
||||
val available = hasUvExecutableLocal()
|
||||
uvAvailable.set(available)
|
||||
uvVersion = if (available) fetchUvVersion() else null
|
||||
uvAvailable.set(hasUvExecutableLocal())
|
||||
withContext(Dispatchers.Main) { onStateChanged() }
|
||||
if (available) refreshUvManagedNames()
|
||||
refreshOutdated()
|
||||
}
|
||||
}
|
||||
|
||||
/** Run `uv tool install <packageName>` with modal progress; refresh the row when it succeeds. */
|
||||
fun installViaUv(toolRow: ToolRow, source: PyToolActionSource) = runUvToolAction(
|
||||
/** Install the row's tool with modal progress via the shared installer path; refresh on success. */
|
||||
fun installTool(toolRow: ToolRow, source: PyToolActionSource) = runToolAction(
|
||||
toolRow = toolRow,
|
||||
progressTitleKey = "settings.external.tools.install.via.uv.progress",
|
||||
errorTitleKey = "settings.external.tools.install.via.uv.error.title",
|
||||
action = { uvTool, name -> uvTool.install(name) },
|
||||
progressTitleKey = "settings.external.tools.install.progress",
|
||||
errorTitleKey = "settings.external.tools.install.error.title",
|
||||
action = { toolRow.tool.performToolInstallation(project.getEelDescriptor().toEelApi()) },
|
||||
onSuccess = {
|
||||
PyToolUsagesCollector.Helper.logToolInstalled(project, toolRow.tool, source)
|
||||
// Baseline copy — surfaced while the post-action `--version` probe is still running and
|
||||
// we don't yet know the freshly installed version.
|
||||
toolRow.lastSuccessMessage =
|
||||
PyToolsUiBundle.message("settings.external.tools.install.via.uv.success.balloon", toolRow.uvPackageName())
|
||||
PyToolsUiBundle.message("settings.external.tools.install.success.balloon", toolRow.uvPackageName())
|
||||
},
|
||||
onVersionResolved = { installedVersion ->
|
||||
// [runUvToolAction] only fires this hook when the probed version is non-null, so we can
|
||||
@@ -171,7 +119,7 @@ internal class UvController(
|
||||
// [onSuccess] stays.
|
||||
if (installedVersion != null) {
|
||||
toolRow.lastSuccessMessage = PyToolsUiBundle.message(
|
||||
"settings.external.tools.install.via.uv.success.to.version.balloon",
|
||||
"settings.external.tools.install.success.to.version.balloon",
|
||||
toolRow.uvPackageName(), installedVersion,
|
||||
)
|
||||
refreshRow(toolRow)
|
||||
@@ -186,20 +134,17 @@ internal class UvController(
|
||||
* pin and resolves to the latest compatible release, which matches what the user expects when
|
||||
* they click the "Upgrade" icon next to an outdated tool.
|
||||
*/
|
||||
fun upgradeViaUv(toolRow: ToolRow, source: PyToolActionSource) {
|
||||
fun upgradeTool(toolRow: ToolRow, source: PyToolActionSource) {
|
||||
// Snapshot the pre-upgrade version so we can tell, after the post-action re-probe, whether
|
||||
// the install actually changed anything (e.g. it could have already been latest).
|
||||
val previousVersion = toolRow.version
|
||||
runUvToolAction(
|
||||
runToolAction(
|
||||
toolRow = toolRow,
|
||||
progressTitleKey = "settings.external.tools.upgrade.via.uv.progress",
|
||||
errorTitleKey = "settings.external.tools.upgrade.via.uv.error.title",
|
||||
action = { uvTool, name -> uvTool.install(name, reinstall = true) },
|
||||
progressTitleKey = "settings.external.tools.upgrade.progress",
|
||||
errorTitleKey = "settings.external.tools.upgrade.error.title",
|
||||
action = { toolRow.tool.performToolUpgrade(project.getEelDescriptor().toEelApi()) },
|
||||
onSuccess = {
|
||||
PyToolUsagesCollector.Helper.logToolUpdated(project, toolRow.tool, source)
|
||||
// Legacy fallback: remember the detected package was upgraded so the icon stops
|
||||
// re-offering it. Tracked under the same name [isUpgradeAvailable] later reads.
|
||||
attemptedUpgrades = attemptedUpgrades + toolRow.uvPackageName()
|
||||
},
|
||||
onVersionResolved = { newVersion ->
|
||||
// Refine the baseline success message now that the post-action version is known
|
||||
@@ -223,60 +168,50 @@ internal class UvController(
|
||||
val name = toolRow.uvPackageName()
|
||||
return when {
|
||||
current != null && previous != null && previous == current ->
|
||||
PyToolsUiBundle.message("settings.external.tools.upgrade.via.uv.up.to.date.balloon", name, current)
|
||||
PyToolsUiBundle.message("settings.external.tools.upgrade.up.to.date.balloon", name, current)
|
||||
current != null ->
|
||||
PyToolsUiBundle.message("settings.external.tools.upgrade.via.uv.success.to.version.balloon", name, current)
|
||||
PyToolsUiBundle.message("settings.external.tools.upgrade.success.to.version.balloon", name, current)
|
||||
else ->
|
||||
PyToolsUiBundle.message("settings.external.tools.upgrade.via.uv.success.balloon", name)
|
||||
PyToolsUiBundle.message("settings.external.tools.upgrade.success.balloon", name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install `uv` itself by piggybacking on the same Python script used by the New Project / Setup
|
||||
* SDK flow (`installExecutableViaPythonScript`). On success, persists the path with
|
||||
* `setUvExecutableLocal` (matching the SDK setup flow), flips [uvAvailable] on, and fires
|
||||
* Install `uv` itself through [UvPyTool]'s `performToolInstallation` (uv can't install itself, so
|
||||
* this falls through to the pip-based installer into a system Python). On success, persists the path
|
||||
* with `setUvExecutableLocal` (matching the SDK setup flow), flips [uvAvailable] on, and fires
|
||||
* [onStateChanged] so the configurable can rebuild the footer hint and the table.
|
||||
*/
|
||||
fun installUv() {
|
||||
val title = PyToolsUiBundle.message("settings.external.tools.install.uv.progress")
|
||||
val errorTitle = PyToolsUiBundle.message("settings.external.tools.install.uv.error.title")
|
||||
val result = runWithModalProgressBlocking(project, title) {
|
||||
val systemPython = SystemPythonService()
|
||||
.findSystemPythons().firstOrNull()
|
||||
?: return@runWithModalProgressBlocking null
|
||||
installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", "uv")
|
||||
UvPyTool.getInstance().performToolInstallation(project.getEelDescriptor().toEelApi())
|
||||
}
|
||||
if (result == null) {
|
||||
Messages.showErrorDialog(project, PyToolsUiBundle.message("settings.external.tools.install.uv.error.no.python"), errorTitle)
|
||||
return
|
||||
val installedPath = when (result) {
|
||||
is Result.Success -> result.result
|
||||
is Result.Failure -> {
|
||||
Messages.showErrorDialog(project, result.error.toString(), errorTitle)
|
||||
return
|
||||
}
|
||||
}
|
||||
val failure = result as? Result.Failure<*>
|
||||
if (failure != null) {
|
||||
Messages.showErrorDialog(project, failure.error.toString(), errorTitle)
|
||||
return
|
||||
}
|
||||
val installedPath = (result as? Result.Success<*>)?.result as? Path ?: return
|
||||
setUvExecutableLocal(installedPath)
|
||||
uvAvailable.set(true)
|
||||
onStateChanged()
|
||||
// Fetch the freshly installed uv's version + the list of uv-managed tools off the EDT.
|
||||
scope?.launch {
|
||||
uvVersion = fetchUvVersion()
|
||||
withContext(Dispatchers.Main) { onStateChanged() }
|
||||
refreshUvManagedNames()
|
||||
}
|
||||
// Now that uv exists, reload the outdated set so upgrade affordances light up.
|
||||
scope?.launch { refreshOutdated() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared driver for `uv tool install` / `uv tool upgrade`. Runs [action] under modal progress,
|
||||
* surfaces errors via a message dialog, and on success invalidates the row's cached probe and
|
||||
* refreshes the uv-managed list (so an Install can flip the row into "uv-managed" state).
|
||||
* Shared driver for a tool install / upgrade. Runs [action] under modal progress, surfaces errors
|
||||
* via a message dialog, and on success invalidates the row's cached probe and refreshes the
|
||||
* outdated-tool set (so an Install can light up an upgrade affordance afterwards).
|
||||
*/
|
||||
private fun runUvToolAction(
|
||||
private fun runToolAction(
|
||||
toolRow: ToolRow,
|
||||
progressTitleKey: String,
|
||||
errorTitleKey: String,
|
||||
action: suspend (UvTool, String) -> PyResult<String>,
|
||||
action: suspend () -> PyResult<*>,
|
||||
onSuccess: () -> Unit = {},
|
||||
/**
|
||||
* Fires once after the post-action `--version` re-probe publishes a version (or skips if
|
||||
@@ -285,10 +220,9 @@ internal class UvController(
|
||||
*/
|
||||
onVersionResolved: (Version?) -> Unit = {},
|
||||
) {
|
||||
// Whatever the alias detection settled on (or the install-info fallback when nothing is
|
||||
// detected) is what we pass to uv — keeps every step of this flow targeted at the same
|
||||
// package: install/reinstall, the post-action `--version` re-probe, and the
|
||||
// [attemptedUpgrades] / [uvOutdatedVersions] lookups in [isUpgradeAvailable].
|
||||
// The package name the operation targets — used for the progress/error titles and, later, the
|
||||
// post-action `--version` re-probe. Showing e.g. "basedpyright" (what uv actually installs) keeps
|
||||
// the dialog consistent with `uv tool list` even when the row's label differs ("Pyright").
|
||||
val packageName = toolRow.uvPackageName()
|
||||
// The progress and error titles surface the same `packageName`: showing "Installing
|
||||
// basedpyright" instead of the tool's presentable name keeps the dialog consistent with
|
||||
@@ -303,22 +237,13 @@ internal class UvController(
|
||||
toolRow.actionInProgress = true
|
||||
refreshRow(toolRow)
|
||||
val result = try {
|
||||
runWithModalProgressBlocking(project, title) {
|
||||
val uvPath = getUvExecutableLocal() ?: return@runWithModalProgressBlocking null
|
||||
action(createUvToolRuntime(uvPath).uvCli().tool(), packageName)
|
||||
}
|
||||
runWithModalProgressBlocking(project, title) { action() }
|
||||
}
|
||||
catch (e: CancellationException) {
|
||||
toolRow.actionInProgress = false
|
||||
refreshRow(toolRow)
|
||||
throw e
|
||||
}
|
||||
if (result == null) {
|
||||
toolRow.actionInProgress = false
|
||||
refreshRow(toolRow)
|
||||
Messages.showErrorDialog(project, PyToolsUiBundle.message("settings.external.tools.install.via.uv.error.no.uv"), errorTitle)
|
||||
return
|
||||
}
|
||||
val failure = result as? Result.Failure<*>
|
||||
if (failure != null) {
|
||||
toolRow.actionInProgress = false
|
||||
@@ -355,39 +280,21 @@ internal class UvController(
|
||||
}
|
||||
refreshRow(updatedRow)
|
||||
}
|
||||
activeScope.launch { refreshUvManagedNames() }
|
||||
}
|
||||
|
||||
/** Best-effort `uv self version --short` lookup; null on any error. */
|
||||
private suspend fun fetchUvVersion(): String? {
|
||||
val uvPath = getUvExecutableLocal() ?: return null
|
||||
val result = createUvToolRuntime(uvPath).uvCli().self().version(short = true)
|
||||
val raw = (result as? Result.Success<*>)?.result as? String ?: return null
|
||||
return raw.trim().takeIf { it.isNotEmpty() }
|
||||
activeScope.launch { refreshOutdated() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the cached set of tools installed by `uv tool install` (used by the Path-cell hover
|
||||
* icon to decide between the plain version-info icon and the upgrade-via-uv button). Runs
|
||||
* fully on background coroutines; the EDT is only touched via [onStateChanged].
|
||||
* Reload the set of outdated tools, aggregated across every registered [PyToolManager] (so it works
|
||||
* without uv). Runs fully on background coroutines; the EDT is only touched via [onStateChanged].
|
||||
*/
|
||||
private suspend fun refreshUvManagedNames() {
|
||||
val uvPath = getUvExecutableLocal() ?: return
|
||||
val tool = createUvToolRuntime(uvPath).uvCli().tool()
|
||||
val installed = tool.listInstalled().getOr { return }
|
||||
val names = installed.map { it.name }.toSet()
|
||||
val outdated = if (supportsOutdated) {
|
||||
tool.listOutdated().getOr { return }.associate { it.name to it.latestVersion }
|
||||
}
|
||||
else emptyMap()
|
||||
if (names == uvManagedNames && outdated == uvOutdatedVersions) return
|
||||
uvManagedNames = names
|
||||
uvOutdatedVersions = outdated
|
||||
private suspend fun refreshOutdated() {
|
||||
val eel = project.getEelDescriptor().toEelApi()
|
||||
val outdated = PyToolManagerProvider.managerFor(eel)?.list().orEmpty()
|
||||
.filterValues { PyPackageVersionComparator.STR_COMPARATOR.compare(it.latestVersion, it.installedVersion) > 0 }
|
||||
.map { (tool, info) -> tool.packageName.name to info.latestVersion }
|
||||
.toMap()
|
||||
if (outdated == outdatedVersions) return
|
||||
outdatedVersions = outdated
|
||||
withContext(Dispatchers.Main) { onStateChanged() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** First uv release that exposes `--outdated` on `uv tool list` (released 2026-03-13). */
|
||||
private val UV_OUTDATED_SUPPORTED_SINCE: PlatformVersion = PlatformVersion(0, 10, 10)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
<extensionPoint qualifiedName="com.intellij.python.pytools.pyTool"
|
||||
interface="com.intellij.python.pytools.PyTool"
|
||||
dynamic="true"/>
|
||||
<extensionPoint qualifiedName="com.intellij.python.pytools.pyToolManagerProvider"
|
||||
interface="com.intellij.python.pytools.PyToolManagerProvider"
|
||||
dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
|
||||
@@ -4,3 +4,4 @@ python.tool.cli.error.response.out.of.pattern=Tool command responded with non-ex
|
||||
selected.tool.is.wrong=The selected tool is not {0}, it is {1}
|
||||
uvx.is.not.installed=uvx is not installed
|
||||
uvx.is.not.supported=uvx is not supported
|
||||
python.tool.install.no.installer=Cannot install {0}: no installer is available
|
||||
|
||||
@@ -7,12 +7,16 @@ import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.python.pytools.statistics.PyToolFusSnapshot
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.Icon
|
||||
import com.intellij.openapi.util.Version as PlatformVersion
|
||||
|
||||
interface PyTool {
|
||||
val presentableName: @NlsSafe String
|
||||
val packageName: PyPackageName
|
||||
|
||||
/** Icon representing the tool (e.g. status-bar widget, advertiser notification, External Tools table). */
|
||||
val icon: Icon
|
||||
|
||||
/**
|
||||
* One-line user-facing description of the tool (e.g. "Linter and code formatter for Python").
|
||||
* Surfaced in the External Tools settings tooltip. Required — every tool must provide a
|
||||
@@ -75,5 +79,10 @@ interface PyTool {
|
||||
|
||||
companion object {
|
||||
val EP_NAME: ExtensionPointName<PyTool> = ExtensionPointName.create("com.intellij.python.pytools.pyTool")
|
||||
|
||||
fun findByPackageName(packageName: String): PyTool? {
|
||||
val normalized = PyPackageName.from(packageName).name
|
||||
return EP_NAME.extensionList.firstOrNull { it.packageName.name == normalized }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.intellij.python.pytools
|
||||
|
||||
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.EelOsFamily
|
||||
import com.intellij.platform.eel.provider.LocalEelDescriptor
|
||||
import com.intellij.platform.eel.provider.asNioPath
|
||||
@@ -136,4 +137,35 @@ private fun PyTool.findExecutableInPath(state: PyToolsState.ToolEntry, executabl
|
||||
fun PyTool.findExecutableInPath(
|
||||
executableName: String = packageName.name,
|
||||
osFamily: EelOsFamily = LocalEelDescriptor.osFamily,
|
||||
): Path? = resolveExecutableOnPath(executableName, osFamily)
|
||||
|
||||
/**
|
||||
* Looks up [executableName] on the system PATH by its OS-specific binary name. This is how the
|
||||
* External Tools settings page resolves tool executables (via [findExecutableInPath]); shared so
|
||||
* other callers can resolve an installed executable the same way.
|
||||
*/
|
||||
fun resolveExecutableOnPath(
|
||||
executableName: String,
|
||||
osFamily: EelOsFamily = LocalEelDescriptor.osFamily,
|
||||
): Path? = PathEnvironmentVariableUtil.findInPath(osFamily.getOsSpecificBinaryName(executableName))?.toPath()
|
||||
|
||||
/**
|
||||
* Installs this tool's executable into the environment described by [eel], using the first available
|
||||
* [PyToolManager] (`uv tool install` when uv is present, otherwise a pip install via a system Python).
|
||||
* Returns the resolved executable path on success.
|
||||
*/
|
||||
suspend fun PyTool.performToolInstallation(eel: EelApi): PyResult<Path> {
|
||||
val manager = PyToolManagerProvider.managerFor(eel)
|
||||
?: return PyResult.localizedError(message("python.tool.install.no.installer", presentableName))
|
||||
return manager.install(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrades this tool to the latest version in the environment described by [eel], using the first
|
||||
* available [PyToolManager]. Returns the resolved executable path on success.
|
||||
*/
|
||||
suspend fun PyTool.performToolUpgrade(eel: EelApi): PyResult<Path> {
|
||||
val manager = PyToolManagerProvider.managerFor(eel)
|
||||
?: return PyResult.localizedError(message("python.tool.install.no.installer", presentableName))
|
||||
return manager.upgrade(this)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.pytools
|
||||
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Installs and upgrades [PyTool] executables in a single environment. An instance is bound to that
|
||||
* environment (a project's EEL) by the [PyToolManagerProvider] that created it, so [install] / [upgrade]
|
||||
* take no `eel` argument.
|
||||
*
|
||||
* Backends (uv, pip, …) provide managers via [PyToolManagerProvider]; callers obtain one with
|
||||
* [PyToolManagerProvider.managerFor] and use it for every operation.
|
||||
*/
|
||||
interface PyToolManager {
|
||||
/** Installs [tool]; returns the resolved executable path. */
|
||||
suspend fun install(tool: PyTool): PyResult<Path>
|
||||
|
||||
/** Upgrades [tool] to the latest version. */
|
||||
suspend fun upgrade(tool: PyTool): PyResult<Path>
|
||||
|
||||
/**
|
||||
* All managed tools installed in this manager's environment, keyed by [PyTool], with their installed
|
||||
* and latest available version (the latest resolved from PyPI). Empty when no managed tool is installed.
|
||||
*/
|
||||
suspend fun list(): Map<PyTool, InstalledInfo>
|
||||
}
|
||||
|
||||
/**
|
||||
* Version and location of an installed managed tool: its resolved executable [path], the currently
|
||||
* [installedVersion], and the [latestVersion] available from the configured repositories (equal to
|
||||
* [installedVersion] when the tool is already up to date).
|
||||
*/
|
||||
data class InstalledInfo(
|
||||
val path: Path,
|
||||
val installedVersion: String,
|
||||
val latestVersion: String,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.pytools
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.platform.eel.EelApi
|
||||
|
||||
/**
|
||||
* Creates a [PyToolManager] for a given environment, contributed per backend (e.g. uv, pip) through
|
||||
* [EP_NAME]. Providers are consulted in registration order; the first one able to operate in the
|
||||
* target environment wins.
|
||||
*
|
||||
* Implementations live in higher-level modules (uv backend, the main python impl) so `python-pytools`
|
||||
* does not need to depend on them.
|
||||
*/
|
||||
interface PyToolManagerProvider {
|
||||
/**
|
||||
* A [PyToolManager] bound to [eel], or `null` when this provider cannot operate there — e.g. its
|
||||
* backing tool (uv on PATH, a system Python, …) is not present.
|
||||
*/
|
||||
suspend fun forEel(eel: EelApi): PyToolManager?
|
||||
|
||||
companion object {
|
||||
val EP_NAME: ExtensionPointName<PyToolManagerProvider> =
|
||||
ExtensionPointName.create("com.intellij.python.pytools.pyToolManagerProvider")
|
||||
|
||||
/** The [PyToolManager] from the highest-priority provider that can operate in [eel], or `null`. */
|
||||
suspend fun managerFor(eel: EelApi): PyToolManager? = EP_NAME.extensionList.firstNotNullOfOrNull { it.forEel(eel) }
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.pytools.configuration
|
||||
|
||||
import com.intellij.openapi.options.UnnamedConfigurable
|
||||
import com.intellij.openapi.project.Project
|
||||
|
||||
/**
|
||||
* Implemented by a [com.intellij.python.pytools.PyTool] that has settings on the External Tools page:
|
||||
* it contributes the detail panel shown in the table's Edit dialog. Presence of this interface is what
|
||||
* marks a tool as configurable (and therefore visible) on that page. Kept separate from `PyTool` so a
|
||||
* tool opts into a settings UI without every tool having to.
|
||||
*/
|
||||
interface ConfigurablePyTool {
|
||||
fun createConfigurable(project: Project): UnnamedConfigurable
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.PyToolsState
|
||||
import com.intellij.python.pytools.statistics.PyToolFusSnapshot
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* Base for every LSP-backed [PyTool]. Captures the shared, non-UI wiring around a per-project
|
||||
@@ -19,9 +18,6 @@ abstract class PyLspTool<C : PyLspToolConfiguration<*>> : PyTool {
|
||||
/** Per-project settings service backing this tool — the single source of its configuration. */
|
||||
abstract fun configuration(project: Project): C
|
||||
|
||||
/** Icon shown for this tool's LSP server (status-bar widget, advertiser notification, ...). */
|
||||
abstract val icon: Icon
|
||||
|
||||
override fun migrateLegacyState(project: Project): PyToolsState.ToolEntry = configuration(project).migrateToPyToolState()
|
||||
|
||||
override fun configurationFusSnapshot(project: Project): PyToolFusSnapshot {
|
||||
|
||||
+2
@@ -5,6 +5,7 @@ import com.intellij.openapi.util.JDOMUtil
|
||||
import com.intellij.python.pytools.PyToolsState
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.configuration.ExecutableDiscoveryMode
|
||||
import com.intellij.python.pytools.icons.PythonPyToolsIcons
|
||||
import com.intellij.configurationStore.serialize
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import com.intellij.util.xmlb.XmlSerializer
|
||||
@@ -77,6 +78,7 @@ internal class PyToolsStateSerializationTest {
|
||||
override val presentableName: String = "ruff"
|
||||
override val packageName: PyPackageName = PyPackageName.from("ruff")
|
||||
override val description: String = "ruff"
|
||||
override val icon = PythonPyToolsIcons.Logo
|
||||
}
|
||||
|
||||
// All tools at their defaults -> nothing to persist -> no .idea/pyLspTools.xml.
|
||||
|
||||
@@ -31,6 +31,7 @@ jvm_library(
|
||||
"//python/python-pytools:pytools",
|
||||
"//python/python-exec-service:community-execService",
|
||||
"//platform/eel-provider",
|
||||
"//python/python-sdk:sdk",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -55,6 +56,7 @@ jvm_library(
|
||||
"//python/python-pytools:pytools_test_lib",
|
||||
"//python/python-exec-service:community-execService_test_lib",
|
||||
"//platform/eel-provider:eel-provider_test_lib",
|
||||
"//python/python-sdk:sdk_test_lib",
|
||||
],
|
||||
)
|
||||
### auto-generated section `build intellij.python.uv.backend` end
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
<orderEntry type="module" module-name="intellij.python.pytools" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.execService" />
|
||||
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
|
||||
<orderEntry type="module" module-name="intellij.python.sdk" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -15,4 +15,8 @@
|
||||
<extensions defaultExtensionNs="com.intellij.python.pyproject.model">
|
||||
<pyprojectmanager implementation="com.intellij.python.uv.backend.UvPyProjectManager"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<python.pytools.pyTool implementation="com.intellij.python.uv.backend.UvPyTool"/>
|
||||
<python.pytools.pyToolManagerProvider implementation="com.intellij.python.uv.backend.UvToolManagerProvider" order="first"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
python.uv.tool.description=An extremely fast Python package and project manager, written in Rust.
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.uv.backend
|
||||
|
||||
import com.intellij.DynamicBundle
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
|
||||
internal object PyUvBundle {
|
||||
private const val BUNDLE = "messages.PyUvBundle"
|
||||
|
||||
private val INSTANCE = DynamicBundle(PyUvBundle::class.java, BUNDLE)
|
||||
|
||||
fun message(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg params: Any): @Nls String {
|
||||
return INSTANCE.getMessage(key, *params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.uv.backend
|
||||
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.uv.backend.PyUvBundle.message
|
||||
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* [uv](https://docs.astral.sh/uv/) — an extremely fast Python package and project manager written in
|
||||
* Rust by Astral. It resolves and installs dependencies, creates and manages virtual environments,
|
||||
* installs and switches Python versions, builds and publishes packages, and installs standalone
|
||||
* command-line tools into isolated environments (`uv tool install`). It aims to replace pip, pipx,
|
||||
* pip-tools, virtualenv, and pyenv with a single fast tool.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class UvPyTool : PyTool {
|
||||
override val presentableName: String = "uv"
|
||||
override val packageName: PyPackageName = PyPackageName.from("uv")
|
||||
override val description: String get() = message("python.uv.tool.description")
|
||||
override val icon: Icon get() = PythonUvCommonIcons.UV
|
||||
|
||||
@Suppress("CompanionObjectInExtension")
|
||||
companion object {
|
||||
fun getInstance(): UvPyTool = PyTool.EP_NAME.findExtensionOrFail(UvPyTool::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.uv.backend
|
||||
|
||||
import com.jetbrains.python.sdk.ToolCommandExecutor
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* Locates the `uv` executable for [UvToolManagerProvider].
|
||||
*
|
||||
* 262 backport note: on master this lives alongside `setUvExecutableLocal` in this package; on 262 the
|
||||
* writer `setUvExecutableLocal` stays in `com.jetbrains.python.sdk.uv.impl`. Both share the
|
||||
* "PyCharm.Uv.Path" setting, so detection here stays consistent with the SDK setup flow.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
val UV_TOOL: ToolCommandExecutor = ToolCommandExecutor(
|
||||
"uv",
|
||||
getToolPathFromSettings = { getValue(UV_PATH_SETTING) },
|
||||
)
|
||||
|
||||
private const val UV_PATH_SETTING: String = "PyCharm.Uv.Path"
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.uv.backend
|
||||
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.python.pytools.InstalledInfo
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.PyToolManager
|
||||
import com.intellij.python.pytools.PyToolManagerProvider
|
||||
import com.intellij.python.uv.backend.runtime.createUvToolRuntime
|
||||
import com.intellij.python.uv.backend.runtime.uvCli
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.sdk.add.v2.FileSystem
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.toFileSystem
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Provides a [UvToolManager] when a local `uv` is available for the target environment. Registered
|
||||
* `order="first"` so uv is preferred over the pip fallback.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class UvToolManagerProvider : PyToolManagerProvider {
|
||||
override suspend fun forEel(eel: EelApi): PyToolManager? {
|
||||
val fileSystem = eel.toFileSystem()
|
||||
val uv = UV_TOOL.getToolExecutable(fileSystem, null)?.path ?: return null
|
||||
return UvToolManager(fileSystem, uv)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `uv tool` manager bound to a resolved `uv` executable and its filesystem. Install/upgrade use
|
||||
* `uv tool install [--reinstall]` (isolated, always-latest env); `--reinstall` drops any prior pin so
|
||||
* an upgrade resolves to the latest release rather than staying on the originally-installed spec.
|
||||
*/
|
||||
private class UvToolManager(
|
||||
private val fileSystem: FileSystem<PathHolder.Eel>,
|
||||
private val uv: Path,
|
||||
) : PyToolManager {
|
||||
override suspend fun install(tool: PyTool): PyResult<Path> = run(tool, reinstall = false)
|
||||
|
||||
override suspend fun upgrade(tool: PyTool): PyResult<Path> = run(tool, reinstall = true)
|
||||
|
||||
/**
|
||||
* All uv-installed tools, from `uv tool list --show-paths`, with latest versions overlaid from
|
||||
* `uv tool list --outdated` (uv 0.10.10+; on older uv that call fails and every tool is reported as
|
||||
* up to date). Tools uv installed that the IDE does not know as a [PyTool], or whose executable path
|
||||
* is missing, are skipped.
|
||||
*/
|
||||
override suspend fun list(): Map<PyTool, InstalledInfo> {
|
||||
val tool = createUvToolRuntime(uv).uvCli().tool()
|
||||
val installed = tool.list(showPaths = true).getOr { return emptyMap() }
|
||||
val latestByName = tool.list(outdated = true).getOrNull().orEmpty()
|
||||
.mapNotNull { outdated -> outdated.latestVersion?.let { outdated.name to it } }
|
||||
.toMap()
|
||||
return installed.mapNotNull { uvTool ->
|
||||
val pyTool = PyTool.findByPackageName(uvTool.name) ?: return@mapNotNull null
|
||||
// A tool may expose several entry points (e.g. pyright, pyright-langserver, …); prefer the one
|
||||
// named after the tool, otherwise take the first uv reported.
|
||||
val executablePath = uvTool.executables[uvTool.name] ?: uvTool.executables.values.firstOrNull() ?: return@mapNotNull null
|
||||
// `uv tool list --outdated` omits up-to-date tools, so absence means latest == installed.
|
||||
val latestVersion = latestByName[uvTool.name] ?: uvTool.version
|
||||
pyTool to InstalledInfo(path = executablePath, installedVersion = uvTool.version, latestVersion = latestVersion)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private suspend fun run(tool: PyTool, reinstall: Boolean): PyResult<Path> {
|
||||
createUvToolRuntime(uv).uvCli().tool().install(tool.packageName.name, reinstall = reinstall).getOr { return it }
|
||||
val executable = fileSystem.detectTool(tool.packageName.name)
|
||||
?: return PyResult.localizedError(PySdkBundle.message("cannot.find.executable", tool.packageName.name, fileSystem.userReadableName))
|
||||
return Result.success(executable.path)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class UvTool(runtime: PyToolRuntime) : UvCommand("tool", runtime) {
|
||||
* Useful for breaking out of a previously pinned install (uv leaves the existing entry alone
|
||||
* otherwise, and `uv tool upgrade` is bounded by the original constraints so it cannot help).
|
||||
*/
|
||||
suspend fun install(name: String, reinstall: Boolean? = null): PyResult<String> {
|
||||
suspend fun install(name: String, reinstall: Boolean = false): PyResult<String> {
|
||||
val options = listOf(reinstall to "--reinstall").makeOptions()
|
||||
return executeAndHandleErrors("install", name, *options, transformer = ZeroCodeStdoutTransformer)
|
||||
}
|
||||
@@ -40,43 +40,55 @@ class UvTool(runtime: PyToolRuntime) : UvCommand("tool", runtime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* List installed tools
|
||||
* `uv tool list`, parsed into one [UvToolListResult] per installed tool. Optional fields are filled
|
||||
* according to the flags: [outdated] populates [UvToolListResult.latestVersion] for tools with a newer
|
||||
* release (uv 0.10.10+ lists only those), and [showPaths] populates [UvToolListResult.envPath] plus the
|
||||
* [UvToolListResult.executables] map (entry-point name -> path).
|
||||
*
|
||||
* uv prints a header line `name vVERSION [latest: NEWER]? (env/path)?` per tool, optionally followed
|
||||
* (with `--show-paths`) by one `- entrypoint (exe/path)` line per entry point (a tool may expose
|
||||
* several). Paths may contain spaces, so the parenthesized tails are matched greedily.
|
||||
*/
|
||||
suspend fun list(showVersionSpecifiers: Boolean? = null, showPaths: Boolean? = null, outdated: Boolean? = null): PyResult<String> {
|
||||
suspend fun list(showVersionSpecifiers: Boolean = false, showPaths: Boolean = false, outdated: Boolean = false): PyResult<List<UvToolListResult>> {
|
||||
val options = listOf(
|
||||
showVersionSpecifiers to "--show-version-specifiers",
|
||||
showPaths to "--show-paths",
|
||||
outdated to "--outdated",
|
||||
).makeOptions()
|
||||
return executeAndHandleErrors("list", *options, transformer = ZeroCodeStdoutTransformer)
|
||||
}
|
||||
val stdout = executeAndHandleErrors("list", *options, transformer = ZeroCodeStdoutTransformer).getOr { return it }
|
||||
|
||||
/**
|
||||
* Parsed form of `uv tool list --show-paths`. Each header line `name vX.Y.Z (path/to/env)` becomes one entry;
|
||||
* the `- executable` lines under each header are ignored (they are re-derivable from the env directory).
|
||||
*/
|
||||
suspend fun listInstalled(): PyResult<List<UvInstalledTool>> {
|
||||
val stdout = list(showPaths = true).getOr { return it }
|
||||
val headerRegex = Regex("""^(\S+) v(\S+) \((.+)\)$""")
|
||||
val tools = stdout.lineSequence()
|
||||
.mapNotNull { headerRegex.matchEntire(it.trim()) }
|
||||
.map { UvInstalledTool(name = it.groupValues[1], version = it.groupValues[2], envPath = Path.of(it.groupValues[3])) }
|
||||
.toList()
|
||||
return Result.success(tools)
|
||||
}
|
||||
val headerRegex = Regex("""^(\S+) v(\S+)(?: \[latest:\s*(\S+)])?(?: \((.+)\))?$""")
|
||||
val entryPointRegex = Regex("""^-\s+(\S+)\s+\((.+)\)$""")
|
||||
|
||||
/**
|
||||
* Parsed form of `uv tool list --outdated` (available since uv 0.10.10). Each header line
|
||||
* `name vCURRENT [latest: NEWER]` becomes one entry; subordinate `- executable` lines are ignored.
|
||||
* Returns only tools that have a newer release available.
|
||||
*/
|
||||
suspend fun listOutdated(): PyResult<List<UvOutdatedTool>> {
|
||||
val stdout = list(outdated = true).getOr { return it }
|
||||
val headerRegex = Regex("""^(\S+) v(\S+) \[latest:\s*(\S+)]$""")
|
||||
val tools = stdout.lineSequence()
|
||||
.mapNotNull { headerRegex.matchEntire(it.trim()) }
|
||||
.map { UvOutdatedTool(name = it.groupValues[1], currentVersion = it.groupValues[2], latestVersion = it.groupValues[3]) }
|
||||
.toList()
|
||||
val tools = mutableListOf<UvToolListResult>()
|
||||
var pending: UvToolListResult? = null
|
||||
val executables = linkedMapOf<String, Path>()
|
||||
|
||||
fun flushPending() {
|
||||
val tool = pending ?: return
|
||||
tools += tool.copy(executables = executables.toMap())
|
||||
executables.clear()
|
||||
pending = null
|
||||
}
|
||||
|
||||
for (rawLine in stdout.lineSequence()) {
|
||||
val line = rawLine.trim()
|
||||
val header = headerRegex.matchEntire(line)
|
||||
if (header != null) {
|
||||
flushPending()
|
||||
pending = UvToolListResult(
|
||||
name = header.groupValues[1],
|
||||
version = header.groupValues[2],
|
||||
latestVersion = header.groupValues[3].ifEmpty { null },
|
||||
envPath = header.groupValues[4].ifEmpty { null }?.let(Path::of),
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (pending == null) continue
|
||||
val entryPoint = entryPointRegex.matchEntire(line) ?: continue
|
||||
executables[entryPoint.groupValues[1]] = Path.of(entryPoint.groupValues[2])
|
||||
}
|
||||
flushPending()
|
||||
return Result.success(tools)
|
||||
}
|
||||
|
||||
@@ -91,22 +103,25 @@ class UvTool(runtime: PyToolRuntime) : UvCommand("tool", runtime) {
|
||||
suspend fun updateShell(): PyResult<Unit> = TODO()
|
||||
|
||||
/**
|
||||
* Show the path to the uv tools directory
|
||||
* `uv tool dir` — the directory uv stores tools in, or (with [bin] = true) the directory their
|
||||
* executables are placed on `PATH`. uv prints a single path line, parsed here into a [Path].
|
||||
*/
|
||||
suspend fun dir(bin: Boolean? = null): PyResult<String> {
|
||||
suspend fun dir(bin: Boolean = false): PyResult<Path> {
|
||||
val options = listOf(bin to "--bin").makeOptions()
|
||||
return executeAndHandleErrors("dir", *options, transformer = ZeroCodeStdoutTransformer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper over [dir] with `--bin` that returns a parsed [Path].
|
||||
*/
|
||||
suspend fun binDir(): PyResult<Path> {
|
||||
val output = dir(bin = true).getOr { return it }
|
||||
val output = executeAndHandleErrors("dir", *options, transformer = ZeroCodeStdoutTransformer).getOr { return it }
|
||||
return Result.success(Path.of(output.trim()))
|
||||
}
|
||||
}
|
||||
|
||||
data class UvInstalledTool(val name: String, val version: String, val envPath: Path)
|
||||
|
||||
data class UvOutdatedTool(val name: String, val currentVersion: String, val latestVersion: String)
|
||||
/**
|
||||
* One entry of `uv tool list`. [name] and installed [version] are always present; [latestVersion] is
|
||||
* set only for outdated tools (`--outdated`), and [envPath] plus [executables] (each entry point's name
|
||||
* mapped to its path, insertion-ordered as uv printed them) only with `--show-paths`.
|
||||
*/
|
||||
data class UvToolListResult(
|
||||
val name: String,
|
||||
val version: String,
|
||||
val latestVersion: String? = null,
|
||||
val envPath: Path? = null,
|
||||
val executables: Map<String, Path> = emptyMap(),
|
||||
)
|
||||
|
||||
+11
-11
@@ -113,7 +113,7 @@ class UvCliTest {
|
||||
@Test
|
||||
fun testTool(): Unit = timeoutRunBlocking(60.seconds) {
|
||||
val tool = myRuntime.uvCli().tool()
|
||||
assertTrue(tool.dir().getOrThrow().isNotBlank())
|
||||
assertTrue(tool.dir().getOrThrow().isAbsolute)
|
||||
tool.list().getOrThrow()
|
||||
}
|
||||
|
||||
@@ -141,21 +141,21 @@ class UvCliTest {
|
||||
"expected ${pkg.name} install under the class-scoped UV_TOOL_DIR ${uvContext.uvToolDirPath}, missing: $expectedToolEnv"
|
||||
}
|
||||
|
||||
// 2. listInstalled() should surface the freshly installed tool at the pinned version.
|
||||
val installed = tool.listInstalled().getOrThrow()
|
||||
// 2. list(showPaths) should surface the freshly installed tool at the pinned version.
|
||||
val installed = tool.list(showPaths = true).getOrThrow()
|
||||
val installedEntry = installed.firstOrNull { it.name == pkg.name }
|
||||
assertNotNull(installedEntry) { "expected ${pkg.name} in listInstalled(), got $installed" }
|
||||
assertNotNull(installedEntry) { "expected ${pkg.name} in list(), got $installed" }
|
||||
assertEquals(pkg.version, installedEntry!!.version) {
|
||||
"expected ${pkg.spec()} right after install, got ${installedEntry.version}"
|
||||
}
|
||||
|
||||
// 3. listOutdated() should report it with a newer latestVersion (we pinned to an older release).
|
||||
val outdatedBefore = tool.listOutdated().getOrThrow()
|
||||
// 3. list(outdated) should report it with a newer latestVersion (we pinned to an older release).
|
||||
val outdatedBefore = tool.list(outdated = true, showPaths = true).getOrThrow()
|
||||
val outdatedEntry = outdatedBefore.firstOrNull { it.name == pkg.name }
|
||||
assertNotNull(outdatedEntry) {
|
||||
"expected ${pkg.name} in listOutdated() before upgrade, got $outdatedBefore"
|
||||
"expected ${pkg.name} in list(outdated = true) before upgrade, got $outdatedBefore"
|
||||
}
|
||||
assertEquals(pkg.version, outdatedEntry!!.currentVersion)
|
||||
assertEquals(pkg.version, outdatedEntry!!.version)
|
||||
assertNotEquals(pkg.version, outdatedEntry.latestVersion) {
|
||||
"latestVersion must differ from the pinned ${pkg.version} for the outdated signal to mean anything"
|
||||
}
|
||||
@@ -166,7 +166,7 @@ class UvCliTest {
|
||||
// This is exactly the production path that surfaces "{tool} is already up to date" in
|
||||
// the External Tools settings balloon.
|
||||
tool.upgrade(pkg.name).getOrThrow()
|
||||
val outdatedAfterUpgrade = tool.listOutdated().getOrThrow()
|
||||
val outdatedAfterUpgrade = tool.list(outdated = true, showPaths = true).getOrThrow()
|
||||
assertTrue(outdatedAfterUpgrade.any { it.name == pkg.name }) {
|
||||
"uv tool upgrade respects the original pin; ${pkg.name} should still be outdated, got $outdatedAfterUpgrade"
|
||||
}
|
||||
@@ -174,9 +174,9 @@ class UvCliTest {
|
||||
// 5. `install(name, reinstall = true)` (uv's `--reinstall`) drops the prior pin and
|
||||
// installs the latest release. After that the outdated list must no longer mention it.
|
||||
tool.install(pkg.name, reinstall = true).getOrThrow()
|
||||
val outdatedAfterReinstall = tool.listOutdated().getOrThrow()
|
||||
val outdatedAfterReinstall = tool.list(outdated = true, showPaths = true).getOrThrow()
|
||||
assertTrue(outdatedAfterReinstall.none { it.name == pkg.name }) {
|
||||
"after install(reinstall=true) ${pkg.name} should drop off listOutdated(), got $outdatedAfterReinstall"
|
||||
"after install(reinstall=true) ${pkg.name} should drop off list(outdated = true), got $outdatedAfterReinstall"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -23,18 +23,23 @@ import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.openapi.util.use
|
||||
import com.intellij.openapi.vfs.newvfs.RefreshQueue
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.toEelApi
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.python.community.common.tools.ToolId
|
||||
import com.intellij.python.pyproject.model.api.ModuleCreateInfo
|
||||
import com.intellij.python.pyproject.model.api.autoConfigureSdkIfNeeded
|
||||
import com.intellij.python.pyproject.model.api.getModuleInfo
|
||||
import com.intellij.python.pyproject.statistics.PyProjectTomlCollector
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.performToolInstallation
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.components.DropDownLink
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.emit
|
||||
import com.jetbrains.python.inspections.InspectionRunnerResult
|
||||
import com.jetbrains.python.orLogException
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
@@ -190,14 +195,19 @@ private class UseProvidedInterpreterFix(private val myCreateSdkInfo: CreateSdkIn
|
||||
private class SuggestToolInstallationFix(
|
||||
private val myModule: Module,
|
||||
private val myCreateSdkInfo: CreateSdkInfo.WillInstallTool,
|
||||
private val myTool: ToolId,
|
||||
) : InterpreterFix {
|
||||
override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink {
|
||||
return ActionLink(myCreateSdkInfo.intentionName) {
|
||||
val pyTool = PyTool.findByPackageName(myCreateSdkInfo.toolToInstall) ?: return@ActionLink
|
||||
executor.execute {
|
||||
val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(myModule, myTool.id)
|
||||
val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(myModule, myCreateSdkInfo.toolToInstall)
|
||||
withBackgroundProgress(project, myCreateSdkInfo.intentionName, false) {
|
||||
lifetime.use { PyProjectSdkConfiguration.installToolAndShowErrorIfNeeded(myModule, myCreateSdkInfo.pathPersister, myCreateSdkInfo.toolToInstall) }
|
||||
lifetime.use {
|
||||
val eel = project.getEelDescriptor().toEelApi()
|
||||
pyTool.performToolInstallation(eel).mapSuccess(myCreateSdkInfo.pathPersister).errorOrNull?.also {
|
||||
ErrorSink().emit(it, project)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +229,7 @@ private suspend fun Module.getQuickFixBySdkSuggestion(i: ModuleCreateInfo?): Fin
|
||||
}
|
||||
is CreateSdkInfo.WillInstallTool -> {
|
||||
logger.trace { "$this: Tool installation will be suggested to the user" }
|
||||
FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo, i.toolId))
|
||||
FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -12,6 +12,7 @@ import com.intellij.openapi.vfs.findPsiFile
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.platform.util.progress.reportRawProgress
|
||||
import com.intellij.python.community.common.tools.ToolId
|
||||
import com.intellij.python.community.impl.poetry.backend.PoetryPyTool
|
||||
import com.intellij.python.community.impl.poetry.common.POETRY_TOOL_ID
|
||||
import com.intellij.python.community.impl.poetry.common.poetryPath
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
@@ -100,11 +101,11 @@ internal class PyPoetrySdkConfiguration : PyProjectTomlConfigurationExtension {
|
||||
*/
|
||||
else if (poetryLockExists || (isPoetryProject && checkToml)) {
|
||||
val pathPersister: (Path) -> Unit = { path -> PropertiesComponent.getInstance().poetryPath = path.toString() }
|
||||
val toolName = "poetry"
|
||||
val tool = PoetryPyTool.getInstance()
|
||||
EnvCheckerResult.SuggestToolInstallation(
|
||||
toolToInstall = toolName,
|
||||
toolToInstall = tool.packageName.name,
|
||||
pathPersister = pathPersister,
|
||||
intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title.using.pip", "poetry")
|
||||
intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title", tool.presentableName)
|
||||
)
|
||||
}
|
||||
else EnvCheckerResult.CannotConfigure
|
||||
|
||||
@@ -3,9 +3,14 @@ package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.platform.eel.provider.toEelApi
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.Version
|
||||
import com.intellij.python.pytools.performToolInstallation
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
@@ -18,7 +23,6 @@ import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.baseDir
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.installExecutableViaPythonScript
|
||||
import com.jetbrains.python.sdk.setAssociationToModule
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
@@ -27,9 +31,7 @@ import kotlinx.coroutines.flow.first
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import java.nio.file.Path
|
||||
|
||||
@Internal
|
||||
internal abstract class CustomNewEnvironmentCreator<P : PathHolder>(
|
||||
private val name: String,
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
protected val errorSink: ErrorSink,
|
||||
) : PythonNewEnvironmentCreator<P>(model) {
|
||||
@@ -50,8 +52,8 @@ internal abstract class CustomNewEnvironmentCreator<P : PathHolder>(
|
||||
fileSystem = model.fileSystem,
|
||||
pathValidator = toolValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", name),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
|
||||
labelText = message("sdk.create.custom.venv.executable.path", pyTool.presentableName),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", pyTool.presentableName),
|
||||
installAction = createInstallFix(errorSink),
|
||||
)
|
||||
|
||||
@@ -112,7 +114,7 @@ internal abstract class CustomNewEnvironmentCreator<P : PathHolder>(
|
||||
*/
|
||||
@RequiresEdt
|
||||
protected fun createInstallFix(errorSink: ErrorSink): ActionLink {
|
||||
return ActionLink(message("sdk.create.custom.venv.install.fix.title.using.pip", name)) {
|
||||
return ActionLink(message("sdk.create.custom.venv.install.fix.title", pyTool.presentableName)) {
|
||||
PythonSdkFlavor.clearExecutablesCache()
|
||||
installExecutable(errorSink)
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
|
||||
@@ -122,55 +124,27 @@ internal abstract class CustomNewEnvironmentCreator<P : PathHolder>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the selected downloadable env (if selected), then installs the necessary executable in the Python environment.
|
||||
*
|
||||
* Initiates a blocking modal progress task to:
|
||||
* 1. Ensure that the environment is downloaded (if selected).
|
||||
* 2. Ensure that pip is installed.
|
||||
* 3. Install the executable (specified by `name`) using either a custom installation script or via pip.
|
||||
* Installs the [pyTool] executable behind a single modal progress via its `performToolInstallation`
|
||||
* extension (prefers `uv tool install`, falls back to a pip install into a system Python). On
|
||||
* success the resolved launcher is persisted.
|
||||
*/
|
||||
@RequiresEdt
|
||||
private fun installExecutable(errorSink: ErrorSink) {
|
||||
val baseInterpreter = model.state.baseInterpreter.get()
|
||||
|
||||
val installedSdk = when (baseInterpreter) {
|
||||
is InstallableSelectableInterpreter -> installBaseSdk(baseInterpreter.installableSdk)
|
||||
?.let {
|
||||
val sdkWrapper =
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
|
||||
model.fileSystem.wrapSdk(it)
|
||||
}
|
||||
val installed = model.addInstalledInterpreter(sdkWrapper.homePath, baseInterpreter.pythonInfo)
|
||||
model.state.baseInterpreter.set(installed)
|
||||
installed
|
||||
}
|
||||
is DetectedSelectableInterpreter, is ExistingSelectableInterpreter, is ManuallyAddedSelectableInterpreter, null -> null
|
||||
}
|
||||
|
||||
// installedSdk is null when the selected sdk isn't downloadable
|
||||
// model.state.baseInterpreter could be null if no SDK was selected
|
||||
val pythonExecutablePath = installedSdk?.homePath ?: model.state.baseInterpreter.get()?.homePath
|
||||
val pythonExecutable = pythonExecutablePath?.let { model.fileSystem.getBinaryToExec(it) } ?: return
|
||||
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title.using.pip", name)) {
|
||||
val versionArgs: List<String> = installationVersion?.let { listOf("-v", it) } ?: emptyList()
|
||||
when (val r = installExecutableViaPythonScript(pythonExecutable, "-n", name, *versionArgs.toTypedArray())) {
|
||||
is Result.Success -> {
|
||||
val pathHolder = PathHolder.Eel(r.result)
|
||||
savePathToExecutableToProperties(pathHolder as? P)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
errorSink.emit(r.error)
|
||||
}
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title", pyTool.presentableName)) {
|
||||
val eel = model.projectPathFlows.projectPath.first()?.getEelDescriptor()?.toEelApi() ?: localEel
|
||||
when (val r = pyTool.performToolInstallation(eel)) {
|
||||
is Result.Success -> savePathToExecutableToProperties(PathHolder.Eel(r.result) as? P)
|
||||
is Result.Failure -> errorSink.emit(r.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract val interpreterType: InterpreterType
|
||||
|
||||
internal abstract val toolValidator: ToolValidator<P>
|
||||
/** The tool this creator installs; drives [installExecutable] via [performToolInstallation]. */
|
||||
internal abstract val pyTool: PyTool
|
||||
|
||||
internal open val installationVersion: String? = null
|
||||
internal abstract val toolValidator: ToolValidator<P>
|
||||
|
||||
protected abstract suspend fun setupEnvSdk(moduleBasePath: Path): PyResult<Sdk>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.intellij.openapi.ui.validation.and
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.python.community.common.tools.ToolId
|
||||
import com.intellij.python.community.impl.conda.icons.PythonCommunityImplCondaIcons
|
||||
import com.intellij.python.community.impl.pipenv.PIPENV_ICON
|
||||
import com.intellij.python.community.impl.pipenv.icons.PythonCommunityImplPipenvIcons
|
||||
import com.intellij.python.community.impl.poetry.common.POETRY_TOOL_ID
|
||||
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
|
||||
import com.intellij.python.hatch.icons.PythonHatchIcons
|
||||
@@ -145,7 +145,7 @@ enum class PythonSupportedEnvironmentManagers(
|
||||
VIRTUALENV(VENV_TOOL_ID, "sdk.create.custom.virtualenv", PythonVenvIcons.VirtualEnv, sshAutoUploadRequired = false, { true }),
|
||||
CONDA(CONDA_TOOL_ID, "sdk.create.custom.conda", PythonCommunityImplCondaIcons.Anaconda, sshAutoUploadRequired = false, { true }),
|
||||
POETRY(POETRY_TOOL_ID, "sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry, sshAutoUploadRequired = false),
|
||||
PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PIPENV_ICON, sshAutoUploadRequired = false),
|
||||
PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PythonCommunityImplPipenvIcons.PythonClosed, sshAutoUploadRequired = false),
|
||||
UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonUvCommonIcons.UV, sshAutoUploadRequired = true, { true }),
|
||||
HATCH(HATCH_TOOL_ID, "sdk.create.custom.hatch", PythonHatchIcons.Logo, sshAutoUploadRequired = false),
|
||||
PYTHON(VENV_TOOL_ID, "sdk.create.custom.python", PythonParserIcons.PythonFile, sshAutoUploadRequired = false, { true })
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
import com.intellij.python.hatch.HatchConfiguration
|
||||
import com.intellij.python.hatch.HatchPyTool
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
@@ -35,8 +37,9 @@ import java.nio.file.Path
|
||||
internal class HatchNewEnvironmentCreator<P : PathHolder>(
|
||||
override val model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator<P>("hatch", model, errorSink) {
|
||||
) : CustomNewEnvironmentCreator<P>(model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.HATCH
|
||||
override val pyTool: PyTool = HatchPyTool.getInstance()
|
||||
override val toolValidator: ToolValidator<P> = model.hatchViewModel.toolValidator
|
||||
private lateinit var hatchFormFields: HatchFormFields<P>
|
||||
override val toolExecutable: ObservableProperty<ValidatedPath.Executable<P>?> = model.hatchViewModel.hatchExecutable
|
||||
@@ -87,7 +90,7 @@ internal class HatchNewEnvironmentCreator<P : PathHolder>(
|
||||
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
|
||||
val basePythonBinaryEelPath = when (basePythonBinaryPath) {
|
||||
is PathHolder.Eel -> basePythonBinaryPath.path
|
||||
else -> return PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
else -> return PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
val hatchExecutablePath = when (val hatchBinary = model.hatchViewModel.hatchExecutable.get()?.pathHolder) {
|
||||
is PathHolder.Eel -> hatchBinary.path
|
||||
|
||||
@@ -4,7 +4,9 @@ package com.jetbrains.python.sdk.add.v2.pipenv
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.observable.properties.ObservableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.python.community.impl.pipenv.PipEnvPyTool
|
||||
import com.intellij.python.community.impl.pipenv.pipenvPath
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
@@ -21,8 +23,9 @@ import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkWithProgressReport
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class EnvironmentCreatorPip<P : PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, errorSink: ErrorSink) : CustomNewEnvironmentCreator<P>("pipenv", model, errorSink) {
|
||||
internal class EnvironmentCreatorPip<P : PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, errorSink: ErrorSink) : CustomNewEnvironmentCreator<P>(model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.PIPENV
|
||||
override val pyTool: PyTool = PipEnvPyTool.getInstance()
|
||||
override val toolValidator: ToolValidator<P> = model.pipenvViewModel.toolValidator
|
||||
override val toolExecutable: ObservableProperty<ValidatedPath.Executable<P>?> = model.pipenvViewModel.pipenvExecutable
|
||||
override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder ->
|
||||
@@ -40,7 +43,7 @@ internal class EnvironmentCreatorPip<P : PathHolder>(model: PythonMutableTargetA
|
||||
installPackages = false
|
||||
)
|
||||
}
|
||||
else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
else -> PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import com.intellij.openapi.observable.properties.ObservableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.python.community.impl.poetry.backend.PoetryPyTool
|
||||
import com.intellij.python.community.impl.poetry.common.poetryPath
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindSelected
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
@@ -54,10 +56,10 @@ internal class EnvironmentCreatorPoetry<P : PathHolder>(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
private val module: Module?,
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator<P>("poetry", model, errorSink) {
|
||||
) : CustomNewEnvironmentCreator<P>(model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.POETRY
|
||||
override val pyTool: PyTool = PoetryPyTool.getInstance()
|
||||
override val toolValidator: ToolValidator<P> = model.poetryViewModel.toolValidator
|
||||
override val installationVersion: String = "1.8.0"
|
||||
override val toolExecutable: ObservableProperty<ValidatedPath.Executable<P>?> = model.poetryViewModel.poetryExecutable
|
||||
override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder ->
|
||||
savePathForEelOnly(pathHolder) { path -> PropertiesComponent.getInstance().poetryPath = path.toString() }
|
||||
@@ -129,7 +131,7 @@ internal class EnvironmentCreatorPoetry<P : PathHolder>(
|
||||
inProjectEnv = isInProjectEnvFlow.value,
|
||||
)
|
||||
}
|
||||
else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
else -> PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +149,7 @@ internal class EnvironmentCreatorPoetry<P : PathHolder>(
|
||||
private fun addInProjectCheckbox(panel: Panel) {
|
||||
with(panel) {
|
||||
row("") {
|
||||
checkBox(PyBundle.message("python.sdk.poetry.dialog.add.new.environment.in.project.checkbox"))
|
||||
checkBox(message("python.sdk.poetry.dialog.add.new.environment.in.project.checkbox"))
|
||||
.bindSelected(isInProjectEnvProp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.python.uv.backend.UvPyTool
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
@@ -70,8 +72,9 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
private val module: Module?,
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator<P>("uv", model, errorSink) {
|
||||
) : CustomNewEnvironmentCreator<P>(model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.UV
|
||||
override val pyTool: PyTool = UvPyTool.getInstance()
|
||||
override val toolValidator: ToolValidator<P> = model.uvViewModel.toolValidator
|
||||
private val executableFlow = MutableStateFlow(model.uvViewModel.uvExecutable.get())
|
||||
private val pythonVersion: ObservableMutableProperty<Version?> = propertyGraph.property(null)
|
||||
|
||||
@@ -6,35 +6,18 @@ import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.python.community.common.tools.ToolId
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.pyproject.model.api.SuggestedSdk
|
||||
import com.intellij.python.pyproject.model.api.suggestSdk
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PythonPluginDisposable
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.emit
|
||||
import com.jetbrains.python.sdk.configuration.suppressors.PyPackageRequirementsInspectionSuppressor
|
||||
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
|
||||
import com.jetbrains.python.sdk.configurePythonSdk
|
||||
import com.jetbrains.python.sdk.installExecutableViaPythonScript
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
|
||||
object PyProjectSdkConfiguration {
|
||||
internal suspend fun installToolAndShowErrorIfNeeded(module: Module, pathPersister: (Path) -> Unit, toolToInstall: String) {
|
||||
performToolInstallation(pathPersister, toolToInstall).errorOrNull?.also {
|
||||
ErrorSink().emit(it, module.project)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performToolInstallation(pathPersister: (Path) -> Unit, toolToInstall: String): PyResult<Unit> {
|
||||
val systemPython = SystemPythonService().findSystemPythons().firstOrNull()
|
||||
?: return PyResult.localizedError(PyBundle.message("sdk.cannot.find.python"))
|
||||
return installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", toolToInstall).mapSuccess(pathPersister)
|
||||
}
|
||||
|
||||
suspend fun setSdkUsingCreateSdkInfo(
|
||||
module: Module, createSdkInfoWithTool: CreateSdkInfoWithTool,
|
||||
): Boolean = withContext(Dispatchers.Default) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.configuration
|
||||
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.services.systemPython.SystemPython
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.pytools.InstalledInfo
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.PyToolManager
|
||||
import com.intellij.python.pytools.PyToolManagerProvider
|
||||
import com.intellij.python.pytools.configuration.ConfigurablePyTool
|
||||
import com.intellij.python.pytools.getToolVersion
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
|
||||
import com.jetbrains.python.packaging.repository.PyPiPackageRepository
|
||||
import com.jetbrains.python.sdk.add.v2.FileSystem
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.toFileSystem
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.sdk.installExecutableViaPythonScript
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Terminal [PyToolManagerProvider] fallback: yields a manager that pip-installs into the first system
|
||||
* Python of the target environment. Registered last, so it is used only when no higher-priority
|
||||
* provider (e.g. uv) can operate there.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class SystemPythonToolManagerProvider : PyToolManagerProvider {
|
||||
override suspend fun forEel(eel: EelApi): PyToolManager? {
|
||||
val systemPython = SystemPythonService().findSystemPythons(eelApi = eel).firstOrNull() ?: return null
|
||||
return SystemPythonToolManager(eel.toFileSystem(), systemPython)
|
||||
}
|
||||
}
|
||||
|
||||
/** pip-installs tools into [systemPython] via the `pycharm_package_installer.py` helper. */
|
||||
private class SystemPythonToolManager(
|
||||
private val fileSystem: FileSystem<PathHolder.Eel>,
|
||||
private val systemPython: SystemPython,
|
||||
) : PyToolManager {
|
||||
override suspend fun install(tool: PyTool): PyResult<Path> {
|
||||
installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", tool.packageName.name).getOr { return it }
|
||||
val executable = fileSystem.detectTool(tool.packageName.name)
|
||||
?: return PyResult.localizedError(PySdkBundle.message("cannot.find.executable", tool.packageName.name, fileSystem.userReadableName))
|
||||
return Result.success(executable.path)
|
||||
}
|
||||
|
||||
/** The pip helper always installs the latest release, so an upgrade is just a fresh install. */
|
||||
override suspend fun upgrade(tool: PyTool): PyResult<Path> = install(tool)
|
||||
|
||||
/**
|
||||
* Every configurable tool that is actually installed (resolved on [fileSystem]), with its `--version`
|
||||
* probed and the latest release looked up from PyPI. When PyPI is unreachable the latest version falls
|
||||
* back to the installed one (i.e. reported as up to date).
|
||||
*/
|
||||
override suspend fun list(): Map<PyTool, InstalledInfo> {
|
||||
return PyTool.EP_NAME.extensionList.filter { it is ConfigurablePyTool }.mapNotNull { tool ->
|
||||
val name = tool.packageName.name
|
||||
val executable = fileSystem.detectTool(name) ?: return@mapNotNull null
|
||||
val installed = BinOnEel(executable.path).getToolVersion(name).getOrNull()?.value ?: return@mapNotNull null
|
||||
val latest = latestPyPiVersion(name) ?: installed
|
||||
tool to InstalledInfo(path = executable.path, installedVersion = installed, latestVersion = latest)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest stable release of [packageName] from PyPI, or `null` if it can't be determined. Queried
|
||||
* app-level through [PyPiPackageRepository] (no project needed); `availableVersions` come back sorted
|
||||
* newest-first, and we skip pre-/dev-releases to match the default "stable only" upgrade policy.
|
||||
*/
|
||||
private suspend fun latestPyPiVersion(packageName: String): String? {
|
||||
val details = withContext(Dispatchers.IO) { PyPiPackageRepository.buildPackageDetails(packageName) }.getOrNull()
|
||||
?: return null
|
||||
return details.availableVersions.firstOrNull { version ->
|
||||
val normalized = PyPackageVersionNormalizer.normalize(version)
|
||||
normalized == null || (normalized.pre == null && normalized.dev == null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +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.
|
||||
package com.jetbrains.python.sdk.pipenv
|
||||
|
||||
import com.intellij.python.community.impl.pipenv.PIPENV_ICON
|
||||
import com.intellij.python.community.impl.pipenv.PipEnvPyTool
|
||||
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import com.jetbrains.python.sdk.flavors.PythonFlavorProvider
|
||||
@@ -10,7 +10,7 @@ import javax.swing.Icon
|
||||
|
||||
|
||||
internal object PyPipEnvSdkFlavor : CPythonSdkFlavor<PyFlavorData.Empty>() {
|
||||
override fun getIcon(): Icon = PIPENV_ICON
|
||||
override fun getIcon(): Icon = PipEnvPyTool.getInstance().icon
|
||||
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
|
||||
|
||||
override fun isValidSdkPath(pythonBinaryPath: Path): Boolean = false
|
||||
|
||||
@@ -42,7 +42,7 @@ internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension {
|
||||
EnvCheckerResult.SuggestToolInstallation(
|
||||
toolToInstall = toolName,
|
||||
pathPersister = pathPersister,
|
||||
intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title.using.pip", toolName)
|
||||
intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title", toolName)
|
||||
)
|
||||
} else baseCheckResult
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user