├── .bumpversion.cfg ├── .gitignore ├── .travis.yml ├── Cask ├── Makefile ├── README.md ├── pyvenv.el ├── scripts └── release └── test ├── pyvenv-activate-test.el ├── pyvenv-deactivate-test.el ├── pyvenv-env-diff-test.el ├── pyvenv-hook-dir-test.el ├── pyvenv-mode-test.el ├── pyvenv-track-virtualenv.el ├── pyvenv-virtualenv-list-test.el ├── pyvenv-workon-home-test.el ├── pyvenv-workon-test.el └── test-helper.el /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.21 3 | parse = (?P\d+)\.(?P\d+) 4 | serialize = {major}.{minor} 5 | files = pyvenv.el 6 | commit = True 7 | tag = True 8 | tag_name = v{new_version} 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cask 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: emacs-lisp 2 | sudo: no 3 | env: 4 | # - EVM_EMACS=emacs-24.1-travis 5 | # - EVM_EMACS=emacs-24.2-travis 6 | - EVM_EMACS=emacs-24.3-travis 7 | - EVM_EMACS=emacs-24.4-travis 8 | - EVM_EMACS=emacs-24.5-travis 9 | before_install: 10 | - curl -fsSkL https://gist.github.com/rejeep/ebcd57c3af83b049833b/raw > travis.sh && source ./travis.sh 11 | - evm install $EVM_EMACS --use --skip 12 | - cask 13 | script: 14 | - make test 15 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package "pyvenv" "1.1" "Python virtualenv support for Emacs") 5 | 6 | (package-file "pyvenv.el") 7 | 8 | (development 9 | (depends-on "ert-runner") 10 | (depends-on "mocker")) 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test test-all cask 2 | 3 | EMACS ?= emacs 4 | VERSION=$(shell sed -ne 's/^;; Version: \(.*\)/\1/p' pyvenv.el) 5 | 6 | all: test 7 | 8 | test: 9 | cask exec ert-runner --quiet 10 | 11 | test-all: clean cask 12 | cask exec ert-runner --quiet 13 | EMACS=emacs-24.1 cask exec ert-runner --quiet 14 | EMACS=emacs-24.2 cask exec ert-runner --quiet 15 | EMACS=emacs-24.3 cask exec ert-runner --quiet 16 | EMACS=emacs-24.4 cask exec ert-runner --quiet 17 | EMACS=emacs-24.5 cask exec ert-runner --quiet 18 | 19 | cask: 20 | cask install 21 | EMACS=emacs-24.1 cask install 22 | EMACS=emacs-24.2 cask install 23 | EMACS=emacs-24.3 cask install 24 | EMACS=emacs-24.4 cask install 25 | EMACS=emacs-24.5 cask install 26 | 27 | compile: 28 | $(EMACS) -batch -L . -f batch-byte-compile *.el 29 | 30 | clean: 31 | rm -rf .cask dist 32 | find -name '*.elc' -delete 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyvenv.el, Python virtual environment support for Emacs 2 | 3 | ![Travis-CI Build Status](https://secure.travis-ci.org/jorgenschaefer/pyvenv.png) 4 | [![MELPA Stable](http://stable.melpa.org/packages/pyvenv-badge.svg)](http://stable.melpa.org/#/pyvenv) 5 | 6 | This is a simple global minor mode which will replicate the changes 7 | done by virtualenv activation inside Emacs. 8 | 9 | The main entry points are `pyvenv-activate`, which queries the user 10 | for a virtual environment directory to activate, and `pyvenv-workon`, 11 | which queries for a virtual environment in `$WORKON_HOME` (from 12 | virtualenvwrapper.sh). 13 | 14 | ## Similar Projects 15 | 16 | [virtualenv.el](https://github.com/aculich/virtualenv.el) is the 17 | original virtualenv implementation for Emacs. I used it for a long 18 | time, but didn’t like some of the design decisions. 19 | 20 | For example, it does not modify `process-environment` so does not set 21 | a virtual environment for `M-x compile` and other external processes. 22 | Also, `M-x virtualenv-workon` requires a prefix argument to actually 23 | change the current virtual environment. And it does not support 24 | virtualenvwrapper’s hooks, which I use to set up a working 25 | environment. 26 | 27 | All in all, too much magic for too little gain. So I figured I’d write 28 | my own. Still, it’s an excellent package and I’m very grateful to have 29 | used it for a long time. 30 | -------------------------------------------------------------------------------- /pyvenv.el: -------------------------------------------------------------------------------- 1 | ;;; pyvenv.el --- Python virtual environment interface -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2013-2017 Jorgen Schaefer 4 | 5 | ;; Author: Jorgen Schaefer 6 | ;; URL: http://github.com/jorgenschaefer/pyvenv 7 | ;; Version: 1.21 8 | ;; Keywords: Python, Virtualenv, Tools 9 | 10 | ;; This program is free software; you can redistribute it and/or 11 | ;; modify it under the terms of the GNU General Public License 12 | ;; as published by the Free Software Foundation; either version 3 13 | ;; of the License, or (at your option) any later version. 14 | 15 | ;; This program is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with this program. If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; This is a simple global minor mode which will replicate the changes 26 | ;; done by virtualenv activation inside Emacs. 27 | 28 | ;; The main entry points are `pyvenv-activate', which queries the user 29 | ;; for a virtual environment directory to activate, and 30 | ;; `pyvenv-workon', which queries for a virtual environment in 31 | ;; $WORKON_HOME (from virtualenvwrapper.sh). 32 | 33 | ;; If you want your inferior Python processes to be restarted 34 | ;; automatically when you switch your virtual environment, add 35 | ;; `pyvenv-restart-python' to `pyvenv-post-activate-hooks'. 36 | 37 | ;;; Code: 38 | 39 | (require 'eshell) 40 | (require 'json) 41 | (require 'subr-x) 42 | 43 | ;; User customization 44 | 45 | (defgroup pyvenv nil 46 | "Python Virtual Environment Interface." 47 | :prefix "pyvenv-" 48 | :group 'languages) 49 | 50 | (defcustom pyvenv-workon nil 51 | "The intended virtualenv in the virtualenvwrapper directory. 52 | 53 | This is rarely useful to set globally. Rather, set this in file- 54 | or directory-local variables using \\[add-file-local-variable] or 55 | \\[add-dir-local-variable]. 56 | 57 | When `pyvenv-mode' is enabled, pyvenv will switch to this 58 | virtualenv. If a virtualenv is already enabled, it will ask first." 59 | :type 'pyvenv-workon 60 | :safe #'stringp 61 | :group 'pyvenv) 62 | 63 | (defcustom pyvenv-activate nil 64 | "The intended virtualenv directory. 65 | 66 | This is rarely useful to set globally. Rather, set this in file- 67 | or directory-local variables using \\[add-file-local-variable] or 68 | \\[add-dir-local-variable]. 69 | 70 | When `pyvenv-mode' is enabled, pyvenv will switch to this 71 | virtualenv. If a virtualenv is already enabled, it will ask first." 72 | :type 'directory 73 | :safe #'stringp 74 | :group 'pyvenv) 75 | 76 | (defcustom pyvenv-tracking-ask-before-change nil 77 | "Non-nil means pyvenv will ask before automatically changing a virtualenv. 78 | 79 | This can happen when a new file is opened with a buffer-local 80 | value (from file-local or directory-local variables) for 81 | `pyvenv-workon' or `pyvenv-workon', or if `pyvenv-tracking-mode' 82 | is active, after every command." 83 | :type 'boolean 84 | :group 'pyvenv) 85 | 86 | (defcustom pyvenv-virtualenvwrapper-python 87 | (or (getenv "VIRTUALENVWRAPPER_PYTHON") 88 | (executable-find "python3") 89 | (executable-find "python") 90 | (executable-find "py") 91 | (executable-find "pythonw") 92 | "python") 93 | "The python process which has access to the virtualenvwrapper module. 94 | 95 | This should be $VIRTUALENVWRAPPER_PYTHON outside of Emacs, but 96 | virtualenvwrapper.sh does not export that variable. We make an 97 | educated guess, but that can be off." 98 | :type '(file :must-match t) 99 | :safe #'file-directory-p 100 | :group 'pyvenv) 101 | 102 | (defcustom pyvenv-exec-shell 103 | (or (executable-find "bash") 104 | (executable-find "sh") 105 | shell-file-name) 106 | "The path to a POSIX compliant shell to use for running 107 | virtualenv hooks. Useful if you use a non-POSIX shell (e.g. 108 | fish)." 109 | :type '(file :must-match t) 110 | :group 'pyvenv) 111 | 112 | (defcustom pyvenv-default-virtual-env-name nil 113 | "Default directory to use when prompting for a virtualenv directory 114 | in `pyvenv-activate'." 115 | :type 'string 116 | :group 'pyvenv) 117 | 118 | ;; API for other libraries 119 | 120 | (defvar pyvenv-virtual-env nil 121 | "The current virtual environment. 122 | 123 | Do not set this variable directly; use `pyvenv-activate' or 124 | `pyvenv-workon'.") 125 | 126 | (defvar pyvenv-virtual-env-path-directories nil 127 | "Directories added to PATH by the current virtual environment. 128 | 129 | Do not set this variable directly; use `pyvenv-activate' or 130 | `pyvenv-workon'.") 131 | 132 | (defvar pyvenv-virtual-env-name nil 133 | "The name of the current virtual environment. 134 | 135 | This is usually the base name of `pyvenv-virtual-env'.") 136 | 137 | 138 | (defvar pyvenv-pre-create-hooks nil 139 | "Hooks run before a virtual environment is created.") 140 | 141 | 142 | (defvar pyvenv-post-create-hooks nil 143 | "Hooks run after a virtual environment is created.") 144 | 145 | 146 | (defvar pyvenv-pre-activate-hooks nil 147 | "Hooks run before a virtual environment is activated. 148 | 149 | `pyvenv-virtual-env' is already set.") 150 | 151 | (defvar pyvenv-post-activate-hooks nil 152 | "Hooks run after a virtual environment is activated. 153 | 154 | `pyvenv-virtual-env' is set.") 155 | 156 | (defvar pyvenv-pre-deactivate-hooks nil 157 | "Hooks run before a virtual environment is deactivated. 158 | 159 | `pyvenv-virtual-env' is set.") 160 | 161 | (defvar pyvenv-post-deactivate-hooks nil 162 | "Hooks run after a virtual environment is deactivated. 163 | 164 | `pyvenv-virtual-env' is still set.") 165 | 166 | (defvar pyvenv-mode-line-indicator '(pyvenv-virtual-env-name 167 | ("[" pyvenv-virtual-env-name "] ")) 168 | "How `pyvenv-mode' will indicate the current environment in the mode line.") 169 | 170 | ;; Internal code. 171 | 172 | (defvar pyvenv-old-process-environment nil 173 | "The old process environment that needs to be restored after deactivating the current environment.") 174 | 175 | 176 | (defun pyvenv-create (venv-name python-executable) 177 | "Create virtualenv. VENV-NAME PYTHON-EXECUTABLE." 178 | (interactive (list 179 | (read-from-minibuffer "Name of virtual environment: ") 180 | (let ((dir (if pyvenv-virtualenvwrapper-python 181 | (file-name-directory pyvenv-virtualenvwrapper-python) 182 | nil)) 183 | (initial (if pyvenv-virtualenvwrapper-python 184 | (file-name-base pyvenv-virtualenvwrapper-python) 185 | nil))) 186 | (read-file-name "Python interpreter to use: " dir nil nil initial)))) 187 | (let ((venv-dir (concat (file-name-as-directory (pyvenv-workon-home)) 188 | venv-name))) 189 | (unless (file-exists-p venv-dir) 190 | (run-hooks 'pyvenv-pre-create-hooks) 191 | (cond 192 | ((executable-find "virtualenv") 193 | (with-current-buffer (generate-new-buffer "*virtualenv*") 194 | (call-process "virtualenv" nil t t 195 | "-p" python-executable venv-dir) 196 | (display-buffer (current-buffer)))) 197 | ((= 0 (call-process python-executable nil nil nil 198 | "-m" "venv" "-h")) 199 | (with-current-buffer (generate-new-buffer "*venv*") 200 | (call-process python-executable nil t t 201 | "-m" "venv" venv-dir) 202 | (display-buffer (current-buffer)))) 203 | (t 204 | (error "Pyvenv necessitates the 'virtualenv' python package"))) 205 | (run-hooks 'pyvenv-post-create-hooks)) 206 | (pyvenv-activate venv-dir))) 207 | 208 | 209 | ;;;###autoload 210 | (defun pyvenv-activate (directory) 211 | "Activate the virtual environment in DIRECTORY." 212 | (interactive (list (read-directory-name "Activate venv: " nil nil nil 213 | pyvenv-default-virtual-env-name))) 214 | (setq directory (expand-file-name directory)) 215 | (pyvenv-deactivate) 216 | (setq pyvenv-virtual-env (file-name-as-directory directory) 217 | pyvenv-virtual-env-name (file-name-nondirectory 218 | (directory-file-name directory)) 219 | python-shell-virtualenv-path directory 220 | python-shell-virtualenv-root directory) 221 | ;; Set venv name as parent directory for generic directories or for 222 | ;; the user's default venv name 223 | (when (or (member pyvenv-virtual-env-name '("venv" ".venv" "env" ".env")) 224 | (and pyvenv-default-virtual-env-name 225 | (string= pyvenv-default-virtual-env-name 226 | pyvenv-virtual-env-name))) 227 | (setq pyvenv-virtual-env-name 228 | (file-name-nondirectory 229 | (directory-file-name 230 | (file-name-directory 231 | (directory-file-name directory)))))) 232 | (pyvenv-run-virtualenvwrapper-hook "pre_activate" nil pyvenv-virtual-env) 233 | (run-hooks 'pyvenv-pre-activate-hooks) 234 | (setq pyvenv-virtual-env-path-directories (pyvenv--virtual-env-bin-dirs directory) 235 | ;; Variables that must be reset during deactivation. 236 | pyvenv-old-process-environment (list (cons "PYTHONHOME" (getenv "PYTHONHOME")) 237 | (cons "VIRTUAL_ENV" nil))) 238 | (setenv "VIRTUAL_ENV" directory) 239 | (setenv "PYTHONHOME" nil) 240 | (pyvenv--add-dirs-to-PATH pyvenv-virtual-env-path-directories) 241 | (pyvenv-run-virtualenvwrapper-hook "post_activate" 'propagate-env) 242 | (run-hooks 'pyvenv-post-activate-hooks)) 243 | 244 | ;;;###autoload 245 | (defun pyvenv-deactivate () 246 | "Deactivate any current virtual environment." 247 | (interactive) 248 | (when pyvenv-virtual-env 249 | (pyvenv-run-virtualenvwrapper-hook "pre_deactivate" 'propagate-env) 250 | (run-hooks 'pyvenv-pre-deactivate-hooks) 251 | (pyvenv--remove-dirs-from-PATH (pyvenv--virtual-env-bin-dirs pyvenv-virtual-env)) 252 | (dolist (envvar pyvenv-old-process-environment) 253 | (setenv (car envvar) (cdr envvar))) 254 | ;; Make sure PROPAGATE-ENV is nil here, so that it does not change 255 | ;; `exec-path', as $PATH is different 256 | (pyvenv-run-virtualenvwrapper-hook "post_deactivate" 257 | nil 258 | pyvenv-virtual-env) 259 | (run-hooks 'pyvenv-post-deactivate-hooks)) 260 | (setq pyvenv-virtual-env nil 261 | pyvenv-virtual-env-path-directories nil 262 | pyvenv-virtual-env-name nil 263 | python-shell-virtualenv-root nil 264 | python-shell-virtualenv-path nil)) 265 | 266 | (defvar pyvenv-workon-history nil 267 | "Prompt history for `pyvenv-workon'.") 268 | 269 | ;;;###autoload 270 | (defun pyvenv-workon (name) 271 | "Activate a virtual environment from $WORKON_HOME. 272 | 273 | If the virtual environment NAME is already active, this function 274 | does not try to reactivate the environment." 275 | (interactive 276 | (list 277 | (completing-read "Work on: " (pyvenv-virtualenv-list) 278 | nil t nil 'pyvenv-workon-history nil nil))) 279 | (unless (member name (list "" nil pyvenv-virtual-env-name)) 280 | (pyvenv-activate (format "%s/%s" 281 | (pyvenv-workon-home) 282 | name)))) 283 | 284 | (defun pyvenv-virtualenv-list (&optional noerror) 285 | "Prompt the user for a name in $WORKON_HOME. 286 | 287 | If NOERROR is set, do not raise an error if WORKON_HOME is not 288 | configured." 289 | (let ((workon-home (pyvenv-workon-home)) 290 | (result nil)) 291 | (if (not (file-directory-p workon-home)) 292 | (when (not noerror) 293 | (error "Can't find a workon home directory, set $WORKON_HOME")) 294 | (dolist (name (directory-files workon-home)) 295 | (when (or (file-exists-p (format "%s/%s/bin/activate" 296 | workon-home name)) 297 | (file-exists-p (format "%s/%s/bin/python" 298 | workon-home name)) 299 | (file-exists-p (format "%s/%s/Scripts/activate.bat" 300 | workon-home name)) 301 | (file-exists-p (format "%s/%s/python.exe" 302 | workon-home name))) 303 | (setq result (cons name result)))) 304 | (sort result (lambda (a b) 305 | (string-lessp (downcase a) 306 | (downcase b))))))) 307 | 308 | (define-widget 'pyvenv-workon 'choice 309 | "Select an available virtualenv from virtualenvwrapper." 310 | :convert-widget 311 | (lambda (widget) 312 | (setq widget (widget-copy widget)) 313 | (widget-put widget 314 | :args (cons '(const :tag "None" nil) 315 | (mapcar (lambda (env) 316 | (list 'const env)) 317 | (pyvenv-virtualenv-list t)))) 318 | (widget-types-convert-widget widget)) 319 | 320 | :prompt-value (lambda (_widget prompt _value _unbound) 321 | (let ((name (completing-read 322 | prompt 323 | (cons "None" 324 | (pyvenv-virtualenv-list t)) 325 | nil t))) 326 | (if (equal name "None") 327 | nil 328 | name)))) 329 | 330 | (defvar pyvenv-mode-map (make-sparse-keymap) 331 | "The mode keymap for `pyvenv-mode'.") 332 | 333 | (easy-menu-define pyvenv-menu pyvenv-mode-map 334 | "Pyvenv Menu" 335 | '("Virtual Envs" 336 | :visible pyvenv-mode 337 | ("Workon" 338 | :help "Activate a virtualenvwrapper environment" 339 | :filter (lambda (&optional ignored) 340 | (mapcar (lambda (venv) 341 | (vector venv `(pyvenv-workon ,venv) 342 | :style 'radio 343 | :selected `(equal pyvenv-virtual-env-name 344 | ,venv))) 345 | (pyvenv-virtualenv-list t)))) 346 | ["Activate" pyvenv-activate 347 | :help "Activate a virtual environment by directory"] 348 | ["Deactivate" pyvenv-deactivate 349 | :help "Deactivate the current virtual environment" 350 | :active pyvenv-virtual-env 351 | :suffix pyvenv-virtual-env-name] 352 | ["Restart Python Processes" pyvenv-restart-python 353 | :help "Restart all Python processes to use the current environment"])) 354 | 355 | ;;;###autoload 356 | (define-minor-mode pyvenv-mode 357 | "Global minor mode for pyvenv. 358 | 359 | Will show the current virtualenv in the mode line, and respect a 360 | `pyvenv-workon' setting in files." 361 | :global t 362 | (cond 363 | (pyvenv-mode 364 | (add-to-list 'mode-line-misc-info '(pyvenv-mode pyvenv-mode-line-indicator)) 365 | (add-hook 'hack-local-variables-hook #'pyvenv-track-virtualenv)) 366 | ((not pyvenv-mode) 367 | (setq mode-line-misc-info (delete '(pyvenv-mode pyvenv-mode-line-indicator) 368 | mode-line-misc-info)) 369 | (remove-hook 'hack-local-variables-hook #'pyvenv-track-virtualenv)))) 370 | 371 | ;;;###autoload 372 | (define-minor-mode pyvenv-tracking-mode 373 | "Global minor mode to track the current virtualenv. 374 | 375 | When this mode is active, pyvenv will activate a buffer-specific 376 | virtualenv whenever the user switches to a buffer with a 377 | buffer-local `pyvenv-workon' or `pyvenv-activate' variable." 378 | :global t 379 | (if pyvenv-tracking-mode 380 | (add-hook 'post-command-hook 'pyvenv-track-virtualenv) 381 | (remove-hook 'post-command-hook 'pyvenv-track-virtualenv))) 382 | 383 | (defun pyvenv-track-virtualenv () 384 | "Set a virtualenv as specified for the current buffer. 385 | 386 | If either `pyvenv-activate' or `pyvenv-workon' are specified, and 387 | they specify a virtualenv different from the current one, switch 388 | to that virtualenv." 389 | (cond 390 | (pyvenv-activate 391 | (when (and (not (equal (file-name-as-directory pyvenv-activate) 392 | pyvenv-virtual-env)) 393 | (or (not pyvenv-tracking-ask-before-change) 394 | (y-or-n-p (format "Switch to virtualenv %s (currently %s)" 395 | pyvenv-activate pyvenv-virtual-env)))) 396 | (pyvenv-activate pyvenv-activate))) 397 | (pyvenv-workon 398 | (when (and (not (equal pyvenv-workon pyvenv-virtual-env-name)) 399 | (or (not pyvenv-tracking-ask-before-change) 400 | (y-or-n-p (format "Switch to virtualenv %s (currently %s)" 401 | pyvenv-workon pyvenv-virtual-env-name)))) 402 | (pyvenv-workon pyvenv-workon))))) 403 | 404 | (defun pyvenv-run-virtualenvwrapper-hook (hook &optional propagate-env &rest args) 405 | "Run a virtualenvwrapper hook, and update the environment. 406 | 407 | This will run a virtualenvwrapper hook and update the local 408 | environment accordingly. 409 | 410 | CAREFUL! If PROPAGATE-ENV is non-nil, this will modify your 411 | `process-environment' and `exec-path'." 412 | (when (pyvenv-virtualenvwrapper-supported) 413 | (with-temp-buffer 414 | (let ((tmpfile (make-temp-file "pyvenv-virtualenvwrapper-")) 415 | (shell-file-name pyvenv-exec-shell)) 416 | (unwind-protect 417 | (let ((default-directory (pyvenv-workon-home))) 418 | (apply #'call-process 419 | pyvenv-virtualenvwrapper-python 420 | nil t nil 421 | "-m" "virtualenvwrapper.hook_loader" 422 | "--script" tmpfile 423 | (if (getenv "HOOK_VERBOSE_OPTION") 424 | (cons (getenv "HOOK_VERBOSE_OPTION") 425 | (cons hook args)) 426 | (cons hook args))) 427 | (call-process-shell-command 428 | (mapconcat 'identity 429 | (list 430 | (format "%s -c 'import os, json; print(json.dumps(dict(os.environ)))'" 431 | pyvenv-virtualenvwrapper-python) 432 | (format ". '%s'" tmpfile) 433 | (format 434 | "%s -c 'import os, json; print(\"\\n=-=-=\"); print(json.dumps(dict(os.environ)))'" 435 | pyvenv-virtualenvwrapper-python)) 436 | "; ") 437 | nil t nil)) 438 | (delete-file tmpfile))) 439 | (goto-char (point-min)) 440 | (when (not (re-search-forward "No module named '?virtualenvwrapper'?" nil t)) 441 | (let* ((json-key-type 'string) 442 | (env-before (json-read)) 443 | (hook-output-start-pos (point)) 444 | (hook-output-end-pos (when (re-search-forward "\n=-=-=\n" nil t) 445 | (match-beginning 0))) 446 | (env-after (when hook-output-end-pos (json-read)))) 447 | (when hook-output-end-pos 448 | (let ((output (string-trim (buffer-substring hook-output-start-pos 449 | hook-output-end-pos)))) 450 | (when (> (length output) 0) 451 | (with-help-window "*Virtualenvwrapper Hook Output*" 452 | (with-current-buffer "*Virtualenvwrapper Hook Output*" 453 | (let ((inhibit-read-only t)) 454 | (erase-buffer) 455 | (insert 456 | (format 457 | "Output from the virtualenvwrapper hook %s:\n\n" 458 | hook) 459 | output)))))) 460 | (when propagate-env 461 | (dolist (binding (pyvenv--env-diff (sort env-before (lambda (x y) (string-lessp (car x) (car y)))) 462 | (sort env-after (lambda (x y) (string-lessp (car x) (car y)))))) 463 | (setenv (car binding) (cdr binding)) 464 | (when (eq (car binding) 'PATH) 465 | (let ((new-path-elts (split-string (cdr binding) 466 | path-separator))) 467 | (setq exec-path new-path-elts) 468 | (setq-default eshell-path-env new-path-elts))))))))))) 469 | 470 | 471 | (defun pyvenv--env-diff (env-before env-after) 472 | "Calculate diff between ENV-BEFORE alist and ENV-AFTER alist. 473 | 474 | Both ENV-BEFORE and ENV-AFTER must be sorted alists of (STR . STR)." 475 | (let (env-diff) 476 | (while (or env-before env-after) 477 | (cond 478 | ;; K-V are the same, both proceed to the next one. 479 | ((equal (car-safe env-before) (car-safe env-after)) 480 | (setq env-before (cdr env-before) 481 | env-after (cdr env-after))) 482 | 483 | ;; env-after is missing one element: add (K-before . nil) to diff 484 | ((and env-before (or (null env-after) (string-lessp (caar env-before) 485 | (caar env-after)))) 486 | (setq env-diff (cons (cons (caar env-before) nil) env-diff) 487 | env-before (cdr env-before))) 488 | ;; Otherwise: add env-after element to the diff, progress env-after, 489 | ;; progress env-before only if keys matched. 490 | (t 491 | (setq env-diff (cons (car env-after) env-diff)) 492 | (when (equal (caar env-after) (caar env-before)) 493 | (setq env-before (cdr env-before))) 494 | (setq env-after (cdr env-after))))) 495 | (nreverse env-diff))) 496 | 497 | 498 | ;;;###autoload 499 | (defun pyvenv-restart-python () 500 | "Restart Python inferior processes." 501 | (interactive) 502 | (dolist (buf (buffer-list)) 503 | (with-current-buffer buf 504 | (when (and (eq major-mode 'inferior-python-mode) 505 | (get-buffer-process buf)) 506 | (let ((cmd (combine-and-quote-strings (process-command 507 | (get-buffer-process buf)))) 508 | (dedicated (if (string-match "\\[.*\\]$" (buffer-name buf)) 509 | t 510 | nil)) 511 | (show nil)) 512 | (delete-process (get-buffer-process buf)) 513 | (goto-char (point-max)) 514 | (insert "\n\n" 515 | "###\n" 516 | (format "### Restarting in virtualenv %s (%s)\n" 517 | pyvenv-virtual-env-name pyvenv-virtual-env) 518 | "###\n" 519 | "\n\n") 520 | (run-python cmd dedicated show) 521 | (goto-char (point-max))))))) 522 | 523 | (defun pyvenv-hook-dir () 524 | "Return the current hook directory. 525 | 526 | This is usually the value of $VIRTUALENVWRAPPER_HOOK_DIR, but 527 | virtualenvwrapper has stopped exporting that variable, so we go 528 | back to the default of $WORKON_HOME or even just ~/.virtualenvs/." 529 | (or (getenv "VIRTUALENVWRAPPER_HOOK_DIR") 530 | (pyvenv-workon-home))) 531 | 532 | (defun pyvenv-workon-home () 533 | "Return the current workon home. 534 | 535 | This is the value of $WORKON_HOME or ~/.virtualenvs." 536 | (or (getenv "WORKON_HOME") 537 | (expand-file-name "~/.virtualenvs"))) 538 | 539 | (defun pyvenv-virtualenvwrapper-supported () 540 | "Return true iff virtualenvwrapper is supported. 541 | 542 | Right now, this just checks if WORKON_HOME is set." 543 | (getenv "WORKON_HOME")) 544 | 545 | (defun pyvenv--virtual-env-bin-dirs (virtual-env) 546 | (let ((virtual-env 547 | (if (string= "/" (directory-file-name virtual-env)) 548 | "" 549 | (directory-file-name virtual-env)))) 550 | (append 551 | ;; Unix 552 | (when (file-exists-p (format "%s/bin" virtual-env)) 553 | (list (format "%s/bin" virtual-env))) 554 | ;; Windows 555 | (when (file-exists-p (format "%s/Scripts" virtual-env)) 556 | (list (format "%s/Scripts" virtual-env) 557 | ;; Apparently, some virtualenv 558 | ;; versions on windows put the 559 | ;; python.exe in the virtualenv root 560 | ;; for some reason? 561 | virtual-env))))) 562 | 563 | (defun pyvenv--replace-once-destructive (list oldvalue newvalue) 564 | "Replace one element equal to OLDVALUE with NEWVALUE values in LIST." 565 | (let ((cur-elt list)) 566 | (while (and cur-elt (not (equal oldvalue (car cur-elt)))) 567 | (setq cur-elt (cdr cur-elt))) 568 | (when cur-elt (setcar cur-elt newvalue)))) 569 | 570 | (defun pyvenv--remove-many-once (values-to-remove list) 571 | "Return a copy of LIST with each element from VALUES-TO-REMOVE removed once." 572 | ;; Non-interned symbol is not eq to anything but itself. 573 | (let ((values-to-remove (copy-sequence values-to-remove)) 574 | (sentinel (make-symbol "sentinel"))) 575 | (delq sentinel 576 | (mapcar (lambda (x) 577 | (if (pyvenv--replace-once-destructive values-to-remove x sentinel) 578 | sentinel 579 | x)) 580 | list)))) 581 | 582 | (defun pyvenv--prepend-to-pathsep-string (values-to-prepend str) 583 | "Prepend values from VALUES-TO-PREPEND list to path-delimited STR." 584 | (mapconcat 'identity 585 | (append values-to-prepend (split-string str path-separator)) 586 | path-separator)) 587 | 588 | (defun pyvenv--remove-from-pathsep-string (values-to-remove str) 589 | "Remove all values from VALUES-TO-REMOVE list from path-delimited STR." 590 | (mapconcat 'identity 591 | (pyvenv--remove-many-once values-to-remove (split-string str path-separator)) 592 | path-separator)) 593 | 594 | (defun pyvenv--add-dirs-to-PATH (dirs-to-add) 595 | "Add DIRS-TO-ADD to different variables related to execution paths." 596 | (let* ((new-eshell-path-env (pyvenv--prepend-to-pathsep-string dirs-to-add (default-value 'eshell-path-env))) 597 | (new-path-envvar (pyvenv--prepend-to-pathsep-string dirs-to-add (getenv "PATH")))) 598 | (setq exec-path (append dirs-to-add exec-path)) 599 | (setq-default eshell-path-env new-eshell-path-env) 600 | (setenv "PATH" new-path-envvar))) 601 | 602 | (defun pyvenv--remove-dirs-from-PATH (dirs-to-remove) 603 | "Remove DIRS-TO-REMOVE from different variables related to execution paths." 604 | (let* ((new-eshell-path-env (pyvenv--remove-from-pathsep-string dirs-to-remove (default-value 'eshell-path-env))) 605 | (new-path-envvar (pyvenv--remove-from-pathsep-string dirs-to-remove (getenv "PATH")))) 606 | (setq exec-path (pyvenv--remove-many-once dirs-to-remove exec-path)) 607 | (setq-default eshell-path-env new-eshell-path-env) 608 | (setenv "PATH" new-path-envvar))) 609 | 610 | ;;; Compatibility 611 | 612 | (when (not (fboundp 'file-name-base)) 613 | ;; Emacs 24.3 614 | (defun file-name-base (&optional filename) 615 | "Return the base name of the FILENAME: no directory, no extension. 616 | FILENAME defaults to `buffer-file-name'." 617 | (file-name-sans-extension 618 | (file-name-nondirectory (or filename (buffer-file-name))))) 619 | ) 620 | 621 | (when (not (boundp 'mode-line-misc-info)) 622 | (defvar mode-line-misc-info nil 623 | "Compatibility variable for 24.3+") 624 | (let ((line mode-line-format)) 625 | (while line 626 | (when (eq 'which-func-mode 627 | (car-safe (car-safe (cdr line)))) 628 | (setcdr line (cons 'mode-line-misc-format 629 | (cdr line))) 630 | (setq line (cdr line))) 631 | (setq line (cdr line))))) 632 | 633 | (provide 'pyvenv) 634 | ;;; pyvenv.el ends here 635 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | cd "$(dirname "$0")/.." 5 | 6 | main() { 7 | run bumpversion minor 8 | run git push 9 | run git push --tags 10 | } 11 | 12 | run() { 13 | echo "\$ $*" 14 | "$@" 15 | } 16 | 17 | main 18 | -------------------------------------------------------------------------------- /test/pyvenv-activate-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-activate () 2 | "`pyvenv-activate' should set the correct variables." 3 | (with-temp-virtualenv tmpdir 4 | (let* ((process-environment process-environment) 5 | (exec-path exec-path) 6 | (pyvenv-virtual-env nil) 7 | (pyvenv-virtual-env-name nil) 8 | (pyvenv-old-process-environment nil) 9 | (pyvenv-old-exec-path nil) 10 | (pre-activate-venv nil) 11 | (post-activate-venv nil) 12 | (pyvenv-pre-activate-hooks 13 | (list (lambda () 14 | (setq pre-activate-venv pyvenv-virtual-env)))) 15 | (pyvenv-post-activate-hooks 16 | (list (lambda () 17 | (setq post-activate-venv pyvenv-virtual-env))))) 18 | (pyvenv-activate tmpdir) 19 | (should (f-equal? pre-activate-venv tmpdir)) 20 | (should (f-equal? post-activate-venv tmpdir)) 21 | (should (f-equal? pyvenv-virtual-env tmpdir)) 22 | (should (equal pyvenv-virtual-env-name (file-name-base tmpdir))) 23 | (should (equal python-shell-virtualenv-path tmpdir)) 24 | (should (equal python-shell-virtualenv-root tmpdir)) 25 | (should (equal (getenv "VIRTUAL_ENV") 26 | tmpdir)) 27 | (should (string-match (format "^%s/bin" (regexp-quote tmpdir)) 28 | (getenv "PATH"))) 29 | (should (equal (getenv "PYTHONHOME") 30 | nil)) 31 | (should (member (format "%s/bin" tmpdir) 32 | exec-path))))) 33 | -------------------------------------------------------------------------------- /test/pyvenv-deactivate-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-deactivate () 2 | "Should set variables back to the original values." 3 | (let* ((process-environment process-environment) 4 | (exec-path exec-path) 5 | (orig-process-environment process-environment) 6 | (orig-exec-path exec-path) 7 | (pre-deactivate-venv nil) 8 | (post-deactivate-venv nil) 9 | (pyvenv-pre-deactivate-hooks 10 | (list (lambda () 11 | (setq pre-deactivate-venv pyvenv-virtual-env)))) 12 | (pyvenv-post-deactivate-hooks 13 | (list (lambda () 14 | (setq post-deactivate-venv pyvenv-virtual-env))))) 15 | (with-temp-virtualenv venv1 16 | (pyvenv-activate venv1) 17 | (pyvenv-deactivate) 18 | (should (f-same? pre-deactivate-venv venv1)) 19 | (should (f-same? post-deactivate-venv venv1)) 20 | (should (equal (canonicalize-environment process-environment) 21 | (canonicalize-environment orig-process-environment))) 22 | (should (equal exec-path orig-exec-path)) 23 | (should (equal pyvenv-virtual-env nil)) 24 | (should (equal pyvenv-virtual-env-name nil)) 25 | (with-temp-virtualenv venv2 26 | ;; Should retain the originals, too. 27 | (pyvenv-activate venv1) 28 | (pyvenv-activate venv2) 29 | (pyvenv-deactivate) 30 | ;; Called for both, but the last one was for the second 31 | (should (f-same? pre-deactivate-venv venv2)) 32 | (should (f-same? post-deactivate-venv venv2)) 33 | (should (equal (canonicalize-environment process-environment) 34 | (canonicalize-environment orig-process-environment))) 35 | (should (equal exec-path orig-exec-path)) 36 | (should (equal pyvenv-virtual-env nil)) 37 | (should (equal pyvenv-virtual-env-name nil)) 38 | (should (equal python-shell-virtualenv-root nil)) 39 | (should (equal python-shell-virtualenv-path nil)))))) 40 | -------------------------------------------------------------------------------- /test/pyvenv-env-diff-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv--env-diff () 2 | ;; This test has a simple convention: lowercase strings are old values, 3 | ;; uppercase strings are new values. 4 | (should (equal (pyvenv--env-diff '() '()) 5 | '())) 6 | (should (equal (pyvenv--env-diff '((a . "a")) '()) 7 | '((a . nil)))) 8 | (should (equal (pyvenv--env-diff '() '((a . "A"))) 9 | '((a . "A")))) 10 | 11 | (should (equal (pyvenv--env-diff '((a . "a")) '((b . "B"))) 12 | '((a . nil) (b . "B")))) 13 | (should (equal (pyvenv--env-diff '((c . "c")) '((b . "B"))) 14 | '((b . "B") (c . nil)))) 15 | (should (equal (pyvenv--env-diff '((b . "b")) '((a . "A") (c . "C"))) 16 | '((a . "A") (b . nil) (c . "C"))))) 17 | -------------------------------------------------------------------------------- /test/pyvenv-hook-dir-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-hook-dir () 2 | ;; Should return VIRTUALENVWRAPPER_HOOK_DIR 3 | (let ((process-environment (cons "VIRTUALENVWRAPPER_HOOK_DIR=/hook_dir" 4 | process-environment))) 5 | (should (equal (pyvenv-hook-dir) 6 | "/hook_dir"))) 7 | ;; Else, should return WORKON_HOME 8 | (let ((process-environment (append '("VIRTUALENVWRAPPER_HOOK_DIR" 9 | "WORKON_HOME=/workon_home") 10 | process-environment))) 11 | (should (equal (pyvenv-hook-dir) 12 | "/workon_home"))) 13 | ;; Else, should return ~/.virtualenvs 14 | (let ((process-environment (append '("VIRTUALENVWRAPPER_HOOK_DIR" 15 | "WORKON_HOME") 16 | process-environment))) 17 | (should (equal (pyvenv-hook-dir) 18 | (expand-file-name "~/.virtualenvs")))) 19 | ) 20 | -------------------------------------------------------------------------------- /test/pyvenv-mode-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-mode-should-add-functions () 2 | (with-temp-buffer 3 | (pyvenv-mode 1) 4 | 5 | (should pyvenv-mode) 6 | (should (member '(pyvenv-mode pyvenv-mode-line-indicator) 7 | mode-line-misc-info)) 8 | (should (memq #'pyvenv-track-virtualenv 9 | hack-local-variables-hook)))) 10 | 11 | (ert-deftest pyvenv-mode-should-remove-functions () 12 | (with-temp-buffer 13 | (pyvenv-mode 1) 14 | (pyvenv-mode -1) 15 | 16 | (should-not pyvenv-mode) 17 | (should-not (memq '(pyvenv-mode pyvenv-mode-line-indicator) 18 | mode-line-misc-info)) 19 | (should-not (memq #'pyvenv-track-virtualenv 20 | hack-local-variables-hook)))) 21 | -------------------------------------------------------------------------------- /test/pyvenv-track-virtualenv.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-track-virtualenv-should-activate-venv-if-none-set () 2 | (with-temp-buffer 3 | (mocker-let ((pyvenv-activate (env) 4 | ((:input '("/test/path"))))) 5 | (let ((pyvenv-activate "/test/path") 6 | (pyvenv-virtual-env nil)) 7 | 8 | (pyvenv-track-virtualenv))))) 9 | 10 | (ert-deftest pyvenv-track-virtualenv-should-ask-if-activate-different-venv () 11 | (with-temp-buffer 12 | (mocker-let ((pyvenv-activate (env) 13 | ((:input '("/test/path")))) 14 | (y-or-n-p (arg) ((:input '("Switch to virtual env /test/path (currently /other/path)? ") 15 | :output t)))) 16 | (let ((pyvenv-activate "/test/path") 17 | (pyvenv-virtual-env "/other/path")) 18 | 19 | (pyvenv-track-virtualenv))))) 20 | 21 | (ert-deftest pyvenv-track-virtualenv-should-do-nothing-if-activate-same-venv () 22 | (with-temp-buffer 23 | (let ((pyvenv-activate "/test/path") 24 | ;; activate takes precedence over workon 25 | (pyvenv-workon "foo") 26 | (pyvenv-virtual-env "/test/path")) 27 | (pyvenv-track-virtualenv) 28 | 29 | (should (equal pyvenv-virtual-env "/test/path"))))) 30 | 31 | (ert-deftest pyvenv-track-virtualenv-should-workon-venv-if-none-set () 32 | (with-temp-buffer 33 | (mocker-let ((pyvenv-workon (env) 34 | ((:input '("test-venv"))))) 35 | (let ((pyvenv-workon "test-venv") 36 | (pyvenv-virtual-env nil)) 37 | 38 | (pyvenv-track-virtualenv))))) 39 | 40 | (ert-deftest pyvenv-track-virtualenv-should-ask-if-workon-different-venv () 41 | (with-temp-buffer 42 | (mocker-let ((pyvenv-workon (env) 43 | ((:input '("test-venv")))) 44 | (y-or-n-p (arg) ((:input '("Switch to virtual env test-venv (currently other-venv)? ") 45 | :output t)))) 46 | (let ((pyvenv-workon "test-venv") 47 | (pyvenv-virtual-env "/some/path/other-venv") 48 | (pyvenv-virtual-env-name "other-venv")) 49 | 50 | (pyvenv-track-virtualenv))))) 51 | 52 | (ert-deftest pyvenv-track-virtualenv-should-do-nothing-if-workon-same-venv () 53 | (with-temp-buffer 54 | (let ((pyvenv-workon "test-venv") 55 | (pyvenv-virtual-env-name "test-venv")) 56 | (pyvenv-track-virtualenv) 57 | 58 | (should (equal pyvenv-virtual-env-name "test-venv"))))) 59 | -------------------------------------------------------------------------------- /test/pyvenv-virtualenv-list-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-virtualenv-list () 2 | "Ensure all correct virtualenvs are returned" 3 | (with-temp-dir workon-home 4 | (let ((process-environment process-environment)) 5 | (setq process-environment 6 | (cons (format "WORKON_HOME=%s/does-not-exist" workon-home) 7 | process-environment)) 8 | (should-error (pyvenv-virtualenv-list)) 9 | (should (null (pyvenv-virtualenv-list t))) 10 | (setq process-environment 11 | (cons (format "WORKON_HOME=%s" workon-home) 12 | (cdr process-environment))) 13 | (should (equal (pyvenv-virtualenv-list) nil)) 14 | (make-directory (format "%s/no-venv-dir" workon-home)) 15 | (write-region "" nil (format "%s/no-venv-file" workon-home)) 16 | (dolist (name '("venv-a" "venv-B" "venv-C")) 17 | (make-directory (format "%s/%s" workon-home name)) 18 | (make-directory (format "%s/%s/bin" workon-home name)) 19 | (write-region "" nil (format "%s/%s/bin/activate" workon-home name))) 20 | (should (equal (pyvenv-virtualenv-list) 21 | '("venv-a" "venv-B" "venv-C")))))) 22 | -------------------------------------------------------------------------------- /test/pyvenv-workon-home-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-hook-dir () 2 | ;; Else, should return WORKON_HOME 3 | (let ((process-environment (cons "WORKON_HOME=/workon_home" 4 | process-environment))) 5 | (should (equal (pyvenv-workon-home) 6 | "/workon_home"))) 7 | ;; Else, should return ~/.virtualenvs 8 | (let ((process-environment (cons "WORKON_HOME" 9 | process-environment))) 10 | (should (equal (pyvenv-workon-home) 11 | (expand-file-name "~/.virtualenvs")))) 12 | ) 13 | -------------------------------------------------------------------------------- /test/pyvenv-workon-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest pyvenv-workon () 2 | ;; Should expand the virtualenv directory then call activate. 3 | (mocker-let ((pyvenv-activate (dir) 4 | ((:input-matcher (lambda (env) 5 | (string-match "/test-env$" 6 | env)) 7 | :output t)))) 8 | (should (pyvenv-workon "test-env"))) 9 | 10 | ;; Should not activate anything when the user just hits RET. 11 | (mocker-let ((pyvenv-activate (dir) 12 | ((:min-occur 0 :max-occur 0)))) 13 | (should-not (pyvenv-workon ""))) 14 | 15 | ;; Some completion frameworks can return nil for the default, see 16 | ;; https://github.com/jorgenschaefer/elpy/issues/144 17 | (mocker-let ((pyvenv-activate (dir) 18 | ((:min-occur 0 :max-occur 0)))) 19 | (should-not (pyvenv-workon nil)))) 20 | -------------------------------------------------------------------------------- /test/test-helper.el: -------------------------------------------------------------------------------- 1 | (require 'f) 2 | (add-to-list 'load-path (f-parent (f-dirname (f-this-file)))) 3 | (require 'pyvenv) 4 | (require 'mocker) 5 | 6 | ;; Fixing a bug in dflet where it would not work in 24.3.1 for some 7 | ;; reason. 8 | (when (version= emacs-version "24.3.1") 9 | (require 'cl)) 10 | 11 | (defmacro with-temp-dir (name &rest body) 12 | (let ((var (make-symbol "temp-dir"))) 13 | `(let* ((,var (make-temp-file "pyvenv-test-" t)) 14 | (,name ,var)) 15 | (unwind-protect 16 | (progn 17 | ,@body) 18 | (delete-directory ,var t))))) 19 | (put 'with-temp-dir 'lisp-indent-function 'defun) 20 | 21 | (defmacro with-temp-virtualenv (name &rest body) 22 | `(with-temp-dir ,name 23 | (make-directory (concat ,name "/bin")) 24 | (write-region "" nil (concat ,name "/bin/activate")) 25 | ,@body)) 26 | (put 'with-temp-virtualenv 'lisp-indent-function 'defun) 27 | 28 | 29 | (defun canonicalize-environment (process-environment-list) 30 | "Prepare PROCESS-ENVIRONMENT-LIST variable for comparison." 31 | (let (result) 32 | ;; Dedupe by var name. 33 | (mapc (lambda (x) (cl-pushnew (split-string x "=") result :key 'car :test 'equal)) 34 | process-environment) 35 | ;; Delete vars that were unset, and sort the result. 36 | (sort (cl-delete-if-not 'cdr result) 37 | (lambda (x y) (string-lessp (car x) (car y)))))) 38 | --------------------------------------------------------------------------------