Skip to content

Commit

Permalink
Update the self update command to be able to handle future versions (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sdispater authored Jun 5, 2020
1 parent e4e8e3c commit 9cd16a9
Show file tree
Hide file tree
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
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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))
Expand All @@ -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:
Expand Down Expand Up @@ -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 9cd16a9

Please sign in to comment.