PY-65295 Poetry installation

Try to install Poetry and Pipenv if they are not found


Merge-request: IJ-MR-141839
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

GitOrigin-RevId: 535426090df23b358ba61a9e21c2f0954c201945
This commit is contained in:
Egor Eliseev
2024-09-10 10:40:05 +00:00
committed by intellij-monorepo-bot
parent 9f592f91ee
commit a54292035d
16 changed files with 1366 additions and 217 deletions

View File

@@ -0,0 +1,945 @@
#!/usr/bin/env python3
r"""
This script will install Poetry and its dependencies in an isolated fashion.
It will perform the following steps:
* Create a new virtual environment using the built-in venv module, or the virtualenv zipapp if venv is unavailable.
This will be created at a platform-specific path (or `$POETRY_HOME` if `$POETRY_HOME` is set:
- `~/Library/Application Support/pypoetry` on macOS
- `$XDG_DATA_HOME/pypoetry` on Linux/Unix (`$XDG_DATA_HOME` is `~/.local/share` if unset)
- `%APPDATA%\pypoetry` on Windows
* Update pip inside the virtual environment to avoid bugs in older versions.
* Install the latest (or a given) version of Poetry inside this virtual environment using pip.
* Install a `poetry` script into a platform-specific path (or `$POETRY_HOME/bin` if `$POETRY_HOME` is set):
- `~/.local/bin` on Unix
- `%APPDATA%\Python\Scripts` on Windows
* Attempt to inform the user if they need to add this bin directory to their `$PATH`, as well as how to do so.
* Upon failure, write an error log to `poetry-installer-error-<hash>.log and restore any previous environment.
This script performs minimal magic, and should be relatively stable. However, it is optimized for interactive developer
use and trivial pipelines. If you are considering using this script in production, you should consider manually-managed
installs, or use of pipx as alternatives to executing arbitrary, unversioned code from the internet. If you prefer this
script to alternatives, consider maintaining a local copy as part of your infrastructure.
For full documentation, visit https://python-poetry.org/docs/#installation.
""" # noqa: E501
import sys
# Eager version check so we fail nicely before possible syntax errors
if sys.version_info < (3, 6): # noqa: UP036
sys.stdout.write("Poetry installer requires Python 3.6 or newer to run!\n")
sys.exit(1)
import argparse
import json
import os
import re
import shutil
import subprocess
import sysconfig
import tempfile
from contextlib import closing
from contextlib import contextmanager
from functools import cmp_to_key
from io import UnsupportedOperation
from pathlib import Path
from typing import Optional
from urllib.request import Request
from urllib.request import urlopen
SHELL = os.getenv("SHELL", "")
WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")
MINGW = sysconfig.get_platform().startswith("mingw")
MACOS = sys.platform == "darwin"
FOREGROUND_COLORS = {
"black": 30,
"red": 31,
"green": 32,
"yellow": 33,
"blue": 34,
"magenta": 35,
"cyan": 36,
"white": 37,
}
BACKGROUND_COLORS = {
"black": 40,
"red": 41,
"green": 42,
"yellow": 43,
"blue": 44,
"magenta": 45,
"cyan": 46,
"white": 47,
}
OPTIONS = {"bold": 1, "underscore": 4, "blink": 5, "reverse": 7, "conceal": 8}
def style(fg, bg, options):
codes = []
if fg:
codes.append(FOREGROUND_COLORS[fg])
if bg:
codes.append(BACKGROUND_COLORS[bg])
if options:
if not isinstance(options, (list, tuple)):
options = [options]
for option in options:
codes.append(OPTIONS[option])
return "\033[{}m".format(";".join(map(str, codes)))
STYLES = {
"info": style("cyan", None, None),
"comment": style("yellow", None, None),
"success": style("green", None, None),
"error": style("red", None, None),
"warning": style("yellow", None, None),
"b": style(None, None, ("bold",)),
}
def is_decorated():
if WINDOWS:
return (
os.getenv("ANSICON") is not None
or os.getenv("ConEmuANSI") == "ON" # noqa: SIM112
or os.getenv("Term") == "xterm" # noqa: SIM112
)
if not hasattr(sys.stdout, "fileno"):
return False
try:
return os.isatty(sys.stdout.fileno())
except UnsupportedOperation:
return False
def is_interactive():
if not hasattr(sys.stdin, "fileno"):
return False
try:
return os.isatty(sys.stdin.fileno())
except UnsupportedOperation:
return False
def colorize(style, text):
if not is_decorated():
return text
return f"{STYLES[style]}{text}\033[0m"
def string_to_bool(value):
value = value.lower()
return value in {"true", "1", "y", "yes"}
def data_dir() -> Path:
if os.getenv("POETRY_HOME"):
return Path(os.getenv("POETRY_HOME")).expanduser()
if WINDOWS:
base_dir = Path(_get_win_folder("CSIDL_APPDATA"))
elif MACOS:
base_dir = Path("~/Library/Application Support").expanduser()
else:
base_dir = Path(os.getenv("XDG_DATA_HOME", "~/.local/share")).expanduser()
base_dir = base_dir.resolve()
return base_dir / "pypoetry"
def bin_dir() -> Path:
if os.getenv("POETRY_HOME"):
return Path(os.getenv("POETRY_HOME")).expanduser() / "bin"
if WINDOWS and not MINGW:
return Path(_get_win_folder("CSIDL_APPDATA")) / "Python/Scripts"
else:
return Path("~/.local/bin").expanduser()
def _get_win_folder_from_registry(csidl_name):
import winreg as _winreg
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
"CSIDL_COMMON_APPDATA": "Common AppData",
"CSIDL_LOCAL_APPDATA": "Local AppData",
}[csidl_name]
key = _winreg.OpenKey(
_winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders",
)
path, _ = _winreg.QueryValueEx(key, shell_folder_name)
return path
def _get_win_folder_with_ctypes(csidl_name):
import ctypes
csidl_const = {
"CSIDL_APPDATA": 26,
"CSIDL_COMMON_APPDATA": 35,
"CSIDL_LOCAL_APPDATA": 28,
}[csidl_name]
buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in buf:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf2 = ctypes.create_unicode_buffer(1024)
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
buf = buf2
return buf.value
if WINDOWS:
try:
from ctypes import windll # noqa: F401
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
_get_win_folder = _get_win_folder_from_registry
PRE_MESSAGE = """# Welcome to {poetry}!
This will download and install the latest version of {poetry},
a dependency and package manager for Python.
It will add the `poetry` command to {poetry}'s bin directory, located at:
{poetry_home_bin}
You can uninstall at any time by executing this script with the --uninstall option,
and these changes will be reverted.
"""
POST_MESSAGE = """{poetry} ({version}) is installed now. Great!
You can test that everything is set up by executing:
`{test_command}`
"""
POST_MESSAGE_NOT_IN_PATH = """{poetry} ({version}) is installed now. Great!
To get started you need {poetry}'s bin directory ({poetry_home_bin}) in your `PATH`
environment variable.
{configure_message}
Alternatively, you can call {poetry} explicitly with `{poetry_executable}`.
You can test that everything is set up by executing:
`{test_command}`
"""
POST_MESSAGE_CONFIGURE_UNIX = """
Add `export PATH="{poetry_home_bin}:$PATH"` to your shell configuration file.
"""
POST_MESSAGE_CONFIGURE_FISH = """
You can execute `set -U fish_user_paths {poetry_home_bin} $fish_user_paths`
"""
POST_MESSAGE_CONFIGURE_WINDOWS = """"""
class PoetryInstallationError(RuntimeError):
def __init__(self, return_code: int = 0, log: Optional[str] = None):
super().__init__()
self.return_code = return_code
self.log = log
class VirtualEnvironment:
def __init__(self, path: Path) -> None:
self._path = path
self._bin_path = self._path.joinpath(
"Scripts" if WINDOWS and not MINGW else "bin"
)
# str is for compatibility with subprocess.run on CPython <= 3.7 on Windows
self._python = str(
self._path.joinpath(self._bin_path, "python.exe" if WINDOWS else "python")
)
@property
def path(self):
return self._path
@property
def bin_path(self):
return self._bin_path
@classmethod
def make(cls, target: Path) -> "VirtualEnvironment":
if not sys.executable:
raise ValueError(
"Unable to determine sys.executable. Set PATH to a sane value or set it"
" explicitly with PYTHONEXECUTABLE."
)
try:
# on some linux distributions (eg: debian), the distribution provided python
# installation might not include ensurepip, causing the venv module to
# fail when attempting to create a virtual environment
# we import ensurepip but do not use it explicitly here
import ensurepip # noqa: F401
import venv
builder = venv.EnvBuilder(clear=True, with_pip=True, symlinks=False)
context = builder.ensure_directories(target)
if (
WINDOWS
and hasattr(context, "env_exec_cmd")
and context.env_exe != context.env_exec_cmd
):
target = target.resolve()
builder.create(target)
except ImportError:
# fallback to using virtualenv package if venv is not available, eg: ubuntu
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
virtualenv_bootstrap_url = (
f"https://bootstrap.pypa.io/virtualenv/{python_version}/virtualenv.pyz"
)
with tempfile.TemporaryDirectory(prefix="poetry-installer") as temp_dir:
virtualenv_pyz = Path(temp_dir) / "virtualenv.pyz"
request = Request(
virtualenv_bootstrap_url, headers={"User-Agent": "Python Poetry"}
)
virtualenv_pyz.write_bytes(urlopen(request).read())
cls.run(
sys.executable, virtualenv_pyz, "--clear", "--always-copy", target
)
# We add a special file so that Poetry can detect
# its own virtual environment
target.joinpath("poetry_env").touch()
env = cls(target)
# this ensures that outdated system default pip does not trigger older bugs
env.pip("install", "--disable-pip-version-check", "--upgrade", "pip")
return env
@staticmethod
def run(*args, **kwargs) -> subprocess.CompletedProcess:
completed_process = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
**kwargs,
)
if completed_process.returncode != 0:
raise PoetryInstallationError(
return_code=completed_process.returncode,
log=completed_process.stdout.decode(),
)
return completed_process
def python(self, *args, **kwargs) -> subprocess.CompletedProcess:
return self.run(self._python, *args, **kwargs)
def pip(self, *args, **kwargs) -> subprocess.CompletedProcess:
return self.python("-m", "pip", *args, **kwargs)
class Cursor:
def __init__(self) -> None:
self._output = sys.stdout
def move_up(self, lines: int = 1) -> "Cursor":
self._output.write(f"\x1b[{lines}A")
return self
def move_down(self, lines: int = 1) -> "Cursor":
self._output.write(f"\x1b[{lines}B")
return self
def move_right(self, columns: int = 1) -> "Cursor":
self._output.write(f"\x1b[{columns}C")
return self
def move_left(self, columns: int = 1) -> "Cursor":
self._output.write(f"\x1b[{columns}D")
return self
def move_to_column(self, column: int) -> "Cursor":
self._output.write(f"\x1b[{column}G")
return self
def move_to_position(self, column: int, row: int) -> "Cursor":
self._output.write(f"\x1b[{row + 1};{column}H")
return self
def save_position(self) -> "Cursor":
self._output.write("\x1b7")
return self
def restore_position(self) -> "Cursor":
self._output.write("\x1b8")
return self
def hide(self) -> "Cursor":
self._output.write("\x1b[?25l")
return self
def show(self) -> "Cursor":
self._output.write("\x1b[?25h\x1b[?0c")
return self
def clear_line(self) -> "Cursor":
"""
Clears all the output from the current line.
"""
self._output.write("\x1b[2K")
return self
def clear_line_after(self) -> "Cursor":
"""
Clears all the output from the current line after the current position.
"""
self._output.write("\x1b[K")
return self
def clear_output(self) -> "Cursor":
"""
Clears all the output from the cursors' current position
to the end of the screen.
"""
self._output.write("\x1b[0J")
return self
def clear_screen(self) -> "Cursor":
"""
Clears the entire screen.
"""
self._output.write("\x1b[2J")
return self
class Installer:
METADATA_URL = "https://pypi.org/pypi/poetry/json"
VERSION_REGEX = re.compile(
r"v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?"
"("
"[._-]?"
r"(?:(stable|beta|b|rc|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?"
"([.-]?dev)?"
")?"
r"(?:\+[^\s]+)?"
)
def __init__(
self,
version: Optional[str] = None,
preview: bool = False,
force: bool = False,
accept_all: bool = False,
git: Optional[str] = None,
path: Optional[str] = None,
) -> None:
self._version = version
self._preview = preview
self._force = force
self._accept_all = accept_all
self._git = git
self._path = path
self._cursor = Cursor()
self._bin_dir = None
self._data_dir = None
@property
def bin_dir(self) -> Path:
if not self._bin_dir:
self._bin_dir = bin_dir()
return self._bin_dir
@property
def data_dir(self) -> Path:
if not self._data_dir:
self._data_dir = data_dir()
return self._data_dir
@property
def version_file(self) -> Path:
return self.data_dir.joinpath("VERSION")
def allows_prereleases(self) -> bool:
return self._preview
def run(self) -> int:
if self._git:
version = self._git
elif self._path:
version = self._path
else:
try:
version, current_version = self.get_version()
except ValueError:
return 1
if version is None:
return 0
self.display_pre_message()
self.ensure_directories()
def _is_self_upgrade_supported(x):
mx = self.VERSION_REGEX.match(x)
if mx is None:
# the version is not semver, perhaps scm or file
# we assume upgrade is supported
return True
vx = (*tuple(int(p) for p in mx.groups()[:3]), mx.group(5))
return vx >= (1, 1, 7)
if version and not _is_self_upgrade_supported(version):
self._write(
colorize(
"warning",
f"You are installing {version}. When using the current installer, "
"this version does not support updating using the 'self update' "
"command. Please use 1.1.7 or later.",
)
)
if not self._accept_all:
continue_install = input("Do you want to continue? ([y]/n) ") or "y"
if continue_install.lower() in {"n", "no"}:
return 0
try:
self.install(version)
except subprocess.CalledProcessError as e:
raise PoetryInstallationError(
return_code=e.returncode, log=e.output.decode()
) from e
self._write("")
self.display_post_message(version)
return 0
def install(self, version):
"""
Installs Poetry in $POETRY_HOME.
"""
self._write(
"Installing {} ({})".format(
colorize("info", "Poetry"), colorize("info", version)
)
)
with self.make_env(version) as env:
self.install_poetry(version, env)
self.make_bin(version, env)
self.version_file.write_text(version)
self._install_comment(version, "Done")
return 0
def uninstall(self) -> int:
if not self.data_dir.exists():
self._write(
"{} is not currently installed.".format(colorize("info", "Poetry"))
)
return 1
version = None
if self.version_file.exists():
version = self.version_file.read_text().strip()
if version:
self._write(
"Removing {} ({})".format(
colorize("info", "Poetry"), colorize("b", version)
)
)
else:
self._write("Removing {}".format(colorize("info", "Poetry")))
shutil.rmtree(str(self.data_dir))
for script in ["poetry", "poetry.bat", "poetry.exe"]:
if self.bin_dir.joinpath(script).exists():
self.bin_dir.joinpath(script).unlink()
return 0
def _install_comment(self, version: str, message: str):
self._overwrite(
"Installing {} ({}): {}".format(
colorize("info", "Poetry"),
colorize("b", version),
colorize("comment", message),
)
)
@contextmanager
def make_env(self, version: str) -> VirtualEnvironment:
env_path = self.data_dir.joinpath("venv")
env_path_saved = env_path.with_suffix(".save")
if env_path.exists():
self._install_comment(version, "Saving existing environment")
if env_path_saved.exists():
shutil.rmtree(env_path_saved)
shutil.move(env_path, env_path_saved)
try:
self._install_comment(version, "Creating environment")
yield VirtualEnvironment.make(env_path)
except Exception as e:
if env_path.exists():
self._install_comment(
version, "An error occurred. Removing partial environment."
)
shutil.rmtree(env_path)
if env_path_saved.exists():
self._install_comment(
version, "Restoring previously saved environment."
)
shutil.move(env_path_saved, env_path)
raise e
else:
if env_path_saved.exists():
shutil.rmtree(env_path_saved, ignore_errors=True)
def make_bin(self, version: str, env: VirtualEnvironment) -> None:
self._install_comment(version, "Creating script")
self.bin_dir.mkdir(parents=True, exist_ok=True)
script = "poetry.exe" if WINDOWS else "poetry"
target_script = env.bin_path.joinpath(script)
if self.bin_dir.joinpath(script).exists():
self.bin_dir.joinpath(script).unlink()
try:
self.bin_dir.joinpath(script).symlink_to(target_script)
except OSError:
# This can happen if the user
# does not have the correct permission on Windows
shutil.copy(target_script, self.bin_dir.joinpath(script))
def install_poetry(self, version: str, env: VirtualEnvironment) -> None:
self._install_comment(version, "Installing Poetry")
if self._git:
specification = "git+" + version
elif self._path:
specification = version
else:
specification = f"poetry=={version}"
env.pip("install", specification)
def display_pre_message(self) -> None:
kwargs = {
"poetry": colorize("info", "Poetry"),
"poetry_home_bin": colorize("comment", self.bin_dir),
}
self._write(PRE_MESSAGE.format(**kwargs))
def display_post_message(self, version: str) -> None:
if WINDOWS:
return self.display_post_message_windows(version)
if SHELL == "fish":
return self.display_post_message_fish(version)
return self.display_post_message_unix(version)
def display_post_message_windows(self, version: str) -> None:
path = self.get_windows_path_var()
message = POST_MESSAGE_NOT_IN_PATH
if path and str(self.bin_dir) in path:
message = POST_MESSAGE
self._write(
message.format(
poetry=colorize("info", "Poetry"),
version=colorize("b", version),
poetry_home_bin=colorize("comment", self.bin_dir),
poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
configure_message=POST_MESSAGE_CONFIGURE_WINDOWS.format(
poetry_home_bin=colorize("comment", self.bin_dir)
),
test_command=colorize("b", "poetry --version"),
)
)
def get_windows_path_var(self) -> Optional[str]:
import winreg
with winreg.ConnectRegistry(
None, winreg.HKEY_CURRENT_USER
) as root, winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key:
path, _ = winreg.QueryValueEx(key, "PATH")
return path
def display_post_message_fish(self, version: str) -> None:
fish_user_paths = subprocess.check_output(
["fish", "-c", "echo $fish_user_paths"]
).decode("utf-8")
message = POST_MESSAGE_NOT_IN_PATH
if fish_user_paths and str(self.bin_dir) in fish_user_paths:
message = POST_MESSAGE
self._write(
message.format(
poetry=colorize("info", "Poetry"),
version=colorize("b", version),
poetry_home_bin=colorize("comment", self.bin_dir),
poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
configure_message=POST_MESSAGE_CONFIGURE_FISH.format(
poetry_home_bin=colorize("comment", self.bin_dir)
),
test_command=colorize("b", "poetry --version"),
)
)
def display_post_message_unix(self, version: str) -> None:
paths = os.getenv("PATH", "").split(":")
message = POST_MESSAGE_NOT_IN_PATH
if paths and str(self.bin_dir) in paths:
message = POST_MESSAGE
self._write(
message.format(
poetry=colorize("info", "Poetry"),
version=colorize("b", version),
poetry_home_bin=colorize("comment", self.bin_dir),
poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
configure_message=POST_MESSAGE_CONFIGURE_UNIX.format(
poetry_home_bin=colorize("comment", self.bin_dir)
),
test_command=colorize("b", "poetry --version"),
)
)
def ensure_directories(self) -> None:
self.data_dir.mkdir(parents=True, exist_ok=True)
self.bin_dir.mkdir(parents=True, exist_ok=True)
def get_version(self):
current_version = None
if self.version_file.exists():
current_version = self.version_file.read_text().strip()
self._write(colorize("info", "Retrieving Poetry metadata"))
metadata = json.loads(self._get(self.METADATA_URL).decode())
def _compare_versions(x, y):
mx = self.VERSION_REGEX.match(x)
my = self.VERSION_REGEX.match(y)
vx = (*tuple(int(p) for p in mx.groups()[:3]), mx.group(5))
vy = (*tuple(int(p) for p in my.groups()[:3]), my.group(5))
if vx < vy:
return -1
elif vx > vy:
return 1
return 0
self._write("")
releases = sorted(
metadata["releases"].keys(), key=cmp_to_key(_compare_versions)
)
if self._version and self._version not in releases:
msg = f"Version {self._version} does not exist."
self._write(colorize("error", msg))
raise ValueError(msg)
version = self._version
if not version:
for release in reversed(releases):
m = self.VERSION_REGEX.match(release)
if m.group(5) and not self.allows_prereleases():
continue
version = release
break
if current_version == version and not self._force:
self._write(
f'The latest version ({colorize("b", version)}) is already installed.'
)
return None, current_version
return version, current_version
def _write(self, line) -> None:
sys.stdout.write(line + "\n")
def _overwrite(self, line) -> None:
if not is_decorated():
return self._write(line)
self._cursor.move_up()
self._cursor.clear_line()
self._write(line)
def _get(self, url):
request = Request(url, headers={"User-Agent": "Python Poetry"})
with closing(urlopen(request)) as r:
return r.read()
def main():
parser = argparse.ArgumentParser(
description="Installs the latest (or given) version of poetry"
)
parser.add_argument(
"-p",
"--preview",
help="install preview version",
dest="preview",
action="store_true",
default=False,
)
parser.add_argument("--version", help="install named version", dest="version")
parser.add_argument(
"-f",
"--force",
help="install on top of existing version",
dest="force",
action="store_true",
default=False,
)
parser.add_argument(
"-y",
"--yes",
help="accept all prompts",
dest="accept_all",
action="store_true",
default=False,
)
parser.add_argument(
"--uninstall",
help="uninstall poetry",
dest="uninstall",
action="store_true",
default=False,
)
parser.add_argument(
"--path",
dest="path",
action="store",
help=(
"Install from a given path (file or directory) instead of "
"fetching the latest version of Poetry available online."
),
)
parser.add_argument(
"--git",
dest="git",
action="store",
help=(
"Install from a git repository instead of fetching the latest version "
"of Poetry available online."
),
)
args = parser.parse_args()
installer = Installer(
version=args.version or os.getenv("POETRY_VERSION"),
preview=args.preview or string_to_bool(os.getenv("POETRY_PREVIEW", "0")),
force=args.force,
accept_all=args.accept_all
or string_to_bool(os.getenv("POETRY_ACCEPT", "0"))
or not is_interactive(),
path=args.path,
git=args.git,
)
if args.uninstall or string_to_bool(os.getenv("POETRY_UNINSTALL", "0")):
return installer.uninstall()
try:
return installer.run()
except PoetryInstallationError as e:
installer._write(colorize("error", "Poetry installation failed."))
if e.log is not None:
import traceback
_, path = tempfile.mkstemp(
suffix=".log",
prefix="poetry-installer-error-",
dir=str(Path.cwd()),
text=True,
)
installer._write(colorize("error", f"See {path} for error logs."))
tb = "".join(traceback.format_tb(e.__traceback__))
text = f"{e.log}\nTraceback:\n\n{tb}"
Path(path).write_text(text)
return e.return_code
if __name__ == "__main__":
sys.exit(main())

