|
|
- ;;; elpy-django.el --- Django extension for elpy
-
- ;; Copyright (C) 2013-2016 Jorgen Schaefer
-
- ;; Author: Daniel Gopar <gopardaniel@gmail.com>
- ;; URL: https://github.com/jorgenschaefer/elpy
-
- ;; This program is free software; you can redistribute it and/or
- ;; modify it under the terms of the GNU General Public License
- ;; as published by the Free Software Foundation; either version 3
- ;; of the License, or (at your option) any later version.
-
- ;; This program is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
-
- ;; You should have received a copy of the GNU General Public License
- ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
-
- ;;; Commentary:
-
- ;; This file serves as an extension to elpy by adding django support
-
- ;;; Code:
-
- (require 's)
-
- ;;;;;;;;;;;;;;;;;;;;;;
- ;;; User customization
-
- (defcustom elpy-django-command "django-admin.py"
- "Command to use when running Django specific commands.
- Best to set it to full path to 'manage.py' if it's available."
- :type 'string
- :safe 'stringp
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-command)
-
- (defcustom elpy-django-server-ipaddr "127.0.0.1"
- "What address Django will use when running the dev server."
- :type 'string
- :safe 'stringp
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-server-ipaddr)
-
- (defcustom elpy-django-server-port "8000"
- "What port Django will use when running the dev server."
- :type 'string
- :safe 'stringp
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-server-port)
-
- (defcustom elpy-django-server-command "runserver"
- "When executing `elpy-django-runserver' what should be the server
- command to use."
- :type 'string
- :safe 'stringp
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-server-command)
-
- (defcustom elpy-django-always-prompt nil
- "When non-nil, it will always prompt for extra arguments
- to pass with the chosen command."
- :type 'boolean
- :safe 'booleanp
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-always-prompt)
-
- (defcustom elpy-django-commands-with-req-arg '("startapp" "startproject"
- "loaddata" "sqlmigrate"
- "sqlsequencereset"
- "squashmigrations")
- "Used to determine if we should prompt for arguments. Some commands
- require arguments in order for it to work."
- :type 'list
- :safe 'listp
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-commands-with-req-arg)
-
- (defcustom elpy-django-test-runner-formats '(("django_nose.NoseTestSuiteRunner" . ":")
- (".*" . "."))
- "List of test runners and their format for calling tests.
-
- The keys are the regular expressions to match the runner used in test,
- while the values are the separators to use to build test target path.
- Some tests runners are called differently. For example, Nose requires a ':' when calling specific tests,
- but the default Django test runner uses '.'"
- :type 'list
- :safe 'listp
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-test-runner-formats)
-
- (defcustom elpy-django-test-runner-args '("test" "--noinput")
- "Arguments to pass to the test runner when calling tests."
- :type '(repeat string)
- :group 'elpy-django)
- (make-variable-buffer-local 'elpy-django-test-runner-args)
-
- (defcustom elpy-test-django-runner-command nil
- "Deprecated. Please define Django command in `elpy-django-command' and
- test arguments in `elpy-django-test-runner-args'"
- :type '(repeat string)
- :group 'elpy-django)
- (make-obsolete-variable 'elpy-test-django-runner-command nil "March 2018")
-
- (defcustom elpy-test-django-runner-manage-command nil
- "Deprecated. Please define Django command in `elpy-django-command' and
- test arguments in `elpy-django-test-runner-args'."
- :type '(repeat string)
- :group 'elpy-django)
- (make-obsolete-variable 'elpy-test-django-runner-manage-command nil "March 2018")
-
- (defcustom elpy-test-django-with-manage nil
- "Deprecated. Please define Django command in `elpy-django-command' and
- test arguments in `elpy-django-test-runner-args'."
- :type 'boolean
- :group 'elpy-django)
- (make-obsolete-variable 'elpy-test-django-with-manage nil "March 2018")
-
- ;;;;;;;;;;;;;;;;;;;;;;
- ;; Key map
-
- (defvar elpy-django-mode-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "c") 'elpy-django-command)
- (define-key map (kbd "r") 'elpy-django-runserver)
- map)
- "Key map for django extension")
-
- ;;;;;;;;;;;;;;;;;;;;;;
- ;;; Helper Functions
-
- (defun elpy-django-setup ()
- "Decides whether to start the minor mode or not."
- ;; Make sure we're in an actual file and we can find
- ;; manage.py. Otherwise user will have to manually
- ;; start this mode if they're using 'django-admin.py'
- (when (locate-dominating-file default-directory "manage.py")
- ;; Let's be nice and point to full path of 'manage.py'
- ;; This only affects the buffer if there's no directory
- ;; variable overwriting it.
- (setq elpy-django-command
- (expand-file-name (concat (locate-dominating-file default-directory "manage.py") "manage.py")))
- (elpy-django 1)))
-
- (defun elpy-django--get-commands ()
- "Return list of django commands."
- (let ((dj-commands-str nil)
- (help-output
- (shell-command-to-string (concat elpy-django-command " -h"))))
- (setq dj-commands-str
- (with-temp-buffer
- (progn
- (insert help-output)
- (goto-char (point-min))
- (delete-region (point) (search-forward "Available subcommands:" nil nil nil))
- ;; cleanup [auth] and stuff
- (goto-char (point-min))
- (save-excursion
- (while (re-search-forward "\\[.*\\]" nil t)
- (replace-match "" nil nil)))
- (buffer-string))))
- ;; get a list of commands from the output of manage.py -h
- ;; What would be the pattern to optimize this ?
- (setq dj-commands-str (split-string dj-commands-str "\n"))
- (setq dj-commands-str (cl-remove-if (lambda (x) (string= x "")) dj-commands-str))
- (setq dj-commands-str (mapcar (lambda (x) (s-trim x)) dj-commands-str))
- (sort dj-commands-str 'string-lessp)))
-
-
- (defvar elpy-django--test-runner-cache nil
- "Internal cache for elpy-django--get-test-runner.
- The cache is keyed on project root and DJANGO_SETTINGS_MODULE env var")
-
- (defvar elpy-django--test-runner-cache-max-size 100
- "Maximum number of entries in test runner cache")
-
-
- (defun elpy-django--get-test-runner ()
- "Return the name of the django test runner.
- Needs `DJANGO_SETTINGS_MODULE' to be set in order to work.
- The result is memoized on project root and `DJANGO_SETTINGS_MODULE'"
- (let ((django-import-cmd "import django;django.setup();from django.conf import settings;print(settings.TEST_RUNNER)")
- (django-settings-env (getenv "DJANGO_SETTINGS_MODULE"))
- (default-directory (elpy-project-root)))
- ;; If no Django settings has been set, then nothing will work. Warn user
- (when (not django-settings-env)
- (error "Please set environment variable `DJANGO_SETTINGS_MODULE' if you'd like to run the test runner"))
-
- (let* ((runner-key (list default-directory django-settings-env))
- (runner (or (elpy-django--get-test-runner-from-cache runner-key)
- (elpy-django--cache-test-runner
- runner-key
- (elpy-django--detect-test-runner django-settings-env)))))
- (elpy-django--limit-test-runner-cache-size)
- runner)))
-
-
- (defun elpy-django--get-test-format ()
- "When running a Django test, some test runners require a different format that others.
- Return the correct string format here."
- (let ((runner (elpy-django--get-test-runner))
- (found nil)
- (formats elpy-django-test-runner-formats))
- (while (and formats (not found))
- (let* ((entry (car formats)) (regex (car entry)))
- (when (string-match regex runner)
- (setq found (cdr entry))))
- (setq formats (cdr formats)))
- (or found (error (format "Unable to find test format for `%s'"
- (elpy-django--get-test-runner))))))
-
-
- (defun elpy-django--detect-test-runner (django-settings-env)
- "Detects django test runner in current configuration"
- ;; We have to be able to import the DJANGO_SETTINGS_MODULE to detect test
- ;; runner; if python process importing settings exits with error,
- ;; then warn the user that settings is not valid
- (unless (= 0 (call-process elpy-rpc-python-command nil nil nil
- "-c" (format "import %s" django-settings-env)))
- (error (format "Unable to import DJANGO_SETTINGS_MODULE: '%s'"
- django-settings-env)))
- (s-trim (shell-command-to-string
- (format "%s -c '%s'" elpy-rpc-python-command
- django-import-cmd))))
-
-
- (defun elpy-django--get-test-runner-from-cache (key)
- "Retrieve from cache test runner with given caching key.
- Return nil if the runner is missing in cache"
- (let ((runner (cdr (assoc key elpy-django--test-runner-cache))))
- ;; if present re-add to implement lru cache
- (when runner (elpy-django--cache-test-runner key runner))))
-
-
- (defun elpy-django--cache-test-runner (key runner)
- "Store in test runner cache a runner with a key"""
- (push (cons key runner) elpy-django--test-runner-cache)
- runner)
-
-
- (defun elpy-django--limit-test-runner-cache-size ()
- "Ensure elpy-django--test-runner-cache does not overflow a fixed size"
- (while (> (length elpy-django--test-runner-cache)
- elpy-django--test-runner-cache-max-size)
- (setq elpy-django--test-runner-cache (cdr elpy-django--test-runner-cache))))
-
-
- ;;;;;;;;;;;;;;;;;;;;;;
- ;;; User Functions
-
- (defun elpy-django-command (cmd)
- "Prompt user for Django command. If called with `C-u',
- it will prompt for other flags/arguments to run."
- (interactive (list (completing-read "Command: " (elpy-django--get-commands) nil nil)))
- ;; Called with C-u, variable is set or is a cmd that requires an argument
- (when (or current-prefix-arg
- elpy-django-always-prompt
- (member cmd elpy-django-commands-with-req-arg))
- (setq cmd (concat cmd " " (read-shell-command (concat cmd ": ") "--noinput"))))
- (compile (concat elpy-django-command " " cmd)))
-
- (defun elpy-django-runserver (arg)
- "Start the server and automatically add the ipaddr and port.
- Also create it's own special buffer so that we can have multiple
- servers running per project.
-
- When called with a prefix (C-u), it will prompt for additional args."
- (interactive "P")
- (let* ((cmd (concat elpy-django-command " " elpy-django-server-command))
- (proj-root (file-name-base (directory-file-name (elpy-project-root))))
- (buff-name (format "*runserver[%s]*" proj-root)))
- ;; Kill any previous instance of runserver since we might be doing something new
- (when (get-buffer buff-name)
- (kill-buffer buff-name))
- (setq cmd (concat cmd " " elpy-django-server-ipaddr ":" elpy-django-server-port))
- (when (or arg elpy-django-always-prompt)
- (setq cmd (concat cmd " "(read-shell-command (concat cmd ": ")))))
- (compile cmd)
- (with-current-buffer "*compilation*"
- (rename-buffer buff-name))))
-
- (defun elpy-test-django-runner (top _file module test)
- "Test the project using the Django discover runner,
- or with manage.py if elpy-test-django-with-manage is true.
-
- This requires Django 1.6 or the django-discover-runner package."
- (interactive (elpy-test-at-point))
- (if module
- (apply #'elpy-test-run
- top
- (append
- (list elpy-django-command)
- elpy-django-test-runner-args
- (list (if test
- (format "%s%s%s" module (elpy-django--get-test-format) test)
- module))))
- (apply #'elpy-test-run
- top
- (append
- (list elpy-django-command)
- elpy-django-test-runner-args))))
- (put 'elpy-test-django-runner 'elpy-test-runner-p t)
-
- (define-minor-mode elpy-django
- "Minor mode for Django commands."
- :group 'elpy-django)
-
- (provide 'elpy-django)
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;; elpy-django.el ends here
|