Skip to content

Commit e077339

Browse files
authored
Merge pull request #4192 from python-poetry/improve-self-update-and-env-detection
Improve the `self update` command and environment detection
2 parents ec0aa0e + a42bdaf commit e077339

File tree

5 files changed

+262
-12
lines changed

5 files changed

+262
-12
lines changed

poetry/console/commands/self/update.py

+144-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import re
66
import shutil
7+
import site
78
import stat
89
import subprocess
910
import sys
@@ -16,6 +17,8 @@
1617
from cleo import option
1718

1819
from poetry.core.packages import Dependency
20+
from poetry.utils._compat import PY2
21+
from poetry.utils._compat import Path
1922

2023
from ..command import Command
2124

@@ -60,6 +63,10 @@ class SelfUpdateCommand(Command):
6063
REPOSITORY_URL = "https://github.com/python-poetry/poetry"
6164
BASE_URL = REPOSITORY_URL + "/releases/download"
6265

66+
_data_dir = None
67+
_bin_dir = None
68+
_pool = None
69+
6370
@property
6471
def home(self):
6572
from poetry.utils._compat import Path
@@ -78,18 +85,75 @@ def lib(self):
7885
def lib_backup(self):
7986
return self.home / "lib-backup"
8087

88+
@property
89+
def data_dir(self): # type: () -> Path
90+
if self._data_dir is not None:
91+
return self._data_dir
92+
93+
from poetry.locations import data_dir
94+
95+
self._data_dir = data_dir()
96+
97+
return self._data_dir
98+
99+
@property
100+
def bin_dir(self): # type: () -> Path
101+
if self._data_dir is not None:
102+
return self._data_dir
103+
104+
from poetry.utils._compat import WINDOWS
105+
106+
if os.getenv("POETRY_HOME"):
107+
return Path(os.getenv("POETRY_HOME"), "bin").expanduser()
108+
109+
user_base = site.getuserbase()
110+
111+
if WINDOWS:
112+
bin_dir = os.path.join(user_base, "Scripts")
113+
else:
114+
bin_dir = os.path.join(user_base, "bin")
115+
116+
self._bin_dir = Path(bin_dir)
117+
118+
return self._bin_dir
119+
120+
@property
121+
def pool(self):
122+
if self._pool is not None:
123+
return self._pool
124+
125+
from poetry.repositories.pool import Pool
126+
from poetry.repositories.pypi_repository import PyPiRepository
127+
128+
pool = Pool()
129+
pool.add_repository(PyPiRepository(fallback=False))
130+
131+
self._pool = pool
132+
133+
return self._pool
134+
81135
def handle(self):
82136
from poetry.__version__ import __version__
83137
from poetry.core.semver import Version
84-
from poetry.repositories.pypi_repository import PyPiRepository
138+
from poetry.utils.env import EnvManager
139+
140+
new_update_method = False
141+
try:
142+
self._check_recommended_installation()
143+
except RuntimeError as e:
144+
env = EnvManager.get_system_env(naive=True)
145+
try:
146+
env.path.relative_to(self.data_dir)
147+
except ValueError:
148+
raise e
85149

86-
self._check_recommended_installation()
150+
new_update_method = True
87151

88152
version = self.argument("version")
89153
if not version:
90154
version = ">=" + __version__
91155

92-
repo = PyPiRepository(fallback=False)
156+
repo = self.pool.repositories[0]
93157
packages = repo.find_packages(
94158
Dependency("poetry", version, allows_prereleases=self.option("preview"))
95159
)
@@ -127,6 +191,9 @@ def handle(self):
127191
self.line("You are using the latest version")
128192
return
129193

194+
if new_update_method:
195+
return self.update_with_new_method(release.version)
196+
130197
self.update(release)
131198

132199
def update(self, release):
@@ -165,6 +232,18 @@ def update(self, release):
165232
)
166233
)
167234

235+
def update_with_new_method(self, version):
236+
self.line("Updating <c1>Poetry</c1> to <c2>{}</c2>".format(version))
237+
self.line("")
238+
239+
self._update_with_new_method(version)
240+
self._make_bin()
241+
242+
self.line("")
243+
self.line(
244+
"<c1>Poetry</c1> (<c2>{}</c2>) is installed now. Great!".format(version)
245+
)
246+
168247
def _update(self, version):
169248
from poetry.utils.helpers import temporary_directory
170249

@@ -235,6 +314,68 @@ def _update(self, version):
235314
finally:
236315
gz.close()
237316

