From 991fbabd2d7211e68ccaf2fb921beba99c21a7c3 Mon Sep 17 00:00:00 2001 From: Jonathan Woodbury Date: Thu, 17 Jul 2025 17:27:31 -0400 Subject: [PATCH 1/7] fix: tab completion for REPL fixes #3090 --- python/bin/repl_stub.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index 1e21b26dc3..1373fa51c0 100644 --- a/python/bin/repl_stub.py +++ b/python/bin/repl_stub.py @@ -17,8 +17,27 @@ console_locals = globals().copy() import code +import readline +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 +47,18 @@ sys.ps1 = "" sys.ps2 = "" +# Set up tab completion. +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!') + # 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) From eb314428ffdae6c87572bb56ae9120079bd298c9 Mon Sep 17 00:00:00 2001 From: Jonathan Woodbury Date: Sat, 2 Aug 2025 00:13:04 -0400 Subject: [PATCH 2/7] Conditionally import readline The readline module is not avialable on all platforms. --- python/bin/repl_stub.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index 1373fa51c0..0db15907aa 100644 --- a/python/bin/repl_stub.py +++ b/python/bin/repl_stub.py @@ -17,10 +17,15 @@ console_locals = globals().copy() import code -import readline import rlcompleter import sys +try: + import readline +except ImportError: + pass # readline is not available on all platforms + + class DynamicCompleter(rlcompleter.Completer): """ A custom completer that dynamically updates its namespace to include new @@ -47,18 +52,19 @@ def complete(self, text, state): sys.ps1 = "" sys.ps2 = "" -# Set up tab completion. -completer = DynamicCompleter(console_locals) -readline.set_completer(completer.complete) +if "readline" in globals(): + # Set up tab completion. + 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!') + # 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!") # 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) From e314aadee5a3a7693c2ac7228b17c1aeeddce876 Mon Sep 17 00:00:00 2001 From: Jonathan Woodbury Date: Sat, 2 Aug 2025 13:01:37 -0400 Subject: [PATCH 3/7] Print error on readline import failure --- python/bin/repl_stub.py | 49 +++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index 0db15907aa..0fdadf3b14 100644 --- a/python/bin/repl_stub.py +++ b/python/bin/repl_stub.py @@ -17,32 +17,8 @@ console_locals = globals().copy() import code -import rlcompleter import sys -try: - import readline -except ImportError: - pass # readline is not available on all platforms - - -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 @@ -52,8 +28,27 @@ def complete(self, text, state): sys.ps1 = "" sys.ps2 = "" -if "readline" in globals(): - # Set up tab completion. +# Set up tab completion. +try: + import readline + import rlcompleter + + 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) + completer = DynamicCompleter(console_locals) readline.set_completer(completer.complete) @@ -65,6 +60,8 @@ def complete(self, text, state): readline.parse_and_bind("tab: complete") else: print("Could not enable tab completion!") +except ImportError: + print("Could not enable tab completion!") # 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) From 9dd58d791d49f538fec0f5865aecab75164b4cad Mon Sep 17 00:00:00 2001 From: Jonathan Woodbury Date: Sat, 2 Aug 2025 20:15:38 -0400 Subject: [PATCH 4/7] Improve error messages This commit also makes an attempt to minimize the amount of code within the `try` block for importing `realine`. --- python/bin/repl_stub.py | 47 ++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index 0fdadf3b14..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 @@ -31,23 +51,6 @@ # Set up tab completion. try: import readline - import rlcompleter - - 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) completer = DynamicCompleter(console_locals) readline.set_completer(completer.complete) @@ -59,9 +62,15 @@ def complete(self, text, state): elif "GNU readline" in readline.__doc__: # type: ignore readline.parse_and_bind("tab: complete") else: - print("Could not enable tab completion!") + print( + "Could not enable tab completion: " + "unable to determine readline backend" + ) except ImportError: - print("Could not enable tab completion!") + 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) From 2b9ce0f95ea43185a4453835bffcdb9d5907873b Mon Sep 17 00:00:00 2001 From: Jonathan Woodbury Date: Sun, 3 Aug 2025 02:32:44 -0400 Subject: [PATCH 5/7] Add CHANGELOG entry for REPL tab completion --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f69e94ec65..3e6941db20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-added} ### Added +* (repl) Default stub now has tab completion. ([#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`. From 95535e1a12513dd39410637ca61762bd8d776228 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:06:03 +0900 Subject: [PATCH 6/7] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6941db20..c2d88d38cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,7 +104,8 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-added} ### Added -* (repl) Default stub now has tab completion. ([#3114](https://github.com/bazel-contrib/rules_python/pull/3114)). +* (repl) Default stub now has tab completion where `readline` support is available, see + ([#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`. From d109577872fcc0457b066c046864a073023417f9 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:07:19 +0900 Subject: [PATCH 7/7] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d88d38cd..422e399026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,7 +104,8 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-added} ### Added -* (repl) Default stub now has tab completion where `readline` support is available, see +* (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