-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
759 additions
and
12 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
from __future__ import annotations | ||
|
||
from subprocess import CalledProcessError | ||
from typing import TYPE_CHECKING | ||
from typing import ClassVar | ||
from typing import cast | ||
|
||
import pbs_installer as pbi | ||
|
||
from cleo.helpers import argument | ||
from cleo.helpers import option | ||
from poetry.core.constraints.version.version import Version | ||
from poetry.core.version.exceptions import InvalidVersionError | ||
|
||
from poetry.config.config import Config | ||
from poetry.console.commands.command import Command | ||
from poetry.console.commands.python.remove import PythonRemoveCommand | ||
from poetry.console.exceptions import ConsoleMessage | ||
from poetry.console.exceptions import PoetryRuntimeError | ||
from poetry.utils.env.python_manager import PoetryPythonPathProvider | ||
|
||
|
||
if TYPE_CHECKING: | ||
from cleo.io.inputs.argument import Argument | ||
from cleo.io.inputs.option import Option | ||
|
||
|
||
BAD_PYTHON_INSTALL_INFO = [ | ||
"This could happen because you are missing platform dependencies required.", | ||
"Please refer to https://gregoryszorc.com/docs/python-build-standalone/main/running.html#runtime-requirements " | ||
"for more information about the necessary requirements.", | ||
"Please remove the failing Python installation using <c1>poetry python remove <version></> before continuing.", | ||
] | ||
|
||
|
||
class PythonInstallCommand(Command): | ||
name = "python install" | ||
|
||
arguments: ClassVar[list[Argument]] = [ | ||
argument("python", "The python version to install.") | ||
] | ||
|
||
options: ClassVar[list[Option]] = [ | ||
option("clean", "c", "Cleanup installation if check fails.", flag=True), | ||
option( | ||
"free-threaded", "f", "Use free-threaded version if available.", flag=True | ||
), | ||
option( | ||
"implementation", | ||
"i", | ||
"Python implementation to use. (cpython, pypy)", | ||
flag=False, | ||
default="cpython", | ||
), | ||
option( | ||
"reinstall", "r", "Reinstall if installation already exists.", flag=True | ||
), | ||
] | ||
|
||
description = "Install the specified Python version from the Python Standalone Builds project." | ||
|
||
def handle(self) -> int: | ||
request = self.argument("python") | ||
impl = self.option("implementation").lower() | ||
reinstall = self.option("reinstall") | ||
free_threaded = self.option("free-threaded") | ||
|
||
try: | ||
version = Version.parse(request) | ||
except (ValueError, InvalidVersionError): | ||
self.io.write_error_line( | ||
f"<error>Invalid Python version requested <b>{request}</></error>" | ||
) | ||
return 1 | ||
|
||
if free_threaded and version < Version.parse("3.13.0"): | ||
self.io.write_error_line("") | ||
self.io.write_error_line( | ||
"Free threading is not supported for Python versions prior to <c1>3.13.0</>.\n\n" | ||
"See https://docs.python.org/3/howto/free-threading-python.html for more information." | ||
) | ||
self.io.write_error_line("") | ||
return 1 | ||
|
||
try: | ||
pyver, _ = pbi.get_download_link( | ||
request, implementation=impl, free_threaded=free_threaded | ||
) | ||
except ValueError: | ||
self.io.write_error_line( | ||
"No suitable standalone build found for the requested Python version." | ||
) | ||
return 1 | ||
|
||
version = Version.from_parts( | ||
major=pyver.major, minor=pyver.minor, patch=pyver.micro | ||
) | ||
|
||
provider: PoetryPythonPathProvider = cast( | ||
PoetryPythonPathProvider, PoetryPythonPathProvider.create() | ||
) | ||
bad_executables = set() | ||
|
||
for python in provider.find_pythons(): | ||
try: | ||
if python.implementation.lower() != impl: | ||
continue | ||
|
||
if version == Version.parse(str(python.version)): | ||
if reinstall: | ||
break | ||
self.io.write_line( | ||
"Python version already installed at " | ||
f"<b>{PoetryPythonPathProvider.installation_dir(version, impl)}</>.\n" | ||
) | ||
self.io.write_line( | ||
f"Use <c1>--reinstall</> to install anyway, " | ||
f"or use <c1>poetry python remove {version}</> first." | ||
) | ||
return 1 | ||
except CalledProcessError: | ||
bad_executables.add(python.executable) | ||
|
||
if bad_executables: | ||
raise PoetryRuntimeError( | ||
reason="One or more installed version do not work on your system. This is not a Poetry issue.", | ||
messages=[ | ||
ConsoleMessage("\n".join(e.as_posix() for e in bad_executables)) | ||
.indent(" - ") | ||
.make_section("Failing Executables") | ||
.wrap("info"), | ||
*[ | ||
ConsoleMessage(m).wrap("warning") | ||
for m in BAD_PYTHON_INSTALL_INFO | ||
], | ||
], | ||
) | ||
|
||
request_title = f"<c1>{request}</> (<b>{impl}</>)" | ||
|
||
try: | ||
self.io.write(f"Downloading and installing {request_title} ... ") | ||
# this can be broken into download, and install_file if required to make | ||
# use of Poetry's own mechanics for download and unpack | ||
pbi.install( | ||
request, | ||
Config().python_installation_dir, | ||
True, | ||
implementation=impl, | ||
free_threaded=free_threaded, | ||
) | ||
except ValueError: | ||
self.io.write("<fg=red>Failed</>\n") | ||
self.io.write_error_line("") | ||
self.io.write_error_line( | ||
"No suitable standalone build found for the requested Python version." | ||
) | ||
self.io.write_error_line("") | ||
return 1 | ||
|
||
self.io.write("<fg=green>Done</>\n") | ||
|
||
self.io.write(f"Testing {request_title} ... ") | ||
|
||
provider = PoetryPythonPathProvider( | ||
PoetryPythonPathProvider.installation_bin_paths(version, impl) | ||
) | ||
|
||
for python in provider.find_pythons(): | ||
try: | ||
# this forces a python -c command internally in pbs-installer | ||
_ = python.version | ||
except CalledProcessError as e: | ||
self.io.write("<fg=red>Failed</>\n") | ||
|
||
installation_dir = PoetryPythonPathProvider.installation_dir( | ||
version, impl | ||
) | ||
if installation_dir.exists() and self.option("clean"): | ||
PythonRemoveCommand.remove_python_installation( | ||
request, impl, self.io | ||
) | ||
|
||
raise PoetryRuntimeError.create( | ||
reason="The installed version did not work on your system. This is not a Poetry issue.", | ||
exception=e, | ||
info=[ | ||
ConsoleMessage(f"{m}\n").wrap("info").text | ||
for m in BAD_PYTHON_INSTALL_INFO | ||
], | ||
) | ||
|
||
self.io.write("<fg=green>Done</>\n") | ||
|
||
return 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING | ||
from typing import ClassVar | ||
from typing import NamedTuple | ||
|
||
from cleo.helpers import argument | ||
from cleo.helpers import option | ||
from pbs_installer._install import THIS_ARCH | ||
from pbs_installer._install import THIS_PLATFORM | ||
from pbs_installer._versions import PYTHON_VERSIONS | ||
|
||
from poetry.config.config import Config | ||
from poetry.console.commands.command import Command | ||
from poetry.utils.env.python_manager import Python | ||
|
||
|
||
if TYPE_CHECKING: | ||
from pathlib import Path | ||
|
||
from cleo.io.inputs.argument import Argument | ||
from cleo.io.inputs.option import Option | ||
|
||
|
||
class PythonInfo(NamedTuple): | ||
major: int | ||
minor: int | ||
patch: int | ||
implementation: str | ||
executable: Path | None | ||
|
||
|
||
class PythonListCommand(Command): | ||
name = "python list" | ||
|
||
arguments: ClassVar[list[Argument]] = [ | ||
argument("version", "Python version to search for.", optional=True) | ||
] | ||
|
||
options: ClassVar[list[Option]] = [ | ||
option( | ||
"all", | ||
"a", | ||
"List all versions, including those available for download.", | ||
flag=True, | ||
), | ||
option( | ||
"implementation", "i", "Python implementation to search for.", flag=False | ||
), | ||
option("managed", "m", "List only Poetry managed Python versions.", flag=True), | ||
] | ||
|
||
description = "Shows Python versions available for this environment." | ||
|
||
def handle(self) -> int: | ||
rows: list[PythonInfo] = [] | ||
|
||
for pv in Python.find_all(): | ||
rows.append( | ||
PythonInfo( | ||
major=pv.major, | ||
minor=pv.minor, | ||
patch=pv.patch, | ||
implementation=pv.implementation.lower(), | ||
executable=pv.executable, | ||
) | ||
) | ||
|
||
if self.option("all"): | ||
for pv in PYTHON_VERSIONS: | ||
for _ in { | ||
k[1] | ||
for k in PYTHON_VERSIONS[pv] | ||
if (k[0], k[1]) == (THIS_PLATFORM, THIS_ARCH) | ||
}: | ||
rows.append( | ||
PythonInfo( | ||
major=pv.major, | ||
minor=pv.minor, | ||
patch=pv.micro, | ||
implementation=pv.implementation.lower(), | ||
executable=None, | ||
) | ||
) | ||
|
||
rows.sort( | ||
key=lambda x: (x.major, x.minor, x.patch, x.implementation), reverse=True | ||
) | ||
|
||
table = self.table(style="compact") | ||
table.set_headers( | ||
[ | ||
"<fg=magenta;options=bold>Version</>", | ||
"<fg=magenta;options=bold>Implementation</>", | ||
"<fg=magenta;options=bold>Manager</>", | ||
"<fg=magenta;options=bold>Path</>", | ||
] | ||
) | ||
|
||
implementations = {"cpython": "CPython", "pypy": "PyPy"} | ||
python_installation_path = Config().python_installation_dir | ||
|
||
row_count = 0 | ||
|
||
for pv in rows: | ||
version = f"{pv.major}.{pv.minor}.{pv.patch}" | ||
implementation = implementations.get( | ||
pv.implementation.lower(), pv.implementation | ||
) | ||
is_poetry_managed = ( | ||
pv.executable is None | ||
or pv.executable.resolve().is_relative_to(python_installation_path) | ||
) | ||
|
||
if self.option("managed") and not is_poetry_managed: | ||
continue | ||
|
||
manager = ( | ||
"<fg=blue>Poetry</>" if is_poetry_managed else "<fg=yellow>System</>" | ||
) | ||
path = ( | ||
f"<fg=green>{pv.executable.as_posix()}</>" | ||
if pv.executable | ||
else "Downloadable." | ||
) | ||
|
||
table.add_row( | ||
[ | ||
f"<c1>{version}</>", | ||
f"<b>{implementation}</>", | ||
f"{manager}", | ||
f"{path}", | ||
] | ||
) | ||
row_count += 1 | ||
|
||
if row_count > 0: | ||
table.render() | ||
else: | ||
self.io.write_line("No Python installations found.") | ||
|
||
return 0 |
Oops, something went wrong.