Subject: [PATCH] Analogue of project-shell for Python

From: Augusto Stoffel <[email-masked]>
Date: Fri, 05 Aug 2022 09:31:02 +0200

The Python shell already allows a shell to be "dedicated" to a buffer.
This patch adds the option to create a shell dedicated to a project, as
well as the option to make all Python shells dedicated by default.

Stefan: you mentioned python.el could use project.el without a hard
dependency, so it remains compatible with old Emacsen.  Is this a good
approach?  (Also: the added seq dependency is kinda superfluous now, but
it's nice to have it available for future developments as well.)

Philip: I'm working around the possible absence of
'read-multiple-choice' here.  Not sure it's a popular/useful enough
function to include in compat, but I thought I would bring this up.

>From 7174b34701cb201c088f0fce6a57aee9a75eef20 Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <[email-masked]>
Date: Thu, 4 Aug 2022 12:08:58 +0200
Subject: [PATCH] Python shells dedicated to a project

These shells run in the project root directory and are shared among
all project buffers.

* lisp/progmodes/python.el: Require 'seq' and (optionally) 'project'
libraries.
(python-shell-dedicated): New user option
(python-shell-get-process-name): Adapt to project-dedicated shells.
(run-python): Offer possibility to create a project-dedicated shell,
or use 'python-shell-dedicated' as the default behavior.
(python-shell-get-buffer): Adapt to project-dedicated shells.
---
 etc/NEWS                 | 11 ++++++
 lisp/progmodes/python.el | 79 ++++++++++++++++++++++++++++------------
 2 files changed, 66 insertions(+), 24 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 963aa22c68..fcbf7deba7 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2055,6 +2055,17 @@ the Galeon web browser was released in September, 2008.
 Note that this historical web browser is different from Mozilla
 Firefox; it is its predecessor.
 
+** Python Mode
++++
+*** Project shells and a new user option 'python-shell-dedicated'
+When called with a prefix argument, 'run-python' now offers the choice
+of creating a shell dedicated to the current project.  This shell runs
+in the project root directory and is shared among all project buffers.
+
+Without a prefix argument, the kind of shell (buffer-dedicated,
+project-dedicated or global) is specified by the new
+'python-shell-dedicated' variable.
+
 ** Ruby Mode
 
 ---
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el
index b8fc7d4c54..bbdd2a9502 100644
--- a/lisp/progmodes/python.el
+++ b/lisp/progmodes/python.el
@@ -5,7 +5,7 @@
 ;; Author: Fabián E. Gallina <[email-masked]>
 ;; URL: https://github.com/fgallina/python.el
 ;; Version: 0.28
-;; Package-Requires: ((emacs "24.4") (cl-lib "1.0"))
+;; Package-Requires: ((emacs "24.4") (cl-lib "1.0") (seq "2.23"))
 ;; Maintainer: [email-masked]
 ;; Created: Jul 2010
 ;; Keywords: languages
@@ -245,6 +245,8 @@
 (require 'ansi-color)
 (require 'cl-lib)
 (require 'comint)
+(require 'project nil 'noerror)
+(require 'seq)
 (eval-when-compile (require 'subr-x))   ;For `string-empty-p'.
 
 ;; Avoid compiler warnings
@@ -2218,6 +2220,16 @@ python-shell-compilation-regexp-alist
   "`compilation-error-regexp-alist' for inferior Python."
   :type '(alist regexp))
 
+(defcustom python-shell-dedicated nil
+  "Whether to make Python shells dedicated by default.
+This option influences `run-python' when called without a prefix
+argument.  If `buffer' or `project', create a Python shell
+dedicated to the current buffer or its project (if one is found)."
+  :version "29.1"
+  :type '(choice (const :tag "To buffer" buffer)
+                 (const :tag "To project" project)
+                 (const :tag "Not dedicated" nil)))
+
 (defvar python-shell-output-filter-in-progress nil)
 (defvar python-shell-output-filter-buffer nil)
 
@@ -2580,12 +2592,19 @@ python-shell-prompt-set-calculated-regexps
 
 (defun python-shell-get-process-name (dedicated)
   "Calculate the appropriate process name for inferior Python process.
-If DEDICATED is t returns a string with the form
-`python-shell-buffer-name'[`buffer-name'] else returns the value
-of `python-shell-buffer-name'."
-  (if dedicated
-      (format "%s[%s]" python-shell-buffer-name (buffer-name))
-    python-shell-buffer-name))
+If DEDICATED is nil, this is simply `python-shell-buffer-name'.
+If DEDICATED is `buffer' or `project', append the current buffer
+name respectively the current project name."
+  (pcase dedicated
+    ('nil python-shell-buffer-name)
+    ('project
+     (if-let ((proj (and (featurep 'project)
+                         (project-current))))
+         (format "%s[%s]" python-shell-buffer-name (file-name-nondirectory
+                                                    (directory-file-name
+                                                     (project-root proj))))
+       python-shell-buffer-name))
+    (_ (format "%s[%s]" python-shell-buffer-name (buffer-name)))))
 
 (defun python-shell-internal-get-process-name ()
   "Calculate the appropriate process name for Internal Python process.
@@ -3043,8 +3062,8 @@ run-python
 Argument CMD defaults to `python-shell-calculate-command' return
 value.  When called interactively with `prefix-arg', it allows
 the user to edit such value and choose whether the interpreter
-should be DEDICATED for the current buffer.  When numeric prefix
-arg is other than 0 or 4 do not SHOW.
+should be DEDICATED to the current buffer or project.  When
+numeric prefix arg is other than 0 or 4 do not SHOW.
 
 For a given buffer and same values of DEDICATED, if a process is
 already running for it, it will do nothing.  This means that if
@@ -3058,13 +3077,27 @@ run-python
    (if current-prefix-arg
        (list
         (read-shell-command "Run Python: " (python-shell-calculate-command))
-        (y-or-n-p "Make dedicated process? ")
+        (if (not (fboundp 'read-multiple-choice))
+            (y-or-n-p "Make dedicated process? ")
+          (alist-get (car (read-multiple-choice "Make dedicated process?"
+                                                '((?b "to buffer")
+                                                  (?p "to project")
+                                                  (?n "no"))))
+                     '((?b . buffer) (?p . project))))
         (= (prefix-numeric-value current-prefix-arg) 4))
-     (list (python-shell-calculate-command) nil t)))
-  (let ((buffer
-         (python-shell-make-comint
-          (or cmd (python-shell-calculate-command))
-          (python-shell-get-process-name dedicated) show)))
+     (list (python-shell-calculate-command)
+           python-shell-dedicated
+           t)))
+  (let* ((project (and (eq 'project dedicated)
+                       (featurep 'project)
+                       (project-current t)))
+         (default-directory (if project
+                                (project-root project)
+                              default-directory))
+         (buffer (python-shell-make-comint
+                  (or cmd (python-shell-calculate-command))
+                  (python-shell-get-process-name dedicated)
+                  show)))
     (get-buffer-process buffer)))
 
 (defun run-python-internal ()
@@ -3094,15 +3127,13 @@ python-shell-get-buffer
 If current buffer is in `inferior-python-mode', return it."
   (if (derived-mode-p 'inferior-python-mode)
       (current-buffer)
-    (let* ((dedicated-proc-name (python-shell-get-process-name t))
-           (dedicated-proc-buffer-name (format "*%s*" dedicated-proc-name))
-           (global-proc-name  (python-shell-get-process-name nil))
-           (global-proc-buffer-name (format "*%s*" global-proc-name))
-           (dedicated-running (comint-check-proc dedicated-proc-buffer-name))
-           (global-running (comint-check-proc global-proc-buffer-name)))
-      ;; Always prefer dedicated
-      (or (and dedicated-running dedicated-proc-buffer-name)
-          (and global-running global-proc-buffer-name)))))
+    (seq-some
+     (lambda (dedicated)
+       (let* ((proc-name (python-shell-get-process-name dedicated))
+              (buffer-name (format "*%s*" proc-name)))
+         (when (comint-check-proc buffer-name)
+           buffer-name)))
+     '(buffer project nil))))
 
 (defun python-shell-get-process ()
   "Return inferior Python process for current buffer."
-- 
2.37.1