feat(repl): add tab completion on platforms with readline support (#3114)

This adds tab completion to the default stub when using the REPL
feature.

However, the feature only works in environments with `readline` support,
which means that with the bundled toolchains, Windows will not have tab
completion.

Work towards #3090

---------

Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f69e94e..422e399 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -104,6 +104,9 @@
 
 {#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 1e21b26..f5b7c0a 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)