;;; pyvenv.el --- Python virtual environment interface -*- lexical-binding: t -*-

;; Copyright (C) 2013-2017  Jorgen Schaefer <contact@jorgenschaefer.de>

;; Author: Jorgen Schaefer <contact@jorgenschaefer.de>
;; URL: http://github.com/jorgenschaefer/pyvenv
;; Package-Version: 20211014.707
;; Package-Commit: 31ea715f2164dd611e7fc77b26390ef3ca93509b
;; Version: 1.21
;; Keywords: Python, Virtualenv, Tools

;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License
;; as published by the Free Software Foundation; either version 3
;; of the License, or (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This is a simple global minor mode which will replicate the changes
;; done by virtualenv activation inside Emacs.

;; The main entry points are `pyvenv-activate', which queries the user
;; for a virtual environment directory to activate, and
;; `pyvenv-workon', which queries for a virtual environment in
;; $WORKON_HOME (from virtualenvwrapper.sh).

;; If you want your inferior Python processes to be restarted
;; automatically when you switch your virtual environment, add
;; `pyvenv-restart-python' to `pyvenv-post-activate-hooks'.

;;; Code:

(require 'eshell)
(require 'json)
(require 'subr-x)

;; User customization

(defgroup pyvenv nil
  "Python Virtual Environment Interface."
  :prefix "pyvenv-"
  :group 'languages)

(defcustom pyvenv-workon nil
  "The intended virtualenv in the virtualenvwrapper directory.

This is rarely useful to set globally. Rather, set this in file-
or directory-local variables using \\[add-file-local-variable] or
\\[add-dir-local-variable].

When `pyvenv-mode' is enabled, pyvenv will switch to this
virtualenv. If a virtualenv is already enabled, it will ask first."
  :type 'pyvenv-workon
  :safe #'stringp
  :group 'pyvenv)

(defcustom pyvenv-activate nil
  "The intended virtualenv directory.

This is rarely useful to set globally. Rather, set this in file-
or directory-local variables using \\[add-file-local-variable] or
\\[add-dir-local-variable].

When `pyvenv-mode' is enabled, pyvenv will switch to this
virtualenv. If a virtualenv is already enabled, it will ask first."
  :type 'directory
  :safe #'stringp
  :group 'pyvenv)

(defcustom pyvenv-tracking-ask-before-change nil
  "Non-nil means pyvenv will ask before automatically changing a virtualenv.

This can happen when a new file is opened with a buffer-local
value (from file-local or directory-local variables) for
`pyvenv-workon' or `pyvenv-workon', or if `pyvenv-tracking-mode'
is active, after every command."
  :type 'boolean
  :group 'pyvenv)

(defcustom pyvenv-virtualenvwrapper-python
  (or (getenv "VIRTUALENVWRAPPER_PYTHON")
      (executable-find "python3")
      (executable-find "python")
      (executable-find "py")
      (executable-find "pythonw")
      "python")
  "The python process which has access to the virtualenvwrapper module.

This should be $VIRTUALENVWRAPPER_PYTHON outside of Emacs, but
virtualenvwrapper.sh does not export that variable. We make an
educated guess, but that can be off."
  :type '(file :must-match t)
  :safe #'file-directory-p
  :group 'pyvenv)

(defcustom pyvenv-exec-shell
  (or (executable-find "bash")
      (executable-find "sh")
      shell-file-name)
  "The path to a POSIX compliant shell to use for running
  virtualenv hooks. Useful if you use a non-POSIX shell (e.g.
  fish)."
  :type '(file :must-match t)
  :group 'pyvenv)

(defcustom pyvenv-default-virtual-env-name nil
  "Default directory to use when prompting for a virtualenv directory
in `pyvenv-activate'."
  :type 'string
  :group 'pyvenv)

;; API for other libraries

(defvar pyvenv-virtual-env nil
  "The current virtual environment.

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.

This is usually the base name of `pyvenv-virtual-env'.")


(defvar pyvenv-pre-create-hooks nil
  "Hooks run before a virtual environment is created.")


(defvar pyvenv-post-create-hooks nil
  "Hooks run after a virtual environment is created.")


(defvar pyvenv-pre-activate-hooks nil
  "Hooks run before a virtual environment is activated.

`pyvenv-virtual-env' is already set.")

(defvar pyvenv-post-activate-hooks nil
  "Hooks run after a virtual environment is activated.

`pyvenv-virtual-env' is set.")

(defvar pyvenv-pre-deactivate-hooks nil
  "Hooks run before a virtual environment is deactivated.

`pyvenv-virtual-env' is set.")

(defvar pyvenv-post-deactivate-hooks nil
  "Hooks run after a virtual environment is deactivated.

`pyvenv-virtual-env' is still set.")

(defvar pyvenv-mode-line-indicator '(pyvenv-virtual-env-name
                                     ("[" pyvenv-virtual-env-name "] "))
  "How `pyvenv-mode' will indicate the current environment in the mode line.")

;; Internal code.

(defvar pyvenv-old-process-environment nil
  "The old process environment that needs to be restored after deactivating the current environment.")


(defun pyvenv-create (venv-name python-executable)
  "Create virtualenv.  VENV-NAME  PYTHON-EXECUTABLE."
  (interactive (list
                (read-from-minibuffer "Name of virtual environment: ")
                (let ((dir (if pyvenv-virtualenvwrapper-python
                               (file-name-directory pyvenv-virtualenvwrapper-python)
                             nil))
                      (initial (if pyvenv-virtualenvwrapper-python
                                   (file-name-base pyvenv-virtualenvwrapper-python)
                                 nil)))
                  (read-file-name "Python interpreter to use: " dir nil nil initial))))
  (let ((venv-dir (concat (file-name-as-directory (pyvenv-workon-home))
                          venv-name)))
    (unless (file-exists-p venv-dir)
      (run-hooks 'pyvenv-pre-create-hooks)
      (cond
       ((executable-find "virtualenv")
        (with-current-buffer (generate-new-buffer "*virtualenv*")
          (call-process "virtualenv" nil t t
                        "-p" python-executable venv-dir)
          (display-buffer (current-buffer))))
       ((= 0 (call-process python-executable nil nil nil
                           "-m" "venv" "-h"))
        (with-current-buffer (generate-new-buffer "*venv*")
          (call-process python-executable nil t t
                        "-m" "venv" venv-dir)
          (display-buffer (current-buffer))))
       (t
        (error "Pyvenv necessitates the 'virtualenv' python package")))
      (run-hooks 'pyvenv-post-create-hooks))
    (pyvenv-activate venv-dir)))


;;;###autoload
(defun pyvenv-activate (directory)
  "Activate the virtual environment in DIRECTORY."
  (interactive (list (read-directory-name "Activate venv: " nil nil nil
					  pyvenv-default-virtual-env-name)))
  (setq directory (expand-file-name directory))
  (pyvenv-deactivate)
  (setq pyvenv-virtual-env (file-name-as-directory directory)
        pyvenv-virtual-env-name (file-name-nondirectory
                                 (directory-file-name directory))
        python-shell-virtualenv-path directory
        python-shell-virtualenv-root directory)
  ;; Set venv name as parent directory for generic directories or for
  ;; the user's default venv name
  (when (or (member pyvenv-virtual-env-name '("venv" ".venv" "env" ".env"))
	    (and pyvenv-default-virtual-env-name
		 (string= pyvenv-default-virtual-env-name
			  pyvenv-virtual-env-name)))
    (setq pyvenv-virtual-env-name
          (file-name-nondirectory
           (directory-file-name
            (file-name-directory
             (directory-file-name directory))))))
  (pyvenv-run-virtualenvwrapper-hook "pre_activate" nil pyvenv-virtual-env)
  (run-hooks 'pyvenv-pre-activate-hooks)
  (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
(defun pyvenv-deactivate ()
  "Deactivate any current virtual environment."
  (interactive)
  (when pyvenv-virtual-env
    (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))

(defvar pyvenv-workon-history nil
  "Prompt history for `pyvenv-workon'.")

;;;###autoload
(defun pyvenv-workon (name)
  "Activate a virtual environment from $WORKON_HOME.

If the virtual environment NAME is already active, this function
does not try to reactivate the environment."
  (interactive
   (list
    (completing-read "Work on: " (pyvenv-virtualenv-list)
                     nil t nil 'pyvenv-workon-history nil nil)))
  (unless (member name (list "" nil pyvenv-virtual-env-name))
    (pyvenv-activate (format "%s/%s"
                             (pyvenv-workon-home)
                             name))))

(defun pyvenv-virtualenv-list (&optional noerror)
  "Prompt the user for a name in $WORKON_HOME.

If NOERROR is set, do not raise an error if WORKON_HOME is not
configured."
  (let ((workon-home (pyvenv-workon-home))
        (result nil))
    (if (not (file-directory-p workon-home))
        (when (not noerror)
          (error "Can't find a workon home directory, set $WORKON_HOME"))
      (dolist (name (directory-files workon-home))
        (when (or (file-exists-p (format "%s/%s/bin/activate"
                                         workon-home name))
                  (file-exists-p (format "%s/%s/bin/python"
                                         workon-home name))
                  (file-exists-p (format "%s/%s/Scripts/activate.bat"
                                         workon-home name))
                  (file-exists-p (format "%s/%s/python.exe"
                                         workon-home name)))
          (setq result (cons name result))))
      (sort result (lambda (a b)
                     (string-lessp (downcase a)
                                   (downcase b)))))))

(define-widget 'pyvenv-workon 'choice
  "Select an available virtualenv from virtualenvwrapper."
  :convert-widget
  (lambda (widget)
    (setq widget (widget-copy widget))
    (widget-put widget
                :args (cons '(const :tag "None" nil)
                            (mapcar (lambda (env)
                                      (list 'const env))
                                    (pyvenv-virtualenv-list t))))
    (widget-types-convert-widget widget))

  :prompt-value (lambda (_widget prompt _value _unbound)
                  (let ((name (completing-read
                               prompt
                               (cons "None"
                                     (pyvenv-virtualenv-list t))
                               nil t)))
                    (if (equal name "None")
                        nil
                      name))))

(defvar pyvenv-mode-map (make-sparse-keymap)
  "The mode keymap for `pyvenv-mode'.")

(easy-menu-define pyvenv-menu pyvenv-mode-map
  "Pyvenv Menu"
  '("Virtual Envs"
    :visible pyvenv-mode
    ("Workon"
     :help "Activate a virtualenvwrapper environment"
     :filter (lambda (&optional ignored)
               (mapcar (lambda (venv)
                         (vector venv `(pyvenv-workon ,venv)
                                 :style 'radio
                                 :selected `(equal pyvenv-virtual-env-name
                                                   ,venv)))
                       (pyvenv-virtualenv-list t))))
    ["Activate" pyvenv-activate
     :help "Activate a virtual environment by directory"]
    ["Deactivate" pyvenv-deactivate
     :help "Deactivate the current virtual environment"
     :active pyvenv-virtual-env
     :suffix pyvenv-virtual-env-name]
    ["Restart Python Processes" pyvenv-restart-python
     :help "Restart all Python processes to use the current environment"]))

;;;###autoload
(define-minor-mode pyvenv-mode
  "Global minor mode for pyvenv.

Will show the current virtualenv in the mode line, and respect a
`pyvenv-workon' setting in files."
  :global t
  (cond
   (pyvenv-mode
    (add-to-list 'mode-line-misc-info '(pyvenv-mode pyvenv-mode-line-indicator))
    (add-hook 'hack-local-variables-hook #'pyvenv-track-virtualenv))
   ((not pyvenv-mode)
    (setq mode-line-misc-info (delete '(pyvenv-mode pyvenv-mode-line-indicator)
                                      mode-line-misc-info))
    (remove-hook 'hack-local-variables-hook #'pyvenv-track-virtualenv))))

;;;###autoload
(define-minor-mode pyvenv-tracking-mode
  "Global minor mode to track the current virtualenv.

When this mode is active, pyvenv will activate a buffer-specific
virtualenv whenever the user switches to a buffer with a
buffer-local `pyvenv-workon' or `pyvenv-activate' variable."
  :global t
  (if pyvenv-tracking-mode
      (add-hook 'post-command-hook 'pyvenv-track-virtualenv)
    (remove-hook 'post-command-hook 'pyvenv-track-virtualenv)))

(defun pyvenv-track-virtualenv ()
  "Set a virtualenv as specified for the current buffer.

If either `pyvenv-activate' or `pyvenv-workon' are specified, and
they specify a virtualenv different from the current one, switch
to that virtualenv."
  (cond
   (pyvenv-activate
    (when (and (not (equal (file-name-as-directory pyvenv-activate)
                           pyvenv-virtual-env))
               (or (not pyvenv-tracking-ask-before-change)
                   (y-or-n-p (format "Switch to virtualenv %s (currently %s)"
                                     pyvenv-activate pyvenv-virtual-env))))
      (pyvenv-activate pyvenv-activate)))
   (pyvenv-workon
    (when (and (not (equal pyvenv-workon pyvenv-virtual-env-name))
               (or (not pyvenv-tracking-ask-before-change)
                   (y-or-n-p (format "Switch to virtualenv %s (currently %s)"
                                     pyvenv-workon pyvenv-virtual-env-name))))
      (pyvenv-workon pyvenv-workon)))))

(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! 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-"))
            (shell-file-name pyvenv-exec-shell))
        (unwind-protect
            (let ((default-directory (pyvenv-workon-home)))
              (apply #'call-process
                     pyvenv-virtualenvwrapper-python
                     nil t nil
                     "-m" "virtualenvwrapper.hook_loader"
                     "--script" tmpfile
                     (if (getenv "HOOK_VERBOSE_OPTION")
                         (cons (getenv "HOOK_VERBOSE_OPTION")
                               (cons hook args))
                       (cons hook args)))
              (call-process-shell-command
               (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 (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 ()
  "Restart Python inferior processes."
  (interactive)
  (dolist (buf (buffer-list))
    (with-current-buffer buf
      (when (and (eq major-mode 'inferior-python-mode)
                 (get-buffer-process buf))
        (let ((cmd (combine-and-quote-strings (process-command
                                               (get-buffer-process buf))))
              (dedicated (if (string-match "\\[.*\\]$" (buffer-name buf))
                             t
                           nil))
              (show nil))
          (delete-process (get-buffer-process buf))
          (goto-char (point-max))
          (insert "\n\n"
                  "###\n"
                  (format "### Restarting in virtualenv %s (%s)\n"
                          pyvenv-virtual-env-name pyvenv-virtual-env)
                  "###\n"
                  "\n\n")
          (run-python cmd dedicated show)
          (goto-char (point-max)))))))

(defun pyvenv-hook-dir ()
  "Return the current hook directory.

This is usually the value of $VIRTUALENVWRAPPER_HOOK_DIR, but
virtualenvwrapper has stopped exporting that variable, so we go
back to the default of $WORKON_HOME or even just ~/.virtualenvs/."
  (or (getenv "VIRTUALENVWRAPPER_HOOK_DIR")
      (pyvenv-workon-home)))

(defun pyvenv-workon-home ()
  "Return the current workon home.

This is the value of $WORKON_HOME or ~/.virtualenvs."
  (or (getenv "WORKON_HOME")
      (expand-file-name "~/.virtualenvs")))

(defun pyvenv-virtualenvwrapper-supported ()
  "Return true iff virtualenvwrapper is supported.

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))
  ;; Emacs 24.3
  (defun file-name-base (&optional filename)
    "Return the base name of the FILENAME: no directory, no extension.
FILENAME defaults to `buffer-file-name'."
    (file-name-sans-extension
     (file-name-nondirectory (or filename (buffer-file-name)))))
  )

(when (not (boundp 'mode-line-misc-info))
  (defvar mode-line-misc-info nil
    "Compatibility variable for 24.3+")
  (let ((line mode-line-format))
    (while line
      (when (eq 'which-func-mode
                (car-safe (car-safe (cdr line))))
        (setcdr line (cons 'mode-line-misc-format
                           (cdr line)))
        (setq line (cdr line)))
      (setq line (cdr line)))))

(provide 'pyvenv)
;;; pyvenv.el ends here