Skip to content

gh-72327: Add help message for pip in REPL #8536

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
110 changes: 68 additions & 42 deletions Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,62 +426,88 @@ def sethelper():
builtins.help = _sitebuiltins._Helper()

def enablerlcompleter():
"""Enable default readline configuration on interactive prompts, by
registering a sys.__interactivehook__.

If the readline module can be imported, the hook will set the Tab key
"""If the readline module can be imported, the hook will set the Tab key
as completion key and register ~/.python_history as history file.
This can be overridden in the sitecustomize or usercustomize module,
or in a PYTHONSTARTUP file.
"""
def register_readline():
import atexit
try:
import readline
import rlcompleter
except ImportError:
return

# Reading the initialization (config) file may not be enough to set a
# completion key, so we set one first and then read the file.
if readline.backend == 'editline':
readline.parse_and_bind('bind ^I rl_complete')
else:
readline.parse_and_bind('tab: complete')
import atexit
try:
import readline
import rlcompleter
except ImportError:
return

# Reading the initialization (config) file may not be enough to set a
# completion key, so we set one first and then read the file.
if readline.backend == 'editline':
readline.parse_and_bind('bind ^I rl_complete')
else:
readline.parse_and_bind('tab: complete')

try:
readline.read_init_file()
except OSError:
# An OSError here could have many causes, but the most likely one
# is that there's no .inputrc file (or .editrc file in the case of
# Mac OS X + libedit) in the expected location. In that case, we
# want to ignore the exception.
pass

if readline.get_current_history_length() == 0:
# If no history was loaded, default to .python_history.
# The guard is necessary to avoid doubling history size at
# each interpreter exit when readline was already configured
# through a PYTHONSTARTUP hook, see:
# http://bugs.python.org/issue5845#msg198636
history = os.path.join(os.path.expanduser('~'),
'.python_history')
try:
readline.read_init_file()
readline.read_history_file(history)
except OSError:
# An OSError here could have many causes, but the most likely one
# is that there's no .inputrc file (or .editrc file in the case of
# Mac OS X + libedit) in the expected location. In that case, we
# want to ignore the exception.
pass

if readline.get_current_history_length() == 0:
# If no history was loaded, default to .python_history.
# The guard is necessary to avoid doubling history size at
# each interpreter exit when readline was already configured
# through a PYTHONSTARTUP hook, see:
# http://bugs.python.org/issue5845#msg198636
history = os.path.join(os.path.expanduser('~'),
'.python_history')
def write_history():
try:
readline.read_history_file(history)
readline.write_history_file(history)
except OSError:
# bpo-19891, bpo-41193: Home directory does not exist
# or is not writable, or the filesystem is read-only.
pass

def write_history():
try:
readline.write_history_file(history)
except OSError:
# bpo-19891, bpo-41193: Home directory does not exist
# or is not writable, or the filesystem is read-only.
pass
atexit.register(write_history)

atexit.register(write_history)
def _register_detect_pip_usage_in_repl():
old_excepthook = sys.excepthook

def detect_pip_usage_in_repl(typ, value, traceback):
if typ is SyntaxError and (
"pip install" in value.text or "pip3 install" in value.text
):
value.add_note(
"The Python package manager (pip) can only be used"
" from outside of Python.\n"
"Please try the `pip` command in a"
" separate terminal or command prompt."
)

old_excepthook(typ, value, traceback)

detect_pip_usage_in_repl.__wrapped__ = old_excepthook
sys.excepthook = detect_pip_usage_in_repl


def _set_interactive_hook():
"""Register a sys.__interactivehook__ to:
- Enable default readline configuration on interactive prompts.
- Register an excepthook to detect pip usage in the REPL.
"""
def interactivehook():
enablerlcompleter()
_register_detect_pip_usage_in_repl()

sys.__interactivehook__ = interactivehook

sys.__interactivehook__ = register_readline
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this change to the way enablerlcompleter works is still concerning.

Reviewing 0e6d9e2 I think the public API to preserve would be:

  1. New top level function that actually does the registration is called _register_readline (as @tomviner originally had it)
  2. Reduced enablerlcompleter implementation just does sys.__interactive_hook__ = _register_readline
  3. _set_interactive_hook call _register_readline directly, bypassing enablerlcompleter (since it is setting its own hook)

enablerlcompleter would never be called by the standard library (outside the test suite), it would purely be a backwards compatibility API accounting for the fact that it was never marked as private in the interactive help.


def venv(known_paths):
global PREFIXES, ENABLE_USER_SITE
Expand Down Expand Up @@ -602,7 +628,7 @@ def main():
setcopyright()
sethelper()
if not sys.flags.isolated:
enablerlcompleter()
_set_interactive_hook()
execsitecustomize()
if ENABLE_USER_SITE:
execusercustomize()
Expand Down
26 changes: 24 additions & 2 deletions Lib/test/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,28 @@ def test_license_exists_at_url(self):
self.assertEqual(code, 200, msg="Can't find " + url)


class DetectPipUsageInReplTests(unittest.TestCase):
def setUp(self):
self.old_excepthook = sys.excepthook
site._register_detect_pip_usage_in_repl()

def tearDown(self):
sys.excepthook = self.old_excepthook

def test_detect_pip_usage_in_repl(self):
for pip_cmd in [
'pip install a', 'pip3 install b', 'python -m pip install c'
]:
with self.subTest(pip_cmd=pip_cmd):
try:
exec(pip_cmd, {}, {})
except SyntaxError as exc:
with captured_stderr() as err_out:
sys.excepthook(SyntaxError, exc, exc.__traceback__)

self.assertIn("the `pip` command", err_out.getvalue())


class StartupImportTests(unittest.TestCase):

@support.requires_subprocess()
Expand Down Expand Up @@ -593,8 +615,8 @@ def test_startup_interactivehook_isolated(self):
def test_startup_interactivehook_isolated_explicit(self):
# issue28192 readline can be explicitly enabled in isolated mode
r = subprocess.Popen([sys.executable, '-I', '-c',
'import site, sys; site.enablerlcompleter(); sys.exit(hasattr(sys, "__interactivehook__"))']).wait()
self.assertTrue(r, "'__interactivehook__' not added by enablerlcompleter()")
'import site, sys; site._set_interactive_hook(); sys.exit(hasattr(sys, "__interactivehook__"))']).wait()
self.assertTrue(r, "'__interactivehook__' not added by _set_interactive_hook()")

class _pthFileTests(unittest.TestCase):

Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1928,6 +1928,7 @@ Olivier Vielpeau
Kannan Vijayan
Kurt Vile
Norman Vine
Tom Viner
Pauli Virtanen
Frank Visser
Long Vo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Give better errors for ``pip install`` command typed into the REPL. Patch by
Tom Viner.