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 REPL command history. #2018

Merged
merged 8 commits into from
Jan 3, 2023
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
15 changes: 15 additions & 0 deletions pex/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,21 @@ def execute_interpreter(self):
sys.argv = args
return self.execute_content(arg, content)
else:
if self._vars.PEX_INTERPRETER_HISTORY:
import atexit
import readline

histfile = os.path.expanduser(self._vars.PEX_INTERPRETER_HISTORY_FILE)
try:
readline.read_history_file(histfile)
readline.set_history_length(1000)
except OSError as e:
sys.stderr.write(
"Failed to read history file at {} due to: {}".format(histfile, e)
)

atexit.register(readline.write_history_file, histfile)

self.demote_bootstrap()

import code
Expand Down
32 changes: 29 additions & 3 deletions pex/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ def from_rc(cls, rc=None):
"""
ret_vars = {} # type: Dict[str, str]
rc_locations = [
"/etc/pexrc",
"~/.pexrc",
os.path.join(os.sep, "etc", "pexrc"),
os.path.join("~", ".pexrc"),
os.path.join(os.path.dirname(sys.argv[0]), ".pexrc"),
]
if rc:
Expand Down Expand Up @@ -520,6 +520,32 @@ def PEX_INTERPRETER(self):
"""
return self._get_bool("PEX_INTERPRETER")

@defaulted_property(default=False)
def PEX_INTERPRETER_HISTORY(self):
# type: () -> bool
"""Boolean.

IF PEX_INTERPRETER is true, use a command history file for REPL user convenience.
The location of the history file is determined by PEX_INTERPRETER_HISTORY_FILE.

Note: Only supported on CPython interpreters.

Default: false.
"""
return self._get_bool("PEX_INTERPRETER_HISTORY")

@defaulted_property(default=os.path.join("~", ".python_history"))
def PEX_INTERPRETER_HISTORY_FILE(self):
# type: () -> str
"""File.

IF PEX_INTERPRETER_HISTORY is true, use this history file.
The default is the standard CPython interpreter history location.

Default: ~/.python_history.
"""
return self._get_string("PEX_INTERPRETER_HISTORY_FILE")

@property
def PEX_MODULE(self):
# type: () -> Optional[str]
Expand Down Expand Up @@ -607,7 +633,7 @@ def PEX_EXTRA_SYS_PATH(self):
)
return self._maybe_get_path_tuple("PEX_EXTRA_SYS_PATH") or ()

@defaulted_property(default="~/.pex")
@defaulted_property(default=os.path.join("~", ".pex"))
def PEX_ROOT(self):
# type: () -> str
"""Directory.
Expand Down
22 changes: 22 additions & 0 deletions pex/venv/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ def sys_executable_paths():
"PEX_EXTRA_SYS_PATH",
"PEX_VENV_BIN_PATH",
"PEX_INTERPRETER",
"PEX_INTERPRETER_HISTORY",
"PEX_INTERPRETER_HISTORY_FILE",
"PEX_SCRIPT",
"PEX_MODULE",
# This is used when loading ENV (Variables()):
Expand Down Expand Up @@ -474,6 +476,13 @@ def sys_executable_paths():
sys.exit(1)
is_exec_override = len(pex_overrides) == 1

pex_interpreter_history = os.environ.get(
"PEX_INTERPRETER_HISTORY", "false"
).lower() in ("1", "true")
pex_interpreter_history_file = os.environ.get(
"PEX_INTERPRETER_HISTORY_FILE", os.path.join("~", ".python_history")
)

if {strip_pex_env!r}:
for key in list(os.environ):
if key.startswith("PEX_"):
Expand Down Expand Up @@ -504,6 +513,19 @@ def sys_executable_paths():
# See https://docs.python.org/3/library/sys.html#sys.path
sys.path.insert(0, "")

if pex_interpreter_history:
import atexit
import readline

histfile = os.path.expanduser(pex_interpreter_history_file)
try:
readline.read_history_file(histfile)
readline.set_history_length(1000)
except OSError:
pass

atexit.register(readline.write_history_file, histfile)

if entry_point == PEX_INTERPRETER_ENTRYPOINT and len(sys.argv) > 1:
args = sys.argv[1:]

Expand Down
34 changes: 34 additions & 0 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from pex.requirements import LogicalLine, PyPIRequirement, parse_requirement_file
from pex.testing import (
IS_MAC,
IS_PYPY,
NOT_CPYTHON27,
NOT_CPYTHON27_OR_OSX,
PY38,
Expand Down Expand Up @@ -227,6 +228,39 @@ def test_pex_repl_built():
assert b">>>" in stdout


@pytest.mark.skipif(
IS_PYPY or IS_MAC,
reason="REPL history is only supported on CPython. It works on macOS in an interactive "
"terminal, but this test fails in CI on macOS with `Inappropriate ioctl for device`, "
"because readline.read_history_file expects a tty on stdout. The linux tests will have "
"to suffice for now.",
)
@pytest.mark.parametrize("venv_pex", [False, True])
def test_pex_repl_history(venv_pex):
# type: (...) -> None
"""Tests enabling REPL command history."""
stdin_payload = b"import sys; import readline; print(readline.get_history_item(1)); sys.exit(3)"

with temporary_dir() as output_dir:
# Create a dummy temporary pex with no entrypoint.
pex_path = os.path.join(output_dir, "dummy.pex")
results = run_pex_command(
["--disable-cache", "-o", pex_path] + (["--venv"] if venv_pex else [])
)
results.assert_success()

history_file = os.path.join(output_dir, ".python_history")
with open(history_file, "w") as fp:
fp.write("2 + 2\n")

# Test that the REPL can see the history.
env = {"PEX_INTERPRETER_HISTORY": "1", "PEX_INTERPRETER_HISTORY_FILE": history_file}
stdout, rc = run_simple_pex(pex_path, stdin=stdin_payload, env=env)
assert rc == 3, "Failed with: {}".format(stdout.decode("utf-8"))
assert b">>>" in stdout
assert b"2 + 2" in stdout


@pytest.mark.skipif(WINDOWS, reason="No symlinks on windows")
def test_pex_python_symlink():
# type: () -> None
Expand Down