View File

@@ -149,5 +149,7 @@
<orderEntry type="module" module-name="intellij.python.community.impl.huggingFace" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.python.syntax" />
<orderEntry type="module" module-name="intellij.platform.ui.jcef" />
<orderEntry type="module" module-name="intellij.libraries.ktor.client" />
<orderEntry type="module" module-name="intellij.libraries.ktor.client.cio" />
</component>
</module>

View File

@@ -26,6 +26,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<plugin id="com.intellij.modules.python-core-capable"/>
<plugin id="com.intellij.modules.json"/>
<plugin id="org.toml.lang"/>
<module name="intellij.libraries.ktor.client"/>
<module name="intellij.libraries.ktor.client.cio"/>
</dependencies>
<!-- Python bundled modules for content -->

View File

@@ -316,6 +316,7 @@ python.sdk.popup.interpreter.settings=Interpreter Settings\u2026
python.sdk.popup.add.interpreter=Add Interpreter\u2026
python.sdk.switch.to=Switch to {0}
python.sdk.installing=Installing {0}
python.sdk.downloading.package.progress.title=Downloading package from {0}
python.sdk.select.conda.path.title=Select Path to Conda Executable
python.sdk.conda.problem.running=Problem running conda
python.sdk.conda.problem.env.empty.invalid=Environment name is empty or invalid
@@ -367,7 +368,6 @@ python.sdk.poetry.associated.project=Associated project:
python.sdk.poetry.associated.module=Associated module:
python.sdk.poetry.execution.exception.no.project.message=Cannot find the project associated with this Poetry environment
python.sdk.poetry.execution.exception.no.poetry.message=Cannot find Poetry
python.sdk.poetry.execution.exception.error.running.poetry.message=Error Running Poetry
python.sdk.poetry.quickfix.fix.pipenv.name=Fix Poetry interpreter
python.sdk.poetry.quickfix.use.pipenv.name=Use Poetry interpreter
python.sdk.poetry.pip.file.lock.not.found=poetry.lock is not found
@@ -494,9 +494,6 @@ sdk.create.type.project.venv=Project venv
sdk.create.type.base.conda=Base conda
sdk.create.type.custom=Custom environment
sdk.create.python.version=Python version:
sdk.create.conda.executable.path=Path to conda:
sdk.create.conda.missing.text=No conda executable found.
sdk.create.conda.install.fix=Install Miniconda
sdk.create.simple.venv.hint=Python virtual environment will be created in the project root: {0}.venv
sdk.create.simple.conda.hint=To create a new conda environment or choose an existing one, proceed with Custom environment
sdk.create.custom.develop.on=Develop on:
@@ -506,6 +503,7 @@ sdk.create.custom.select.existing=Select existing
sdk.create.custom.type=Type:
sdk.create.custom.base.python=Base python:
sdk.create.custom.location=Location:
sdk.create.custom.venv.missing.text=No {0} executable found.
sdk.create.custom.inherit.packages=Inherit packages from base interpreter
sdk.create.custom.make.available=Make available to all projects
sdk.create.custom.python.path=Python path:
@@ -516,10 +514,7 @@ sdk.create.custom.conda.env.name=Name:
sdk.create.custom.conda.create.progress=Creating conda interpreter
sdk.create.custom.conda.select.progress=Selecting conda interpreter
sdk.create.custom.conda.refresh.envs=Reload environments
sdk.create.custom.pipenv.path=Path to pipenv:
sdk.create.custom.pipenv.missing.text=No pipenv executable found.
sdk.create.custom.poetry.path=Path to poetry:
sdk.create.custom.poetry.missing.text=No poetry executable non found.
sdk.create.custom.venv.executable.path=Path to {0}:
sdk.create.custom.select.executable.link=Select path
sdk.create.custom.venv.environment.exists=Environment "{0}" already exists in the specified folder
sdk.create.custom.venv.folder.not.empty=The specified folder already exists
@@ -529,7 +524,8 @@ sdk.create.not.executable.empty.error=Specify path to executable
sdk.create.not.executable.does.not.exist.error=Executable does not exist
sdk.create.executable.directory.error=Path can't be a directory
sdk.create.tooltip.browse=Browse\u2026
sdk.create.custom.venv.install.fix.title=Install {0} {1}
sdk.create.custom.venv.run.error.message= Error Running {0}
sdk.create.targets.local=Local Machine
sdk.create.custom.virtualenv=Virtualenv

