Klimi's new dotfiles with stow.
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.

546 line
21 KiB

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