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