Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

557 lines
21 KiB

  1. ;;; pyvenv.el --- Python virtual environment interface -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2013-2017 Jorgen Schaefer <contact@jorgenschaefer.de>
  3. ;; Author: Jorgen Schaefer <contact@jorgenschaefer.de>
  4. ;; URL: http://github.com/jorgenschaefer/pyvenv
  5. ;; Package-Version: 1.21
  6. ;; Package-Commit: 103d2f158ef2a760741682e18741e44107c68f3f
  7. ;; Version: 1.21
  8. ;; Keywords: Python, Virtualenv, Tools
  9. ;; This program is free software; you can redistribute it and/or
  10. ;; modify it under the terms of the GNU General Public License
  11. ;; as published by the Free Software Foundation; either version 3
  12. ;; of the License, or (at your option) any later version.
  13. ;; This program is distributed in the hope that it will be useful,
  14. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;; GNU General Public License for more details.
  17. ;; You should have received a copy of the GNU General Public License
  18. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. ;;; Commentary:
  20. ;; This is a simple global minor mode which will replicate the changes
  21. ;; done by virtualenv activation inside Emacs.
  22. ;; The main entry points are `pyvenv-activate', which queries the user
  23. ;; for a virtual environment directory to activate, and
  24. ;; `pyvenv-workon', which queries for a virtual environment in
  25. ;; $WORKON_HOME (from virtualenvwrapper.sh).
  26. ;; If you want your inferior Python processes to be restarted
  27. ;; automatically when you switch your virtual environment, add
  28. ;; `pyvenv-restart-python' to `pyvenv-post-activate-hooks'.
  29. ;;; Code:
  30. (require 'eshell)
  31. (require 'json)
  32. ;; User customization
  33. (defgroup pyvenv nil
  34. "Python Virtual Environment Interface."
  35. :prefix "pyvenv-"
  36. :group 'languages)
  37. (defcustom pyvenv-workon nil
  38. "The intended virtualenv in the virtualenvwrapper directory.
  39. This is rarely useful to set globally. Rather, set this in file-
  40. or directory-local variables using \\[add-file-local-variable] or
  41. \\[add-dir-local-variable].
  42. When `pyvenv-mode' is enabled, pyvenv will switch to this
  43. virtualenv. If a virtualenv is already enabled, it will ask first."
  44. :type 'pyvenv-workon
  45. :safe #'stringp
  46. :group 'pyvenv)
  47. (defcustom pyvenv-activate nil
  48. "The intended virtualenv directory.
  49. This is rarely useful to set globally. Rather, set this in file-
  50. or directory-local variables using \\[add-file-local-variable] or
  51. \\[add-dir-local-variable].
  52. When `pyvenv-mode' is enabled, pyvenv will switch to this
  53. virtualenv. If a virtualenv is already enabled, it will ask first."
  54. :type 'directory
  55. :safe #'stringp
  56. :group 'pyvenv)
  57. (defcustom pyvenv-tracking-ask-before-change nil
  58. "Non-nil means pyvenv will ask before automatically changing a virtualenv.
  59. This can happen when a new file is opened with a buffer-local
  60. value (from file-local or directory-local variables) for
  61. `pyvenv-workon' or `pyvenv-workon', or if `pyvenv-tracking-mode'
  62. is active, after every command."
  63. :type 'boolean
  64. :group 'pyvenv)
  65. (defcustom pyvenv-virtualenvwrapper-python
  66. (or (getenv "VIRTUALENVWRAPPER_PYTHON")
  67. (executable-find "python")
  68. (executable-find "py")
  69. (executable-find "pythonw")
  70. "python")
  71. "The python process which has access to the virtualenvwrapper module.
  72. This should be $VIRTUALENVWRAPPER_PYTHON outside of Emacs, but
  73. virtualenvwrapper.sh does not export that variable. We make an
  74. educated guess, but that can be off."
  75. :type '(file :must-match t)
  76. :safe #'file-directory-p
  77. :group 'pyvenv)
  78. (defcustom pyvenv-exec-shell
  79. (or (executable-find "bash")
  80. (executable-find "sh")
  81. shell-file-name)
  82. "The path to a POSIX compliant shell to use for running
  83. virtualenv hooks. Useful if you use a non-POSIX shell (e.g.
  84. fish)."
  85. :type '(file :must-match t)
  86. :group 'pyvenv)
  87. ;; API for other libraries
  88. (defvar pyvenv-virtual-env nil
  89. "The current virtual environment.
  90. Do not set this variable directly; use `pyvenv-activate' or
  91. `pyvenv-workon'.")
  92. (defvar pyvenv-virtual-env-name nil
  93. "The name of the current virtual environment.
  94. This is usually the base name of `pyvenv-virtual-env'.")
  95. (defvar pyvenv-pre-create-hooks nil
  96. "Hooks run before a virtual environment is created.")
  97. (defvar pyvenv-post-create-hooks nil
  98. "Hooks run after a virtual environment is created.")
  99. (defvar pyvenv-pre-activate-hooks nil
  100. "Hooks run before a virtual environment is activated.
  101. `pyvenv-virtual-env' is already set.")
  102. (defvar pyvenv-post-activate-hooks nil
  103. "Hooks run after a virtual environment is activated.
  104. `pyvenv-virtual-env' is set.")
  105. (defvar pyvenv-pre-deactivate-hooks nil
  106. "Hooks run before a virtual environment is deactivated.
  107. `pyvenv-virtual-env' is set.")
  108. (defvar pyvenv-post-deactivate-hooks nil
  109. "Hooks run after a virtual environment is deactivated.
  110. `pyvenv-virtual-env' is still set.")
  111. (defvar pyvenv-mode-line-indicator '(pyvenv-virtual-env-name
  112. ("[" pyvenv-virtual-env-name "] "))
  113. "How `pyvenv-mode' will indicate the current environment in the mode line.")
  114. ;; Internal code.
  115. (defvar pyvenv-old-process-environment nil
  116. "The old process environment before the last activate.")
  117. (defvar pyvenv-old-exec-path nil
  118. "The old exec path before the last activate.")
  119. (defvar pyvenv-old-eshell-path nil
  120. "The old eshell path before the last activate.")
  121. (defun pyvenv-create (venv-name python-executable)
  122. "Create virtualenv. VENV-NAME PYTHON-EXECUTABLE."
  123. (interactive (list
  124. (read-from-minibuffer "Name of virtual environment: ")
  125. (read-file-name "Python interpreter to use: "
  126. (file-name-directory (executable-find "python"))
  127. nil nil "python")))
  128. (let ((venv-dir (concat (file-name-as-directory (pyvenv-workon-home))
  129. venv-name)))
  130. (unless (file-exists-p venv-dir)
  131. (run-hooks 'pyvenv-pre-create-hooks)
  132. (cond
  133. ((executable-find "virtualenv")
  134. (with-current-buffer (generate-new-buffer "*virtualenv*")
  135. (call-process "virtualenv" nil t t
  136. "-p" python-executable venv-dir)
  137. (display-buffer (current-buffer))))
  138. ((= 0 (call-process python-executable nil nil nil
  139. "-m" "venv" "-h"))
  140. (with-current-buffer (generate-new-buffer "*venv*")
  141. (call-process python-executable nil t t
  142. "-m" "venv" venv-dir)
  143. (display-buffer (current-buffer))))
  144. (t
  145. (error "Pyvenv necessitates the 'virtualenv' python package")))
  146. (run-hooks 'pyvenv-post-create-hooks))
  147. (pyvenv-activate venv-dir)))
  148. ;;;###autoload
  149. (defun pyvenv-activate (directory)
  150. "Activate the virtual environment in DIRECTORY."
  151. (interactive "DActivate venv: ")
  152. (setq directory (expand-file-name directory))
  153. (pyvenv-deactivate)
  154. (setq pyvenv-virtual-env (file-name-as-directory directory)
  155. pyvenv-virtual-env-name (file-name-nondirectory
  156. (directory-file-name directory))
  157. python-shell-virtualenv-path directory
  158. python-shell-virtualenv-root directory)
  159. ;; Set venv name as parent directory for generic directories
  160. (when (member pyvenv-virtual-env-name '("venv" ".venv"))
  161. (setq pyvenv-virtual-env-name
  162. (file-name-nondirectory
  163. (directory-file-name
  164. (file-name-directory
  165. (directory-file-name directory))))))
  166. ;; Preserve variables from being overwritten.
  167. (let ((old-exec-path exec-path)
  168. (old-eshell-path eshell-path-env)
  169. (old-process-environment process-environment))
  170. (unwind-protect
  171. (pyvenv-run-virtualenvwrapper-hook "pre_activate" pyvenv-virtual-env)
  172. (setq exec-path old-exec-path
  173. eshell-path-env old-eshell-path
  174. process-environment old-process-environment)))
  175. (run-hooks 'pyvenv-pre-activate-hooks)
  176. (let ((new-directories (append
  177. ;; Unix
  178. (when (file-exists-p (format "%s/bin" directory))
  179. (list (format "%s/bin" directory)))
  180. ;; Windows
  181. (when (file-exists-p (format "%s/Scripts" directory))
  182. (list (format "%s/Scripts" directory)
  183. ;; Apparently, some virtualenv
  184. ;; versions on windows put the
  185. ;; python.exe in the virtualenv root
  186. ;; for some reason?
  187. directory)))))
  188. (setq pyvenv-old-exec-path exec-path
  189. pyvenv-old-eshell-path eshell-path-env
  190. pyvenv-old-process-environment process-environment
  191. ;; For some reason, Emacs adds some directories to `exec-path'
  192. ;; but not to `process-environment'?
  193. exec-path (append new-directories exec-path)
  194. ;; set eshell path to same as exec-path
  195. eshell-path-env (mapconcat 'identity exec-path ":")
  196. process-environment (append
  197. (list
  198. (format "VIRTUAL_ENV=%s" directory)
  199. (format "PATH=%s"
  200. (mapconcat 'identity
  201. (append new-directories
  202. (split-string (getenv "PATH")
  203. path-separator))
  204. path-separator))
  205. ;; No "=" means to unset
  206. "PYTHONHOME")
  207. process-environment)
  208. ))
  209. (pyvenv-run-virtualenvwrapper-hook "post_activate")
  210. (run-hooks 'pyvenv-post-activate-hooks))
  211. ;;;###autoload
  212. (defun pyvenv-deactivate ()
  213. "Deactivate any current virtual environment."
  214. (interactive)
  215. (when pyvenv-virtual-env
  216. (pyvenv-run-virtualenvwrapper-hook "pre_deactivate")
  217. (run-hooks 'pyvenv-pre-deactivate-hooks))
  218. (when pyvenv-old-process-environment
  219. (setq process-environment pyvenv-old-process-environment
  220. pyvenv-old-process-environment nil))
  221. (when pyvenv-old-exec-path
  222. (setq exec-path pyvenv-old-exec-path
  223. pyvenv-old-exec-path nil))
  224. (when pyvenv-old-eshell-path
  225. (setq eshell-path-env pyvenv-old-eshell-path
  226. pyvenv-old-eshell-path nil))
  227. (when pyvenv-virtual-env
  228. ;; Make sure this does not change `exec-path', as $PATH is
  229. ;; different
  230. (let ((old-exec-path exec-path)
  231. (old-eshell-path eshell-path-env)
  232. (old-process-environment process-environment))
  233. (unwind-protect
  234. (pyvenv-run-virtualenvwrapper-hook "post_deactivate"
  235. pyvenv-virtual-env)
  236. (setq exec-path old-exec-path
  237. eshell-path-env old-eshell-path
  238. process-environment old-process-environment)))
  239. (run-hooks 'pyvenv-post-deactivate-hooks))
  240. (setq pyvenv-virtual-env nil
  241. pyvenv-virtual-env-name nil
  242. python-shell-virtualenv-root nil
  243. python-shell-virtualenv-path nil))
  244. (defvar pyvenv-workon-history nil
  245. "Prompt history for `pyvenv-workon'.")
  246. ;;;###autoload
  247. (defun pyvenv-workon (name)
  248. "Activate a virtual environment from $WORKON_HOME.
  249. If the virtual environment NAME is already active, this function
  250. does not try to reactivate the environment."
  251. (interactive
  252. (list
  253. (completing-read "Work on: " (pyvenv-virtualenv-list)
  254. nil t nil 'pyvenv-workon-history nil nil)))
  255. (unless (member name (list "" nil pyvenv-virtual-env-name))
  256. (pyvenv-activate (format "%s/%s"
  257. (pyvenv-workon-home)
  258. name))))
  259. (defun pyvenv-virtualenv-list (&optional noerror)
  260. "Prompt the user for a name in $WORKON_HOME.
  261. If NOERROR is set, do not raise an error if WORKON_HOME is not
  262. configured."
  263. (let ((workon-home (pyvenv-workon-home))
  264. (result nil))
  265. (if (not (file-directory-p workon-home))
  266. (when (not noerror)
  267. (error "Can't find a workon home directory, set $WORKON_HOME"))
  268. (dolist (name (directory-files workon-home))
  269. (when (or (file-exists-p (format "%s/%s/bin/activate"
  270. workon-home name))
  271. (file-exists-p (format "%s/%s/bin/python"
  272. workon-home name))
  273. (file-exists-p (format "%s/%s/Scripts/activate.bat"
  274. workon-home name))
  275. (file-exists-p (format "%s/%s/python.exe"
  276. workon-home name)))
  277. (setq result (cons name result))))
  278. (sort result (lambda (a b)
  279. (string-lessp (downcase a)
  280. (downcase b)))))))
  281. (define-widget 'pyvenv-workon 'choice
  282. "Select an available virtualenv from virtualenvwrapper."
  283. :convert-widget
  284. (lambda (widget)
  285. (setq widget (widget-copy widget))
  286. (widget-put widget
  287. :args (cons '(const :tag "None" nil)
  288. (mapcar (lambda (env)
  289. (list 'const env))
  290. (pyvenv-virtualenv-list t))))
  291. (widget-types-convert-widget widget))
  292. :prompt-value (lambda (widget prompt value unbound)
  293. (let ((name (completing-read
  294. prompt
  295. (cons "None"
  296. (pyvenv-virtualenv-list t))
  297. nil t)))
  298. (if (equal name "None")
  299. nil
  300. name))))
  301. (defvar pyvenv-mode-map (make-sparse-keymap)
  302. "The mode keymap for `pyvenv-mode'.")
  303. (easy-menu-define pyvenv-menu pyvenv-mode-map
  304. "Pyvenv Menu"
  305. '("Virtual Envs"
  306. :visible pyvenv-mode
  307. ("Workon"
  308. :help "Activate a virtualenvwrapper environment"
  309. :filter (lambda (&optional ignored)
  310. (mapcar (lambda (venv)
  311. (vector venv `(pyvenv-workon ,venv)
  312. :style 'radio
  313. :selected `(equal pyvenv-virtual-env-name
  314. ,venv)))
  315. (pyvenv-virtualenv-list t))))
  316. ["Activate" pyvenv-activate
  317. :help "Activate a virtual environment by directory"]
  318. ["Deactivate" pyvenv-deactivate
  319. :help "Deactivate the current virtual environment"
  320. :active pyvenv-virtual-env
  321. :suffix pyvenv-virtual-env-name]
  322. ["Restart Python Processes" pyvenv-restart-python
  323. :help "Restart all Python processes to use the current environment"]))
  324. ;;;###autoload
  325. (define-minor-mode pyvenv-mode
  326. "Global minor mode for pyvenv.
  327. Will show the current virtualenv in the mode line, and respect a
  328. `pyvenv-workon' setting in files."
  329. :global t
  330. (cond
  331. (pyvenv-mode
  332. (add-to-list 'mode-line-misc-info '(pyvenv-mode pyvenv-mode-line-indicator))
  333. (add-hook 'hack-local-variables-hook #'pyvenv-track-virtualenv))
  334. ((not pyvenv-mode)
  335. (setq mode-line-misc-info (delete '(pyvenv-mode pyvenv-mode-line-indicator)
  336. mode-line-misc-info))
  337. (remove-hook 'hack-local-variables-hook #'pyvenv-track-virtualenv))))
  338. ;;;###autoload
  339. (define-minor-mode pyvenv-tracking-mode
  340. "Global minor mode to track the current virtualenv.
  341. When this mode is active, pyvenv will activate a buffer-specific
  342. virtualenv whenever the user switches to a buffer with a
  343. buffer-local `pyvenv-workon' or `pyvenv-activate' variable."
  344. :global t
  345. (if pyvenv-tracking-mode
  346. (add-hook 'post-command-hook 'pyvenv-track-virtualenv)
  347. (remove-hook 'post-command-hook 'pyvenv-track-virtualenv)))
  348. (defun pyvenv-track-virtualenv ()
  349. "Set a virtualenv as specified for the current buffer.
  350. If either `pyvenv-activate' or `pyvenv-workon' are specified, and
  351. they specify a virtualenv different from the current one, switch
  352. to that virtualenv."
  353. (cond
  354. (pyvenv-activate
  355. (when (and (not (equal (file-name-as-directory pyvenv-activate)
  356. pyvenv-virtual-env))
  357. (or (not pyvenv-tracking-ask-before-change)
  358. (y-or-n-p (format "Switch to virtualenv %s (currently %s)"
  359. pyvenv-activate pyvenv-virtual-env))))
  360. (pyvenv-activate pyvenv-activate)))
  361. (pyvenv-workon
  362. (when (and (not (equal pyvenv-workon pyvenv-virtual-env-name))
  363. (or (not pyvenv-tracking-ask-before-change)
  364. (y-or-n-p (format "Switch to virtualenv %s (currently %s)"
  365. pyvenv-workon pyvenv-virtual-env-name))))
  366. (pyvenv-workon pyvenv-workon)))))
  367. (defun pyvenv-run-virtualenvwrapper-hook (hook &rest args)
  368. "Run a virtualenvwrapper hook, and update the environment.
  369. This will run a virtualenvwrapper hook and update the local
  370. environment accordingly.
  371. CAREFUL! This will modify your `process-environment' and
  372. `exec-path'."
  373. (when (pyvenv-virtualenvwrapper-supported)
  374. (with-temp-buffer
  375. (let ((tmpfile (make-temp-file "pyvenv-virtualenvwrapper-"))
  376. (shell-file-name pyvenv-exec-shell))
  377. (unwind-protect
  378. (let ((default-directory (pyvenv-workon-home)))
  379. (apply #'call-process
  380. pyvenv-virtualenvwrapper-python
  381. nil t nil
  382. "-m" "virtualenvwrapper.hook_loader"
  383. "--script" tmpfile
  384. (if (getenv "HOOK_VERBOSE_OPTION")
  385. (cons (getenv "HOOK_VERBOSE_OPTION")
  386. (cons hook args))
  387. (cons hook args)))
  388. (call-process-shell-command
  389. (format ". '%s' ; python -c 'import os, json; print(\"\\n=-=-=\"); print(json.dumps(dict(os.environ)))'"
  390. tmpfile)
  391. nil t nil))
  392. (delete-file tmpfile)))
  393. (goto-char (point-min))
  394. (when (and (not (re-search-forward "No module named '?virtualenvwrapper'?" nil t))
  395. (re-search-forward "\n=-=-=\n" nil t))
  396. (let ((output (buffer-substring (point-min)
  397. (match-beginning 0))))
  398. (when (> (length output) 0)
  399. (with-help-window "*Virtualenvwrapper Hook Output*"
  400. (with-current-buffer "*Virtualenvwrapper Hook Output*"
  401. (let ((inhibit-read-only t))
  402. (erase-buffer)
  403. (insert
  404. (format
  405. "Output from the virtualenvwrapper hook %s:\n\n"
  406. hook)
  407. output))))))
  408. (dolist (binding (json-read))
  409. (let ((env (format "%s=%s" (car binding) (cdr binding))))
  410. (when (not (member env process-environment))
  411. (setq process-environment (cons env process-environment))))
  412. (when (eq (car binding) 'PATH)
  413. (setq exec-path (split-string (cdr binding)
  414. path-separator))))))))
  415. ;;;###autoload
  416. (defun pyvenv-restart-python ()
  417. "Restart Python inferior processes."
  418. (interactive)
  419. (dolist (buf (buffer-list))
  420. (with-current-buffer buf
  421. (when (and (eq major-mode 'inferior-python-mode)
  422. (get-buffer-process buf))
  423. (let ((cmd (combine-and-quote-strings (process-command
  424. (get-buffer-process buf))))
  425. (dedicated (if (string-match "\\[.*\\]$" (buffer-name buf))
  426. t
  427. nil))
  428. (show nil))
  429. (delete-process (get-buffer-process buf))
  430. (goto-char (point-max))
  431. (insert "\n\n"
  432. "###\n"
  433. (format "### Restarting in virtualenv %s (%s)\n"
  434. pyvenv-virtual-env-name pyvenv-virtual-env)
  435. "###\n"
  436. "\n\n")
  437. (run-python cmd dedicated show)
  438. (goto-char (point-max)))))))
  439. (defun pyvenv-hook-dir ()
  440. "Return the current hook directory.
  441. This is usually the value of $VIRTUALENVWRAPPER_HOOK_DIR, but
  442. virtualenvwrapper has stopped exporting that variable, so we go
  443. back to the default of $WORKON_HOME or even just ~/.virtualenvs/."
  444. (or (getenv "VIRTUALENVWRAPPER_HOOK_DIR")
  445. (pyvenv-workon-home)))
  446. (defun pyvenv-workon-home ()
  447. "Return the current workon home.
  448. This is the value of $WORKON_HOME or ~/.virtualenvs."
  449. (or (getenv "WORKON_HOME")
  450. (expand-file-name "~/.virtualenvs")))
  451. (defun pyvenv-virtualenvwrapper-supported ()
  452. "Return true iff virtualenvwrapper is supported.
  453. Right now, this just checks if WORKON_HOME is set."
  454. (getenv "WORKON_HOME"))
  455. ;;; Compatibility
  456. (when (not (fboundp 'file-name-base))
  457. ;; Emacs 24.3
  458. (defun file-name-base (&optional filename)
  459. "Return the base name of the FILENAME: no directory, no extension.
  460. FILENAME defaults to `buffer-file-name'."
  461. (file-name-sans-extension
  462. (file-name-nondirectory (or filename (buffer-file-name)))))
  463. )
  464. (when (not (boundp 'mode-line-misc-info))
  465. (defvar mode-line-misc-info nil
  466. "Compatibility variable for 24.3+")
  467. (let ((line mode-line-format))
  468. (while line
  469. (when (eq 'which-func-mode
  470. (car-safe (car-safe (cdr line))))
  471. (setcdr line (cons 'mode-line-misc-format
  472. (cdr line)))
  473. (setq line (cdr line)))
  474. (setq line (cdr line)))))
  475. (provide 'pyvenv)
  476. ;;; pyvenv.el ends here