diff --git a/CHANGELOG.md b/CHANGELOG.md index f69e94ec65..422e399026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,9 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-added} ### Added +* (repl) Default stub now has tab completion, where `readline` support is available, + see ([#3114](https://github.com/bazel-contrib/rules_python/pull/3114)). + ([#3114](https://github.com/bazel-contrib/rules_python/pull/3114)). * (pypi) To configure the environment for `requirements.txt` evaluation, use the newly added developer preview of the `pip.default` tag class. Only `rules_python` and root modules can use this feature. You can also configure custom `config_settings` using `pip.default`. diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index 1e21b26dc3..f5b7c0aa4f 100644 --- a/python/bin/repl_stub.py +++ b/python/bin/repl_stub.py @@ -17,8 +17,28 @@ console_locals = globals().copy() import code +import rlcompleter import sys + +class DynamicCompleter(rlcompleter.Completer): + """ + A custom completer that dynamically updates its namespace to include new + imports made within the interactive session. + """ + + def __init__(self, namespace): + # Store a reference to the namespace, not a copy, so that changes to the namespace are + # reflected. + self.namespace = namespace + + def complete(self, text, state): + # Update the completer's internal namespace with the current interactive session's locals + # and globals. This is the key to making new imports discoverable. + rlcompleter.Completer.__init__(self, self.namespace) + return super().complete(text, state) + + if sys.stdin.isatty(): # Use the default options. exitmsg = None @@ -28,5 +48,29 @@ sys.ps1 = "" sys.ps2 = "" +# Set up tab completion. +try: + import readline + + completer = DynamicCompleter(console_locals) + readline.set_completer(completer.complete) + + # TODO(jpwoodbu): Use readline.backend instead of readline.__doc__ once we can depend on having + # Python >=3.13. + if "libedit" in readline.__doc__: # type: ignore + readline.parse_and_bind("bind ^I rl_complete") + elif "GNU readline" in readline.__doc__: # type: ignore + readline.parse_and_bind("tab: complete") + else: + print( + "Could not enable tab completion: " + "unable to determine readline backend" + ) +except ImportError: + print( + "Could not enable tab completion: " + "readline module not available on this platform" + ) + # We set the banner to an empty string because the repl_template.py file already prints the banner. code.interact(local=console_locals, banner="", exitmsg=exitmsg)