New Upstream Snapshot - pyvenv-el

Ready changes

Summary

Merged new upstream version: 1.21+git20211014.31ea715 (was: 1.21+git20201124.37e7cb1).

Resulting package

Built on 2022-10-21T20:53 (took 12m31s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots elpa-pyvenv

Lintian Result

Diff

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d4691b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.cask
diff --git a/debian/changelog b/debian/changelog
index 510454c..1c8fbfe 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pyvenv-el (1.21+git20211014.31ea715-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 21 Oct 2022 20:43:55 -0000
+
 pyvenv-el (1.21+git20201124.37e7cb1-1) unstable; urgency=medium
 
   * New upstream snapshot 1.21+git20201124.37e7cb1 (Closes: #975560)
diff --git a/debian/patches/0001-fix-documentation.diff b/debian/patches/0001-fix-documentation.diff
index d4b3ebb..b1fc695 100644
--- a/debian/patches/0001-fix-documentation.diff
+++ b/debian/patches/0001-fix-documentation.diff
@@ -5,8 +5,10 @@ This patch removes badges icons from README file. These icons are
 intended rather for developers and are loaded from several external
 web sites.
 
---- a/README.md
-+++ b/README.md
+Index: pyvenv-el.git/README.md
+===================================================================
+--- pyvenv-el.git.orig/README.md
++++ pyvenv-el.git/README.md
 @@ -1,8 +1,5 @@
  # pyvenv.el, Python virtual environment support for Emacs
  
diff --git a/pyvenv.el b/pyvenv.el
index c75f755..6c04286 100644
--- a/pyvenv.el
+++ b/pyvenv.el
@@ -38,6 +38,7 @@
 
 (require 'eshell)
 (require 'json)
+(require 'subr-x)
 
 ;; User customization
 
@@ -122,6 +123,12 @@ in `pyvenv-activate'."
 Do not set this variable directly; use `pyvenv-activate' or
 `pyvenv-workon'.")
 
+(defvar pyvenv-virtual-env-path-directories nil
+  "Directories added to PATH by the current virtual environment.
+
+Do not set this variable directly; use `pyvenv-activate' or
+`pyvenv-workon'.")
+
 (defvar pyvenv-virtual-env-name nil
   "The name of the current virtual environment.
 
@@ -163,13 +170,7 @@ This is usually the base name of `pyvenv-virtual-env'.")
 ;; Internal code.
 
 (defvar pyvenv-old-process-environment nil
-  "The old process environment before the last activate.")
-
-(defvar pyvenv-old-exec-path nil
-  "The old exec path before the last activate.")
-
-(defvar pyvenv-old-eshell-path nil
-  "The old eshell path before the last activate.")
+  "The old process environment that needs to be restored after deactivating the current environment.")
 
 
 (defun pyvenv-create (venv-name python-executable)
@@ -228,50 +229,16 @@ This is usually the base name of `pyvenv-virtual-env'.")
            (directory-file-name
             (file-name-directory
              (directory-file-name directory))))))
-  ;; Preserve variables from being overwritten.
-  (let ((old-exec-path exec-path)
-        (old-eshell-path eshell-path-env)
-        (old-process-environment process-environment))
-    (unwind-protect
-        (pyvenv-run-virtualenvwrapper-hook "pre_activate" pyvenv-virtual-env)
-      (setq exec-path old-exec-path
-            eshell-path-env old-eshell-path
-            process-environment old-process-environment)))
+  (pyvenv-run-virtualenvwrapper-hook "pre_activate" nil pyvenv-virtual-env)
   (run-hooks 'pyvenv-pre-activate-hooks)
-  (let ((new-directories (append
-                          ;; Unix
-                          (when (file-exists-p (format "%s/bin" directory))
-                            (list (format "%s/bin" directory)))
-                          ;; Windows
-                          (when (file-exists-p (format "%s/Scripts" directory))
-                            (list (format "%s/Scripts" directory)
-                                  ;; Apparently, some virtualenv
-                                  ;; versions on windows put the
-                                  ;; python.exe in the virtualenv root
-                                  ;; for some reason?
-                                  directory)))))
-    (setq pyvenv-old-exec-path exec-path
-          pyvenv-old-eshell-path eshell-path-env
-          pyvenv-old-process-environment process-environment
-          ;; For some reason, Emacs adds some directories to `exec-path'
-          ;; but not to `process-environment'?
-          exec-path (append new-directories exec-path)
-          ;; set eshell path to same as exec-path
-          eshell-path-env (mapconcat 'identity exec-path ":")
-          process-environment (append
-                               (list
-                                (format "VIRTUAL_ENV=%s" directory)
-                                (format "PATH=%s"
-                                        (mapconcat 'identity
-                                                   (append new-directories
-                                                           (split-string (getenv "PATH")
-                                                                         path-separator))
-                                                   path-separator))
-                                ;; No "=" means to unset
-                                "PYTHONHOME")
-                               process-environment)
-          ))
-  (pyvenv-run-virtualenvwrapper-hook "post_activate")
+  (setq pyvenv-virtual-env-path-directories (pyvenv--virtual-env-bin-dirs directory)
+        ;; Variables that must be reset during deactivation.
+        pyvenv-old-process-environment (list (cons "PYTHONHOME" (getenv "PYTHONHOME"))
+                                       (cons "VIRTUAL_ENV" nil)))
+  (setenv "VIRTUAL_ENV" directory)
+  (setenv "PYTHONHOME" nil)
+  (pyvenv--add-dirs-to-PATH pyvenv-virtual-env-path-directories)
+  (pyvenv-run-virtualenvwrapper-hook "post_activate" 'propagate-env)
   (run-hooks 'pyvenv-post-activate-hooks))
 
 ;;;###autoload
@@ -279,31 +246,19 @@ This is usually the base name of `pyvenv-virtual-env'.")
   "Deactivate any current virtual environment."
   (interactive)
   (when pyvenv-virtual-env
-    (pyvenv-run-virtualenvwrapper-hook "pre_deactivate")
-    (run-hooks 'pyvenv-pre-deactivate-hooks))
-  (when pyvenv-old-process-environment
-    (setq process-environment pyvenv-old-process-environment
-          pyvenv-old-process-environment nil))
-  (when pyvenv-old-exec-path
-    (setq exec-path pyvenv-old-exec-path
-          pyvenv-old-exec-path nil))
-  (when pyvenv-old-eshell-path
-    (setq eshell-path-env pyvenv-old-eshell-path
-          pyvenv-old-eshell-path nil))
-  (when pyvenv-virtual-env
-    ;; Make sure this does not change `exec-path', as $PATH is
-    ;; different
-    (let ((old-exec-path exec-path)
-          (old-eshell-path eshell-path-env)
-          (old-process-environment process-environment))
-      (unwind-protect
-          (pyvenv-run-virtualenvwrapper-hook "post_deactivate"
-                                             pyvenv-virtual-env)
-        (setq exec-path old-exec-path
-              eshell-path-env old-eshell-path
-              process-environment old-process-environment)))
+    (pyvenv-run-virtualenvwrapper-hook "pre_deactivate" 'propagate-env)
+    (run-hooks 'pyvenv-pre-deactivate-hooks)
+    (pyvenv--remove-dirs-from-PATH (pyvenv--virtual-env-bin-dirs pyvenv-virtual-env))
+    (dolist (envvar pyvenv-old-process-environment)
+      (setenv (car envvar) (cdr envvar)))
+    ;; Make sure PROPAGATE-ENV is nil here, so that it does not change
+    ;; `exec-path', as $PATH is different
+    (pyvenv-run-virtualenvwrapper-hook "post_deactivate"
+                                 nil
+                                 pyvenv-virtual-env)
     (run-hooks 'pyvenv-post-deactivate-hooks))
   (setq pyvenv-virtual-env nil
+        pyvenv-virtual-env-path-directories nil
         pyvenv-virtual-env-name nil
         python-shell-virtualenv-root nil
         python-shell-virtualenv-path nil))
@@ -446,14 +401,14 @@ to that virtualenv."
                                      pyvenv-workon pyvenv-virtual-env-name))))
       (pyvenv-workon pyvenv-workon)))))
 
-(defun pyvenv-run-virtualenvwrapper-hook (hook &rest args)
+(defun pyvenv-run-virtualenvwrapper-hook (hook &optional propagate-env &rest args)
   "Run a virtualenvwrapper hook, and update the environment.
 
 This will run a virtualenvwrapper hook and update the local
 environment accordingly.
 
-CAREFUL! This will modify your `process-environment' and
-`exec-path'."
+CAREFUL! If PROPAGATE-ENV is non-nil, this will modify your
+`process-environment' and `exec-path'."
   (when (pyvenv-virtualenvwrapper-supported)
     (with-temp-buffer
       (let ((tmpfile (make-temp-file "pyvenv-virtualenvwrapper-"))
@@ -470,32 +425,75 @@ CAREFUL! This will modify your `process-environment' and
                                (cons hook args))
                        (cons hook args)))
               (call-process-shell-command
-               (format ". '%s' ; python -c 'import os, json; print(\"\\n=-=-=\"); print(json.dumps(dict(os.environ)))'"
-                       tmpfile)
+               (mapconcat 'identity
+                          (list
+                           (format "%s -c 'import os, json; print(json.dumps(dict(os.environ)))'"
+                                   pyvenv-virtualenvwrapper-python)
+                           (format ". '%s'" tmpfile)
+                           (format
+                            "%s -c 'import os, json; print(\"\\n=-=-=\"); print(json.dumps(dict(os.environ)))'"
+                            pyvenv-virtualenvwrapper-python))
+                          "; ")
                nil t nil))
           (delete-file tmpfile)))
       (goto-char (point-min))
-      (when (and (not (re-search-forward "No module named '?virtualenvwrapper'?" nil t))
-                 (re-search-forward "\n=-=-=\n" nil t))
-        (let ((output (buffer-substring (point-min)
-                                        (match-beginning 0))))
-          (when (> (length output) 0)
-            (with-help-window "*Virtualenvwrapper Hook Output*"
-              (with-current-buffer "*Virtualenvwrapper Hook Output*"
-                (let ((inhibit-read-only t))
-                  (erase-buffer)
-                  (insert
-                   (format
-                    "Output from the virtualenvwrapper hook %s:\n\n"
-                    hook)
-                   output))))))
-        (dolist (binding (json-read))
-          (let ((env (format "%s=%s" (car binding) (cdr binding))))
-            (when (not (member env process-environment))
-              (setq process-environment (cons env process-environment))))
-          (when (eq (car binding) 'PATH)
-            (setq exec-path (split-string (cdr binding)
-                                          path-separator))))))))
+      (when (not (re-search-forward "No module named '?virtualenvwrapper'?" nil t))
+        (let* ((json-key-type 'string)
+               (env-before (json-read))
+               (hook-output-start-pos (point))
+               (hook-output-end-pos (when (re-search-forward "\n=-=-=\n" nil t)
+                                      (match-beginning 0)))
+               (env-after (when hook-output-end-pos (json-read))))
+          (when hook-output-end-pos
+            (let ((output (string-trim (buffer-substring hook-output-start-pos
+                                            hook-output-end-pos))))
+              (when (> (length output) 0)
+                (with-help-window "*Virtualenvwrapper Hook Output*"
+                  (with-current-buffer "*Virtualenvwrapper Hook Output*"
+                    (let ((inhibit-read-only t))
+                      (erase-buffer)
+                      (insert
+                       (format
+                        "Output from the virtualenvwrapper hook %s:\n\n"
+                        hook)
+                       output))))))
+            (when propagate-env
+              (dolist (binding (pyvenv--env-diff (sort env-before (lambda (x y) (string-lessp (car x) (car y))))
+                                           (sort env-after (lambda (x y) (string-lessp (car x) (car y))))))
+                (setenv (car binding) (cdr binding))
+                (when (eq (car binding) 'PATH)
+                  (let ((new-path-elts (split-string (cdr binding)
+                                                     path-separator)))
+                    (setq exec-path new-path-elts)
+                    (setq-default eshell-path-env new-path-elts)))))))))))
+
+
+(defun pyvenv--env-diff (env-before env-after)
+  "Calculate diff between ENV-BEFORE alist and ENV-AFTER alist.
+
+Both ENV-BEFORE and ENV-AFTER must be sorted alists of (STR . STR)."
+  (let (env-diff)
+    (while (or env-before env-after)
+      (cond
+       ;; K-V are the same, both proceed to the next one.
+       ((equal (car-safe env-before) (car-safe env-after))
+        (setq env-before (cdr env-before)
+              env-after (cdr env-after)))
+
+       ;; env-after is missing one element: add (K-before . nil) to diff
+       ((and env-before (or (null env-after) (string-lessp (caar env-before)
+                                                           (caar env-after))))
+        (setq env-diff (cons (cons (caar env-before) nil) env-diff)
+              env-before (cdr env-before)))
+       ;; Otherwise: add env-after element to the diff, progress env-after,
+       ;; progress env-before only if keys matched.
+       (t
+        (setq env-diff (cons (car env-after) env-diff))
+        (when (equal (caar env-after) (caar env-before))
+          (setq env-before (cdr env-before)))
+        (setq env-after (cdr env-after)))))
+    (nreverse env-diff)))
+
 
 ;;;###autoload
 (defun pyvenv-restart-python ()
@@ -544,6 +542,71 @@ This is the value of $WORKON_HOME or ~/.virtualenvs."
 Right now, this just checks if WORKON_HOME is set."
   (getenv "WORKON_HOME"))
 
+(defun pyvenv--virtual-env-bin-dirs (virtual-env)
+  (let ((virtual-env
+	 (if (string= "/" (directory-file-name virtual-env))
+	     ""
+	   (directory-file-name virtual-env))))
+   (append
+    ;; Unix
+    (when (file-exists-p (format "%s/bin" virtual-env))
+      (list (format "%s/bin" virtual-env)))
+    ;; Windows
+    (when (file-exists-p (format "%s/Scripts" virtual-env))
+      (list (format "%s/Scripts" virtual-env)
+            ;; Apparently, some virtualenv
+            ;; versions on windows put the
+            ;; python.exe in the virtualenv root
+            ;; for some reason?
+            virtual-env)))))
+
+(defun pyvenv--replace-once-destructive (list oldvalue newvalue)
+  "Replace one element equal to OLDVALUE with NEWVALUE values in LIST."
+  (let ((cur-elt list))
+    (while (and cur-elt (not (equal oldvalue (car cur-elt))))
+      (setq cur-elt (cdr cur-elt)))
+    (when cur-elt (setcar cur-elt newvalue))))
+
+(defun pyvenv--remove-many-once (values-to-remove list)
+  "Return a copy of LIST with each element from VALUES-TO-REMOVE removed once."
+  ;; Non-interned symbol is not eq to anything but itself.
+  (let ((values-to-remove (copy-sequence values-to-remove))
+        (sentinel (make-symbol "sentinel")))
+    (delq sentinel
+          (mapcar (lambda (x)
+                    (if (pyvenv--replace-once-destructive values-to-remove x sentinel)
+                        sentinel
+                      x))
+                  list))))
+
+(defun pyvenv--prepend-to-pathsep-string (values-to-prepend str)
+  "Prepend values from VALUES-TO-PREPEND list to path-delimited STR."
+  (mapconcat 'identity
+             (append values-to-prepend (split-string str path-separator))
+             path-separator))
+
+(defun pyvenv--remove-from-pathsep-string (values-to-remove str)
+  "Remove all values from VALUES-TO-REMOVE list from path-delimited STR."
+  (mapconcat 'identity
+             (pyvenv--remove-many-once values-to-remove (split-string str path-separator))
+             path-separator))
+
+(defun pyvenv--add-dirs-to-PATH (dirs-to-add)
+  "Add DIRS-TO-ADD to different variables related to execution paths."
+  (let* ((new-eshell-path-env (pyvenv--prepend-to-pathsep-string dirs-to-add (default-value 'eshell-path-env)))
+         (new-path-envvar (pyvenv--prepend-to-pathsep-string dirs-to-add (getenv "PATH"))))
+    (setq exec-path (append dirs-to-add exec-path))
+    (setq-default eshell-path-env new-eshell-path-env)
+    (setenv "PATH" new-path-envvar)))
+
+(defun pyvenv--remove-dirs-from-PATH (dirs-to-remove)
+  "Remove DIRS-TO-REMOVE from different variables related to execution paths."
+  (let* ((new-eshell-path-env (pyvenv--remove-from-pathsep-string dirs-to-remove (default-value 'eshell-path-env)))
+         (new-path-envvar (pyvenv--remove-from-pathsep-string dirs-to-remove (getenv "PATH"))))
+    (setq exec-path (pyvenv--remove-many-once dirs-to-remove exec-path))
+    (setq-default eshell-path-env new-eshell-path-env)
+    (setenv "PATH" new-path-envvar)))
+
 ;;; Compatibility
 
 (when (not (fboundp 'file-name-base))
diff --git a/test/pyvenv-deactivate-test.el b/test/pyvenv-deactivate-test.el
index b1a9ffc..5adaf34 100644
--- a/test/pyvenv-deactivate-test.el
+++ b/test/pyvenv-deactivate-test.el
@@ -17,7 +17,8 @@
       (pyvenv-deactivate)
       (should (f-same? pre-deactivate-venv venv1))
       (should (f-same? post-deactivate-venv venv1))
-      (should (equal process-environment orig-process-environment))
+      (should (equal (canonicalize-environment process-environment)
+                     (canonicalize-environment orig-process-environment)))
       (should (equal exec-path orig-exec-path))
       (should (equal pyvenv-virtual-env nil))
       (should (equal pyvenv-virtual-env-name nil))
@@ -29,7 +30,8 @@
         ;; Called for both, but the last one was for the second
         (should (f-same? pre-deactivate-venv venv2))
         (should (f-same? post-deactivate-venv venv2))
-        (should (equal process-environment orig-process-environment))
+        (should (equal (canonicalize-environment process-environment)
+                       (canonicalize-environment orig-process-environment)))
         (should (equal exec-path orig-exec-path))
         (should (equal pyvenv-virtual-env nil))
         (should (equal pyvenv-virtual-env-name nil))
diff --git a/test/pyvenv-env-diff-test.el b/test/pyvenv-env-diff-test.el
new file mode 100644
index 0000000..f7029cf
--- /dev/null
+++ b/test/pyvenv-env-diff-test.el
@@ -0,0 +1,16 @@
+(ert-deftest pyvenv--env-diff ()
+  ;; This test has a simple convention: lowercase strings are old values,
+  ;; uppercase strings are new values.
+  (should (equal (pyvenv--env-diff '() '())
+                 '()))
+  (should (equal (pyvenv--env-diff '((a . "a")) '())
+                 '((a . nil))))
+  (should (equal (pyvenv--env-diff '() '((a . "A")))
+                 '((a . "A"))))
+
+  (should (equal (pyvenv--env-diff '((a . "a")) '((b . "B")))
+                 '((a . nil) (b . "B"))))
+  (should (equal (pyvenv--env-diff '((c . "c")) '((b . "B")))
+                 '((b . "B") (c . nil))))
+  (should (equal (pyvenv--env-diff '((b . "b")) '((a . "A") (c . "C")))
+                 '((a . "A") (b . nil) (c . "C")))))
diff --git a/test/test-helper.el b/test/test-helper.el
index e8b7fc4..25f272d 100644
--- a/test/test-helper.el
+++ b/test/test-helper.el
@@ -24,3 +24,14 @@
      (write-region "" nil (concat ,name "/bin/activate"))
      ,@body))
 (put 'with-temp-virtualenv 'lisp-indent-function 'defun)
+
+
+(defun canonicalize-environment (process-environment-list)
+  "Prepare PROCESS-ENVIRONMENT-LIST variable for comparison."
+  (let (result)
+    ;; Dedupe by var name.
+    (mapc (lambda (x) (cl-pushnew (split-string x "=") result :key 'car :test 'equal))
+          process-environment)
+    ;; Delete vars that were unset, and sort the result.
+    (sort (cl-delete-if-not 'cdr result)
+          (lambda (x y) (string-lessp (car x) (car y))))))

Debdiff

File lists identical (after any substitutions)

No differences were encountered in the control files

More details

Full run details