View File

@@ -0,0 +1,93 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk
import com.intellij.execution.RunCanceledByUserException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.NlsContexts
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.packaging.IndicatedProcessOutputListener
import com.jetbrains.python.packaging.PyExecutionException
import kotlinx.coroutines.CoroutineScope
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.pathString
internal object Logger {
val LOG = logger<Logger>()
}
/**
* Used for CoroutineScope in com.jetbrains.python.sdk
*/
@Service(Service.Level.PROJECT)
internal class PythonSdkRunCommandService(val cs: CoroutineScope)
/**
* Runs a command line operation in a background thread.
*
* @param [commandLine] The command line to execute.
* @return A [Result] object containing the output of the command execution.
*/
@RequiresBackgroundThread
internal fun runCommandLine(commandLine: GeneralCommandLine): Result<ProcessOutput> {
Logger.LOG.info("Running command: ${commandLine.commandLineString}")
val commandOutput = with(CapturingProcessHandler(commandLine)) {
runProcess()
}
return processOutput(
commandOutput,
commandLine.commandLineString,
emptyList()
)
}
fun runCommand(executable: Path, projectPath: Path?, @NlsContexts.DialogMessage errorMessage: String, vararg args: String): String {
val command = listOf(executable.absolutePathString()) + args
val commandLine = GeneralCommandLine(command).withWorkingDirectory(projectPath)
val handler = CapturingProcessHandler(commandLine)
val indicator = ProgressManager.getInstance().progressIndicator
val result = with(handler) {
when {
indicator != null -> {
addProcessListener(IndicatedProcessOutputListener(indicator))
runProcessWithProgressIndicator(indicator)
}
else ->
runProcess()
}
}
return processOutput(result, executable.pathString, args.asList(), errorMessage).getOrThrow().stdout.trim()
}
/**
* Processes the output of a command execution.
*
* @param[output] the output of the executed command.
* @param[commandString] the command string that was executed.
* @param[args] the arguments passed to the command.
* @param[errorMessage] the error message to be used if the command fails.
* @return A [Result] object containing the processed output.
*/
internal fun processOutput(
output: ProcessOutput,
commandString: String,
args: List<String>,
@NlsContexts.DialogMessage errorMessage: String = "",
): Result<ProcessOutput> {
return with(output) {
when {
isCancelled ->
Result.failure(RunCanceledByUserException())
exitCode != 0 ->
Result.failure(PyExecutionException(errorMessage, commandString, args, stdout, stderr, exitCode, emptyList()))
else -> Result.success(output)
}
}
}

