1
0
Fork 0
mirror of https://github.com/python-poetry/install.python-poetry.org.git synced 2024-11-24 23:10:57 -05:00

refactor: update path handling and comments

This is part of a major overhaul of the installer I hope to complete in
a relatively short time. This first pass makes sure we use consistent
directories on all platforms by preferring the documented paths to a
possibly unexpected `site.getuserbase()` (which results in a Library
path on macOS Framework builds, like Homebrew and OS-provided Pythons).

Path handling as a whole is updated to take advantage of `pathlib` and
DRY up some tedious code. It also starts the first stage of increasing
use of classes and careful thought as to the visibility of class-level
attributes in this script -- I hope to end up with a DSL-like end result
that is as informative as possible to the reader of this script.
This commit is contained in:
Bjorn Neergaard 2022-09-06 00:53:25 -06:00
parent 07dd8a55c5
commit ac6fb50a38

View file

@ -1,17 +1,27 @@
""" #!/usr/bin/env python3
This script will install Poetry and its dependencies. r"""
This script will install Poetry and its dependencies in an isolated fashion.
It does, in order: 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.
- Creates a virtual environment using venv (or virtualenv zipapp) in the correct OS data dir which will be This script performs minimal magic, and should be relatively stable. However, it is optimized for interactive developer
- `%APPDATA%\\pypoetry` on Windows use and trivial pipelines. If you are considering using this script in production, you should consider manually-managed
- ~/Library/Application Support/pypoetry on MacOS installs, or use of pipx as alternatives to executing arbitrary, unversioned code from the internet. If you prefer this
- `${XDG_DATA_HOME}/pypoetry` (or `~/.local/share/pypoetry` if it's not set) on UNIX systems script to alternatives, consider maintaining a local copy as part of your infrastructure.
- In `${POETRY_HOME}` if it's set.
- Installs the latest or given version of Poetry inside this virtual environment. For full documentation, visit https://python-poetry.org/docs/#installation.
- Installs a `poetry` script in the Python user directory (or `${POETRY_HOME/bin}` if `POETRY_HOME` is set).
- On failure, the error log is written to poetry-installer-error-*.log and any previously existing environment
is restored.
""" """
import argparse import argparse
@ -19,7 +29,6 @@ import json
import os import os
import re import re
import shutil import shutil
import site
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
@ -134,38 +143,29 @@ def string_to_bool(value):
return value in {"true", "1", "y", "yes"} return value in {"true", "1", "y", "yes"}
def data_dir(version: Optional[str] = None) -> Path: def data_dir() -> Path:
if os.getenv("POETRY_HOME"): if os.getenv("POETRY_HOME"):
return Path(os.getenv("POETRY_HOME")).expanduser() return Path(os.getenv("POETRY_HOME")).expanduser()
if WINDOWS: if WINDOWS:
const = "CSIDL_APPDATA" base_dir = Path(_get_win_folder("CSIDL_APPDATA"))
path = os.path.normpath(_get_win_folder(const))
path = os.path.join(path, "pypoetry")
elif MACOS: elif MACOS:
path = os.path.expanduser("~/Library/Application Support/pypoetry") base_dir = Path("~/Library/Application Support").expanduser()
else: else:
path = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) base_dir = Path(os.getenv("XDG_DATA_HOME", "~/.local/share")).expanduser()
path = os.path.join(path, "pypoetry")
if version: base_dir = base_dir.resolve()
path = os.path.join(path, version) return base_dir / "pypoetry"
return Path(path)
def bin_dir(version: Optional[str] = None) -> Path: def bin_dir() -> Path:
if os.getenv("POETRY_HOME"): if os.getenv("POETRY_HOME"):
return Path(os.getenv("POETRY_HOME"), "bin").expanduser() return Path(os.getenv("POETRY_HOME")).expanduser() / "bin"
user_base = site.getuserbase()
if WINDOWS and not MINGW: if WINDOWS and not MINGW:
bin_dir = os.path.join(user_base, "Scripts") return Path(_get_win_folder("CSIDL_APPDATA")) / "Python/Scripts"
else: else:
bin_dir = os.path.join(user_base, "bin") return Path("~/.local/bin").expanduser()
return Path(bin_dir)
def _get_win_folder_from_registry(csidl_name): def _get_win_folder_from_registry(csidl_name):
@ -181,9 +181,9 @@ def _get_win_folder_from_registry(csidl_name):
_winreg.HKEY_CURRENT_USER, _winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders",
) )
dir, type = _winreg.QueryValueEx(key, shell_folder_name) path, _ = _winreg.QueryValueEx(key, shell_folder_name)
return dir return path
def _get_win_folder_with_ctypes(csidl_name): def _get_win_folder_with_ctypes(csidl_name):
@ -477,9 +477,26 @@ class Installer:
self._accept_all = accept_all self._accept_all = accept_all
self._git = git self._git = git
self._path = path self._path = path
self._data_dir = data_dir()
self._bin_dir = bin_dir()
self._cursor = Cursor() 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: def allows_prereleases(self) -> bool:
return self._preview return self._preview
@ -536,7 +553,7 @@ class Installer:
return 0 return 0
def install(self, version, upgrade=False): def install(self, version):
""" """
Installs Poetry in $POETRY_HOME. Installs Poetry in $POETRY_HOME.
""" """
@ -549,13 +566,13 @@ class Installer:
with self.make_env(version) as env: with self.make_env(version) as env:
self.install_poetry(version, env) self.install_poetry(version, env)
self.make_bin(version, env) self.make_bin(version, env)
self._data_dir.joinpath("VERSION").write_text(version) self.version_file.write_text(version)
self._install_comment(version, "Done") self._install_comment(version, "Done")
return 0 return 0
def uninstall(self) -> int: def uninstall(self) -> int:
if not self._data_dir.exists(): if not self.data_dir.exists():
self._write( self._write(
"{} is not currently installed.".format(colorize("info", "Poetry")) "{} is not currently installed.".format(colorize("info", "Poetry"))
) )
@ -563,8 +580,8 @@ class Installer:
return 1 return 1
version = None version = None
if self._data_dir.joinpath("VERSION").exists(): if self.version_file.exists():
version = self._data_dir.joinpath("VERSION").read_text().strip() version = self.version_file.read_text().strip()
if version: if version:
self._write( self._write(
@ -575,10 +592,10 @@ class Installer:
else: else:
self._write("Removing {}".format(colorize("info", "Poetry"))) self._write("Removing {}".format(colorize("info", "Poetry")))
shutil.rmtree(str(self._data_dir)) shutil.rmtree(str(self.data_dir))
for script in ["poetry", "poetry.bat", "poetry.exe"]: for script in ["poetry", "poetry.bat", "poetry.exe"]:
if self._bin_dir.joinpath(script).exists(): if self.bin_dir.joinpath(script).exists():
self._bin_dir.joinpath(script).unlink() self.bin_dir.joinpath(script).unlink()
return 0 return 0
@ -593,7 +610,7 @@ class Installer:
@contextmanager @contextmanager
def make_env(self, version: str) -> VirtualEnvironment: def make_env(self, version: str) -> VirtualEnvironment:
env_path = self._data_dir.joinpath("venv") env_path = self.data_dir.joinpath("venv")
env_path_saved = env_path.with_suffix(".save") env_path_saved = env_path.with_suffix(".save")
if env_path.exists(): if env_path.exists():
@ -625,20 +642,20 @@ class Installer:
def make_bin(self, version: str, env: VirtualEnvironment) -> None: def make_bin(self, version: str, env: VirtualEnvironment) -> None:
self._install_comment(version, "Creating script") self._install_comment(version, "Creating script")
self._bin_dir.mkdir(parents=True, exist_ok=True) self.bin_dir.mkdir(parents=True, exist_ok=True)
script = "poetry.exe" if WINDOWS else "poetry" script = "poetry.exe" if WINDOWS else "poetry"
target_script = env.bin_path.joinpath(script) target_script = env.bin_path.joinpath(script)
if self._bin_dir.joinpath(script).exists(): if self.bin_dir.joinpath(script).exists():
self._bin_dir.joinpath(script).unlink() self.bin_dir.joinpath(script).unlink()
try: try:
self._bin_dir.joinpath(script).symlink_to(target_script) self.bin_dir.joinpath(script).symlink_to(target_script)
except OSError: except OSError:
# This can happen if the user # This can happen if the user
# does not have the correct permission on Windows # does not have the correct permission on Windows
shutil.copy(target_script, self._bin_dir.joinpath(script)) shutil.copy(target_script, self.bin_dir.joinpath(script))
def install_poetry(self, version: str, env: VirtualEnvironment) -> None: def install_poetry(self, version: str, env: VirtualEnvironment) -> None:
self._install_comment(version, "Installing Poetry") self._install_comment(version, "Installing Poetry")
@ -655,7 +672,7 @@ class Installer:
def display_pre_message(self) -> None: def display_pre_message(self) -> None:
kwargs = { kwargs = {
"poetry": colorize("info", "Poetry"), "poetry": colorize("info", "Poetry"),
"poetry_home_bin": colorize("comment", self._bin_dir), "poetry_home_bin": colorize("comment", self.bin_dir),
} }
self._write(PRE_MESSAGE.format(**kwargs)) self._write(PRE_MESSAGE.format(**kwargs))
@ -672,17 +689,17 @@ class Installer:
path = self.get_windows_path_var() path = self.get_windows_path_var()
message = POST_MESSAGE_NOT_IN_PATH message = POST_MESSAGE_NOT_IN_PATH
if path and str(self._bin_dir) in path: if path and str(self.bin_dir) in path:
message = POST_MESSAGE message = POST_MESSAGE
self._write( self._write(
message.format( message.format(
poetry=colorize("info", "Poetry"), poetry=colorize("info", "Poetry"),
version=colorize("b", version), version=colorize("b", version),
poetry_home_bin=colorize("comment", self._bin_dir), poetry_home_bin=colorize("comment", self.bin_dir),
poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")), poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
configure_message=POST_MESSAGE_CONFIGURE_WINDOWS.format( configure_message=POST_MESSAGE_CONFIGURE_WINDOWS.format(
poetry_home_bin=colorize("comment", self._bin_dir) poetry_home_bin=colorize("comment", self.bin_dir)
), ),
test_command=colorize("b", "poetry --version"), test_command=colorize("b", "poetry --version"),
) )
@ -703,17 +720,17 @@ class Installer:
).decode("utf-8") ).decode("utf-8")
message = POST_MESSAGE_NOT_IN_PATH message = POST_MESSAGE_NOT_IN_PATH
if fish_user_paths and str(self._bin_dir) in fish_user_paths: if fish_user_paths and str(self.bin_dir) in fish_user_paths:
message = POST_MESSAGE message = POST_MESSAGE
self._write( self._write(
message.format( message.format(
poetry=colorize("info", "Poetry"), poetry=colorize("info", "Poetry"),
version=colorize("b", version), version=colorize("b", version),
poetry_home_bin=colorize("comment", self._bin_dir), poetry_home_bin=colorize("comment", self.bin_dir),
poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")), poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
configure_message=POST_MESSAGE_CONFIGURE_FISH.format( configure_message=POST_MESSAGE_CONFIGURE_FISH.format(
poetry_home_bin=colorize("comment", self._bin_dir) poetry_home_bin=colorize("comment", self.bin_dir)
), ),
test_command=colorize("b", "poetry --version"), test_command=colorize("b", "poetry --version"),
) )
@ -723,30 +740,30 @@ class Installer:
paths = os.getenv("PATH", "").split(":") paths = os.getenv("PATH", "").split(":")
message = POST_MESSAGE_NOT_IN_PATH message = POST_MESSAGE_NOT_IN_PATH
if paths and str(self._bin_dir) in paths: if paths and str(self.bin_dir) in paths:
message = POST_MESSAGE message = POST_MESSAGE
self._write( self._write(
message.format( message.format(
poetry=colorize("info", "Poetry"), poetry=colorize("info", "Poetry"),
version=colorize("b", version), version=colorize("b", version),
poetry_home_bin=colorize("comment", self._bin_dir), poetry_home_bin=colorize("comment", self.bin_dir),
poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")), poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
configure_message=POST_MESSAGE_CONFIGURE_UNIX.format( configure_message=POST_MESSAGE_CONFIGURE_UNIX.format(
poetry_home_bin=colorize("comment", self._bin_dir) poetry_home_bin=colorize("comment", self.bin_dir)
), ),
test_command=colorize("b", "poetry --version"), test_command=colorize("b", "poetry --version"),
) )
) )
def ensure_directories(self) -> None: def ensure_directories(self) -> None:
self._data_dir.mkdir(parents=True, exist_ok=True) self.data_dir.mkdir(parents=True, exist_ok=True)
self._bin_dir.mkdir(parents=True, exist_ok=True) self.bin_dir.mkdir(parents=True, exist_ok=True)
def get_version(self): def get_version(self):
current_version = None current_version = None
if self._data_dir.joinpath("VERSION").exists(): if self.version_file.exists():
current_version = self._data_dir.joinpath("VERSION").read_text().strip() current_version = self.version_file.read_text().strip()
self._write(colorize("info", "Retrieving Poetry metadata")) self._write(colorize("info", "Retrieving Poetry metadata"))