Subject: bug#68559: [PATCH] Improve Python shell completion

From: Liu Hui <[email-masked]>
Date: Mon, 11 Mar 2024 19:35:31 +0800

On Fri, Mar 8, 2024 at 11:44 PM Basil L. Contovounesios
<[email-masked]> wrote:
>
> Liu Hui [2024-03-06 18:14 +0800] wrote:
>
> > I have updated the patch.
>
> Thanks!  The build and tests succeed here, without skipped tests.
>
> > +                (unless (string-match-p "No readline support" output)
>
> Nit: why not plain 'string-search' instead of a regexp search?

Done.

> > -                   (string-match-p "ipython[23]?\\'" python-shell-interpreter)))))
> > +                   (or (string-match-p "ipython[23]?\\'" python-shell-interpreter)
> > +                       (string= python-shell-readline-completer-delims ""))))))
>
> Just curious: what does the empty string signify?
>
> [ If it's not just a dumb question, perhaps the meaning could be added
>   to the variable's docstring/commentary. ]

The empty string means no characters are considered delimiters and the
readline completion could consider the entire line of input without
breaking it into parts based on typical delimiters like spaces or
punctuation. The docstring is updated in the attached patch.
From bb1f8f92d5cd3c19d404e023ab33315bfa8a535a Mon Sep 17 00:00:00 2001
From: Liu Hui <[email-masked]>
Date: Mon, 26 Feb 2024 18:46:36 +0800
Subject: [PATCH] Detect the readline support for Python shell completion

* lisp/progmodes/python.el
(python-shell-comint-watch-for-first-prompt-output-filter):
Detect the readline support.
(python-shell-readline-completer-delims): Update docstring.
(python-shell-completion-native-setup): Move the readline
detection code to ...
(python-shell-readline-detect): ... new function.
(python-shell-completion-native-turn-on-maybe): Skip if Python
has no readline support.
(python-shell-completion-at-point): Respect the delimiter of
readline completer in non-native completion.
* test/lisp/progmodes/python-tests.el
(python-shell-completion-at-point-1)
(python-shell-completion-at-point-native-1)
(python-completion-at-point-1, python-completion-at-point-2)
(python-completion-at-point-pdb-1)
(python-completion-at-point-while-running-1)
(python-completion-at-point-native-1)
(python-completion-at-point-native-2)
(python-completion-at-point-native-with-ffap-1)
(python-completion-at-point-native-with-eldoc-1): Skip tests if
Python has no readline support.
(python-shell-completion-at-point-jedi-completer): Add test for
non-native Python shell completion. (bug#68559)
---
 lisp/progmodes/python.el            | 29 ++++++++++++++++++++-------
 test/lisp/progmodes/python-tests.el | 31 ++++++++++++++++++++++++-----
 2 files changed, 48 insertions(+), 12 deletions(-)

diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el
index 587d0b36304..0ea0b918872 100644
--- a/lisp/progmodes/python.el
+++ b/lisp/progmodes/python.el
@@ -3601,6 +3601,7 @@ python-shell-comint-watch-for-first-prompt-output-filter
           (python-shell-send-string-no-output python-shell-eval-file-setup-code))
         (with-current-buffer (current-buffer)
           (let ((inhibit-quit nil))
+            (python-shell-readline-detect)
             (run-hooks 'python-shell-first-prompt-hook))))))
   output)
 
@@ -4439,7 +4440,23 @@ python-shell-completion-native-try-output-timeout
 
 (defvar python-shell-readline-completer-delims nil
   "Word delimiters used by the readline completer.
-It is automatically set by Python shell.")
+It is automatically set by Python shell.  An empty string means no
+characters are considered delimiters and the readline completion
+considers the entire line of input.  A value of nil means the Python
+shell has no readline support.")
+
+(defun python-shell-readline-detect ()
+  "Detect the readline support for Python shell completion."
+  (let* ((process (python-shell-get-process))
+         (output (python-shell-send-string-no-output "
+try:
+    import readline
+    print(readline.get_completer_delims())
+except:
+    print('No readline support')" process)))
+    (setq-local python-shell-readline-completer-delims
+                (unless (string-search "No readline support" output)
+                  (string-trim-right output)))))
 
 (defvar python-shell-completion-native-redirect-buffer
   " *Python completions redirect*"
@@ -4579,10 +4596,6 @@ python-shell-completion-native-setup
 __PYTHON_EL_native_completion_setup()" process)))
     (when (string-match-p "python\\.el: native completion setup loaded"
                           output)
-      (setq-local python-shell-readline-completer-delims
-                  (string-trim-right
-                   (python-shell-send-string-no-output
-                    "import readline; print(readline.get_completer_delims())")))
       (python-shell-completion-native-try))))
 
 (defun python-shell-completion-native-turn-off (&optional msg)
@@ -4611,7 +4624,8 @@ python-shell-completion-native-turn-on-maybe
       (cond
        ((python-shell-completion-native-interpreter-disabled-p)
         (python-shell-completion-native-turn-off msg))
-       ((python-shell-completion-native-setup)
+       ((and python-shell-readline-completer-delims
+             (python-shell-completion-native-setup))
         (when msg
           (message "Shell native completion is enabled.")))
        (t
@@ -4783,7 +4797,8 @@ python-shell-completion-at-point
                (with-current-buffer (process-buffer process)
                  (if python-shell-completion-native-enable
                      (string= python-shell-readline-completer-delims "")
-                   (string-match-p "ipython[23]?\\'" python-shell-interpreter)))))
+                   (or (string-match-p "ipython[23]?\\'" python-shell-interpreter)
+                       (string= python-shell-readline-completer-delims ""))))))
          (start
           (if (< (point) line-start)
               (point)
diff --git a/test/lisp/progmodes/python-tests.el b/test/lisp/progmodes/python-tests.el
index 1ceee690cfb..e11440cdb5b 100644
--- a/test/lisp/progmodes/python-tests.el
+++ b/test/lisp/progmodes/python-tests.el
@@ -4783,6 +4783,7 @@ python-shell-completion-at-point-1
   (python-tests-with-temp-buffer-with-shell
    ""
    (python-shell-with-shell-buffer
+     (skip-unless python-shell-readline-completer-delims)
      (insert "import abc")
      (comint-send-input)
      (python-tests-shell-wait-for-prompt)
@@ -4797,6 +4798,7 @@ python-shell-completion-at-point-native-1
    ""
    (python-shell-completion-native-turn-on)
    (python-shell-with-shell-buffer
+     (skip-unless python-shell-readline-completer-delims)
      (insert "import abc")
      (comint-send-input)
      (python-tests-shell-wait-for-prompt)
@@ -4883,11 +4885,14 @@ python-shell-completion-at-point-jedi-completer
     (python-tests-with-temp-buffer-with-shell
      ""
      (python-shell-with-shell-buffer
-      (python-shell-completion-native-turn-on)
-      (skip-unless (string= python-shell-readline-completer-delims ""))
-      (python-tests--completion-module)
-      (python-tests--completion-parameters)
-      (python-tests--completion-extra-context)))))
+       (skip-unless (string= python-shell-readline-completer-delims ""))
+       (python-shell-completion-native-turn-off)
+       (python-tests--completion-module)
+       (python-tests--completion-parameters)
+       (python-shell-completion-native-turn-on)
+       (python-tests--completion-module)
+       (python-tests--completion-parameters)
+       (python-tests--completion-extra-context)))))
 
 (ert-deftest python-shell-completion-at-point-ipython ()
   "Check if Python shell completion works for IPython."
@@ -4924,6 +4929,8 @@ python-completion-at-point-1
 import abc
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-send-buffer)
      (python-tests-shell-wait-for-prompt)
      (goto-char (point-max))
@@ -4940,6 +4947,8 @@ python-completion-at-point-2
 import abc
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-send-buffer)
      (python-tests-shell-wait-for-prompt)
      (python-shell-with-shell-buffer
@@ -4959,6 +4968,8 @@ python-completion-at-point-pdb-1
 print('Hello')
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-send-buffer)
      (python-tests-shell-wait-for-prompt)
      (goto-char (point-max))
@@ -4975,6 +4986,8 @@ python-completion-at-point-while-running-1
 time.sleep(3)
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-send-buffer)
      (goto-char (point-max))
      (insert "time.")
@@ -4987,6 +5000,8 @@ python-completion-at-point-native-1
 import abc
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-completion-native-turn-on)
      (python-shell-send-buffer)
      (python-tests-shell-wait-for-prompt)
@@ -5004,6 +5019,8 @@ python-completion-at-point-native-2
 import abc
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-completion-native-turn-on)
      (python-shell-send-buffer)
      (python-tests-shell-wait-for-prompt)
@@ -5020,6 +5037,8 @@ python-completion-at-point-native-with-ffap-1
 import abc
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-completion-native-turn-on)
      (python-shell-send-buffer)
      (python-tests-shell-wait-for-prompt)
@@ -5036,6 +5055,8 @@ python-completion-at-point-native-with-eldoc-1
 import abc
 "
    (let ((inhibit-message t))
+     (python-shell-with-shell-buffer
+       (skip-unless python-shell-readline-completer-delims))
      (python-shell-completion-native-turn-on)
      (python-shell-send-buffer)
      (python-tests-shell-wait-for-prompt)
-- 
2.25.1