Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
python-poetry
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
open
python-poetry
Commits
c02703ee
Unverified
Commit
c02703ee
authored
Apr 30, 2021
by
Sébastien Eustace
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improve `self update` following the introduction of install-poetry.py
parent
f5158013
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
204 additions
and
288 deletions
+204
-288
poetry/console/commands/self/update.py
+119
-221
poetry/packages/locker.py
+5
-0
tests/console/commands/self/test_update.py
+80
-67
No files found.
poetry/console/commands/self/update.py
View file @
c02703ee
from
__future__
import
unicode_literals
import
hashlib
import
os
import
re
import
shutil
import
stat
import
subprocess
import
sys
import
tarfile
import
site
from
functools
import
cmp_to_key
from
gzip
import
GzipFile
from
pathlib
import
Path
from
typing
import
TYPE_CHECKING
from
typing
import
Any
from
urllib.error
import
HTTPError
from
urllib.request
import
urlopen
from
cleo.helpers
import
argument
from
cleo.helpers
import
option
...
...
@@ -26,27 +15,7 @@ from ..command import Command
if
TYPE_CHECKING
:
from
poetry.core.packages.package
import
Package
from
poetry.core.semver.version
import
Version
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
'
from
poetry.repositories.pool
import
Pool
class
SelfUpdateCommand
(
Command
):
...
...
@@ -55,48 +24,81 @@ class SelfUpdateCommand(Command):
description
=
"Updates Poetry to the latest version."
arguments
=
[
argument
(
"version"
,
"The version to update to."
,
optional
=
True
)]
options
=
[
option
(
"preview"
,
None
,
"Install prereleases."
)]
options
=
[
option
(
"preview"
,
None
,
"Allow the installation of pre-release versions."
),
option
(
"dry-run"
,
None
,
"Output the operations but do not execute anything "
"(implicitly enables --verbose)."
,
),
]
REPOSITORY_URL
=
"https://github.com/python-poetry/poetry"
BASE_URL
=
REPOSITORY_URL
+
"/releases/download"
_data_dir
=
None
_bin_dir
=
None
_pool
=
None
@property
def
home
(
self
)
->
Path
:
from
pathlib
import
Path
def
data_dir
(
self
)
->
Path
:
if
self
.
_data_dir
is
not
None
:
return
self
.
_data_dir
return
Path
(
os
.
environ
.
get
(
"POETRY_HOME"
,
"~/.poetry"
))
.
expanduser
()
from
poetry.locations
import
data_dir
@property
def
bin
(
self
)
->
Path
:
return
self
.
home
/
"bin"
self
.
_data_dir
=
data_dir
()
return
self
.
_data_dir
@property
def
lib
(
self
)
->
Path
:
return
self
.
home
/
"lib"
def
bin_dir
(
self
)
->
Path
:
if
self
.
_data_dir
is
not
None
:
return
self
.
_data_dir
from
poetry.utils._compat
import
WINDOWS
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"
)
self
.
_bin_dir
=
Path
(
bin_dir
)
return
self
.
_bin_dir
@property
def
lib_backup
(
self
)
->
Path
:
return
self
.
home
/
"lib-backup"
def
pool
(
self
)
->
"Pool"
:
if
self
.
_pool
is
not
None
:
return
self
.
_pool
from
poetry.repositories.pool
import
Pool
from
poetry.repositories.pypi_repository
import
PyPiRepository
pool
=
Pool
()
pool
.
add_repository
(
PyPiRepository
())
def
handle
(
self
)
->
None
:
return
pool
def
handle
(
self
)
->
int
:
from
poetry.__version__
import
__version__
from
poetry.core.packages.dependency
import
Dependency
from
poetry.core.semver.version
import
Version
from
poetry.repositories.pypi_repository
import
PyPiRepository
self
.
_check_recommended_installation
()
version
=
self
.
argument
(
"version"
)
if
not
version
:
version
=
">="
+
__version__
repo
=
PyPiRepository
(
fallback
=
False
)
repo
=
self
.
pool
.
repositories
[
0
]
packages
=
repo
.
find_packages
(
Dependency
(
"poetry"
,
version
,
allows_prereleases
=
self
.
option
(
"preview"
))
)
if
not
packages
:
self
.
line
(
"No release found for the specified version"
)
return
return
1
packages
.
sort
(
key
=
cmp_to_key
(
...
...
@@ -122,205 +124,101 @@ class SelfUpdateCommand(Command):
if
release
is
None
:
self
.
line
(
"No new release found"
)
return
return
1
if
release
.
version
==
Version
.
parse
(
__version__
):
self
.
line
(
"You are using the latest version"
)
return
self
.
update
(
release
)
def
update
(
self
,
release
:
"Package"
)
->
None
:
version
=
release
.
version
self
.
line
(
"Updating to <info>{}</info>"
.
format
(
version
))
if
self
.
lib_backup
.
exists
():
shutil
.
rmtree
(
str
(
self
.
lib_backup
))
# Backup the current installation
if
self
.
lib
.
exists
():
shutil
.
copytree
(
str
(
self
.
lib
),
str
(
self
.
lib_backup
))
shutil
.
rmtree
(
str
(
self
.
lib
))
return
0
try
:
self
.
_update
(
version
)
except
Exception
:
if
not
self
.
lib_backup
.
exists
():
raise
shutil
.
copytree
(
str
(
self
.
lib_backup
),
str
(
self
.
lib
))
shutil
.
rmtree
(
str
(
self
.
lib_backup
))
raise
finally
:
if
self
.
lib_backup
.
exists
():
shutil
.
rmtree
(
str
(
self
.
lib_backup
))
self
.
line
(
"Updating <c1>Poetry</c1> to <c2>{}</c2>"
.
format
(
release
.
version
))
self
.
line
(
""
)
self
.
make_bin
(
)
self
.
update
(
release
)
self
.
line
(
""
)
self
.
line
(
""
)
self
.
line
(
"<
info>Poetry</info> (<comment>{}</comment
>) is installed now. Great!"
.
format
(
version
"<
c1>Poetry</c1> (<c2>{}</c2
>) is installed now. Great!"
.
format
(
release
.
version
)
)
def
_update
(
self
,
version
:
"Version"
)
->
None
:
from
poetry.utils.helpers
import
temporary_directory
release_name
=
self
.
_get_release_name
(
version
)
checksum
=
"{}.sha256sum"
.
format
(
release_name
)
return
0
base_url
=
self
.
BASE_URL
try
:
r
=
urlopen
(
base_url
+
"/{}/{}"
.
format
(
version
,
checksum
))
except
HTTPError
as
e
:
if
e
.
code
==
404
:
raise
RuntimeError
(
"Could not find {} file"
.
format
(
checksum
))
raise
checksum
=
r
.
read
()
.
decode
()
.
strip
()
# We get the payload from the remote host
name
=
"{}.tar.gz"
.
format
(
release_name
)
try
:
r
=
urlopen
(
base_url
+
"/{}/{}"
.
format
(
version
,
name
))
except
HTTPError
as
e
:
if
e
.
code
==
404
:
raise
RuntimeError
(
"Could not find {} file"
.
format
(
name
))
raise
meta
=
r
.
info
()
size
=
int
(
meta
[
"Content-Length"
])
current
=
0
block_size
=
8192
bar
=
self
.
progress_bar
(
max
=
size
)
bar
.
set_format
(
" - Downloading <info>{}</> <comment>
%
percent
%%
</>"
.
format
(
name
))
bar
.
start
()
sha
=
hashlib
.
sha256
()
with
temporary_directory
(
prefix
=
"poetry-updater-"
)
as
dir_
:
tar
=
os
.
path
.
join
(
dir_
,
name
)
with
open
(
tar
,
"wb"
)
as
f
:
while
True
:
buffer
=
r
.
read
(
block_size
)
if
not
buffer
:
break
current
+=
len
(
buffer
)
f
.
write
(
buffer
)
sha
.
update
(
buffer
)
bar
.
set_progress
(
current
)
def
update
(
self
,
release
:
"Package"
)
->
None
:
from
poetry.utils.env
import
EnvManager
bar
.
finish
()
version
=
release
.
version
# Checking hashes
if
checksum
!=
sha
.
hexdigest
():
raise
RuntimeError
(
"Hashes for {} do not match: {} != {}"
.
format
(
name
,
checksum
,
sha
.
hexdigest
()
)
)
env
=
EnvManager
.
get_system_env
(
naive
=
True
)
gz
=
GzipFile
(
tar
,
mode
=
"rb"
)
# We can't use is_relative_to() since it's only available in Python 3.9+
try
:
with
tarfile
.
TarFile
(
tar
,
fileobj
=
gz
,
format
=
tarfile
.
PAX_FORMAT
)
as
f
:
f
.
extractall
(
str
(
self
.
lib
))
finally
:
gz
.
close
()
def
process
(
self
,
*
args
:
Any
)
->
str
:
return
subprocess
.
check_output
(
list
(
args
),
stderr
=
subprocess
.
STDOUT
)
def
_check_recommended_installation
(
self
)
->
None
:
from
pathlib
import
Path
env
.
path
.
relative_to
(
self
.
data_dir
)
except
ValueError
:
# Poetry was not installed using the recommended installer
from
poetry.console.exceptions
import
PoetrySimpleConsoleException
current
=
Path
(
__file__
)
try
:
current
.
relative_to
(
self
.
home
)
except
ValueError
:
raise
PoetrySimpleConsoleException
(
"Poetry was not installed with the recommended installer, "
"so it cannot be updated automatically."
)
def
_get_release_name
(
self
,
version
:
"Version"
)
->
str
:
platform
=
sys
.
platform
if
platform
==
"linux2"
:
platform
=
"linux"
self
.
_update
(
version
)
self
.
_make_bin
()
return
"poetry-{}-{}"
.
format
(
version
,
platform
)
def
_update
(
self
,
version
:
"Version"
)
->
None
:
from
poetry.config.config
import
Config
from
poetry.core.packages.dependency
import
Dependency
from
poetry.core.packages.project_package
import
ProjectPackage
from
poetry.installation.installer
import
Installer
from
poetry.packages.locker
import
NullLocker
from
poetry.repositories.installed_repository
import
InstalledRepository
from
poetry.utils.env
import
EnvManager
env
=
EnvManager
.
get_system_env
()
installed
=
InstalledRepository
.
load
(
env
)
root
=
ProjectPackage
(
"poetry-updater"
,
"0.0.0"
)
root
.
python_versions
=
"."
.
join
(
str
(
c
)
for
c
in
env
.
version_info
[:
3
])
root
.
add_dependency
(
Dependency
(
"poetry"
,
version
.
text
))
installer
=
Installer
(
self
.
io
,
env
,
root
,
NullLocker
(
self
.
data_dir
.
joinpath
(
"poetry.lock"
),
{}),
self
.
pool
,
Config
(),
installed
=
installed
,
)
installer
.
update
(
True
)
installer
.
dry_run
(
self
.
option
(
"dry-run"
))
installer
.
run
()
def
make_bin
(
self
)
->
None
:
def
_
make_bin
(
self
)
->
None
:
from
poetry.utils._compat
import
WINDOWS
self
.
bin
.
mkdir
(
0
o755
,
parents
=
True
,
exist_ok
=
True
)
self
.
line
(
""
)
self
.
line
(
"Updating the <c1>poetry</c1> script"
)
python_executable
=
self
.
_which_python
(
)
self
.
bin_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
script
=
"poetry"
target_script
=
"venv/bin/poetry"
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
)
script
=
"poetry.exe"
target_script
=
"venv/Scripts/poetry.exe"
def
_which_python
(
self
)
->
str
:
"""
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"
]
if
self
.
bin_dir
.
joinpath
(
script
)
.
exists
():
self
.
bin_dir
.
joinpath
(
script
)
.
unlink
()
# \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
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
)
)
poetry/packages/locker.py
View file @
c02703ee
...
...
@@ -595,3 +595,8 @@ class Locker:
data
[
"develop"
]
=
package
.
develop
return
data
class
NullLocker
(
Locker
):
def
set_lock_data
(
self
,
root
:
Package
,
packages
:
List
[
Package
])
->
bool
:
pass
tests/console/commands/self/test_update.py
View file @
c02703ee
import
os
from
pathlib
import
Path
import
pytest
from
poetry.__version__
import
__version__
from
poetry.console.exceptions
import
PoetrySimpleConsoleException
from
poetry.core.packages.package
import
Package
from
poetry.core.semver.version
import
Version
from
poetry.utils._compat
import
WINDOWS
from
poetry.factory
import
Factory
from
poetry.repositories.installed_repository
import
InstalledRepository
from
poetry.repositories.pool
import
Pool
from
poetry.repositories.repository
import
Repository
from
poetry.utils.env
import
EnvManager
FIXTURES
=
Path
(
__file__
)
.
parent
.
joinpath
(
"fixtures"
)
...
...
@@ -18,75 +21,85 @@ def tester(command_tester_factory):
return
command_tester_factory
(
"self update"
)
def
test_self_update_
should_install_all_necessary_elements
(
tester
,
http
,
mocker
,
environ
,
tmp_
dir
def
test_self_update_
can_update_from_recommended_installation
(
tester
,
http
,
mocker
,
environ
,
tmp_
venv
):
os
.
environ
[
"POETRY_HOME"
]
=
tmp_dir
mocker
.
patch
.
object
(
EnvManager
,
"get_system_env"
,
return_value
=
tmp_venv
)
command
=
tester
.
command
command
.
_data_dir
=
tmp_venv
.
path
.
parent
new_version
=
Version
.
parse
(
__version__
)
.
next_minor
()
.
text
old_poetry
=
Package
(
"poetry"
,
__version__
)
old_poetry
.
add_dependency
(
Factory
.
create_dependency
(
"cleo"
,
"^0.8.2"
))
new_poetry
=
Package
(
"poetry"
,
new_version
)
new_poetry
.
add_dependency
(
Factory
.
create_dependency
(
"cleo"
,
"^1.0.0"
))
installed_repository
=
Repository
()
installed_repository
.
add_package
(
old_poetry
)
installed_repository
.
add_package
(
Package
(
"cleo"
,
"0.8.2"
))
repository
=
Repository
()
repository
.
add_package
(
new_poetry
)
repository
.
add_package
(
Package
(
"cleo"
,
"1.0.0"
))
pool
=
Pool
()
pool
.
add_repository
(
repository
)
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
(),
)
command
.
_pool
=
pool
mocker
.
patch
.
object
(
InstalledRepository
,
"load"
,
return_value
=
installed_repository
)
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()
expected_output
=
"""
\
Updating Poetry to 1.2.0
Updating dependencies
Resolving dependencies...
Package operations: 0 installs, 2 updates, 0 removals
- Updating cleo (0.8.2 -> 1.0.0)
- Updating poetry (1.2.0a0 -> 1.2.0)
Updating the poetry script
Poetry (1.2.0) is installed now. Great!
"""
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\n
python "{}"
%*
\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
()
assert
tester
.
io
.
fetch_output
()
==
expected_output
def
test_self_update_does_not_update_non_recommended_installation
(
tester
,
http
,
mocker
,
environ
,
tmp_venv
):
mocker
.
patch
.
object
(
EnvManager
,
"get_system_env"
,
return_value
=
tmp_venv
)
command
=
tester
.
command
new_version
=
Version
.
parse
(
__version__
)
.
next_minor
()
.
text
old_poetry
=
Package
(
"poetry"
,
__version__
)
old_poetry
.
add_dependency
(
Factory
.
create_dependency
(
"cleo"
,
"^0.8.2"
))
new_poetry
=
Package
(
"poetry"
,
new_version
)
new_poetry
.
add_dependency
(
Factory
.
create_dependency
(
"cleo"
,
"^1.0.0"
))
installed_repository
=
Repository
()
installed_repository
.
add_package
(
old_poetry
)
installed_repository
.
add_package
(
Package
(
"cleo"
,
"0.8.2"
))
repository
=
Repository
()
repository
.
add_package
(
new_poetry
)
repository
.
add_package
(
Package
(
"cleo"
,
"1.0.0"
))
pool
=
Pool
()
pool
.
add_repository
(
repository
)
command
.
_pool
=
pool
with
pytest
.
raises
(
PoetrySimpleConsoleException
):
tester
.
execute
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment