Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support running with the Windows embeddable Python distribution #465

Merged
merged 3 commits into from
Sep 6, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dev
- [bugfix] Requiring userpath v1.4.1 or later so ensure Windows bug is fixed for `ensurepath` (#437)
- [feature] log pipx version (#423)
- [feature] `--suffix` option for `install` to allow multiple versions of same tool to be installed (#445)
- [feature] pipx can now be used with the Windows embeddable Python distribution

0.15.4.0

Expand Down
1 change: 0 additions & 1 deletion src/pipx/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from pathlib import Path
from textwrap import dedent

DEFAULT_PYTHON = sys.executable
DEFAULT_PIPX_HOME = Path.home() / ".local/pipx"
DEFAULT_PIPX_BIN_DIR = Path.home() / ".local/bin"
PIPX_HOME = Path(os.environ.get("PIPX_HOME", DEFAULT_PIPX_HOME)).resolve()
Expand Down
60 changes: 60 additions & 0 deletions src/pipx/interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import shutil
import subprocess
import sys

from pipx.constants import WINDOWS
from pipx.util import PipxError


def has_venv() -> bool:
try:
import venv # noqa

return True
except ImportError:
return False


# If we are running under the Windows embeddable distribution,
# venv isn't available (and we probably don't want to use the
# embeddable distribution as our applications' base Python anyway)
# so we try to locate the system Python and use that instead.

# This code was copied from https://github.com/uranusjr/pipx-standalone
# which uses this technique to build a completely standalone pipx
# distribution
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these comment blocks belong inside the function below instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was intentional to put the comments before the function rather than inside it (I find it more readable that way). But I can reword it a bit, I guess. Maybe swap the paragraphs, and say "The following code was copied from ... which uses the same technique..."?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the paragraphs seem more natural to me if swapped.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.



def _find_default_windows_python() -> str:

if has_venv():
return sys.executable

py = shutil.which("py")
if py:
return py
python = shutil.which("python")
if python is None:
raise PipxError("no suitable Python found")

if "WindowsApps" not in python:
return python
# Special treatment to detect Windows Store stub.
# https://twitter.com/zooba/status/1212454929379581952

proc = subprocess.run(
[python, "-V"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
)
if proc.returncode != 0:
# Cover the 9009 return code pre-emptively.
raise PipxError("no suitable Python found")
if not proc.stdout.strip():
# A real Python should print version, Windows Store stub won't.
raise PipxError("no suitable Python found")
return python # This executable seems to work.


if WINDOWS:
DEFAULT_PYTHON = _find_default_windows_python()
else:
DEFAULT_PYTHON = sys.executable
7 changes: 4 additions & 3 deletions src/pipx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from . import commands, constants
from .animate import hide_cursor, show_cursor
from .colors import bold, green
from .interpreter import DEFAULT_PYTHON
from .util import PipxError, mkdir
from .venv import VenvContainer
from .version import __version__
Expand Down Expand Up @@ -266,7 +267,7 @@ def _add_install(subparsers):
)
p.add_argument(
"--python",
default=constants.DEFAULT_PYTHON,
default=DEFAULT_PYTHON,
help=(
"The Python executable used to create the Virtual Environment and run the "
"associated app/apps. Must be v3.5+."
Expand Down Expand Up @@ -379,7 +380,7 @@ def _add_reinstall_all(subparsers):
)
p.add_argument(
"--python",
default=constants.DEFAULT_PYTHON,
default=DEFAULT_PYTHON,
help=(
"The Python executable used to recreate the Virtual Environment "
"and run the associated app/apps. Must be v3.5+."
Expand Down Expand Up @@ -448,7 +449,7 @@ def _add_run(subparsers):
p.add_argument("--verbose", action="store_true")
p.add_argument(
"--python",
default=constants.DEFAULT_PYTHON,
default=DEFAULT_PYTHON,
help="The Python version to run package's CLI app with. Must be v3.5+.",
)
add_pip_venv_args(p)
Expand Down
3 changes: 2 additions & 1 deletion src/pipx/shared_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import datetime

from pipx.animate import animate
from pipx.constants import DEFAULT_PYTHON, PIPX_SHARED_LIBS, WINDOWS
from pipx.constants import PIPX_SHARED_LIBS, WINDOWS
from pipx.interpreter import DEFAULT_PYTHON
from pipx.util import get_site_packages, get_venv_paths, run

SHARED_LIBS_MAX_AGE_SEC = datetime.timedelta(days=30).total_seconds()
Expand Down
3 changes: 2 additions & 1 deletion src/pipx/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from typing import Dict, Generator, List, NamedTuple, Set

from pipx.animate import animate
from pipx.constants import DEFAULT_PYTHON, PIPX_SHARED_PTH
from pipx.constants import PIPX_SHARED_PTH
from pipx.interpreter import DEFAULT_PYTHON
from pipx.package_specifier import (
parse_specifier_for_install,
parse_specifier_for_metadata,
Expand Down
81 changes: 81 additions & 0 deletions tests/test_interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import shutil
import subprocess
import sys
import pytest # type: ignore
import pipx.interpreter
from pipx.interpreter import _find_default_windows_python
from pipx.util import PipxError


def test_windows_python_venv_present(monkeypatch):
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: True)
assert _find_default_windows_python() == sys.executable


def test_windows_python_no_venv_py_present(monkeypatch):
def which(name):
if name == "py":
return "py"

monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)
assert _find_default_windows_python() == "py"


def test_windows_python_no_venv_python_present(monkeypatch):
def which(name):
if name == "python":
return "python"
# Note: returns False for "py"

monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)
assert _find_default_windows_python() == "python"


def test_windows_python_no_venv_no_python(monkeypatch):
def which(name):
return None

monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)
with pytest.raises(PipxError):
_find_default_windows_python()


# Test the checks for the store Python.
def test_windows_python_no_venv_store_python(monkeypatch):
def which(name):
if name == "python":
return "WindowsApps"

class dummy_runner:
def __init__(self, rc, out):
self.rc = rc
self.out = out

def __call__(self, *args, **kw):
class Ret:
pass

ret = Ret()
ret.returncode = self.rc
ret.stdout = self.out
return ret

monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)

# Store version stub gives return code 9009
monkeypatch.setattr(subprocess, "run", dummy_runner(9009, ""))
with pytest.raises(PipxError):
_find_default_windows_python()

# Even if it doesn't, it returns no output
monkeypatch.setattr(subprocess, "run", dummy_runner(0, ""))
with pytest.raises(PipxError):
_find_default_windows_python()

# If it *does* pass the tests, we use it as it's not the stub
monkeypatch.setattr(subprocess, "run", dummy_runner(0, "3.8"))
assert _find_default_windows_python() == "WindowsApps"