From f5d3b21e0a8ffd05f663ce4d4a27c7f5510fd24c Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:46:18 -0400 Subject: [PATCH 01/20] Windows: use the py launcher to select version --- docs/examples.md | 4 ++-- src/pipx/commands/install.py | 6 ++++++ src/pipx/commands/run.py | 8 ++++++-- src/pipx/interpreter.py | 13 +++++++++---- src/pipx/main.py | 19 +++++++++++++------ tests/test_install.py | 9 +++++++++ tests/test_interpreter.py | 35 ++++++++++++++++++++++++++--------- tests/test_run.py | 20 ++++++++++++++++++++ 8 files changed, 91 insertions(+), 23 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index cae08fbee6..61bed698e8 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -2,8 +2,8 @@ ``` pipx install pycowsay -pipx install --python python3.7 pycowsay -pipx install --python python3.8 pycowsay +pipx install --python python3.10 pycowsay +pipx install --python 3.11 pycowsay pipx install git+https://github.com/psf/black pipx install git+https://github.com/psf/black.git@branch-name pipx install git+https://github.com/psf/black.git@git-hash diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index ddd230f47b..a3945cdccd 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -4,6 +4,7 @@ from pipx import constants from pipx.commands.common import package_name_from_spec, run_post_install_actions from pipx.constants import EXIT_CODE_INSTALL_VENV_EXISTS, EXIT_CODE_OK, ExitCode +from pipx.interpreter import find_py_launcher_python from pipx.util import pipx_wrap from pipx.venv import Venv, VenvContainer @@ -26,6 +27,11 @@ def install( # package_spec is anything pip-installable, including package_name, vcs spec, # zip file, or tar.gz file. + if constants.WINDOWS and python and not Path(python).is_file(): + py_launcher = find_py_launcher_python(python) + if py_launcher: + python = py_launcher + if package_name is None: package_name = package_name_from_spec( package_spec, python, pip_args=pip_args, verbose=verbose diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index b4d1b90ebd..f7616ac54b 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -14,6 +14,7 @@ from pipx.commands.common import package_name_from_spec from pipx.constants import TEMP_VENV_EXPIRATION_THRESHOLD_DAYS, WINDOWS from pipx.emojis import hazard +from pipx.interpreter import find_py_launcher_python from pipx.util import ( PipxError, exec_app, @@ -106,7 +107,6 @@ def run_package( verbose: bool, use_cache: bool, ) -> NoReturn: - if which(app): logger.warning( pipx_wrap( @@ -189,6 +189,11 @@ def run( # we can't parse this as a package package_name = app + if constants.WINDOWS and python and not Path(python).is_file(): + py_launcher = find_py_launcher_python(python) + if py_launcher: + python = py_launcher + if spec is not None: content = None else: @@ -325,7 +330,6 @@ def _http_get_request(url: str) -> str: def _get_requirements_from_script(content: str) -> Optional[List[str]]: - # An iterator over the lines in the script. We will # read through this in sections, so it needs to be an # iterator, not just a list. diff --git a/src/pipx/interpreter.py b/src/pipx/interpreter.py index 331caac3ee..8d13fdf9d4 100644 --- a/src/pipx/interpreter.py +++ b/src/pipx/interpreter.py @@ -2,6 +2,7 @@ import shutil import subprocess import sys +from typing import Optional from pipx.constants import WINDOWS from pipx.util import PipxError @@ -26,14 +27,18 @@ def has_venv() -> bool: # so we try to locate the system Python and use that instead. +def find_py_launcher_python(python_version: Optional[str] = None) -> Optional[str]: + py = shutil.which("py") + if py and python_version: + os.environ["PY_PYTHON"] = python_version + return py + + def _find_default_windows_python() -> str: if has_venv(): return sys.executable + python = find_py_launcher_python() or shutil.which("python") - py = shutil.which("py") - if py: - return py - python = shutil.which("python") if python is None: raise PipxError("No suitable Python found") diff --git a/src/pipx/main.py b/src/pipx/main.py index 59f6222288..21048a41e6 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -342,7 +342,8 @@ def _add_install(subparsers: argparse._SubParsersAction) -> None: default=DEFAULT_PYTHON, help=( "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. Must be v3.6+." + "associated app/apps. The Python version for the Python Launcher for Windows. " + "Must be v3.6+." ), ) add_pip_venv_args(p) @@ -484,8 +485,9 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter) -> None: "--python", default=DEFAULT_PYTHON, help=( - "The Python executable used to recreate the Virtual Environment " - "and run the associated app/apps. Must be v3.6+." + "The Python executable used to create the Virtual Environment and run the " + "associated app/apps. The Python version for the Python Launcher for Windows. " + "Must be v3.6+." ), ) p.add_argument("--verbose", action="store_true") @@ -512,8 +514,9 @@ def _add_reinstall_all(subparsers: argparse._SubParsersAction) -> None: "--python", default=DEFAULT_PYTHON, help=( - "The Python executable used to recreate the Virtual Environment " - "and run the associated app/apps. Must be v3.6+." + "The Python executable used to create the Virtual Environment and run the " + "associated app/apps. The Python version for the Python Launcher for Windows. " + "Must be v3.6+." ), ) p.add_argument("--skip", nargs="+", default=[], help="skip these packages") @@ -588,7 +591,11 @@ def _add_run(subparsers: argparse._SubParsersAction) -> None: p.add_argument( "--python", default=DEFAULT_PYTHON, - help="The Python version to run package's CLI app with. Must be v3.6+.", + help=( + "The Python executable used to create the Virtual Environment and run the " + "associated app/apps. The Python version for the Python Launcher for Windows. " + "Must be v3.6+." + ), ) add_pip_venv_args(p) p.set_defaults(subparser=p) diff --git a/tests/test_install.py b/tests/test_install.py index a135b86101..a208db4384 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -262,3 +262,12 @@ def test_install_local_archive(pipx_temp_env, monkeypatch, capsys): assert not run_pipx_cli(["install", "repeatme-0.1-py3-none-any.whl"]) captured = capsys.readouterr() assert f"- {app_name('repeatme')}\n" in captured.out + + +@pytest.mark.skipif( + not sys.platform.startswith("win"), reason="uses windows version format" +) +def test_install_with_python_windows(capsys, pipx_temp_env): + run_pipx_cli(["install", "pycowsay", "--python", "3.11"]) + captured = capsys.readouterr() + assert "Python 3.11" in captured.out diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py index 167c6c3b06..b47016ede3 100644 --- a/tests/test_interpreter.py +++ b/tests/test_interpreter.py @@ -1,3 +1,4 @@ +import os import shutil import subprocess import sys @@ -8,26 +9,42 @@ from pipx.interpreter import ( _find_default_windows_python, _get_absolute_python_interpreter, + find_py_launcher_python, ) from pipx.util import PipxError -def test_windows_python_venv_present(monkeypatch): +def py_which(name): + if name == "py": + return "py" + + +def test_windows_python_with_version_no_venv(monkeypatch): + monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) + monkeypatch.setattr(shutil, "which", py_which) + assert find_py_launcher_python("3.9") == "py" + assert os.environ.get("PY_PYTHON") == "3.9" + + +def test_windows_python_with_version_with_venv(monkeypatch): monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: True) - assert _find_default_windows_python() == sys.executable + monkeypatch.setattr(shutil, "which", py_which) + assert find_py_launcher_python("3.9") == "py" + assert os.environ.get("PY_PYTHON") == "3.9" -def test_windows_python_no_venv_py_present(monkeypatch): - def which(name): - if name == "py": - return "py" +def test_windows_python_no_version_with_venv(monkeypatch): + monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: True) + assert _find_default_windows_python() == sys.executable + +def test_windows_python_no_version_no_venv_with_py(monkeypatch): monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) - monkeypatch.setattr(shutil, "which", which) + monkeypatch.setattr(shutil, "which", py_which) assert _find_default_windows_python() == "py" -def test_windows_python_no_venv_python_present(monkeypatch): +def test_windows_python_no_version_no_venv_python_present(monkeypatch): def which(name): if name == "python": return "python" @@ -38,7 +55,7 @@ def which(name): assert _find_default_windows_python() == "python" -def test_windows_python_no_venv_no_python(monkeypatch): +def test_windows_python_no_version_no_venv_no_python(monkeypatch): def which(name): return None diff --git a/tests/test_run.py b/tests/test_run.py index 6d5e4e1471..5c2126289d 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -316,3 +316,23 @@ def test_run_script_by_relative_name(caplog, pipx_temp_env, monkeypatch, tmp_pat m.chdir(tmp_path) run_pipx_cli_exit(["run", "test.py"]) assert out.read_text() == test_str + + +@pytest.mark.skipif( + not sys.platform.startswith("win"), reason="uses windows version format" +) +@mock.patch("os.execvpe", new=execvpe_mock) +def test_run_with_windows_python_version(caplog, pipx_temp_env, tmp_path): + script = tmp_path / "test.py" + out = tmp_path / "output.txt" + script.write_text( + textwrap.dedent( + f""" + import sys + from pathlib import Path + Path({repr(str(out))}).write_text(sys.version) + """ + ).strip() + ) + run_pipx_cli_exit(["run", script.as_uri(), "--python", "3.11"]) + assert "3.11" in out.read_text() From 9bab10cdb803f6311af2da853d489fe132488c7d Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:50:35 -0400 Subject: [PATCH 02/20] Refactor string concatenation to f-string --- tests/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_install.py b/tests/test_install.py index a208db4384..168b12637c 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -144,7 +144,7 @@ def test_extra(pipx_temp_env, capsys): def test_install_local_extra(pipx_temp_env, capsys): assert not run_pipx_cli( - ["install", TEST_DATA_PATH + "/local_extras[cow]", "--include-deps"] + ["install", f"{TEST_DATA_PATH}/local_extras[cow]", "--include-deps"] ) captured = capsys.readouterr() assert f"- {app_name('pycowsay')}\n" in captured.out From c8b7fb6e3ca01ef942b368c46a096321ee2a8b5f Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:53:37 -0400 Subject: [PATCH 03/20] Refactor simplify m.group(1) to m[1] --- tests/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_install.py b/tests/test_install.py index 168b12637c..f6db593ea1 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -250,7 +250,7 @@ def test_install_pip_failure(pipx_temp_env, capsys): r"Full pip output in file:\s+(\S.+)$", captured.err, re.MULTILINE ) assert pip_log_file_match - assert Path(pip_log_file_match.group(1)).exists() + assert Path(pip_log_file_match[1]).exists() assert re.search(r"pip (failed|seemed to fail) to build package", captured.err) From fe25d1f735cc8a1c22c10a12e889af6e5884a5cf Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:54:12 -0400 Subject: [PATCH 04/20] Refactor remove unnecessary cast to str --- src/pipx/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index f7616ac54b..abe0e30262 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -75,7 +75,7 @@ def run_script( ) -> NoReturn: requirements = _get_requirements_from_script(content) if requirements is None: - exec_app([str(python), "-c", content, *app_args]) + exec_app([python, "-c", content, *app_args]) else: # Note that the environment name is based on the identified # requirements, and *not* on the script name. This is deliberate, as From 3493dc23a9a3b5342d6dbb4e066a370c9c4ad615 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:55:00 -0400 Subject: [PATCH 05/20] Refactor to use if-expression --- src/pipx/commands/run.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index abe0e30262..c549a1a6a2 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -194,11 +194,7 @@ def run( if py_launcher: python = py_launcher - if spec is not None: - content = None - else: - content = maybe_script_content(app, is_path) - + content = None if spec is not None else maybe_script_content(app, is_path) if content is not None: run_script(content, app_args, python, pip_args, venv_args, verbose, use_cache) else: From 5b8ce5b1e20112e7ecec39b712c8213c7f03e139 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:55:49 -0400 Subject: [PATCH 06/20] Refactor move assignment closer to usage --- src/pipx/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index c549a1a6a2..171aab5fb7 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -180,7 +180,6 @@ def run( package """ - package_or_url = spec if spec is not None else app # For any package, we need to just use the name try: package_name = Requirement(app).name @@ -198,6 +197,7 @@ def run( if content is not None: run_script(content, app_args, python, pip_args, venv_args, verbose, use_cache) else: + package_or_url = spec if spec is not None else app run_package( package_name, package_or_url, From 10496c5330d7b56b2d957aff19b0f382b602e599 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:56:30 -0400 Subject: [PATCH 07/20] Refactor explicitly raise from last error --- src/pipx/commands/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index 171aab5fb7..e50d5c5274 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -322,7 +322,7 @@ def _http_get_request(url: str) -> str: return res.read().decode(charset) except Exception as e: logger.debug("Uncaught Exception:", exc_info=True) - raise PipxError(str(e)) + raise PipxError(str(e)) from e def _get_requirements_from_script(content: str) -> Optional[List[str]]: @@ -356,7 +356,7 @@ def _get_requirements_from_script(content: str) -> Optional[List[str]]: try: req = Requirement(line_content) except InvalidRequirement as e: - raise PipxError(f"Invalid requirement {line_content}: {str(e)}") + raise PipxError(f"Invalid requirement {line_content}: {str(e)}") from e # Use the normalised form of the requirement, # not the original line. From d36e22a030331805aaa29b81b1c3b74a5ae3b94f Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 21:58:24 -0400 Subject: [PATCH 08/20] Refactor remove redundant slice index --- src/pipx/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index e50d5c5274..56458e1410 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -288,7 +288,7 @@ def _get_temporary_venv_path( m.update(python.encode()) m.update("".join(pip_args).encode()) m.update("".join(venv_args).encode()) - venv_folder_name = m.hexdigest()[0:15] # 15 chosen arbitrarily + venv_folder_name = m.hexdigest()[:15] # 15 chosen arbitrarily return Path(constants.PIPX_VENV_CACHEDIR) / venv_folder_name From f15c373d727354e2d04f52452c8f098394815c25 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 11 Jun 2023 22:00:33 -0400 Subject: [PATCH 09/20] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69d3ca4ce7..bfbc1ecb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## dev +- Windows: use the py launcher to select Python version with `--python` option - Support including requirements in scripts run using `pipx run` (#916) - Pass `pip_args` to `shared_libs.upgrade()` - Fallback to user's log path if the default log path (`$PIPX_HOME/logs`) is not writable to aid with pipx being used for multi-user (e.g. system-wide) installs of applications From 94abad14290f9865bea1e3ea35655a66e3a570ee Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Mon, 12 Jun 2023 07:19:41 -0400 Subject: [PATCH 10/20] Fix py launcher not overriding venv version --- src/pipx/interpreter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pipx/interpreter.py b/src/pipx/interpreter.py index 8d13fdf9d4..af1da14c8d 100644 --- a/src/pipx/interpreter.py +++ b/src/pipx/interpreter.py @@ -30,7 +30,11 @@ def has_venv() -> bool: def find_py_launcher_python(python_version: Optional[str] = None) -> Optional[str]: py = shutil.which("py") if py and python_version: - os.environ["PY_PYTHON"] = python_version + py = subprocess.run( + ["py", f"-{python_version}", "-c", "import sys; print(sys.executable)"], + capture_output=True, + text=True, + ).stdout.strip() return py From 1dceb7a2cdac0514613f9631f3311eae1f510e10 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 18 Jun 2023 13:16:24 -0400 Subject: [PATCH 11/20] Parameterize interpreter test --- tests/test_interpreter.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py index b47016ede3..961693f9f3 100644 --- a/tests/test_interpreter.py +++ b/tests/test_interpreter.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess import sys @@ -14,23 +13,18 @@ from pipx.util import PipxError -def py_which(name): - if name == "py": +@pytest.mark.parametrize("venv", [True, False]) +def test_windows_python_with_version(monkeypatch, venv): + def which(name): return "py" - -def test_windows_python_with_version_no_venv(monkeypatch): - monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) - monkeypatch.setattr(shutil, "which", py_which) - assert find_py_launcher_python("3.9") == "py" - assert os.environ.get("PY_PYTHON") == "3.9" - - -def test_windows_python_with_version_with_venv(monkeypatch): - monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: True) - monkeypatch.setattr(shutil, "which", py_which) - assert find_py_launcher_python("3.9") == "py" - assert os.environ.get("PY_PYTHON") == "3.9" + major = sys.version_info.major + minor = sys.version_info.minor + monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: venv) + monkeypatch.setattr(shutil, "which", which) + assert find_py_launcher_python(f"{major}.{minor}").endswith( + f"Python{major}{minor}\\python.exe" + ) def test_windows_python_no_version_with_venv(monkeypatch): @@ -39,8 +33,11 @@ def test_windows_python_no_version_with_venv(monkeypatch): def test_windows_python_no_version_no_venv_with_py(monkeypatch): + def which(name): + return "py" + monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) - monkeypatch.setattr(shutil, "which", py_which) + monkeypatch.setattr(shutil, "which", which) assert _find_default_windows_python() == "py" From a497ed7b719088357354cbd937bf658cc3915f67 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 18 Jun 2023 13:34:16 -0400 Subject: [PATCH 12/20] Move py launch finder to main module --- src/pipx/commands/install.py | 6 ------ src/pipx/main.py | 6 +++++- tests/test_install.py | 9 --------- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index a3945cdccd..ddd230f47b 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -4,7 +4,6 @@ from pipx import constants from pipx.commands.common import package_name_from_spec, run_post_install_actions from pipx.constants import EXIT_CODE_INSTALL_VENV_EXISTS, EXIT_CODE_OK, ExitCode -from pipx.interpreter import find_py_launcher_python from pipx.util import pipx_wrap from pipx.venv import Venv, VenvContainer @@ -27,11 +26,6 @@ def install( # package_spec is anything pip-installable, including package_name, vcs spec, # zip file, or tar.gz file. - if constants.WINDOWS and python and not Path(python).is_file(): - py_launcher = find_py_launcher_python(python) - if py_launcher: - python = py_launcher - if package_name is None: package_name = package_name_from_spec( package_spec, python, pip_args=pip_args, verbose=verbose diff --git a/src/pipx/main.py b/src/pipx/main.py index 21048a41e6..c52ce0c911 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -25,7 +25,7 @@ from pipx.colors import bold, green from pipx.constants import WINDOWS, ExitCode from pipx.emojis import hazard -from pipx.interpreter import DEFAULT_PYTHON +from pipx.interpreter import DEFAULT_PYTHON, find_py_launcher_python from pipx.util import PipxError, mkdir, pipx_wrap, rmdir from pipx.venv import VenvContainer from pipx.version import __version__ @@ -181,6 +181,10 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 logger.info(f"Virtual Environment location is {venv_dir}") if "skip" in args: skip_list = [canonicalize_name(x) for x in args.skip] + if "python" in args and WINDOWS and not Path(args.python).is_file(): + py_launcher = find_py_launcher_python(args.python) + if py_launcher: + args.python = py_launcher if args.command == "run": commands.run( diff --git a/tests/test_install.py b/tests/test_install.py index f6db593ea1..6e8a3566dd 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -262,12 +262,3 @@ def test_install_local_archive(pipx_temp_env, monkeypatch, capsys): assert not run_pipx_cli(["install", "repeatme-0.1-py3-none-any.whl"]) captured = capsys.readouterr() assert f"- {app_name('repeatme')}\n" in captured.out - - -@pytest.mark.skipif( - not sys.platform.startswith("win"), reason="uses windows version format" -) -def test_install_with_python_windows(capsys, pipx_temp_env): - run_pipx_cli(["install", "pycowsay", "--python", "3.11"]) - captured = capsys.readouterr() - assert "Python 3.11" in captured.out From 848c48b5bcb060be68185fa01d199199fb530b0a Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 18 Jun 2023 15:59:40 -0400 Subject: [PATCH 13/20] Skip test that requires Windows --- tests/test_interpreter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py index 961693f9f3..234af15096 100644 --- a/tests/test_interpreter.py +++ b/tests/test_interpreter.py @@ -13,6 +13,7 @@ from pipx.util import PipxError +@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Looks for Python.exe") @pytest.mark.parametrize("venv", [True, False]) def test_windows_python_with_version(monkeypatch, venv): def which(name): From fcbecfe0834047c86e8ce623038202266278d0e0 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 18 Jun 2023 16:01:19 -0400 Subject: [PATCH 14/20] Use the found py launcher executable --- src/pipx/interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/interpreter.py b/src/pipx/interpreter.py index af1da14c8d..cab29e8ee0 100644 --- a/src/pipx/interpreter.py +++ b/src/pipx/interpreter.py @@ -31,7 +31,7 @@ def find_py_launcher_python(python_version: Optional[str] = None) -> Optional[st py = shutil.which("py") if py and python_version: py = subprocess.run( - ["py", f"-{python_version}", "-c", "import sys; print(sys.executable)"], + [py, f"-{python_version}", "-c", "import sys; print(sys.executable)"], capture_output=True, text=True, ).stdout.strip() From 53bfe443665c63b75f828b15421f27373b53d528 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 18 Jun 2023 17:27:34 -0400 Subject: [PATCH 15/20] Fix Python path in Windows has other forms --- tests/test_interpreter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py index 234af15096..1c2f3c92de 100644 --- a/tests/test_interpreter.py +++ b/tests/test_interpreter.py @@ -23,9 +23,9 @@ def which(name): minor = sys.version_info.minor monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: venv) monkeypatch.setattr(shutil, "which", which) - assert find_py_launcher_python(f"{major}.{minor}").endswith( - f"Python{major}{minor}\\python.exe" - ) + python_path = find_py_launcher_python(f"{major}.{minor}") + assert f"{major}.{minor}" in python_path or f"{major}{minor}" in python_path + assert python_path.endswith("python.exe") def test_windows_python_no_version_with_venv(monkeypatch): From 478f4967ad5d40e6c18b2e4ddbd45d27e25bdad7 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Tue, 20 Jun 2023 22:04:21 -0400 Subject: [PATCH 16/20] Remove Windows dependency for py launcher --- CHANGELOG.md | 2 +- src/pipx/commands/run.py | 2 +- src/pipx/main.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbc1ecb79..1441433270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## dev -- Windows: use the py launcher to select Python version with `--python` option +- Use the py launcher, if available, to select Python version with the `--python` option - Support including requirements in scripts run using `pipx run` (#916) - Pass `pip_args` to `shared_libs.upgrade()` - Fallback to user's log path if the default log path (`$PIPX_HOME/logs`) is not writable to aid with pipx being used for multi-user (e.g. system-wide) installs of applications diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index 56458e1410..f1cc9a922c 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -188,7 +188,7 @@ def run( # we can't parse this as a package package_name = app - if constants.WINDOWS and python and not Path(python).is_file(): + if python and not Path(python).is_file(): py_launcher = find_py_launcher_python(python) if py_launcher: python = py_launcher diff --git a/src/pipx/main.py b/src/pipx/main.py index c52ce0c911..4f47a89b7c 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -346,7 +346,7 @@ def _add_install(subparsers: argparse._SubParsersAction) -> None: default=DEFAULT_PYTHON, help=( "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher for Windows. " + "associated app/apps. The Python version for the Python Launcher. " "Must be v3.6+." ), ) @@ -490,7 +490,7 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter) -> None: default=DEFAULT_PYTHON, help=( "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher for Windows. " + "associated app/apps. The Python version for the Python Launcher. " "Must be v3.6+." ), ) @@ -519,7 +519,7 @@ def _add_reinstall_all(subparsers: argparse._SubParsersAction) -> None: default=DEFAULT_PYTHON, help=( "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher for Windows. " + "associated app/apps. The Python version for the Python Launcher. " "Must be v3.6+." ), ) @@ -597,7 +597,7 @@ def _add_run(subparsers: argparse._SubParsersAction) -> None: default=DEFAULT_PYTHON, help=( "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher for Windows. " + "associated app/apps. The Python version for the Python Launcher. " "Must be v3.6+." ), ) From fd967dfa26464c1cab681d5481b460c1b0af2ba4 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Wed, 21 Jun 2023 21:09:27 -0400 Subject: [PATCH 17/20] Make variable names more clear --- src/pipx/commands/run.py | 6 +++--- src/pipx/main.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index f1cc9a922c..e7448871d6 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -189,9 +189,9 @@ def run( package_name = app if python and not Path(python).is_file(): - py_launcher = find_py_launcher_python(python) - if py_launcher: - python = py_launcher + py_launcher_python = find_py_launcher_python(python) + if py_launcher_python: + python = py_launcher_python content = None if spec is not None else maybe_script_content(app, is_path) if content is not None: diff --git a/src/pipx/main.py b/src/pipx/main.py index 4f47a89b7c..27f302a65a 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -182,9 +182,9 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 if "skip" in args: skip_list = [canonicalize_name(x) for x in args.skip] if "python" in args and WINDOWS and not Path(args.python).is_file(): - py_launcher = find_py_launcher_python(args.python) - if py_launcher: - args.python = py_launcher + py_launcher_python = find_py_launcher_python(args.python) + if py_launcher_python: + args.python = py_launcher_python if args.command == "run": commands.run( From 295f231252bed55e458fe992c31f11dfb1d9ede4 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Wed, 21 Jun 2023 21:24:06 -0400 Subject: [PATCH 18/20] Clarify --python help statements --- src/pipx/main.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 27f302a65a..11b00094a6 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -345,9 +345,8 @@ def _add_install(subparsers: argparse._SubParsersAction) -> None: "--python", default=DEFAULT_PYTHON, help=( - "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher. " - "Must be v3.6+." + "Python to install with. Possible values can be the executable name (python3.11), " + "the version to pass to py launcher (3.11), or the full path to the executable." ), ) add_pip_venv_args(p) @@ -489,9 +488,8 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter) -> None: "--python", default=DEFAULT_PYTHON, help=( - "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher. " - "Must be v3.6+." + "Python to reinstall with. Possible values can be the executable name (python3.11), " + "the version to pass to py launcher (3.11), or the full path to the executable." ), ) p.add_argument("--verbose", action="store_true") @@ -518,9 +516,8 @@ def _add_reinstall_all(subparsers: argparse._SubParsersAction) -> None: "--python", default=DEFAULT_PYTHON, help=( - "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher. " - "Must be v3.6+." + "Python to reinstall with. Possible values can be the executable name (python3.11), " + "the version to pass to py launcher (3.11), or the full path to the executable." ), ) p.add_argument("--skip", nargs="+", default=[], help="skip these packages") @@ -596,9 +593,8 @@ def _add_run(subparsers: argparse._SubParsersAction) -> None: "--python", default=DEFAULT_PYTHON, help=( - "The Python executable used to create the Virtual Environment and run the " - "associated app/apps. The Python version for the Python Launcher. " - "Must be v3.6+." + "Python to run with. Possible values can be the executable name (python3.11), " + "the version to pass to py launcher (3.11), or the full path to the executable." ), ) add_pip_venv_args(p) From 82849edb59cc77872aaf5d45488a2346d8c36116 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sat, 24 Jun 2023 11:00:27 -0400 Subject: [PATCH 19/20] Remove redundant find py launcher in run --- src/pipx/commands/run.py | 6 ------ src/pipx/main.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index e7448871d6..9cc1d8baf8 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -14,7 +14,6 @@ from pipx.commands.common import package_name_from_spec from pipx.constants import TEMP_VENV_EXPIRATION_THRESHOLD_DAYS, WINDOWS from pipx.emojis import hazard -from pipx.interpreter import find_py_launcher_python from pipx.util import ( PipxError, exec_app, @@ -188,11 +187,6 @@ def run( # we can't parse this as a package package_name = app - if python and not Path(python).is_file(): - py_launcher_python = find_py_launcher_python(python) - if py_launcher_python: - python = py_launcher_python - content = None if spec is not None else maybe_script_content(app, is_path) if content is not None: run_script(content, app_args, python, pip_args, venv_args, verbose, use_cache) diff --git a/src/pipx/main.py b/src/pipx/main.py index 11b00094a6..051b9979ed 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -181,7 +181,7 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 logger.info(f"Virtual Environment location is {venv_dir}") if "skip" in args: skip_list = [canonicalize_name(x) for x in args.skip] - if "python" in args and WINDOWS and not Path(args.python).is_file(): + if "python" in args and not Path(args.python).is_file(): py_launcher_python = find_py_launcher_python(args.python) if py_launcher_python: args.python = py_launcher_python From c22a19fc27a9c5b1ca3b3eaba15ab53f482833a2 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 25 Jun 2023 07:39:09 -0400 Subject: [PATCH 20/20] Restore minimum python verbiage, add constant --- src/pipx/constants.py | 1 + src/pipx/main.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pipx/constants.py b/src/pipx/constants.py index bdc87a7a43..f2efb6c8c4 100644 --- a/src/pipx/constants.py +++ b/src/pipx/constants.py @@ -19,6 +19,7 @@ LOCAL_BIN_DIR = Path(os.environ.get("PIPX_BIN_DIR", DEFAULT_PIPX_BIN_DIR)).resolve() PIPX_VENV_CACHEDIR = PIPX_HOME / ".cache" TEMP_VENV_EXPIRATION_THRESHOLD_DAYS = 14 +MINIMUM_PYTHON_VERSION = "3.8" ExitCode = NewType("ExitCode", int) # pipx shell exit codes diff --git a/src/pipx/main.py b/src/pipx/main.py index 051b9979ed..988f2d324e 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -23,7 +23,7 @@ from pipx import commands, constants from pipx.animate import hide_cursor, show_cursor from pipx.colors import bold, green -from pipx.constants import WINDOWS, ExitCode +from pipx.constants import MINIMUM_PYTHON_VERSION, WINDOWS, ExitCode from pipx.emojis import hazard from pipx.interpreter import DEFAULT_PYTHON, find_py_launcher_python from pipx.util import PipxError, mkdir, pipx_wrap, rmdir @@ -347,6 +347,7 @@ def _add_install(subparsers: argparse._SubParsersAction) -> None: help=( "Python to install with. Possible values can be the executable name (python3.11), " "the version to pass to py launcher (3.11), or the full path to the executable." + f"Requires Python {MINIMUM_PYTHON_VERSION} or above." ), ) add_pip_venv_args(p) @@ -490,6 +491,7 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter) -> None: help=( "Python to reinstall with. Possible values can be the executable name (python3.11), " "the version to pass to py launcher (3.11), or the full path to the executable." + f"Requires Python {MINIMUM_PYTHON_VERSION} or above." ), ) p.add_argument("--verbose", action="store_true") @@ -518,6 +520,7 @@ def _add_reinstall_all(subparsers: argparse._SubParsersAction) -> None: help=( "Python to reinstall with. Possible values can be the executable name (python3.11), " "the version to pass to py launcher (3.11), or the full path to the executable." + f"Requires Python {MINIMUM_PYTHON_VERSION} or above." ), ) p.add_argument("--skip", nargs="+", default=[], help="skip these packages") @@ -594,7 +597,8 @@ def _add_run(subparsers: argparse._SubParsersAction) -> None: default=DEFAULT_PYTHON, help=( "Python to run with. Possible values can be the executable name (python3.11), " - "the version to pass to py launcher (3.11), or the full path to the executable." + "the version to pass to py launcher (3.11), or the full path to the executable. " + f"Requires Python {MINIMUM_PYTHON_VERSION} or above." ), ) add_pip_venv_args(p)