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

Add PEX info to the PEX repl. #2496

Merged
merged 6 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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