install.python-poetry.org
install.python-poetry.org
/usr/bin/env python3
r"""
This script will install Poetry and its dependencies in an isolated fashion.
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.
import argparse
import json
import os
import re
import shutil
import subprocess
import sysconfig
import tempfile
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,
}
if fg:
codes.append(FOREGROUND_COLORS[fg])
if bg:
codes.append(BACKGROUND_COLORS[bg])
if options:
if not isinstance(options, (list, tuple)):
options = [options]
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
)
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
return f"{STYLES[style]}{text}\033[0m"
def string_to_bool(value):
value = value.lower()
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"
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)
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
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.
"""
`{test_command}`
"""
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}`.
`{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 = """
You can choose and execute one of the following commands in PowerShell:
```
[Environment]::SetEnvironmentVariable("Path",
[Environment]::GetEnvironmentVariable("Path", "User") + ";{poetry_home_bin}",
"User")
```
B. Try to append the bin directory to PATH every when you run PowerShell (>=6
recommended):
```
echo 'if (-not (Get-Command poetry -ErrorAction Ignore)) {{ $env:Path += ";
{poetry_home_bin}" }}' | Out-File -Append $PROFILE
```
"""
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
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
)
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
return self
return self
return self
return self
return self
return self
return self
return self
return self
return self
return self
return self
return self
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")
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
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
return 0
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
@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)
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))
if self._git:
specification = "git+" + version
elif self._path:
specification = version
else:
specification = f"poetry=={version}"
env.pip("install", specification)
if SHELL == "fish":
return self.display_post_message_fish(version)
return self.display_post_message_unix(version)
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"),
)
)
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
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"),
)
)
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 get_version(self):
current_version = None
if self.version_file.exists():
current_version = self.version_file.read_text().strip()
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)
)
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
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,
)
try:
return installer.run()
except PoetryInstallationError as e:
installer._write(colorize("error", "Poetry installation failed."))
_, 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())