Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update the self update command to be able to handle future versions
Browse files Browse the repository at this point in the history
sdispater committed May 15, 2020
1 parent 621f698 commit 4f0da65
Showing 5 changed files with 214 additions and 24 deletions.
150 changes: 126 additions & 24 deletions poetry/console/commands/self/update.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from __future__ import unicode_literals

import hashlib
import os
import re
import shutil
import stat
import subprocess
import sys
import tarfile
@@ -22,6 +26,27 @@
from urllib2 import urlopen


BIN = """# -*- coding: utf-8 -*-
import glob
import sys
import os
lib = os.path.normpath(os.path.join(os.path.realpath(__file__), "../..", "lib"))
vendors = os.path.join(lib, "poetry", "_vendor")
current_vendors = os.path.join(
vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
)
sys.path.insert(0, lib)
sys.path.insert(0, current_vendors)
if __name__ == "__main__":
from poetry.console import main
main()
"""

BAT = '@echo off\r\n{python_executable} "{poetry_bin}" %*\r\n'


class SelfUpdateCommand(Command):

name = "update"
@@ -32,17 +57,23 @@ class SelfUpdateCommand(Command):

REPOSITORY_URL = "https://github.com/python-poetry/poetry"
BASE_URL = REPOSITORY_URL + "/releases/download"
FALLBACK_BASE_URL = "https://github.com/sdispater/poetry/releases/download"

@property
def home(self):
from poetry.utils._compat import Path
from poetry.utils.appdirs import expanduser

if os.environ.get("POETRY_HOME"):
return Path(expanduser(os.environ["POETRY_HOME"]))

home = Path(expanduser("~"))

return home / ".poetry"

@property
def bin(self):
return self.home / "bin"

@property
def lib(self):
return self.home / "lib"
@@ -55,16 +86,8 @@ def handle(self):
from poetry.__version__ import __version__
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.semver import Version
from poetry.utils._compat import Path

current = Path(__file__)
try:
current.relative_to(self.home)
except ValueError:
raise RuntimeError(
"Poetry was not installed with the recommended installer. "
"Cannot update automatically."
)
self._check_recommended_installation()

version = self.argument("version")
if not version:
@@ -136,6 +159,8 @@ def update(self, release):
if self.lib_backup.exists():
shutil.rmtree(str(self.lib_backup))

self.make_bin()

