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.

312 lines
12 KiB

4 years ago
  1. ;;; elpy-django.el --- Django extension for elpy
  2. ;; Copyright (C) 2013-2016 Jorgen Schaefer
  3. ;; Author: Daniel Gopar <gopardaniel@gmail.com>
  4. ;; URL: https://github.com/jorgenschaefer/elpy
  5. ;; This program is free software; you can redistribute it and/or
  6. ;; modify it under the terms of the GNU General Public License
  7. ;; as published by the Free Software Foundation; either version 3
  8. ;; of the License, or (at your option) any later version.
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; This file serves as an extension to elpy by adding django support
  17. ;;; Code:
  18. (require 's)
  19. ;;;;;;;;;;;;;;;;;;;;;;
  20. ;;; User customization
  21. (defcustom elpy-django-command "django-admin.py"
  22. "Command to use when running Django specific commands.
  23. Best to set it to full path to 'manage.py' if it's available."
  24. :type 'string
  25. :safe 'stringp
  26. :group 'elpy-django)
  27. (make-variable-buffer-local 'elpy-django-command)
  28. (defcustom elpy-django-server-ipaddr "127.0.0.1"
  29. "What address Django will use when running the dev server."
  30. :type 'string
  31. :safe 'stringp
  32. :group 'elpy-django)
  33. (make-variable-buffer-local 'elpy-django-server-ipaddr)
  34. (defcustom elpy-django-server-port "8000"
  35. "What port Django will use when running the dev server."
  36. :type 'string
  37. :safe 'stringp
  38. :group 'elpy-django)
  39. (make-variable-buffer-local 'elpy-django-server-port)
  40. (defcustom elpy-django-server-command "runserver"
  41. "When executing `elpy-django-runserver' what should be the server
  42. command to use."
  43. :type 'string
  44. :safe 'stringp
  45. :group 'elpy-django)
  46. (make-variable-buffer-local 'elpy-django-server-command)
  47. (defcustom elpy-django-always-prompt nil
  48. "When non-nil, it will always prompt for extra arguments
  49. to pass with the chosen command."
  50. :type 'boolean
  51. :safe 'booleanp
  52. :group 'elpy-django)
  53. (make-variable-buffer-local 'elpy-django-always-prompt)
  54. (defcustom elpy-django-commands-with-req-arg '("startapp" "startproject"
  55. "loaddata" "sqlmigrate"
  56. "sqlsequencereset"
  57. "squashmigrations")
  58. "Used to determine if we should prompt for arguments. Some commands
  59. require arguments in order for it to work."
  60. :type 'list
  61. :safe 'listp
  62. :group 'elpy-django)
  63. (make-variable-buffer-local 'elpy-django-commands-with-req-arg)
  64. (defcustom elpy-django-test-runner-formats '(("django_nose.NoseTestSuiteRunner" . ":")
  65. (".*" . "."))
  66. "List of test runners and their format for calling tests.
  67. The keys are the regular expressions to match the runner used in test,
  68. while the values are the separators to use to build test target path.
  69. Some tests runners are called differently. For example, Nose requires a ':' when calling specific tests,
  70. but the default Django test runner uses '.'"
  71. :type 'list
  72. :safe 'listp
  73. :group 'elpy-django)
  74. (make-variable-buffer-local 'elpy-django-test-runner-formats)
  75. (defcustom elpy-django-test-runner-args '("test" "--noinput")
  76. "Arguments to pass to the test runner when calling tests."
  77. :type '(repeat string)
  78. :group 'elpy-django)
  79. (make-variable-buffer-local 'elpy-django-test-runner-args)
  80. (defcustom elpy-test-django-runner-command nil
  81. "Deprecated. Please define Django command in `elpy-django-command' and
  82. test arguments in `elpy-django-test-runner-args'"
  83. :type '(repeat string)
  84. :group 'elpy-django)
  85. (make-obsolete-variable 'elpy-test-django-runner-command nil "March 2018")
  86. (defcustom elpy-test-django-runner-manage-command nil
  87. "Deprecated. Please define Django command in `elpy-django-command' and
  88. test arguments in `elpy-django-test-runner-args'."
  89. :type '(repeat string)
  90. :group 'elpy-django)
  91. (make-obsolete-variable 'elpy-test-django-runner-manage-command nil "March 2018")
  92. (defcustom elpy-test-django-with-manage nil
  93. "Deprecated. Please define Django command in `elpy-django-command' and
  94. test arguments in `elpy-django-test-runner-args'."
  95. :type 'boolean
  96. :group 'elpy-django)
  97. (make-obsolete-variable 'elpy-test-django-with-manage nil "March 2018")
  98. ;;;;;;;;;;;;;;;;;;;;;;
  99. ;; Key map
  100. (defvar elpy-django-mode-map
  101. (let ((map (make-sparse-keymap)))
  102. (define-key map (kbd "c") 'elpy-django-command)
  103. (define-key map (kbd "r") 'elpy-django-runserver)
  104. map)
  105. "Key map for django extension")
  106. ;;;;;;;;;;;;;;;;;;;;;;
  107. ;;; Helper Functions
  108. (defun elpy-django-setup ()
  109. "Decides whether to start the minor mode or not."
  110. ;; Make sure we're in an actual file and we can find
  111. ;; manage.py. Otherwise user will have to manually
  112. ;; start this mode if they're using 'django-admin.py'
  113. (when (locate-dominating-file default-directory "manage.py")
  114. ;; Let's be nice and point to full path of 'manage.py'
  115. ;; This only affects the buffer if there's no directory
  116. ;; variable overwriting it.
  117. (setq elpy-django-command
  118. (expand-file-name (concat (locate-dominating-file default-directory "manage.py") "manage.py")))
  119. (elpy-django 1)))
  120. (defun elpy-django--get-commands ()
  121. "Return list of django commands."
  122. (let ((dj-commands-str nil)
  123. (help-output
  124. (shell-command-to-string (concat elpy-django-command " -h"))))
  125. (setq dj-commands-str
  126. (with-temp-buffer
  127. (progn
  128. (insert help-output)
  129. (goto-char (point-min))
  130. (delete-region (point) (search-forward "Available subcommands:" nil nil nil))
  131. ;; cleanup [auth] and stuff
  132. (goto-char (point-min))
  133. (save-excursion
  134. (while (re-search-forward "\\[.*\\]" nil t)
  135. (replace-match "" nil nil)))
  136. (buffer-string))))
  137. ;; get a list of commands from the output of manage.py -h
  138. ;; What would be the pattern to optimize this ?
  139. (setq dj-commands-str (split-string dj-commands-str "\n"))
  140. (setq dj-commands-str (cl-remove-if (lambda (x) (string= x "")) dj-commands-str))
  141. (setq dj-commands-str (mapcar (lambda (x) (s-trim x)) dj-commands-str))
  142. (sort dj-commands-str 'string-lessp)))
  143. (defvar elpy-django--test-runner-cache nil
  144. "Internal cache for elpy-django--get-test-runner.
  145. The cache is keyed on project root and DJANGO_SETTINGS_MODULE env var")
  146. (defvar elpy-django--test-runner-cache-max-size 100
  147. "Maximum number of entries in test runner cache")
  148. (defun elpy-django--get-test-runner ()
  149. "Return the name of the django test runner.
  150. Needs `DJANGO_SETTINGS_MODULE' to be set in order to work.
  151. The result is memoized on project root and `DJANGO_SETTINGS_MODULE'"
  152. (let ((django-import-cmd "import django;django.setup();from django.conf import settings;print(settings.TEST_RUNNER)")
  153. (django-settings-env (getenv "DJANGO_SETTINGS_MODULE"))
  154. (default-directory (elpy-project-root)))
  155. ;; If no Django settings has been set, then nothing will work. Warn user
  156. (when (not django-settings-env)
  157. (error "Please set environment variable `DJANGO_SETTINGS_MODULE' if you'd like to run the test runner"))
  158. (let* ((runner-key (list default-directory django-settings-env))
  159. (runner (or (elpy-django--get-test-runner-from-cache runner-key)
  160. (elpy-django--cache-test-runner
  161. runner-key
  162. (elpy-django--detect-test-runner django-settings-env)))))
  163. (elpy-django--limit-test-runner-cache-size)
  164. runner)))
  165. (defun elpy-django--get-test-format ()
  166. "When running a Django test, some test runners require a different format that others.
  167. Return the correct string format here."
  168. (let ((runner (elpy-django--get-test-runner))
  169. (found nil)
  170. (formats elpy-django-test-runner-formats))
  171. (while (and formats (not found))
  172. (let* ((entry (car formats)) (regex (car entry)))
  173. (when (string-match regex runner)
  174. (setq found (cdr entry))))
  175. (setq formats (cdr formats)))
  176. (or found (error (format "Unable to find test format for `%s'"
  177. (elpy-django--get-test-runner))))))
  178. (defun elpy-django--detect-test-runner (django-settings-env)
  179. "Detects django test runner in current configuration"
  180. ;; We have to be able to import the DJANGO_SETTINGS_MODULE to detect test
  181. ;; runner; if python process importing settings exits with error,
  182. ;; then warn the user that settings is not valid
  183. (unless (= 0 (call-process elpy-rpc-python-command nil nil nil
  184. "-c" (format "import %s" django-settings-env)))
  185. (error (format "Unable to import DJANGO_SETTINGS_MODULE: '%s'"
  186. django-settings-env)))
  187. (s-trim (shell-command-to-string
  188. (format "%s -c '%s'" elpy-rpc-python-command
  189. django-import-cmd))))
  190. (defun elpy-django--get-test-runner-from-cache (key)
  191. "Retrieve from cache test runner with given caching key.
  192. Return nil if the runner is missing in cache"
  193. (let ((runner (cdr (assoc key elpy-django--test-runner-cache))))
  194. ;; if present re-add to implement lru cache
  195. (when runner (elpy-django--cache-test-runner key runner))))
  196. (defun elpy-django--cache-test-runner (key runner)
  197. "Store in test runner cache a runner with a key"""
  198. (push (cons key runner) elpy-django--test-runner-cache)
  199. runner)
  200. (defun elpy-django--limit-test-runner-cache-size ()
  201. "Ensure elpy-django--test-runner-cache does not overflow a fixed size"
  202. (while (> (length elpy-django--test-runner-cache)
  203. elpy-django--test-runner-cache-max-size)
  204. (setq elpy-django--test-runner-cache (cdr elpy-django--test-runner-cache))))
  205. ;;;;;;;;;;;;;;;;;;;;;;
  206. ;;; User Functions
  207. (defun elpy-django-command (cmd)
  208. "Prompt user for Django command. If called with `C-u',
  209. it will prompt for other flags/arguments to run."
  210. (interactive (list (completing-read "Command: " (elpy-django--get-commands) nil nil)))
  211. ;; Called with C-u, variable is set or is a cmd that requires an argument
  212. (when (or current-prefix-arg
  213. elpy-django-always-prompt
  214. (member cmd elpy-django-commands-with-req-arg))
  215. (setq cmd (concat cmd " " (read-shell-command (concat cmd ": ") "--noinput"))))
  216. (compile (concat elpy-django-command " " cmd)))
  217. (defun elpy-django-runserver (arg)
  218. "Start the server and automatically add the ipaddr and port.
  219. Also create it's own special buffer so that we can have multiple
  220. servers running per project.
  221. When called with a prefix (C-u), it will prompt for additional args."
  222. (interactive "P")
  223. (let* ((cmd (concat elpy-django-command " " elpy-django-server-command))
  224. (proj-root (file-name-base (directory-file-name (elpy-project-root))))
  225. (buff-name (format "*runserver[%s]*" proj-root)))
  226. ;; Kill any previous instance of runserver since we might be doing something new
  227. (when (get-buffer buff-name)
  228. (kill-buffer buff-name))
  229. (setq cmd (concat cmd " " elpy-django-server-ipaddr ":" elpy-django-server-port))
  230. (when (or arg elpy-django-always-prompt)
  231. (setq cmd (concat cmd " "(read-shell-command (concat cmd ": ")))))
  232. (compile cmd)
  233. (with-current-buffer "*compilation*"
  234. (rename-buffer buff-name))))
  235. (defun elpy-test-django-runner (top _file module test)
  236. "Test the project using the Django discover runner,
  237. or with manage.py if elpy-test-django-with-manage is true.
  238. This requires Django 1.6 or the django-discover-runner package."
  239. (interactive (elpy-test-at-point))
  240. (if module
  241. (apply #'elpy-test-run
  242. top
  243. (append
  244. (list elpy-django-command)
  245. elpy-django-test-runner-args
  246. (list (if test
  247. (format "%s%s%s" module (elpy-django--get-test-format) test)
  248. module))))
  249. (apply #'elpy-test-run
  250. top
  251. (append
  252. (list elpy-django-command)
  253. elpy-django-test-runner-args))))
  254. (put 'elpy-test-django-runner 'elpy-test-runner-p t)
  255. (define-minor-mode elpy-django
  256. "Minor mode for Django commands."
  257. :group 'elpy-django)
  258. (provide 'elpy-django)
  259. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  260. ;;; elpy-django.el ends here