Skip to content

Commit

Permalink
Add PEX info to the PEX repl. (#2496)
Browse files Browse the repository at this point in the history
It should now be clear when you've dropped into a PEX repl (vs. a plain
Python repl). This may help stem some of the confusion brough about by
running `pex` with no arguments. In addition, you gain some
introspection capabilities via the `pex_info` REPL command.

Fixes #157
  • Loading branch information
jsirois authored Aug 8, 2024
1 parent ee5b1c9 commit e719bac
Show file tree
Hide file tree
Showing 48 changed files with 1,640 additions and 189 deletions.
4 changes: 2 additions & 2 deletions pex/bin/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentError, ArgumentParser
from textwrap import TextWrapper

from pex import dependency_configuration, pex_warnings, scie
from pex import dependency_configuration, pex_warnings, repl, scie
from pex.argparse import HandleBoolAction
from pex.commands.command import (
GlobalConfigurationError,
Expand Down Expand Up @@ -1375,7 +1375,7 @@ def do_main(
"Running PEX file at %s with args %s" % (pex_builder.path(), cmdline),
V=options.verbosity,
)
sys.exit(pex.run(args=list(cmdline), env=env))
sys.exit(pex.run(args=list(cmdline), env=repl.export_pex_cli_no_args_use(env=env)))


def seed_cache(
Expand Down
34 changes: 34 additions & 0 deletions pex/cli_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2024 Pex project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import

import os
import sys

from pex.compatibility import commonpath
from pex.typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Optional


def prog_path(prog=None):
# type: (Optional[str]) -> str
"""Generate the most concise path possible that is still runnable on the command line."""

exe_path = os.path.abspath(prog or sys.argv[0])
cwd = os.path.abspath(os.getcwd())
if commonpath((exe_path, cwd)) == cwd:
exe_path = os.path.relpath(exe_path, cwd)
# Handle users that do not have . as a PATH entry.
if not os.path.dirname(exe_path) and os.curdir not in os.environ.get("PATH", "").split(
os.pathsep
):
exe_path = os.path.join(os.curdir, exe_path)
else:
for path_entry in os.environ.get("PATH", "").split(os.pathsep):
abs_path_entry = os.path.abspath(path_entry)
if commonpath((exe_path, abs_path_entry)) == abs_path_entry:
return os.path.relpath(exe_path, abs_path_entry)
return exe_path
5 changes: 5 additions & 0 deletions pex/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ def path(self):
# type: () -> str
return self._pex

@property
def pex_info(self):
# type: () -> PexInfo
return self._pex_info

@property
def source_pex(self):
# type: () -> str
Expand Down
80 changes: 22 additions & 58 deletions pex/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
import itertools
import os
import sys
import warnings
from site import USER_SITE
from types import ModuleType

from pex import bootstrap, pex_warnings
from pex import bootstrap, repl
from pex.bootstrap import Bootstrap
from pex.common import die
from pex.dist_metadata import CallableEntryPoint, Distribution, ModuleEntryPoint, parse_entry_point
Expand Down Expand Up @@ -41,6 +40,7 @@
Mapping,
NoReturn,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
Expand Down Expand Up @@ -166,8 +166,8 @@ def __init__(
self._pex_info = PexInfo.from_pex(self._pex)
self._pex_info_overrides = PexInfo.from_env(env=env)
self._vars = env
self._envs = None # type: Optional[Iterable[PEXEnvironment]]
self._activated_dists = None # type: Optional[Iterable[Distribution]]
self._envs = None # type: Optional[Sequence[PEXEnvironment]]
self._activated_dists = None # type: Optional[Sequence[Distribution]]
self._layout = None # type: Optional[Layout.Value]
if verify_entry_point:
self._do_entry_point_verification()
Expand All @@ -194,7 +194,7 @@ def interpreter(self):

@property
def _loaded_envs(self):
# type: () -> Iterable[PEXEnvironment]
# type: () -> Sequence[PEXEnvironment]
if self._envs is None:
# set up the local .pex environment
pex_info = self.pex_info()
Expand All @@ -209,7 +209,7 @@ def _loaded_envs(self):
pex_info = PexInfo.from_pex(pex_path)
pex_info.update(self._pex_info_overrides)
envs.append(PEXEnvironment.mount(pex_path, pex_info, target=target))
self._envs = tuple(envs)
self._envs = envs
return self._envs

def resolve(self):
Expand Down Expand Up @@ -239,7 +239,7 @@ def iter_distributions(self, result_type_wheel_file=False):
yield dist

def _activate(self):
# type: () -> Iterable[Distribution]
# type: () -> Sequence[Distribution]

activated_dists = [] # type: List[Distribution]
for env in self._loaded_envs:
Expand All @@ -251,7 +251,7 @@ def _activate(self):
return activated_dists

def activate(self):
# type: () -> Iterable[Distribution]
# type: () -> Sequence[Distribution]
if self._activated_dists is None:
# 1. Scrub the sys.path to present a minimal Python environment.
self.patch_sys()
Expand Down Expand Up @@ -672,61 +672,25 @@ def execute_interpreter(self):
sys.argv = args
return self.execute_content(arg, content)
else:
try:
import readline
except ImportError:
if self._vars.PEX_INTERPRETER_HISTORY:
pex_warnings.warn(
"PEX_INTERPRETER_HISTORY was requested which requires the `readline` "
"module, but the current interpreter at {python} does not have readline "
"support.".format(python=sys.executable)
)
else:
# This import is used for its side effects by the parse_and_bind lines below.
import rlcompleter # NOQA

# N.B.: This hacky method of detecting use of libedit for the readline
# implementation is the recommended means.
# See https://docs.python.org/3/library/readline.html
if "libedit" in readline.__doc__:
# Mac can use libedit, and libedit has different config syntax.
readline.parse_and_bind("bind ^I rl_complete")
else:
readline.parse_and_bind("tab: complete")

try:
# Under current PyPy readline does not implement read_init_file and emits a
# warning; so we squelch that noise.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
readline.read_init_file()
except (IOError, OSError):
# No init file (~/.inputrc for readline or ~/.editrc for libedit).
pass

if self._vars.PEX_INTERPRETER_HISTORY:
import atexit

histfile = os.path.expanduser(self._vars.PEX_INTERPRETER_HISTORY_FILE)
try:
readline.read_history_file(histfile)
readline.set_history_length(1000)
except (IOError, OSError) as e:
sys.stderr.write(
"Failed to read history file at {path} due to: {err}\n".format(
path=histfile, err=e
pex_repl = repl.create_pex_repl(
pex_info=self.pex_info(),
requirements=(
tuple(
OrderedSet(
itertools.chain.from_iterable(
env.pex_info.requirements for env in self._envs
)
)

atexit.register(readline.write_history_file, histfile)
)
if self._envs
else ()
),
activated_dists=self._activated_dists or (),
)

bootstrap.demote()

import code

local = {} # type: Dict[str, Any]
code.interact(local=local)
return Globals(local)
return Globals(pex_repl())

@staticmethod
def execute_with_options(
Expand Down
4 changes: 2 additions & 2 deletions pex/pex_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ def _prepare_bootstrap(self):
# NB: We use pip here in the builder, but that's only at build time, and
# although we don't use pyparsing directly, packaging.markers, which we
# do use at runtime, does.
root_module_names = ["attr", "packaging", "pkg_resources", "pyparsing"]
root_module_names = ["attr", "colors", "packaging", "pkg_resources", "pyparsing"]
prepared_sources = vendor.vendor_runtime(
chroot=self._chroot,
dest_basedir=self._pex_info.bootstrap,
Expand All @@ -558,7 +558,7 @@ def _prepare_bootstrap(self):
)

bootstrap_digest = hashlib.sha1()
bootstrap_packages = ["third_party", "venv"]
bootstrap_packages = ["repl", "third_party", "venv"]
if self._pex_info.includes_tools:
bootstrap_packages.extend(["commands", "tools"])
for root, dirs, files in deterministic_walk(_ABS_PEX_PACKAGE_DIR):
Expand Down
18 changes: 8 additions & 10 deletions pex/pex_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from pex.version import __version__ as pex_version

if TYPE_CHECKING:
# MyPy run for 2.7 does not recognize the Collection type
from typing import Collection # type: ignore[attr-defined]
from typing import Any, Dict, Iterable, Mapping, Optional, Text, Tuple, Union

from pex.dist_metadata import Requirement
Expand Down Expand Up @@ -459,6 +461,7 @@ def add_requirement(self, requirement):

@property
def requirements(self):
# type: () -> Collection[str]
return self._requirements

def add_exclude(self, requirement):
Expand Down Expand Up @@ -593,21 +596,16 @@ def as_json_dict(self):
# type: () -> Dict[str, Any]
data = self._pex_info.copy()
data["inherit_path"] = self.inherit_path.value
data["requirements"] = list(self._requirements)
data["excluded"] = list(self._excluded)
data["overridden"] = list(self._overridden)
data["interpreter_constraints"] = [str(ic) for ic in self.interpreter_constraints]
data["requirements"] = sorted(self._requirements)
data["excluded"] = sorted(self._excluded)
data["overridden"] = sorted(self._overridden)
data["interpreter_constraints"] = sorted(str(ic) for ic in self.interpreter_constraints)
data["distributions"] = self._distributions.copy()
return data

def dump(self, **extra_json_dumps_kwargs):
# type: (**Any) -> str
data = self.as_json_dict()
data["requirements"].sort()
data["excluded"].sort()
data["overridden"].sort()
data["interpreter_constraints"].sort()
return json.dumps(data, sort_keys=True, **extra_json_dumps_kwargs)
return json.dumps(self.as_json_dict(), sort_keys=True, **extra_json_dumps_kwargs)

def copy(self):
# type: () -> PexInfo
Expand Down
13 changes: 13 additions & 0 deletions pex/repl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2024 Pex project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import

# For re-export
from pex.repl.pex_repl import ( # noqa
create_pex_repl,
create_pex_repl_exe,
export_pex_cli_no_args_use,
)

__all__ = ("create_pex_repl", "create_pex_repl_exe", "export_pex_cli_no_args_use")
Loading

0 comments on commit e719bac

Please sign in to comment.