317+
def _update_with_new_method(self, version):
318+
from poetry.config.config import Config
319+
from poetry.core.packages.dependency import Dependency
320+
from poetry.core.packages.project_package import ProjectPackage
321+
from poetry.installation.installer import Installer
322+
from poetry.packages.locker import NullLocker
323+
from poetry.repositories.installed_repository import InstalledRepository
324+
from poetry.utils.env import EnvManager
325+
326+
env = EnvManager.get_system_env()
327+
installed = InstalledRepository.load(env)
328+
329+
root = ProjectPackage("poetry-updater", "0.0.0")
330+
root.python_versions = ".".join(str(c) for c in env.version_info[:3])
331+
root.add_dependency(Dependency("poetry", version.text))
332+
333+
installer = Installer(
334+
self.io,
335+
env,
336+
root,
337+
NullLocker(self.data_dir.joinpath("poetry.lock"), {}),
338+
self.pool,
339+
Config(),
340+
installed=installed,
341+
)
342+
installer.update(True)
343+
installer.run()
344+
345+
def _make_bin(self):
346+
from poetry.utils._compat import WINDOWS
347+
348+
self.line("")
349+
self.line("Updating the <c1>poetry</c1> script")
350+
351+
self.bin_dir.mkdir(parents=True, exist_ok=True)
352+
353+
script = "poetry"
354+
target_script = "venv/bin/poetry"
355+
if WINDOWS:
356+
script = "poetry.exe"
357+
target_script = "venv/Scripts/poetry.exe"
358+
359+
if self.bin_dir.joinpath(script).exists():
360+
self.bin_dir.joinpath(script).unlink()
361+
362+
if not PY2 and not WINDOWS:
363+
try:
364+
self.bin_dir.joinpath(script).symlink_to(
365+
self.data_dir.joinpath(target_script)
366+
)
367+
except OSError:
368+
# This can happen if the user
369+
# does not have the correct permission on Windows
370+
shutil.copy(
371+
self.data_dir.joinpath(target_script), self.bin_dir.joinpath(script)
372+
)
373+
else:
374+
shutil.copy(
375+
str(self.data_dir.joinpath(target_script)),
376+
str(self.bin_dir.joinpath(script)),
377+
)
378+
238379
def process(self, *args):
239380
return subprocess.check_output(list(args), stderr=subprocess.STDOUT)
240381

poetry/locations.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
import os
2+
13
from .utils._compat import Path
24
from .utils.appdirs import user_cache_dir
35
from .utils.appdirs import user_config_dir
6+
from .utils.appdirs import user_data_dir
47

58

69
CACHE_DIR = user_cache_dir("pypoetry")
710
CONFIG_DIR = user_config_dir("pypoetry")
811

912
REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories"
13+
14+
15+
def data_dir(): # type: () -> Path
16+
if os.getenv("POETRY_HOME"):
17+
return Path(os.getenv("POETRY_HOME")).expanduser()
18+
19+
return Path(user_data_dir("pypoetry", roaming=True))

poetry/packages/locker.py

+5
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,8 @@ def _dump_package(self, package): # type: (Package) -> dict
607607
data["develop"] = package.develop
608608

609609
return data
610+
611+
612+
class NullLocker(Locker):
613+
def set_lock_data(self, root, packages): # type: (Package, List[Package]) -> None
614+
pass

poetry/utils/env.py

+31-5
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ def get(self, reload=False): # type: (bool) -> Env
472472
create_venv = self._poetry.config.get("virtualenvs.create", True)
473473

474474
if not create_venv:
475-
return SystemEnv(Path(sys.prefix))
475+
return self.get_system_env()
476476

477477
venv_path = self._poetry.config.get("virtualenvs.path")
478478
if venv_path is None:
@@ -485,7 +485,7 @@ def get(self, reload=False): # type: (bool) -> Env
485485
venv = venv_path / name
486486

487487
if not venv.exists():
488-
return SystemEnv(Path(sys.prefix))
488+
return self.get_system_env()
489489

490490
return VirtualEnv(venv)
491491

@@ -790,7 +790,7 @@ def create_venv(
790790
p_venv = os.path.normcase(str(venv))
791791
if any(p.startswith(p_venv) for p in paths):
792792
# Running properly in the virtualenv, don't need to do anything
793-
return SystemEnv(Path(sys.prefix), self.get_base_prefix())
793+
return SystemEnv(Path(sys.prefix), Path(self.get_base_prefix()))
794794

795795
return VirtualEnv(venv)
796796

@@ -833,7 +833,33 @@ def remove_venv(cls, path): # type: (Union[Path,str]) -> None
833833
elif file_path.is_dir():
834834
shutil.rmtree(str(file_path))
835835

836-
def get_base_prefix(self): # type: () -> Path
836+
@classmethod
837+
def get_system_env(cls, naive=False): # type: (bool) -> "SystemEnv"
838+
"""
839+
Retrieve the current Python environment.
840+
This can be the base Python environment or an activated virtual environment.
841+
This method also works around the issue that the virtual environment
842+
used by Poetry internally (when installed via the custom installer)
843+
is incorrectly detected as the system environment. Note that this workaround
844+
happens only when `naive` is False since there are times where we actually
845+
want to retrieve Poetry's custom virtual environment
846+
(e.g. plugin installation or self update).
847+
"""
848+
prefix, base_prefix = Path(sys.prefix), Path(cls.get_base_prefix())
849+
if naive is False:
850+
from poetry.locations import data_dir
851+
852+
try:
853+
prefix.relative_to(data_dir())
854+
except ValueError:
855+
pass
856+
else:
857+
prefix = base_prefix
858+
859+
return SystemEnv(prefix)
860+
861+
@classmethod
862+
def get_base_prefix(cls): # type: () -> str
837863
if hasattr(sys, "real_prefix"):
838864
return sys.real_prefix
839865

@@ -993,7 +1019,7 @@ def supported_tags(self): # type: () -> List[Tag]
9931019
return self._supported_tags
9941020

9951021
@classmethod
996-
def get_base_prefix(cls): # type: () -> Path
1022+
def get_base_prefix(cls): # type: () -> str
9971023
if hasattr(sys, "real_prefix"):
9981024
return sys.real_prefix
9991025

0 commit comments

Comments
 (0)