View File

@@ -16,15 +16,11 @@
package com.jetbrains.python.sdk
import com.intellij.execution.ExecutionException
import com.intellij.execution.RunCanceledByUserException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.target.*
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
@@ -50,8 +46,6 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.PathUtil
import com.intellij.webcore.packaging.PackagesNotificationPanel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.IndicatedProcessOutputListener
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.ui.PyPackageManagementService
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.remote.PyRemoteSdkAdditionalData
@@ -64,14 +58,12 @@ import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
import com.jetbrains.python.target.PyTargetAwareAdditionalData
import com.jetbrains.python.ui.PyUiUtil
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.absolutePathString
import kotlin.io.path.div
import kotlin.io.path.pathString
@@ -135,18 +127,22 @@ fun detectSystemWideSdks(
{ it.homePath }).reversed())
}
private fun PythonSdkFlavor<*>.detectSdks(module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>): List<PyDetectedSdk> =
private fun PythonSdkFlavor<*>.detectSdks(
module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>,
): List<PyDetectedSdk> =
detectSdkPaths(module, context, targetModuleSitsOn, existingPaths)
.map { createDetectedSdk(it, targetModuleSitsOn?.asTargetConfig, this) }
internal fun PythonSdkFlavor<*>.detectSdkPaths(module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>): List<String> =
internal fun PythonSdkFlavor<*>.detectSdkPaths(
module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>,
): List<String> =
suggestLocalHomePaths(module, context)
.mapNotNull {
// If a module sits on target, this target maps its path.
@@ -565,44 +561,10 @@ val Sdk.sdkSeemsValid: Boolean
return pythonSdkAdditionalData.flavorAndData.sdkSeemsValid(this, targetEnvConfiguration)
}
/**
* Used for CoroutineScope in com.jetbrains.python.sdk
*/
@Service(Service.Level.PROJECT)
class PythonSdkRunCommandService(val cs: CoroutineScope)
fun runCommand(executable: Path, projectPath: Path?, @NlsContexts.DialogMessage errorMessage: String, vararg args: String): String {
val command = listOf(executable.absolutePathString()) + args
val commandLine = GeneralCommandLine(command).withWorkingDirectory(projectPath)
val handler = CapturingProcessHandler(commandLine)
val indicator = ProgressManager.getInstance().progressIndicator
val result = with(handler) {
when {
indicator != null -> {
addProcessListener(IndicatedProcessOutputListener(indicator))
runProcessWithProgressIndicator(indicator)
}
else ->
runProcess()
}
}
return with(result) {
when {
isCancelled ->
throw RunCanceledByUserException()
exitCode != 0 ->
throw PyExecutionException(errorMessage, executable.pathString,
args.asList(),
stdout, stderr, exitCode, emptyList())
else -> stdout.trim()
}
}
}
inline fun <reified T : PythonSdkAdditionalData> setCorrectTypeSdk(sdk: Sdk, additionalDataClass: Class<T>, value: Boolean) {
val oldData = sdk.sdkAdditionalData
val newData = if (value) {
when(oldData) {
when (oldData) {
is PythonSdkAdditionalData -> additionalDataClass.getDeclaredConstructors().first { it.parameterCount == 1 }.newInstance(oldData) as T
else -> additionalDataClass.getDeclaredConstructor().newInstance()
}

View File

@@ -0,0 +1,130 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk
import com.intellij.execution.RunCanceledByUserException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.packaging.PyExecutionException
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel
import io.ktor.util.cio.writeChannel
import io.ktor.utils.io.copyAndClose
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus.Internal
import java.net.URL
import java.nio.file.Path
import kotlin.io.path.absolutePathString
/**
* Returns the string representation of the Python executable ("py" on Windows or "python" on Unix-like OS) based on the current system.
*
* @return the string representation of the Python executable
*/
@Internal
fun getPythonExecutableString() = if (SystemInfo.isWindows) "py" else "python"
@Service(Service.Level.APP)
internal class PackageInstallationFilesService {
val ktorClient = HttpClient(CIO) {
install(HttpTimeout)
}
val urlToFilePathMap = mutableMapOf<URL, Path>()
}
/**
* Installs a package with Python using the given URL and Python executable.
*
* @param [url] The [URL] from which to download the package.
* @param pythonExecutable The path to the Python executable (could be "py" or "python").
* @return A [Result] object that represents the [ProcessOutput] of the installation command.
*/
@RequiresBackgroundThread
internal suspend fun installPackageWithPython(url: URL, pythonExecutable: String): Result<ProcessOutput> {
val installationFile = downloadFile(url).getOrThrow()
val command = GeneralCommandLine(pythonExecutable, installationFile.absolutePathString())
return runCommandLine(command)
}
/**
* Downloads a file from the specified URL.
*
* @param[url] The [URL] from which to download the file.
* @return A [Result] object that represents the [Path] to the downloaded file.
*/
internal suspend fun downloadFile(url: URL): Result<Path> {
val installationService = service<PackageInstallationFilesService>()
installationService.urlToFilePathMap[url]?.let { return Result.success(it) }
return withContext(Dispatchers.IO) {
val installationFile = FileUtil.createTempFile("_installation_file.py", null)
installationService.ktorClient.get(url).bodyAsChannel().copyAndClose(installationFile.writeChannel())
installationService.urlToFilePathMap[url] = installationFile.toPath()
Result.success(installationFile.toPath())
}
}
/**
* Checks if a package is installed by running the specified command(s) with the "--version"
* argument and checking the success of the command.
*
* @param [commands] The commands to execute. These commands should include the package and any required arguments.
* @return true if the package is installed, false otherwise
*/
@Internal
@RequiresBackgroundThread
fun isPackageInstalled(vararg commands: String): Boolean {
val command = GeneralCommandLine(*commands, "--version")
return runCommandLine(command).isSuccess
}
/**
* Installs an executable via pip.
*
* @param [executableName] The name of the executable to install.
* @param [pythonExecutable] The path to the Python executable (could be "py" or "python").
* @param [isUserSitePackages] Whether to install the executable in the user's site packages directory. Defaults to true.
*/
@Internal
@RequiresBackgroundThread
fun installExecutableViaPip(
executableName: String,
pythonExecutable: String,
isUserSitePackages: Boolean = true,
) {
val commandList = mutableListOf(pythonExecutable, "-m", "pip", "install", executableName)
if (isUserSitePackages) {
commandList.add("--user")
}
runCommandLine(GeneralCommandLine(commandList)).getOrThrow()
}
@RequiresBackgroundThread
internal suspend fun installPipIfNeeded(pythonExecutable: String) {
if (!isPackageInstalled(pythonExecutable, "-m", "pip") && !isPackageInstalled("pip")) {
installPackageWithPython(URL("https://bootstrap.pypa.io/get-pip.py"), pythonExecutable).getOrThrow()
}
}
/**
* Installs an executable via a Python script.
*
* @param [scriptPath] The [Path] to the Python script used for installation.
* @param [pythonExecutable] The path to the Python executable (could be "py" or "python").
*
* @throws [RunCanceledByUserException] if the user cancels the command execution.
* @throws [PyExecutionException] if the command execution fails.
*/
@RequiresBackgroundThread
internal fun installExecutableViaPythonScript(scriptPath: Path, pythonExecutable: String) =
runCommandLine(GeneralCommandLine(pythonExecutable, scriptPath.absolutePathString())).getOrThrow()

View File

@@ -28,8 +28,8 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
with(panel) {
executableSelector(state.condaExecutable,
validationRequestor,
message("sdk.create.conda.executable.path"),
message("sdk.create.conda.missing.text"),
message("sdk.create.custom.venv.executable.path", "conda"),
message("sdk.create.custom.venv.missing.text", "conda"),
createInstallCondaFix(model))
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)

View File

@@ -38,8 +38,8 @@ class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel)
executableSelector(model.state.condaExecutable,
validationRequestor,
message("sdk.create.conda.executable.path"),
message("sdk.create.conda.missing.text"),
message("sdk.create.custom.venv.executable.path", "conda"),
message("sdk.create.custom.venv.missing.text", "conda"),
createInstallCondaFix(model))
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)
}

