Skip to content

Commit ac6fb50

Browse files
committed
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.
1 parent 07dd8a5 commit ac6fb50

File tree

1 file changed

+86
-69
lines changed

1 file changed

+86
-69
lines changed

install-poetry.py

Lines changed: 86 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
1-
"""
2-
This script will install Poetry and its dependencies.
3-
4-
It does, in order:
5-
6-
- Creates a virtual environment using venv (or virtualenv zipapp) in the correct OS data dir which will be
7-
- `%APPDATA%\\pypoetry` on Windows
8-
- ~/Library/Application Support/pypoetry on MacOS
9-
- `${XDG_DATA_HOME}/pypoetry` (or `~/.local/share/pypoetry` if it's not set) on UNIX systems
10-
- In `${POETRY_HOME}` if it's set.
11-
- Installs the latest or given version of Poetry inside this virtual environment.
12-
- Installs a `poetry` script in the Python user directory (or `${POETRY_HOME/bin}` if `POETRY_HOME` is set).
13-
- On failure, the error log is written to poetry-installer-error-*.log and any previously existing environment
14-
is restored.
1+
#!/usr/bin/env python3
2+
r"""
3+
This script will install Poetry and its dependencies in an isolated fashion.
4+
5+
It will perform the following steps:
6+
* Create a new virtual environment using the built-in venv module, or the virtualenv zipapp if venv is unavailable.
7+
This will be created at a platform-specific path (or `$POETRY_HOME` if `$POETRY_HOME` is set:
8+
- `~/Library/Application Support/pypoetry` on macOS
9+
- `$XDG_DATA_HOME/pypoetry` on Linux/Unix (`$XDG_DATA_HOME` is `~/.local/share` if unset)
10+
- `%APPDATA%\pypoetry` on Windows
11+
* Update pip inside the virtual environment to avoid bugs in older versions.
12+
* Install the latest (or a given) version of Poetry inside this virtual environment using pip.
13+
* Install a `poetry` script into a platform-specific path (or `$POETRY_HOME/bin` if `$POETRY_HOME` is set):
14+
- `~/.local/bin` on Unix
15+
- `%APPDATA%\Python\Scripts` on Windows
16+
* Attempt to inform the user if they need to add this bin directory to their `$PATH`, as well as how to do so.
17+
* Upon failure, write an error log to `poetry-installer-error-<hash>.log and restore any previous environment.
18+
19+
This script performs minimal magic, and should be relatively stable. However, it is optimized for interactive developer
20+
use and trivial pipelines. If you are considering using this script in production, you should consider manually-managed
21+
installs, or use of pipx as alternatives to executing arbitrary, unversioned code from the internet. If you prefer this
22+
script to alternatives, consider maintaining a local copy as part of your infrastructure.
23+
24+
For full documentation, visit https://python-poetry.org/docs/#installation.
1525
"""
1626

1727
import argparse
1828
import json
1929
import os
2030
import re
2131
import shutil
22-
import site
2332
import subprocess
2433
import sys
2534
import sysconfig
@@ -134,38 +143,29 @@ def string_to_bool(value):
134143
return value in {"true", "1", "y", "yes"}
135144

136145

137-
def data_dir(version: Optional[str] = None) -> Path:
146+
def data_dir() -> Path:
138147
if os.getenv("POETRY_HOME"):
139148
return Path(os.getenv("POETRY_HOME")).expanduser()
140149

141150
if WINDOWS:
142-
const = "CSIDL_APPDATA"
143-
path = os.path.normpath(_get_win_folder(const))
144-
path = os.path.join(path, "pypoetry")
151+
base_dir = Path(_get_win_folder("CSIDL_APPDATA"))
145152
elif MACOS:
146-
path = os.path.expanduser("~/Library/Application Support/pypoetry")
153+
base_dir = Path("~/Library/Application Support").expanduser()
147154
else:
148-
path = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
149-
path = os.path.join(path, "pypoetry")
150-
151-
if version:
152-
path = os.path.join(path, version)
155+
base_dir = Path(os.getenv("XDG_DATA_HOME", "~/.local/share")).expanduser()
153156

154-
return Path(path)
157+
base_dir = base_dir.resolve()
158+
return base_dir / "pypoetry"
155159

156160

157-
def bin_dir(version: Optional[str] = None) -> Path:
161+
def bin_dir() -> Path:
158162
if os.getenv("POETRY_HOME"):
159-
return Path(os.getenv("POETRY_HOME"), "bin").expanduser()
160-
161-
user_base = site.getuserbase()
163+
return Path(os.getenv("POETRY_HOME")).expanduser() / "bin"
162164

163165
if WINDOWS and not MINGW:
164-
bin_dir = os.path.join(user_base, "Scripts")
166+
return Path(_get_win_folder("CSIDL_APPDATA")) / "Python/Scripts"
165167
else:
166-
bin_dir = os.path.join(user_base, "bin")
167-
168-
return Path(bin_dir)
168+
return Path("~/.local/bin").expanduser()
169169

170170

171171
def _get_win_folder_from_registry(csidl_name):
@@ -181,9 +181,9 @@ def _get_win_folder_from_registry(csidl_name):
181181
_winreg.HKEY_CURRENT_USER,
182182
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders",
183183
)
184-
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
184+
path, _ = _winreg.QueryValueEx(key, shell_folder_name)
185185

186-
return dir
186+
return path
187187

188188

189189
def _get_win_folder_with_ctypes(csidl_name):
@@ -477,9 +477,26 @@ def __init__(
477477
self._accept_all = accept_all
478478
self._git = git
479479
self._path = path
480-
self._data_dir = data_dir()
481-
self._bin_dir = bin_dir()
480+
482481
self._cursor = Cursor()
482+
self._bin_dir = None
483+
self._data_dir = None
484+
485+
@property
486+
def bin_dir(self) -> Path:
487+
if not self._bin_dir:
488+
self._bin_dir = bin_dir()
489+
return self._bin_dir
490+
491+
@property
492+
def data_dir(self) -> Path:
493+
if not self._data_dir:
494+
self._data_dir = data_dir()
495+
return self._data_dir
496+
497+
@property
498+
def version_file(self) -> Path:
499+
return self.data_dir.joinpath("VERSION")
483500

484501
def allows_prereleases(self) -> bool:
485502
return self._preview
@@ -536,7 +553,7 @@ def _is_self_upgrade_supported(x):
536553

537554
return 0
538555

539-
def install(self, version, upgrade=False):
556+
def install(self, version):
540557
"""
541558
Installs Poetry in $POETRY_HOME.
542559
"""
@@ -549,22 +566,22 @@ def install(self, version, upgrade=False):
549566
with self.make_env(version) as env:
550567
self.install_poetry(version, env)
551568
self.make_bin(version, env)
552-
self._data_dir.joinpath("VERSION").write_text(version)
569+
self.version_file.write_text(version)
553570
self._install_comment(version, "Done")
554571

555572
return 0
556573

557574
def uninstall(self) -> int:
558-
if not self._data_dir.exists():
575+
if not self.data_dir.exists():
559576
self._write(
560577
"{} is not currently installed.".format(colorize("info", "Poetry"))
561578
)
562579

563580
return 1
564581

565582
version = None
566-
if self._data_dir.joinpath("VERSION").exists():
567-
version = self._data_dir.joinpath("VERSION").read_text().strip()
583+
if self.version_file.exists():
584+
version = self.version_file.read_text().strip()
568585

569586
if version:
570587
self._write(
@@ -575,10 +592,10 @@ def uninstall(self) -> int:
575592
else:
576593
self._write("Removing {}".format(colorize("info", "Poetry")))
577594

578-
shutil.rmtree(str(self._data_dir))
595+
shutil.rmtree(str(self.data_dir))
579596
for script in ["poetry", "poetry.bat", "poetry.exe"]:
580-
if self._bin_dir.joinpath(script).exists():
581-
self._bin_dir.joinpath(script).unlink()
597+
if self.bin_dir.joinpath(script).exists():
598+
self.bin_dir.joinpath(script).unlink()
582599

583600
return 0
584601

@@ -593,7 +610,7 @@ def _install_comment(self, version: str, message: str):
593610

594611
@contextmanager
595612
def make_env(self, version: str) -> VirtualEnvironment:
596-
env_path = self._data_dir.joinpath("venv")
613+
env_path = self.data_dir.joinpath("venv")
597614
env_path_saved = env_path.with_suffix(".save")
598615

599616
if env_path.exists():
@@ -625,20 +642,20 @@ def make_env(self, version: str) -> VirtualEnvironment:
625642

626643
def make_bin(self, version: str, env: VirtualEnvironment) -> None:
627644
self._install_comment(version, "Creating script")
628-
self._bin_dir.mkdir(parents=True, exist_ok=True)
645+
self.bin_dir.mkdir(parents=True, exist_ok=True)
629646

630647
script = "poetry.exe" if WINDOWS else "poetry"
631648
target_script = env.bin_path.joinpath(script)
632649

633-
if self._bin_dir.joinpath(script).exists():
634-
self._bin_dir.joinpath(script).unlink()
650+
if self.bin_dir.joinpath(script).exists():
651+
self.bin_dir.joinpath(script).unlink()
635652

636653
try:
637-
self._bin_dir.joinpath(script).symlink_to(target_script)
654+
self.bin_dir.joinpath(script).symlink_to(target_script)
638655
except OSError:
639656
# This can happen if the user
640657
# does not have the correct permission on Windows
641-
shutil.copy(target_script, self._bin_dir.joinpath(script))
658+
shutil.copy(target_script, self.bin_dir.joinpath(script))
642659

643660
def install_poetry(self, version: str, env: VirtualEnvironment) -> None:
644661
self._install_comment(version, "Installing Poetry")
@@ -655,7 +672,7 @@ def install_poetry(self, version: str, env: VirtualEnvironment) -> None:
655672
def display_pre_message(self) -> None:
656673
kwargs = {
657674
"poetry": colorize("info", "Poetry"),
658-
"poetry_home_bin": colorize("comment", self._bin_dir),
675+
"poetry_home_bin": colorize("comment", self.bin_dir),
659676
}
660677
self._write(PRE_MESSAGE.format(**kwargs))
661678

@@ -672,17 +689,17 @@ def display_post_message_windows(self, version: str) -> None:
672689
path = self.get_windows_path_var()
673690

674691
message = POST_MESSAGE_NOT_IN_PATH
675-
if path and str(self._bin_dir) in path:
692+
if path and str(self.bin_dir) in path:
676693
message = POST_MESSAGE
677694

678695
self._write(
679696
message.format(
680697
poetry=colorize("info", "Poetry"),
681698
version=colorize("b", version),
682-
poetry_home_bin=colorize("comment", self._bin_dir),
683-
poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")),
699+
poetry_home_bin=colorize("comment", self.bin_dir),
700+
poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
684701
configure_message=POST_MESSAGE_CONFIGURE_WINDOWS.format(
685-
poetry_home_bin=colorize("comment", self._bin_dir)
702+
poetry_home_bin=colorize("comment", self.bin_dir)
686703
),
687704
test_command=colorize("b", "poetry --version"),
688705
)
@@ -703,17 +720,17 @@ def display_post_message_fish(self, version: str) -> None:
703720
).decode("utf-8")
704721

705722
message = POST_MESSAGE_NOT_IN_PATH
706-
if fish_user_paths and str(self._bin_dir) in fish_user_paths:
723+
if fish_user_paths and str(self.bin_dir) in fish_user_paths:
707724
message = POST_MESSAGE
708725

709726
self._write(
710727
message.format(
711728
poetry=colorize("info", "Poetry"),
712729
version=colorize("b", version),
713-
poetry_home_bin=colorize("comment", self._bin_dir),
714-
poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")),
730+
poetry_home_bin=colorize("comment", self.bin_dir),
731+
poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
715732
configure_message=POST_MESSAGE_CONFIGURE_FISH.format(
716-
poetry_home_bin=colorize("comment", self._bin_dir)
733+
poetry_home_bin=colorize("comment", self.bin_dir)
717734
),
718735
test_command=colorize("b", "poetry --version"),
719736
)
@@ -723,30 +740,30 @@ def display_post_message_unix(self, version: str) -> None:
723740
paths = os.getenv("PATH", "").split(":")
724741

725742
message = POST_MESSAGE_NOT_IN_PATH
726-
if paths and str(self._bin_dir) in paths:
743+
if paths and str(self.bin_dir) in paths:
727744
message = POST_MESSAGE
728745

729746
self._write(
730747
message.format(
731748
poetry=colorize("info", "Poetry"),
732749
version=colorize("b", version),
733-
poetry_home_bin=colorize("comment", self._bin_dir),
734-
poetry_executable=colorize("b", self._bin_dir.joinpath("poetry")),
750+
poetry_home_bin=colorize("comment", self.bin_dir),
751+
poetry_executable=colorize("b", self.bin_dir.joinpath("poetry")),
735752
configure_message=POST_MESSAGE_CONFIGURE_UNIX.format(
736-
poetry_home_bin=colorize("comment", self._bin_dir)
753+
poetry_home_bin=colorize("comment", self.bin_dir)
737754
),
738755
test_command=colorize("b", "poetry --version"),
739756
)
740757
)
741758

742759
def ensure_directories(self) -> None:
743-
self._data_dir.mkdir(parents=True, exist_ok=True)
744-
self._bin_dir.mkdir(parents=True, exist_ok=True)
760+
self.data_dir.mkdir(parents=True, exist_ok=True)
761+
self.bin_dir.mkdir(parents=True, exist_ok=True)
745762

746763
def get_version(self):
747764
current_version = None
748-
if self._data_dir.joinpath("VERSION").exists():
749-
current_version = self._data_dir.joinpath("VERSION").read_text().strip()
765+
if self.version_file.exists():
766+
current_version = self.version_file.read_text().strip()
750767

751768
self._write(colorize("info", "Retrieving Poetry metadata"))
752769

0 commit comments

Comments
 (0)