diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 07475596c09..51d23eddc2d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,14 +39,21 @@ jobs: - name: Bootstrap poetry shell: bash - run: | - python -m ensurepip - python -m pip install --upgrade pip - python -m pip install . + run: python install-poetry.py -y + + - name: Update PATH + if: ${{ matrix.os != 'Windows' }} + shell: bash + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Update Path for Windows + if: ${{ matrix.os == 'Windows' }} + shell: bash + run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH - name: Configure poetry shell: bash - run: python -m poetry config virtualenvs.in-project true + run: poetry config virtualenvs.in-project true - name: Set up cache uses: actions/cache@v2 @@ -58,12 +65,12 @@ jobs: - name: Ensure cache is healthy if: steps.cache.outputs.cache-hit == 'true' shell: bash - run: timeout 10s python -m poetry run pip --version || rm -rf .venv + run: timeout 10s poetry run pip --version || rm -rf .venv - name: Install dependencies shell: bash - run: python -m poetry install + run: poetry install - name: Run pytest shell: bash - run: python -m poetry run python -m pytest -p no:sugar -q tests/ + run: poetry run python -m pytest -p no:sugar -q tests/ diff --git a/README.md b/README.md index 7759fca1232..b1a30d7a012 100644 --- a/README.md +++ b/README.md @@ -17,42 +17,76 @@ The [complete documentation](https://python-poetry.org/docs/) is available on th ## Installation Poetry provides a custom installer that will install `poetry` isolated -from the rest of your system by vendorizing its dependencies. This is the -recommended way of installing `poetry`. +from the rest of your system. +### osx / linux / bashonwindows install instructions ```bash -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python +curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - ``` +### windows powershell install instructions +```powershell +(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - +``` + +**Warning**: The previous `get-poetry.py` installer is now deprecated, if you are currently using it +you should migrate to the new, supported, `install-poetry.py` installer. + +The installer installs the `poetry` tool to Poetry's `bin` directory. This location depends on you system: + +- `$HOME/.local/bin` for Unix +- `%APPDATA%\Python\Scripts` on Windows -Alternatively, you can download the `get-poetry.py` file and execute it separately. +If this directory is not on you `PATH`, you will need to add it manually +if you want to invoke Poetry with simply `poetry`. + +Alternatively, you can use the full path to `poetry` to use it. + +Once Poetry is installed you can execute the following: + +```bash +poetry --version +``` -The setup script must be able to find one of following executables in your shell's path environment: +If you see something like `Poetry (version 1.2.0)` then you are ready to use Poetry. +If you decide Poetry isn't your thing, you can completely remove it from your system +by running the installer again with the `--uninstall` option or by setting +the `POETRY_UNINSTALL` environment variable before executing the installer. -- `python` (which can be a py3 or py2 interpreter) -- `python3` -- `py.exe -3` (Windows) -- `py.exe -2` (Windows) +```bash +python install-poetry.py --uninstall +POETRY_UNINSTALL=1 python install-poetry.py +``` -If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py`: +By default, Poetry is installed into the user's platform-specific home directory. +If you wish to change this, you may define the `POETRY_HOME` environment variable: ```bash -python get-poetry.py --preview +POETRY_HOME=/etc/poetry python install-poetry.py ``` -Similarly, if you want to install a specific version, you can use `--version`: +If you want to install prerelease versions, you can do so by passing `--preview` option to `install-poetry.py` +or by using the `POETRY_PREVIEW` environment variable: ```bash -python get-poetry.py --version 0.7.0 +python install-poetry.py --preview +POETRY_PREVIEW=1 python install-poetry.py ``` -Using `pip` to install `poetry` is also possible. +Similarly, if you want to install a specific version, you can use `--version` option or the `POETRY_VERSION` +environment variable: ```bash -pip install --user poetry +python install-poetry.py --version 1.2.0 +POETRY_VERSION=1.2.0 python install-poetry.py ``` -Be aware, however, that it will also install poetry's dependencies -which might cause conflicts. +You can also install Poetry for a `git` repository by using the `--git` option: + +```bash +python install-poetry.py --git https://github.com/python-poetry/poetry.git@master +```` + +**Note**: Note that the installer does not support Python < 3.6. ## Updating `poetry` @@ -72,13 +106,9 @@ And finally, if you want to install a specific version you can pass it as an arg to `self update`. ```bash -poetry self update 1.0.0 +poetry self update 1.2.0 ``` -*Note:* - - If you are still on poetry version < 1.0 use `poetry self:update` instead. - ## Enable tab completion for Bash, Fish, or Zsh diff --git a/docs/docs/index.md b/docs/docs/index.md index 56b3040b02a..b12c510d358 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -18,109 +18,89 @@ on Windows, Linux and OSX. ## Installation Poetry provides a custom installer that will install `poetry` isolated -from the rest of your system by vendorizing its dependencies. This is the -recommended way of installing `poetry`. +from the rest of your system. ### osx / linux / bashonwindows install instructions ```bash -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - +curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - ``` ### windows powershell install instructions ```powershell -(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - +(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - ``` -!!! note +!!!warning + + The previous `get-poetry.py` installer is now deprecated, if you are currently using it + you should migrate to the new, supported, `install-poetry.py` installer. - You only need to install Poetry once. It will automatically pick up the current - Python version and use it to [create virtualenvs](/docs/managing-environments) accordingly. +The installer installs the `poetry` tool to Poetry's `bin` directory. This location depends on you system: -The installer installs the `poetry` tool to Poetry's `bin` directory. -On Unix it is located at `$HOME/.poetry/bin` and on Windows at `%USERPROFILE%\.poetry\bin`. +- `$HOME/.local/bin` for Unix +- `%APPDATA%\Python\Scripts` on Windows -This directory will be automatically added to your `$PATH` environment variable, -by appending a statement to your `$HOME/.profile` configuration (or equivalent files). -If you do not feel comfortable with this, please pass the `--no-modify-path` flag to -the installer and manually add the Poetry's `bin` directory to your path. +If this directory is not on you `PATH`, you will need to add it manually +if you want to invoke Poetry with simply `poetry`. -Finally, open a new shell and type the following: +Alternatively, you can use the full path to `poetry` to use it. + +Once Poetry is installed you can execute the following: ```bash poetry --version ``` -If you see something like `Poetry 0.12.0` then you are ready to use Poetry. +If you see something like `Poetry (version 1.2.0)` then you are ready to use Poetry. If you decide Poetry isn't your thing, you can completely remove it from your system by running the installer again with the `--uninstall` option or by setting the `POETRY_UNINSTALL` environment variable before executing the installer. ```bash -python get-poetry.py --uninstall -POETRY_UNINSTALL=1 python get-poetry.py +python install-poetry.py --uninstall +POETRY_UNINSTALL=1 python install-poetry.py ``` -By default, Poetry is installed into the user's platform-specific home directory. If you wish to change this, you may define the `POETRY_HOME` environment variable: +By default, Poetry is installed into the user's platform-specific home directory. +If you wish to change this, you may define the `POETRY_HOME` environment variable: ```bash -POETRY_HOME=/etc/poetry python get-poetry.py +POETRY_HOME=/etc/poetry python install-poetry.py ``` -If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py` +If you want to install prerelease versions, you can do so by passing `--preview` option to `install-poetry.py` or by using the `POETRY_PREVIEW` environment variable: ```bash -python get-poetry.py --preview -POETRY_PREVIEW=1 python get-poetry.py +python install-poetry.py --preview +POETRY_PREVIEW=1 python install-poetry.py ``` -Similarly, if you want to install a specific version, you can use `--version` or the `POETRY_VERSION` +Similarly, if you want to install a specific version, you can use `--version` option or the `POETRY_VERSION` environment variable: ```bash -python get-poetry.py --version 0.12.0 -POETRY_VERSION=0.12.0 python get-poetry.py +python install-poetry.py --version 1.2.0 +POETRY_VERSION=1.2.0 python install-poetry.py ``` -!!!note - - Note that the installer does not support Poetry releases < 0.12.0. +You can also install Poetry for a `git` repository by using the `--git` option: -!!!note - - The setup script must be able to find one of following executables in your shell's path environment: - - - `python` (which can be a py3 or py2 interpreter) - - `python3` - - `py.exe -3` (Windows) - - `py.exe -2` (Windows) - -### Alternative installation methods (not recommended) +```bash +python install-poetry.py --git https://github.com/python-poetry/poetry.git@master +```` !!!note - Using alternative installation methods will make Poetry always - use the Python version for which it has been installed to create - virtualenvs. + Note that the installer does not support Python < 3.6. - So, you will need to install Poetry for each Python version you - want to use and switch between them. - -#### Installing with `pip` - -Using `pip` to install Poetry is possible. - -```bash -pip install --user poetry -``` - -!!!warning - Be aware that it will also install Poetry's dependencies - which might cause conflicts with other packages. +### Alternative installation methods #### Installing with `pipx` -Using [`pipx`](https://github.com/cs01/pipx) to install Poetry is also possible. `pipx` is used to install Python CLI applications globally while still isolating them in virtual environments. This allows for clean upgrades and uninstalls. `pipx` supports Python 3.6 and later. If using an earlier version of Python, consider [`pipsi`](https://github.com/mitsuhiko/pipsi). +Using [`pipx`](https://github.com/pipxproject/pipx) to install Poetry is also possible. +`pipx` is used to install Python CLI applications globally while still isolating them in virtual environments. +This allows for clean upgrades and uninstalls. ```bash pipx install poetry @@ -134,7 +114,19 @@ pipx upgrade poetry pipx uninstall poetry ``` -[Github repository](https://github.com/cs01/pipx). + +#### Installing with `pip` + +Using `pip` to install Poetry is possible. + +```bash +pip install --user poetry +``` + +!!!warning + + Be aware that it will also install Poetry's dependencies + which might cause conflicts with other packages. ## Updating `poetry` @@ -155,18 +147,9 @@ And finally, if you want to install a specific version, you can pass it as an ar to `self update`. ```bash -poetry self update 0.8.0 +poetry self update 1.2.0 ``` -!!!note - - The `self update` command will only work if you used the recommended - installer to install Poetry. - -!!!note - - If you are still on poetry version < 1.0 use `poetry self:update` instead. - ## Enable tab completion for Bash, Fish, or Zsh diff --git a/install-poetry.py b/install-poetry.py new file mode 100644 index 00000000000..7cb19c85296 --- /dev/null +++ b/install-poetry.py @@ -0,0 +1,808 @@ +""" +This script will install Poetry and its dependencies. + +It does, in order: + + - Downloads the virtualenv package to a temporary directory and add it to sys.path. + - Creates a virtual environment in the correct OS data dir which will be + - `%APPDATA%\\pypoetry` on Windows + - ~/Library/Application Support/pypoetry on MacOS + - `${XDG_DATA_HOME}/pypoetry` (or `~/.local/share/pypoetry` if it's not set) on UNIX systems + - In `${POETRY_HOME}` if it's set. + - Installs the latest or given version of Poetry inside this virtual environment. + - Installs a `poetry` script in the Python user directory (or `${POETRY_HOME/bin}` if `POETRY_HOME` is set). +""" + +import argparse +import json +import os +import re +import shutil +import site +import subprocess +import sys +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") +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 "ON" == os.getenv("ConEmuANSI") + or "xterm" == os.getenv("Term") + ) + + 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 "{}{}\033[0m".format(STYLES[style], text) + + +def string_to_bool(value): + value = value.lower() + + return value in {"true", "1", "y", "yes"} + + +def data_dir(version: Optional[str] = None) -> Path: + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME")).expanduser() + + if WINDOWS: + const = "CSIDL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + path = os.path.join(path, "pypoetry") + elif MACOS: + path = os.path.expanduser("~/Library/Application Support/pypoetry") + else: + path = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) + path = os.path.join(path, "pypoetry") + + if version: + path = os.path.join(path, version) + + return Path(path) + + +def bin_dir(version: Optional[str] = None) -> Path: + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME"), "bin").expanduser() + + user_base = site.getuserbase() + + if WINDOWS: + bin_dir = os.path.join(user_base, "Scripts") + else: + bin_dir = os.path.join(user_base, "bin") + + return Path(bin_dir) + + +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", + ) + dir, type = _winreg.QueryValueEx(key, shell_folder_name) + + return dir + + +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 + # . + 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 + + _get_win_folder = _get_win_folder_with_ctypes + except ImportError: + _get_win_folder = _get_win_folder_from_registry + + +@contextmanager +def temporary_directory(*args, **kwargs): + try: + from tempfile import TemporaryDirectory + except ImportError: + name = tempfile.mkdtemp(*args, **kwargs) + + yield name + + shutil.rmtree(name) + else: + with TemporaryDirectory(*args, **kwargs) as name: + yield name + + +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 Cursor: + def __init__(self) -> None: + self._output = sys.stdout + + def move_up(self, lines: int = 1) -> "Cursor": + self._output.write("\x1b[{}A".format(lines)) + + return self + + def move_down(self, lines: int = 1) -> "Cursor": + self._output.write("\x1b[{}B".format(lines)) + + return self + + def move_right(self, columns: int = 1) -> "Cursor": + self._output.write("\x1b[{}C".format(columns)) + + return self + + def move_left(self, columns: int = 1) -> "Cursor": + self._output.write("\x1b[{}D".format(columns)) + + return self + + def move_to_column(self, column: int) -> "Cursor": + self._output.write("\x1b[{}G".format(column)) + + return self + + def move_to_position(self, column: int, row: int) -> "Cursor": + self._output.write("\x1b[{};{}H".format(row + 1, column)) + + 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._data_dir = data_dir() + self._bin_dir = bin_dir() + self._cursor = Cursor() + + 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: + version, current_version = self.get_version() + + if version is None: + return 0 + + self.display_pre_message() + self.ensure_directories() + + try: + self.install(version) + except subprocess.CalledProcessError as e: + print(colorize("error", "An error has occured: {}".format(str(e)))) + print(e.output.decode()) + + return e.returncode + + self._write("") + self.display_post_message(version) + + return 0 + + def install(self, version, upgrade=False): + """ + Installs Poetry in $POETRY_HOME. + """ + self._write( + "Installing {} ({})".format( + colorize("info", "Poetry"), colorize("info", version) + ) + ) + + env_path = self.make_env(version) + self.install_poetry(version, env_path) + self.make_bin(version) + + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("success", "Done"), + ) + ) + + self._data_dir.joinpath("VERSION").write_text(version) + + 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._data_dir.joinpath("VERSION").exists(): + version = self._data_dir.joinpath("VERSION").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"]: + if self._bin_dir.joinpath(script).exists(): + self._bin_dir.joinpath(script).unlink() + + return 0 + + def make_env(self, version: str) -> Path: + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("comment", "Creating environment"), + ) + ) + + env_path = self._data_dir.joinpath("venv") + + with temporary_directory() as tmp_dir: + subprocess.call( + [sys.executable, "-m", "pip", "install", "virtualenv", "-t", tmp_dir], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + sys.path.insert(0, tmp_dir) + + import virtualenv + + virtualenv.cli_run([str(env_path), "--clear"]) + + return env_path + + def make_bin(self, version: str) -> None: + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("comment", "Creating script"), + ) + ) + + self._bin_dir.mkdir(parents=True, exist_ok=True) + + script = "poetry" + target_script = "venv/bin/poetry" + if WINDOWS: + script = "poetry.exe" + target_script = "venv/Scripts/poetry.exe" + + if self._bin_dir.joinpath(script).exists(): + self._bin_dir.joinpath(script).unlink() + + try: + self._bin_dir.joinpath(script).symlink_to( + self._data_dir.joinpath(target_script) + ) + except OSError: + # This can happen if the user + # does not have the correct permission on Windows + shutil.copy( + self._data_dir.joinpath(target_script), self._bin_dir.joinpath(script) + ) + + def install_poetry(self, version: str, env_path: Path) -> None: + self._overwrite( + "Installing {} ({}): {}".format( + colorize("info", "Poetry"), + colorize("b", version), + colorize("comment", "Installing Poetry"), + ) + ) + + if WINDOWS: + python = env_path.joinpath("Scripts/python.exe") + else: + python = env_path.joinpath("bin/python") + + if self._git: + specification = "git+" + version + elif self._path: + specification = version + else: + specification = f"poetry=={version}" + + subprocess.run( + [str(python), "-m", "pip", "install", specification], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + 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: + with 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._data_dir.joinpath("VERSION").exists(): + current_version = self._data_dir.joinpath("VERSION").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: + self._write( + colorize("error", "Version {} does not exist.".format(self._version)) + ) + + return None, None + + 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( + "The latest version ({}) is already installed.".format( + colorize("b", version) + ) + ) + + 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() + + return installer.run() + + +if __name__ == "__main__": + sys.exit(main())