View File

@@ -0,0 +1,134 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.installPipIfNeeded
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import org.jetbrains.annotations.ApiStatus.Internal
import java.nio.file.Path
@Internal
abstract class CustomNewEnvironmentCreator(private val name: String, model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
protected lateinit var basePythonComboBox: PythonInterpreterComboBox
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox = pythonInterpreterComboBox(model.state.baseInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(Align.FILL)
.component
}
executableSelector(executable,
validationRequestor,
message("sdk.create.custom.venv.executable.path", name),
message("sdk.create.custom.venv.missing.text", name),
createInstallFix()).component
}
}
override fun onShown() {
basePythonComboBox.setItems(model.baseInterpreters)
}
override fun getOrCreateSdk(): Sdk {
savePathToExecutableToProperties()
// todo think about better error handling
val selectedBasePython = model.state.baseInterpreter.get()!!
val homePath = model.installPythonIfNeeded(selectedBasePython)
val newSdk = setupEnvSdk(null,
null,
model.baseSdks,
model.projectPath.value,
homePath,
false)!!
SdkConfigurationUtil.addSdk(newSdk)
return newSdk
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo =
InterpreterStatisticsInfo(interpreterType,
target.toStatisticsField(),
false,
false,
false, //presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
/**
* Creates an installation fix for executable (poetry, pipenv).
*
* 1. Checks does a `pythonExecutable` have pip.
* 2. If no, checks is pip is installed globally.
* 3. If no, downloads and installs pip from "https://bootstrap.pypa.io/get-pip.py"
* 4. Runs (pythonExecutable -m) pip install `package_name` --user
* 5. Reruns `detectExecutable`
*/
private fun createInstallFix(): ActionLink {
return ActionLink(message("sdk.create.custom.venv.install.fix.title", name, "via pip")) {
PythonSdkFlavor.clearExecutablesCache()
installExecutable()
detectExecutable()
}
}
/**
* Installs the necessary executable in the Python environment.
*
* Initiates a blocking modal progress task to:
* 1. Ensure pip is installed.
* 2. Install the executable (specified by `name`) using either a custom installation script or via pip.
*/
@RequiresEdt
private fun installExecutable() {
val pythonExecutable = model.state.baseInterpreter.get()?.homePath ?: getPythonExecutableString()
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title", name, "via pip")) {
installPipIfNeeded(pythonExecutable)
if (installationScript != null) {
installExecutableViaPythonScript(installationScript!!, pythonExecutable)
}
else {
installExecutableViaPip(name, pythonExecutable)
}
}
}
internal abstract val interpreterType: InterpreterType
internal abstract val executable: ObservableMutableProperty<String>
/**
* The `installationScript` specifies a custom script for installing an executable in the Python environment.
*
* If this property is not null, the provided script will be used for installation instead of the default pip installation.
*/
internal abstract val installationScript: Path?
internal abstract fun savePathToExecutableToProperties()
internal abstract fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Sdk?
internal abstract fun detectExecutable()
}