self.line("")
self.line("")
self.line(
@@ -147,20 +172,11 @@ def update(self, release):
def _update(self, version):
from poetry.utils.helpers import temporary_directory

platform = sys.platform
if platform == "linux2":
platform = "linux"
release_name = self._get_release_name(version)

checksum = "poetry-{}-{}.sha256sum".format(version, platform)
checksum = "{}.sha256sum".format(release_name)

base_url = self.BASE_URL
try:
urlopen(self.REPOSITORY_URL)
except HTTPError as e:
if e.code == 404:
base_url = self.FALLBACK_BASE_URL
else:
raise

try:
r = urlopen(base_url + "/{}/{}".format(version, checksum))
@@ -170,10 +186,10 @@ def _update(self, version):

raise

checksum = r.read().decode()
checksum = r.read().decode().strip()

# We get the payload from the remote host
name = "poetry-{}-{}.tar.gz".format(version, platform)
name = "{}.tar.gz".format(release_name)
try:
r = urlopen(base_url + "/{}/{}".format(version, name))
except HTTPError as e:
@@ -226,8 +242,94 @@ def _update(self, version):
def process(self, *args):
return subprocess.check_output(list(args), stderr=subprocess.STDOUT)

def _check_recommended_installation(self):
from poetry.utils._compat import Path

current = Path(__file__)
try:
current.relative_to(self.home)
except ValueError:
raise RuntimeError(
"Poetry was not installed with the recommended installer. "
"Cannot update automatically."
)

def _get_release_name(self, version):
platform = sys.platform
if platform == "linux2":
platform = "linux"

return "poetry-{}-{}".format(version, platform)

def _bin_path(self, base_path, bin):
if sys.platform == "win32":
from poetry.utils._compat import WINDOWS

if WINDOWS:
return (base_path / "Scripts" / bin).with_suffix(".exe")

return base_path / "bin" / bin

def make_bin(self):
from poetry.utils._compat import WINDOWS

self.bin.mkdir(0o755, parents=True, exist_ok=True)

python_executable = self._which_python()

if WINDOWS:
with self.bin.joinpath("poetry.bat").open("w", newline="") as f:
f.write(
BAT.format(
python_executable=python_executable,
poetry_bin=str(self.bin / "poetry").replace(
os.environ["USERPROFILE"], "%USERPROFILE%"
),
)
)

bin_content = BIN
if not WINDOWS:
bin_content = "#!/usr/bin/env {}\n".format(python_executable) + bin_content

self.bin.joinpath("poetry").write_text(bin_content, encoding="utf-8")

if not WINDOWS:
# Making the file executable
st = os.stat(str(self.bin.joinpath("poetry")))
os.chmod(str(self.bin.joinpath("poetry")), st.st_mode | stat.S_IEXEC)

def _which_python(self):
"""
Decides which python executable we'll embed in the launcher script.
"""
from poetry.utils._compat import WINDOWS

allowed_executables = ["python", "python3"]
if WINDOWS:
allowed_executables += ["py.exe -3", "py.exe -2"]

# \d in regex ensures we can convert to int later
version_matcher = re.compile(r"^Python (?P<major>\d+)\.(?P<minor>\d+)\..+$")
fallback = None
for executable in allowed_executables:
try:
raw_version = subprocess.check_output(
executable + " --version", stderr=subprocess.STDOUT, shell=True
).decode("utf-8")
except subprocess.CalledProcessError:
continue

match = version_matcher.match(raw_version.strip())
if match and tuple(map(int, match.groups())) >= (3, 0):
# favor the first py3 executable we can find.
return executable

if fallback is None:
# keep this one as the fallback; it was the first valid executable we found.
fallback = executable

if fallback is None:
# Avoid breaking existing scripts
fallback = "python"

return fallback
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
be3d3b916cb47038899d6ff37e875fd08ba3fed22bcdbf5a92f3f48fd2f15da8
Binary file not shown.
87 changes: 87 additions & 0 deletions tests/console/commands/self/test_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os

from cleo.testers import CommandTester

from poetry.__version__ import __version__
from poetry.packages.package import Package
from poetry.semver.version import Version
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path


FIXTURES = Path(__file__).parent.joinpath("fixtures")


def test_self_update_should_install_all_necessary_elements(
app, http, mocker, environ, tmp_dir
):
os.environ["POETRY_HOME"] = tmp_dir

command = app.find("self update")

version = Version.parse(__version__).next_minor.text
mocker.patch(
"poetry.repositories.pypi_repository.PyPiRepository.find_packages",
return_value=[Package("poetry", version)],
)
mocker.patch.object(command, "_check_recommended_installation", return_value=None)
mocker.patch.object(
command, "_get_release_name", return_value="poetry-{}-darwin".format(version)
)
mocker.patch("subprocess.check_output", return_value=b"Python 3.8.2")

http.register_uri(
"GET",
command.BASE_URL + "/{}/poetry-{}-darwin.sha256sum".format(version, version),
body=FIXTURES.joinpath("poetry-1.0.5-darwin.sha256sum").read_bytes(),
)
http.register_uri(
"GET",
command.BASE_URL + "/{}/poetry-{}-darwin.tar.gz".format(version, version),
body=FIXTURES.joinpath("poetry-1.0.5-darwin.tar.gz").read_bytes(),
)

tester = CommandTester(command)
tester.execute()

bin_ = Path(tmp_dir).joinpath("bin")
lib = Path(tmp_dir).joinpath("lib")
assert bin_.exists()

script = bin_.joinpath("poetry")
assert script.exists()

expected_script = """\
# -*- coding: utf-8 -*-
import glob
import sys
import os
lib = os.path.normpath(os.path.join(os.path.realpath(__file__), "../..", "lib"))
vendors = os.path.join(lib, "poetry", "_vendor")
current_vendors = os.path.join(
vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
)
sys.path.insert(0, lib)
sys.path.insert(0, current_vendors)
if __name__ == "__main__":
from poetry.console import main
main()
"""
if not WINDOWS:
expected_script = "#!/usr/bin/env python\n" + expected_script

assert expected_script == script.read_text()

if WINDOWS:
bat = bin_.joinpath("poetry.bat")
expected_bat = '@echo off\r\npython "{}" %*\r\n'.format(
str(script).replace(os.environ.get("USERPROFILE", ""), "%USERPROFILE%")
)
assert bat.exists()
with bat.open(newline="") as f:
assert expected_bat == f.read()

assert lib.exists()
assert lib.joinpath("poetry").exists()

0 comments on commit 4f0da65

Please sign in to comment.