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

Override default readline via python -m override_readline #72

Merged
merged 12 commits into from
Jun 10, 2024

Conversation

ludwigschwardt
Copy link
Owner

Over the years we have tried and debated many ways to override the default readline module in Python so that tab completion can be fixed in the standard interactive interpreter (see e.g. #26 and #27).

The original solution was easy_install readline: ugly but effective. In the pip era this option has fallen away. The current installation route of pip install gnureadline leaves it up to the user to override sys.modules['readline'] or to import gnureadline as readline.

The problem is that those tricks have to happen before the interpreter even prints its startup banner. Python 3 roughly starts up like this:

  • run site.py:

    • set up sys.__interactivehook__ [but DON'T import readline yet]
    • run sitecustomize.py
    • run usercustomize.py
  • import rlcompleter [imports readline!]

  • output:

  Python 3.11.3 (main, May  9 2023, 18:38:16) [...] on darwin
  Type "help", "copyright", "credits" or "license" for more information.
  • run PYTHONSTARTUP
  • run sys.__interactivehook__() [also imports readline fwiw]
  • output:
  >>>

PYTHONSTARTUP is a bit late to the party and will need to redo the interactive hook as well (see for example this useful comment). This makes sitecustomize and usercustomize more convenient, as they only need to override readline itself.

Here is a rough comparison of the various customization routes:

Route Affected versions if on Python 3.11.3 Affects non-interactive scripts too
sitecustomize 3.11.3 yes
usercustomize 3.11.x yes
PYTHONSTARTUP all no

It feels a bit iffy when an installed package modifies those customization modules but, in mitigation, the user has to push the button and the override tries to explain what it does in some detail.

When you run python -m override_readline, the script first targets usercustomize.py because then you don't have to update it after every patch release (third number in version triplet), just like the gnureadline package itself. If you don't have permission or you are in a virtualenv, it falls back to sitecustomize.py. If you want to go straight to sitecustomize.py, simply add the standard -s flag and run python -s -m override_readline.

It does sys.modules['readline'] = gnureadline and also lets sys.__interactivehook__ print a reminder that readline is overridden (and where the override is). This is better than a direct print() which also pops up in non-interactive Python invocations like pip, etc. Here is an example:

Python 3.12.2 (main, Apr 17 2024, 20:25:57) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
Using GNU readline instead of the default readline (see sitecustomize.py)
>>>

The override is reasonably foolproof and won't crash even if gnureadline is uninstalled and the override inadvertently remains behind. And best of all, it all works in Python 2.7 too because site customization has been around forever 😛 The nice thing about running a module as a script is that it inherently uses the appropriate python interpreter, which avoids confusion.

I've also updated the tests to confirm that override_readline works. The original test involving the alternative readline.py module is downplayed. I don't want to give anyone bad ideas 😅

I also refreshed the main README to mention things currently in use, instead of easy_install and the old pyreadline, and incorporated @keeely's very useful suggestion.

Over the years we have tried and debated many ways to override the
default readline module in Python so that tab completion can be fixed
in the standard interactive interpreter (see e.g. #26 and #27).

The original solution was `easy_install readline`: ugly but effective.
In the pip era this option has fallen away. The current installation
route of `pip install gnureadline` leaves it up to the user to override
`sys.modules['readline']` or to `import gnureadline as readline`.

The problem is that those tricks have to happen before the interpreter
even prints its startup banner. Python 3 roughly starts up like this:

  - run site.py:
    - set up `sys.__interactivehook__` [but DON'T import readline yet]
    - run sitecustomize.py
    - run usercustomize.py
  - import rlcompleter [imports readline!]

  - output:
  Python 3.11.3 (main, May  9 2023, 18:38:16) [...] on darwin
  Type "help", "copyright", "credits" or "license" for more information.

  - run PYTHONSTARTUP
  - run sys.__interactivehook__() [also imports readline fwiw]
  - output:
  >>>

PYTHONSTARTUP is too late. It can modify the interactive hook but not
readline itself. That leaves sitecustomize and usercustomize. It feels
a bit iffy when an installed package modifies those but, in mitigation,
the user has to push the button and the override tries to explain what
it does in some detail.

When you run `python -m override_readline`, the script first targets
sitecustomize.py because it is better for virtual environments and
system-wide overrides. If you don't have permission, it falls back to
usercustomize.py. It does `sys.modules['readline'] = gnureadline` and
also lets `sys.__interactivehook__` print a reminder that readline is
overridden (and where the override is). This is better than a direct
print() which also pops up in non-interactive Python invocations like
pip, etc.

The override is reasonably foolproof and won't crash even if gnureadline
is uninstalled and the override inadvertently remains behind. And best
of all, it all works in Python 2.7 too because site customization has
been around forever :-P

The nice thing about running a module as a script is that it inherently
uses the appropriate python interpreter, which avoids confusion. This
script could even form the core of a revamped `readline` package that
switches between, say, pyreadline and gnureadline based on the platform.
The local package version of `readline.py` is less important now that
there is an override mechanism. I kept it though because it still caters
for cases like ActiveState Python which has no readline module at all.
In those cases it is sufficient to install gnureadline and be done with
it, with no need for the override.

Add a note to explain this in case I forget again :-) And remind folks
not to go down the dark `sys.path` with this module.
This is mostly to support Homebrew Python, which already has a
sitecustomize.py in the version-specific Cellar directory. This
directory, however, is not returned by `site.getsitepackages()` so
the override goes nowhere.

Furthermore, the sitecustomize.py is replaced with every patch release
of Homebrew Python (e.g. going from 3.11.3 to 3.11.4), requiring a new
override invocation even though the `gnureadline` package has not been
reinstalled. This is mildly irritating.

Make two changes:
  - Prefer usercustomize to sitecustomize. This override is valid for
    a minor release of any Python, making it more convenient.
  - If you do pick sitecustomize, import the module and use its actual
  `__file__` location to avoid getting the run-around.

It is easy to target sitecustomize instead by simply adding the -s flag;
i.e. `python -s -m override_readline`. No need for fancy argparsing :-)
Print the full path of the customisation module (it was printed when the
file had to be created from scratch but not when it was appended to).

Suggest that the file can be deleted if unwanted *only if* we created it
in the first place, to prevent possible tears :-)

Make the text print banners private to the module.
Explain the use case of fixing tab completion in more detail, based on
the new override_readline script. Thanks Steven aka @onlynone for your
excellent .pythonrc example script :-)

