Skip to content

Commit

Permalink
Merge branch 'main' into resolve_home_path_in_dir_env_vars
Browse files Browse the repository at this point in the history
  • Loading branch information
Gitznik authored Feb 11, 2024
2 parents 8a7f342 + 407b797 commit a1df342
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
hooks:
- id: pyproject-fmt
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.14
rev: v0.2.0
hooks:
- id: ruff-format
- id: ruff
Expand Down
1 change: 1 addition & 0 deletions changelog.d/1227.doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update the example for running scripts with dependencies.
3 changes: 3 additions & 0 deletions changelog.d/1242.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add a `--fetch-missing-python` flag to all commands that accept a `--python` flag.

When combined, this will automatically download a standalone copy of the requested python version if it's not already available on the user's system.
9 changes: 4 additions & 5 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,14 @@ pipx run --spec test-py test.py # Always a package on PyPI

You can also run scripts that have dependencies:

If you have a script `test.py` that needs a 3rd party library like requests:
If you have a script `test.py` that needs 3rd party libraries, you can add [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) in the style of PEP 723.

```
# test.py
# Requirements:
# requests
#
# The list of requirements is terminated by a blank line or an empty comment line.
# /// script
# dependencies = ["requests"]
# ///
import sys
import requests
Expand Down
3 changes: 3 additions & 0 deletions src/pipx/commands/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
PIPX_LOCAL_VENVS,
PIPX_LOG_DIR,
PIPX_SHARED_LIBS,
PIPX_STANDALONE_PYTHON_CACHEDIR,
PIPX_TRASH_DIR,
PIPX_VENV_CACHEDIR,
ExitCode,
Expand All @@ -25,6 +26,7 @@ def environment(value: str) -> ExitCode:
"PIPX_MAN_DIR",
"PIPX_SHARED_LIBS",
"PIPX_DEFAULT_PYTHON",
"PIPX_FETCH_MISSING_PYTHON",
"USE_EMOJI",
]
derived_values = {
Expand All @@ -36,6 +38,7 @@ def environment(value: str) -> ExitCode:
"PIPX_LOG_DIR": PIPX_LOG_DIR,
"PIPX_TRASH_DIR": PIPX_TRASH_DIR,
"PIPX_VENV_CACHEDIR": PIPX_VENV_CACHEDIR,
"PIPX_STANDALONE_PYTHON_CACHEDIR": PIPX_STANDALONE_PYTHON_CACHEDIR,
"PIPX_DEFAULT_PYTHON": DEFAULT_PYTHON,
"USE_EMOJI": str(EMOJI_SUPPORT).lower(),
}
Expand Down
3 changes: 3 additions & 0 deletions src/pipx/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ def load_dir_from_environ(dir_name: str, default: Path) -> Path:
if FALLBACK_PIPX_HOME.exists() or os.environ.get("PIPX_HOME") is not None:
PIPX_HOME = load_dir_from_environ("PIPX_HOME", FALLBACK_PIPX_HOME)
PIPX_LOCAL_VENVS = PIPX_HOME / "venvs"
PIPX_STANDALONE_PYTHON_CACHEDIR = PIPX_HOME / "py"
PIPX_LOG_DIR = PIPX_HOME / "logs"
DEFAULT_PIPX_SHARED_LIBS = PIPX_HOME / "shared"
PIPX_TRASH_DIR = PIPX_HOME / ".trash"
PIPX_VENV_CACHEDIR = PIPX_HOME / ".cache"
else:
PIPX_HOME = DEFAULT_PIPX_HOME
PIPX_LOCAL_VENVS = PIPX_HOME / "venvs"
PIPX_STANDALONE_PYTHON_CACHEDIR = PIPX_HOME / "py"
PIPX_LOG_DIR = user_log_path("pipx")
DEFAULT_PIPX_SHARED_LIBS = PIPX_HOME / "shared"
PIPX_TRASH_DIR = PIPX_HOME / "trash"
Expand All @@ -38,6 +40,7 @@ def load_dir_from_environ(dir_name: str, default: Path) -> Path:
PIPX_SHARED_PTH = "pipx_shared.pth"
LOCAL_BIN_DIR = load_dir_from_environ("PIPX_BIN_DIR", DEFAULT_PIPX_BIN_DIR)
LOCAL_MAN_DIR = load_dir_from_environ("PIPX_MAN_DIR", DEFAULT_PIPX_MAN_DIR)
FETCH_MISSING_PYTHON = os.environ.get("PIPX_FETCH_MISSING_PYTHON", False)
TEMP_VENV_EXPIRATION_THRESHOLD_DAYS = 14
MINIMUM_PYTHON_VERSION = "3.8"

Expand Down
15 changes: 13 additions & 2 deletions src/pipx/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from pathlib import Path
from typing import Optional

from pipx.constants import WINDOWS
from pipx.constants import FETCH_MISSING_PYTHON, WINDOWS
from pipx.standalone_python import download_python_build_standalone
from pipx.util import PipxError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -42,10 +43,12 @@ def __init__(self, source: str, version: str, wrap_message: bool = True):
message += (
"The provided version looks like a version for Python Launcher, " "but `py` was not found on PATH."
)
if source == "the python-build-standalone project":
message += "listed in https://github.com/indygreg/python-build-standalone/releases/latest."
super().__init__(message, wrap_message)


def find_python_interpreter(python_version: str) -> str:
def find_python_interpreter(python_version: str, fetch_missing_python: bool = False) -> str:
if Path(python_version).is_file():
return python_version

Expand All @@ -58,6 +61,14 @@ def find_python_interpreter(python_version: str) -> str:

if shutil.which(python_version):
return python_version

if fetch_missing_python or FETCH_MISSING_PYTHON:
try:
standalone_executable = download_python_build_standalone(python_version)
return standalone_executable
except PipxError as e:
raise InterpreterResolutionError(source="the python-build-standalone project", version=python_version) from e

raise InterpreterResolutionError(source="PATH", version=python_version)


Expand Down
110 changes: 58 additions & 52 deletions src/pipx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,18 @@
from pipx import commands, constants
from pipx.animate import hide_cursor, show_cursor
from pipx.colors import bold, green
from pipx.constants import EXIT_CODE_SPECIFIED_PYTHON_EXECUTABLE_NOT_FOUND, MINIMUM_PYTHON_VERSION, WINDOWS, ExitCode
from pipx.constants import (
EXIT_CODE_SPECIFIED_PYTHON_EXECUTABLE_NOT_FOUND,
MINIMUM_PYTHON_VERSION,
WINDOWS,
ExitCode,
)
from pipx.emojis import hazard
from pipx.interpreter import DEFAULT_PYTHON, InterpreterResolutionError, find_python_interpreter
from pipx.interpreter import (
DEFAULT_PYTHON,
InterpreterResolutionError,
find_python_interpreter,
)
from pipx.util import PipxError, mkdir, pipx_wrap, rmdir
from pipx.venv import VenvContainer
from pipx.version import version as __version__
Expand Down Expand Up @@ -188,8 +197,9 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901
skip_list = [canonicalize_name(x) for x in args.skip]

if "python" in args and args.python is not None:
fetch_missing_python = args.fetch_missing_python
try:
interpreter = find_python_interpreter(args.python)
interpreter = find_python_interpreter(args.python, fetch_missing_python=fetch_missing_python)
args.python = interpreter
except InterpreterResolutionError as e:
print(
Expand Down Expand Up @@ -271,7 +281,11 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901
)
elif args.command == "list":
return commands.list_packages(
venv_container, args.include_injected, args.json, args.short, args.skip_maintenance
venv_container,
args.include_injected,
args.json,
args.short,
args.skip_maintenance,
)
elif args.command == "uninstall":
return commands.uninstall(venv_dir, constants.LOCAL_BIN_DIR, constants.LOCAL_MAN_DIR, verbose)
Expand Down Expand Up @@ -336,6 +350,25 @@ def add_include_dependencies(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--include-deps", help="Include apps of dependent packages", action="store_true")


def add_python_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--python",
default=DEFAULT_PYTHON,
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."
),
)
parser.add_argument(
"--fetch-missing-python",
action="store_true",
help=(
"Whether to fetch a standalone python build from GitHub if the specified python version is not found locally on the system."
),
)


def _add_install(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None:
p = subparsers.add_parser(
"install",
Expand All @@ -360,15 +393,7 @@ def _add_install(subparsers: argparse._SubParsersAction, shared_parser: argparse
"NOTE: The suffix feature is experimental and subject to change."
),
)
p.add_argument(
"--python",
# Don't pass a default Python here so we know whether --python flag was passed
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_python_options(p)
p.add_argument(
"--preinstall",
action="append",
Expand Down Expand Up @@ -519,15 +544,7 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter, shared_parser: arg
parents=[shared_parser],
)
p.add_argument("package").completer = venv_completer
p.add_argument(
"--python",
default=DEFAULT_PYTHON,
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."
),
)
add_python_options(p)


def _add_reinstall_all(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None:
Expand All @@ -548,15 +565,7 @@ def _add_reinstall_all(subparsers: argparse._SubParsersAction, shared_parser: ar
),
parents=[shared_parser],
)
p.add_argument(
"--python",
default=DEFAULT_PYTHON,
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."
),
)
add_python_options(p)
p.add_argument("--skip", nargs="+", default=[], help="skip these packages")


Expand Down Expand Up @@ -622,15 +631,7 @@ def _add_run(subparsers: argparse._SubParsersAction, shared_parser: argparse.Arg
help="Require app to be run from local __pypackages__ directory",
)
p.add_argument("--spec", help=SPEC_HELP)
p.add_argument(
"--python",
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. "
f"Requires Python {MINIMUM_PYTHON_VERSION} or above."
),
)
add_python_options(p)
add_pip_venv_args(p)
p.set_defaults(subparser=p)

Expand Down Expand Up @@ -864,18 +865,23 @@ def setup(args: argparse.Namespace) -> None:
mkdir(constants.LOCAL_BIN_DIR)
mkdir(constants.LOCAL_MAN_DIR)
mkdir(constants.PIPX_VENV_CACHEDIR)

cachedir_tag = constants.PIPX_VENV_CACHEDIR / "CACHEDIR.TAG"
if not cachedir_tag.exists():
logger.debug("Adding CACHEDIR.TAG to cache directory")
signature = (
"Signature: 8a477f597d28d172789f06886806bc55\n"
"# This file is a cache directory tag created by pipx.\n"
"# For information about cache directory tags, see:\n"
"# https://bford.info/cachedir/\n"
)
with open(cachedir_tag, "w") as file:
file.write(signature)
mkdir(constants.PIPX_STANDALONE_PYTHON_CACHEDIR)

for cachedir in [
constants.PIPX_VENV_CACHEDIR,
constants.PIPX_STANDALONE_PYTHON_CACHEDIR,
]:
cachedir_tag = cachedir / "CACHEDIR.TAG"
if not cachedir_tag.exists():
logger.debug("Adding CACHEDIR.TAG to cache directory")
signature = (
"Signature: 8a477f597d28d172789f06886806bc55\n"
"# This file is a cache directory tag created by pipx.\n"
"# For information about cache directory tags, see:\n"
"# https://bford.info/cachedir/\n"
)
with open(cachedir_tag, "w") as file:
file.write(signature)

rmdir(constants.PIPX_TRASH_DIR, False)

Expand Down
Loading

0 comments on commit a1df342

Please sign in to comment.