View File

@@ -2,89 +2,30 @@
package com.jetbrains.python.sdk.add.v2
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import java.nio.file.Path
class PipEnvNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
private lateinit var basePythonComboBox: PythonInterpreterComboBox
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox = pythonInterpreterComboBox(model.state.baseInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(Align.FILL)
.component
}
executableSelector(model.state.pipenvExecutable,
validationRequestor,
message("sdk.create.custom.pipenv.path"),
message("sdk.create.custom.pipenv.missing.text")).component
}
class PipEnvNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : CustomNewEnvironmentCreator("pipenv", model) {
override val interpreterType: InterpreterType = InterpreterType.PIPENV
override val executable: ObservableMutableProperty<String> = model.state.pipenvExecutable
override val installationScript: Path? = null
override fun savePathToExecutableToProperties() {
PropertiesComponent.getInstance().pipEnvPath = executable.get().nullize()
}
override fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Sdk? =
setupPipEnvSdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages)
override fun onShown() {
basePythonComboBox.setItems(model.baseInterpreters)
//val savedPath = PropertiesComponent.getInstance().pipEnvPath
//if (savedPath != null) {
// model.state.pipenvExecutable.set(savedPath)
//}
//else {
// val modalityState = ModalityState.current().asContextElement()
// model.scope.launch(Dispatchers.IO) {
// val detectedExecutable = detectPipEnvExecutable()
// withContext(Dispatchers.EDT + modalityState) {
// detectedExecutable?.let { model.state.pipenvExecutable.set(it.path) }
// }
// }
//}
}
override fun getOrCreateSdk(): Sdk {
if (model is PythonLocalAddInterpreterModel) {
PropertiesComponent.getInstance().pipEnvPath = model.state.pipenvExecutable.get().nullize()
}
// todo think about better error handling
val selectedBasePython = model.state.baseInterpreter.get()!!
val homePath = model.installPythonIfNeeded(selectedBasePython)
val newSdk = setupPipEnvSdkUnderProgress(null, null,
model.baseSdks,
model.projectPath.value,
homePath, false)!!
SdkConfigurationUtil.addSdk(newSdk)
return newSdk
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField() // todo fix for wsl
return InterpreterStatisticsInfo(InterpreterType.PIPENV,
statisticsTarget,
false,
false,
false,
//presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
override fun detectExecutable() {
model.detectPipEnvExecutable()
}
}

View File

@@ -2,44 +2,24 @@
package com.jetbrains.python.sdk.add.v2
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.poetry.PyProjectTomlPythonVersionsService
import com.jetbrains.python.PythonHelpersLocator
import com.jetbrains.python.sdk.poetry.poetryPath
import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.flow.StateFlow
class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : PythonNewEnvironmentCreator(model) {
private lateinit var basePythonComboBox: PythonInterpreterComboBox
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox = pythonInterpreterComboBox(model.state.baseInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(Align.FILL)
.component
}
executableSelector(model.state.poetryExecutable,
validationRequestor,
message("sdk.create.custom.poetry.path"),
message("sdk.create.custom.poetry.missing.text")).component
}
}
class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : CustomNewEnvironmentCreator("poetry", model) {
override val interpreterType: InterpreterType = InterpreterType.POETRY
override val executable: ObservableMutableProperty<String> = model.state.poetryExecutable
override val installationScript = PythonHelpersLocator.findPathInHelpers("pycharm_poetry_installer.py")
override fun onShown() {
val moduleDir = when (moduleOrProject) {
@@ -58,50 +38,16 @@ class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel,
}
basePythonComboBox.setItems(validatedInterpreters)
//val savedPath = PropertiesComponent.getInstance().poetryPath
//if (savedPath != null) {
// model.state.poetryExecutable.set(savedPath)
//}
//else {
// val modalityState = ModalityState.current().asContextElement()
// model.scope.launch(Dispatchers.IO) {
// val poetryExecutable = detectPoetryExecutable()
// withContext(Dispatchers.EDT + modalityState) {
// poetryExecutable?.let { model.state.poetryExecutable.set(it.path) }
// }
// }
//}
}
override fun getOrCreateSdk(): Sdk {
if (model is PythonLocalAddInterpreterModel) {
PropertiesComponent.getInstance().poetryPath = model.state.poetryExecutable.get().nullize()
}
val selectedBasePython = model.state.baseInterpreter.get()!!
val homePath = model.installPythonIfNeeded(selectedBasePython)
val newSdk = setupPoetrySdkUnderProgress(null,
null,
model.baseSdks,
model.projectPath.value,
homePath, false)!!
SdkConfigurationUtil.addSdk(newSdk)
return newSdk
override fun savePathToExecutableToProperties() {
PropertiesComponent.getInstance().poetryPath = executable.get().nullize()
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField() // todo fix for wsl
return InterpreterStatisticsInfo(InterpreterType.POETRY,
statisticsTarget,
false,
false,
false,
//presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
override fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Sdk? =
setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages)
override fun detectExecutable() {
model.detectPoetryExecutable()
}
}

View File

@@ -92,8 +92,8 @@ class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<String>, onlyAllow
rowsRange {
executableSelector(model.state.condaExecutable,
validationRequestor,
message("sdk.create.conda.executable.path"),
message("sdk.create.conda.missing.text"),
message("sdk.create.custom.venv.executable.path", "conda"),
message("sdk.create.custom.venv.missing.text", "conda"),
createInstallCondaFix(model))
//.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext)
}.visibleIf(_baseConda)

View File

@@ -157,7 +157,7 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel
detectPipEnvExecutable()
}
suspend fun detectPoetryExecutable() {
fun detectPoetryExecutable() {
// todo this is local case, fix for targets
val savedPath = PropertiesComponent.getInstance().poetryPath
if (savedPath != null) {
@@ -174,7 +174,7 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel
}
}
suspend fun detectPipEnvExecutable() {
fun detectPipEnvExecutable() {
// todo this is local case, fix for targets
val savedPath = PropertiesComponent.getInstance().pipEnvPath
if (savedPath != null) {

View File

@@ -423,7 +423,7 @@ fun Panel.executableSelector(
inline = true)
.align(Align.FILL)
.component
}.visibleIf(executable.equalsTo(UNKNOWN_EXECUTABLE))
}.visibleIf(executable.equalsTo(UNKNOWN_EXECUTABLE)).visibleIf(executable.equalsTo(""))
row(labelText) {
textFieldCell = textFieldWithBrowseButton()
@@ -451,7 +451,7 @@ fun Panel.executableSelector(
}
internal fun createInstallCondaFix(model: PythonAddInterpreterModel): ActionLink {
return ActionLink(message("sdk.create.conda.install.fix")) {
return ActionLink(message("sdk.create.custom.venv.install.fix.title", "Miniconda", "")) {
PythonSdkFlavor.clearExecutablesCache()
CondaInstallManager.installLatest(null)
model.scope.launch(model.uiContext) {

View File

@@ -97,8 +97,7 @@ fun runPoetry(projectPath: Path?, vararg args: String): String {
?: throw PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.no.poetry.message"), "poetry",
emptyList(), ProcessOutput())
@Suppress("DialogTitleCapitalization")
return runCommand(executable, projectPath, PyBundle.message("python.sdk.poetry.execution.exception.error.running.poetry.message"), *args)
return runCommand(executable, projectPath, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"), *args)
}
/**
@@ -133,12 +132,11 @@ private fun runCommand(projectPath: Path, command: String, vararg args: String):
runProcess()
}
return with(result) {
@Suppress("DialogTitleCapitalization")
when {
isCancelled ->
throw RunCanceledByUserException()
exitCode != 0 ->
throw PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.error.running.poetry.message"), command,
throw PyExecutionException(PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"), command,
args.asList(),
stdout, stderr, exitCode, emptyList())
else -> stdout
@@ -154,7 +152,7 @@ internal fun runPoetryInBackground(module: Module, args: List<String>, @NlsSafe
runPoetry(sdk, *args.toTypedArray())
}
catch (e: ExecutionException) {
showSdkExecutionException(sdk, e, PyBundle.message("python.sdk.poetry.execution.exception.error.running.poetry.message"))
showSdkExecutionException(sdk, e, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"))
}
finally {
PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true)