Also, don't mention the defunct easy_install and pyreadline (last commit
in 2015) but rather point out that prompt_toolkit works on Windows.
After removing the pyreadline link I discovered its continuation,
pyreadline3, and added that instead. It has more downloads than
gnureadline :-)
This is in preparation of more improvements to the tests.
Drop simplistic import test. Consolidate all `import readline` checks
in a single test (`test_identity_of_readline_module`). The test module
gains two script options that will explicitly check whether readline
is or is not the same as gnureadline. Use this to check whether
`override_readline` works, by first verifying that the modules differ,
applying the override and verifying that the modules are the same
afterwards. This is all run from within the tests directory to avoid
picking up the alternative readline.py module in the project directory.
Python 3.6 is stuck with the previous-generation tox 3, which calls
the option `changedir` instead of `change_dir`. Mercifully the tox 4
rewrite supports both versions. Once we drop 3.6 we can return to the
modern name.
This combination seems to be unpopular, since the (binary) Python
distribution is not on the runner machine and has to be downloaded:

  Version 3.7 was not found in the local cache
  Version 3.7 is available for downloading
  Download from "https://github.com/actions/python-versions/releases/download/3.7.17-5356448435/python-3.7.17-darwin-x64.tar.gz"

However, the standard readline module (readline.cpython-37m-darwin.so)
cannot be imported because the ncurses library could not be found
(it's looking for /usr/local/opt/ncurses/lib/libncursesw.6.dylib
instead of the more usual /usr/lib/libncurses.5.4.dylib).

I opened an issue on GitHub Actions (actions/setup-python#859), so
this option might still come back.
@ludwigschwardt ludwigschwardt changed the title Override default readline via python -m override_readline script Override default readline via python -m override_readline May 7, 2024
@ludwigschwardt ludwigschwardt self-assigned this May 7, 2024
@ludwigschwardt
Copy link
Owner Author

ludwigschwardt commented Jun 10, 2024

I've now verified that @onlynone's pythonrc works as expected. I checked it by replacing the line

readline.parse_and_bind('tab: complete')

with silly completion code customised for each library:

if "libedit" in readline.__doc__:
    readline.parse_and_bind("bind e rl_complete")
else:
    readline.parse_and_bind("r: complete")

So if thee key does tab completion, you are definitely using libedit, while tab completion via the r key confirms the presence of GNU readline. 😁

This approach only requires the PYTHONSTARTUP environment variable to be set to ~/.pythonrc, and there is no need to run override_readline as well.

Thanks Steven for the very useful example!

@ludwigschwardt ludwigschwardt merged commit 0ae068d into main Jun 10, 2024
16 checks passed
@ludwigschwardt ludwigschwardt deleted the override-readline branch June 10, 2024 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant