2062 lines
83 KiB
EmacsLisp
2062 lines
83 KiB
EmacsLisp
;;; doctest-mode.el --- Major mode for editing Python doctest files
|
|
|
|
;; Copyright (C) 2004-2007 Edward Loper
|
|
|
|
;; Author: Edward Loper
|
|
;; Maintainer: edloper@alum.mit.edu
|
|
;; Created: Aug 2004
|
|
;; Keywords: python doctest unittest test docstring
|
|
|
|
(defconst doctest-version "0.5 alpha"
|
|
"`doctest-mode' version number.")
|
|
|
|
;; This software is provided as-is, without express or implied
|
|
;; warranty. Permission to use, copy, modify, distribute or sell this
|
|
;; software, without fee, for any purpose and by any individual or
|
|
;; organization, is hereby granted, provided that the above copyright
|
|
;; notice and this paragraph appear in all copies.
|
|
|
|
;; This is a major mode for editing text files that contain Python
|
|
;; doctest examples. Doctest is a testing framework for Python that
|
|
;; emulates an interactive session, and checks the result of each
|
|
;; command. For more information, see the Python library reference:
|
|
;; <http://docs.python.org/lib/module-doctest.html>
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Table of Contents
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; 1. Customization: use-editable variables to customize
|
|
;; doctest-mode.
|
|
;;
|
|
;; 2. Fonts: defines new font-lock faces.
|
|
;;
|
|
;; 3. Constants: various consts (mainly regexps) used by the rest
|
|
;; of the code.
|
|
;;
|
|
;; 4. Syntax Highlighting: defines variables and functions used by
|
|
;; font-lock-mode to perform syntax highlighting.
|
|
;;
|
|
;; 5. Source code editing & indentation: commands used to
|
|
;; automatically indent, dedent, & handle prompts.
|
|
;;
|
|
;; 6. Code Execution: commands used to start doctest processes,
|
|
;; and handle their output.
|
|
;;
|
|
;; 7. Markers: functions used to insert markers at the start of
|
|
;; doctest examples. These are used to keep track of the
|
|
;; correspondence between examples in the source buffer and
|
|
;; results in the output buffer.
|
|
;;
|
|
;; 8. Navigation: commands used to navigate between failed examples.
|
|
;;
|
|
;; 9. Replace Output: command used to replace a doctest example's
|
|
;; expected output with its actual output.
|
|
;;
|
|
;; 10. Helper functions: various helper functions used by the rest
|
|
;; of the code.
|
|
;;
|
|
;; 11. Emacs compatibility functions: defines compatible versions of
|
|
;; functions that are defined for some versions of emacs but not
|
|
;; others.
|
|
;;
|
|
;; 12. Doctest Results Mode: defines doctest-results-mode, which is
|
|
;; used for the output generated by doctest.
|
|
;;
|
|
;; 13. Doctest Mode: defines doctest-mode itself.
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Customizable Constants
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defgroup doctest nil
|
|
"Support for the Python doctest framework"
|
|
:group 'languages
|
|
:prefix "doctest-")
|
|
|
|
(defcustom doctest-default-margin 4
|
|
"The default pre-prompt margin for doctest examples."
|
|
:type 'integer
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-avoid-trailing-whitespace t
|
|
"If true, then delete trailing whitespace when inserting a newline."
|
|
:type 'boolean
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-temp-directory
|
|
(let ((ok '(lambda (x)
|
|
(and x
|
|
(setq x (expand-file-name x)) ; always true
|
|
(file-directory-p x)
|
|
(file-writable-p x)
|
|
x))))
|
|
(or (funcall ok (getenv "TMPDIR"))
|
|
(funcall ok "/usr/tmp")
|
|
(funcall ok "/tmp")
|
|
(funcall ok "/var/tmp")
|
|
(funcall ok ".")
|
|
(error (concat "Couldn't find a usable temp directory -- "
|
|
"set `doctest-temp-directory'"))))
|
|
"Directory used for temporary files created when running doctest.
|
|
By default, the first directory from this list that exists and that you
|
|
can write into: the value (if any) of the environment variable TMPDIR,
|
|
/usr/tmp, /tmp, /var/tmp, or the current directory."
|
|
:type 'string
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-hide-example-source nil
|
|
"If true, then don't display the example source code for each
|
|
failure in the results buffer."
|
|
:type 'boolean
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-python-command "python"
|
|
"Shell command used to start the python interpreter"
|
|
:type 'string
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-results-buffer-name "*doctest-output (%N)*"
|
|
"The name of the buffer used to store the output of the doctest
|
|
command. This name can contain the following special sequences:
|
|
%n -- replaced by the doctest buffer's name.
|
|
%N -- replaced by the doctest buffer's name, with '.doctest'
|
|
stripped off.
|
|
%f -- replaced by the doctest buffer's filename."
|
|
:type 'string
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-optionflags '()
|
|
"Option flags for doctest"
|
|
:group 'doctest
|
|
:type '(repeat (choice (const :tag "Select an option..." "")
|
|
(const :tag "Normalize whitespace"
|
|
"NORMALIZE_WHITESPACE")
|
|
(const :tag "Ellipsis"
|
|
"ELLIPSIS")
|
|
(const :tag "Don't accept True for 1"
|
|
"DONT_ACCEPT_TRUE_FOR_1")
|
|
(const :tag "Don't accept <BLANKLINE>"
|
|
"DONT_ACCEPT_BLANKLINE")
|
|
(const :tag "Ignore Exception detail"
|
|
"IGNORE_EXCEPTION_DETAIL")
|
|
(const :tag "Report only first failure"
|
|
"REPORT_ONLY_FIRST_FAILURE")
|
|
)))
|
|
|
|
(defcustom doctest-async t
|
|
"If true, then doctest will be run asynchronously."
|
|
:type 'boolean
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-trim-exceptions t
|
|
"If true, then any exceptions inserted by doctest-replace-output
|
|
will have the stack trace lines trimmed."
|
|
:type 'boolean
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-highlight-strings t
|
|
"If true, then highlight strings. If you find that doctest-mode
|
|
is responding slowly when you type, turning this off might help."
|
|
:type 'boolean
|
|
:group 'doctest)
|
|
|
|
(defcustom doctest-follow-output t
|
|
"If true, then when doctest is run asynchronously, the output buffer
|
|
will scroll to display its output as it is generated. If false, then
|
|
the output buffer not scroll."
|
|
:type 'boolean
|
|
:group 'doctest)
|
|
|
|
(defvar doctest-mode-hook nil
|
|
"Hook called by `doctest-mode'.")
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Fonts
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defface doctest-prompt-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#68f"))
|
|
(t (:foreground "#226")))
|
|
"Face for Python prompts in doctest examples."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-output-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#afd"))
|
|
(t (:foreground "#262")))
|
|
"Face for the output of doctest examples."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-output-marker-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#0f0"))
|
|
(t (:foreground "#080")))
|
|
"Face for markers in the output of doctest examples."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-output-traceback-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#f88"))
|
|
(t (:foreground "#622")))
|
|
"Face for traceback headers in the output of doctest examples."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-results-divider-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#08f"))
|
|
(t (:foreground "#00f")))
|
|
"Face for dividers in the doctest results window."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-results-loc-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#0f8"))
|
|
(t (:foreground "#084")))
|
|
"Face for location headers in the doctest results window."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-results-header-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#8ff"))
|
|
(t (:foreground "#088")))
|
|
"Face for sub-headers in the doctest results window."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-results-selection-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#ff0" :background "#008"))
|
|
(t (:background "#088" :foreground "#fff")))
|
|
"Face for selected failure's location header in the results window."
|
|
:group 'doctest)
|
|
|
|
(defface doctest-selection-face
|
|
'((((class color) (background dark))
|
|
(:foreground "#ff0" :background "#00f" :bold t))
|
|
(t (:foreground "#f00")))
|
|
"Face for selected example's prompt"
|
|
:group 'doctest)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Constants
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defconst doctest-prompt-re
|
|
"^\\(?:\\([ \t]*\\)\\(>>> ?\\|[.][.][.] ?\\)\\([ \t]*\\)\\)"
|
|
"Regular expression for doctest prompts. It defines three groups:
|
|
the pre-prompt margin; the prompt; and the post-prompt indentation.")
|
|
|
|
(defconst doctest-open-block-re
|
|
"[^\n]+:[ \t]*\\(#.*\\)?$"
|
|
"Regular expression for a line that opens a block")
|
|
|
|
(defconst doctest-close-block-re
|
|
"\\(return\\|raise\\|break\\|continue\\|pass\\)\\b"
|
|
"Regular expression for a line that closes a block")
|
|
|
|
(defconst doctest-example-source-re
|
|
"^Failed example:\n\\(\n\\| [^\n]*\n\\)+"
|
|
"Regular expression for example source in doctest's output.")
|
|
|
|
(defconst doctest-results-divider-re
|
|
"^\\([*]\\{60,\\}\\)$"
|
|
"Regular expression for example dividers in doctest's output.")
|
|
|
|
(defconst doctest-py24-results-loc-re
|
|
"^File \"[^\"]+\", line \\([0-9]+\\), in [^\n]+"
|
|
"Regular expression for example location markers in doctest's output.")
|
|
|
|
(defconst doctest-py21-results-loc-re
|
|
"^from line #\\([0-9]+\\) of [^\n]*"
|
|
"Regular expression for example location markers in doctest's output,
|
|
when the output was generated by an old version of doctest.")
|
|
|
|
(defconst doctest-results-header-re
|
|
"^\\([^ \n\t].+:\\|Expected nothing\\|Got nothing\\)$"
|
|
"Regular expression for example headers in doctest's output.")
|
|
|
|
(defconst doctest-traceback-header-re
|
|
"^[ \t]*Traceback (\\(most recent call last\\|innermost last\\)):"
|
|
"Regular expression for Python traceback headers.")
|
|
|
|
(defconst doctest-py21-results-re
|
|
"^from line #"
|
|
"Regular expression used to test which version of doctest was used.")
|
|
|
|
;; nb: There's a bug in Python's traceback.print_exception function
|
|
;; which causes SyntaxError exceptions to be displayed incorrectly;
|
|
;; which prevents this regexp from matching. But there shouldn't be
|
|
;; too many people testing for SyntaxErrors, so I won't worry about
|
|
;; it.
|
|
(defconst doctest-traceback-re
|
|
(let ((nonprompt
|
|
;; This matches any non-blank line that doesn't start with
|
|
;; a prompt (... or >>>).
|
|
(concat
|
|
"\\(?:[.][.][^.\n]\\|[>][>][^>\n]\\|"
|
|
"[.][^.\n]\\|[>][^>\n]\\|[^.>\n \t]\\)[^\n]*")))
|
|
(concat
|
|
"^\\(\\([ \t]*\\)Traceback "
|
|
"(\\(?:most recent call last\\|innermost last\\)):\n\\)"
|
|
"\\(?:\\2[ \t]+[^ \t\n][^\n]*\n\\)*"
|
|
"\\(\\(?:\\2" nonprompt "\n\\)"
|
|
"\\(?:\\2[ \t]*" nonprompt "\n\\)*\\)"))
|
|
"Regular expression that matches a complete exception traceback.
|
|
It contains three groups: group 1 is the header line; group 2 is
|
|
the indentation; and group 3 is the exception message.")
|
|
|
|
(defconst doctest-blankline-re
|
|
"^[ \t]*<BLANKLINE>"
|
|
"Regular expression that matches blank line markers in doctest
|
|
output.")
|
|
|
|
(defconst doctest-outdent-re
|
|
(concat "\\(" (mapconcat 'identity
|
|
'("else:"
|
|
"except\\(\\s +.*\\)?:"
|
|
"finally:"
|
|
"elif\\s +.*:")
|
|
"\\|")
|
|
"\\)")
|
|
"Regular expression for a line that should be outdented. Any line
|
|
that matches `doctest-outdent-re', but does not follow a line matching
|
|
`doctest-no-outdent-re', will be outdented.")
|
|
|
|
;; It's not clear to me *why* the behavior given by this definition of
|
|
;; doctest-no-outdent-re is desirable; but it's basically just copied
|
|
;; from python-mode.
|
|
(defconst doctest-no-outdent-re
|
|
(concat
|
|
"\\("
|
|
(mapconcat 'identity
|
|
(list "try:"
|
|
"except\\(\\s +.*\\)?:"
|
|
"while\\s +.*:"
|
|
"for\\s +.*:"
|
|
"if\\s +.*:"
|
|
"elif\\s +.*:"
|
|
"\\(return\\|raise\\|break\\|continue\\|pass\\)[ \t\n]"
|
|
)
|
|
"\\|")
|
|
"\\)")
|
|
"Regular expression matching lines not to outdent after. Any line
|
|
that matches `doctest-outdent-re', but does not follow a line matching
|
|
`doctest-no-outdent-re', will be outdented.")
|
|
|
|
(defconst doctest-script
|
|
"\
|
|
from doctest import *
|
|
import sys
|
|
if '%m':
|
|
import imp
|
|
try:
|
|
m = imp.load_source('__imported__', '%m')
|
|
globs = m.__dict__
|
|
except Exception, e:
|
|
print ('doctest-mode encountered an error while importing '
|
|
'the current buffer:\\n\\n %s' % e)
|
|
sys.exit(1)
|
|
else:
|
|
globs = {}
|
|
doc = open('%t').read()
|
|
if sys.version_info[:2] >= (2,4):
|
|
test = DocTestParser().get_doctest(doc, globs, '%n', '%f', 0)
|
|
r = DocTestRunner(optionflags=%l)
|
|
r.run(test)
|
|
else:
|
|
Tester(globs=globs).runstring(doc, '%f')"
|
|
;; Docstring:
|
|
"Python script used to run doctest.
|
|
The following special sequences are defined:
|
|
%n -- replaced by the doctest buffer's name.
|
|
%f -- replaced by the doctest buffer's filename.
|
|
%l -- replaced by the doctest flags string.
|
|
%t -- replaced by the name of the tempfile containing the doctests."
|
|
)
|
|
|
|
(defconst doctest-keyword-re
|
|
(let* ((kw1 (mapconcat 'identity
|
|
'("and" "assert" "break" "class"
|
|
"continue" "def" "del" "elif"
|
|
"else" "except" "exec" "for"
|
|
"from" "global" "if" "import"
|
|
"in" "is" "lambda" "not"
|
|
"or" "pass" "print" "raise"
|
|
"return" "while" "yield"
|
|
)
|
|
"\\|"))
|
|
(kw2 (mapconcat 'identity
|
|
'("else:" "except:" "finally:" "try:")
|
|
"\\|"))
|
|
(kw3 (mapconcat 'identity
|
|
'("ArithmeticError" "AssertionError"
|
|
"AttributeError" "DeprecationWarning" "EOFError"
|
|
"Ellipsis" "EnvironmentError" "Exception" "False"
|
|
"FloatingPointError" "FutureWarning" "IOError"
|
|
"ImportError" "IndentationError" "IndexError"
|
|
"KeyError" "KeyboardInterrupt" "LookupError"
|
|
"MemoryError" "NameError" "None" "NotImplemented"
|
|
"NotImplementedError" "OSError" "OverflowError"
|
|
"OverflowWarning" "PendingDeprecationWarning"
|
|
"ReferenceError" "RuntimeError" "RuntimeWarning"
|
|
"StandardError" "StopIteration" "SyntaxError"
|
|
"SyntaxWarning" "SystemError" "SystemExit"
|
|
"TabError" "True" "TypeError" "UnboundLocalError"
|
|
"UnicodeDecodeError" "UnicodeEncodeError"
|
|
"UnicodeError" "UnicodeTranslateError"
|
|
"UserWarning" "ValueError" "Warning"
|
|
"ZeroDivisionError" "__debug__"
|
|
"__import__" "__name__" "abs" "apply" "basestring"
|
|
"bool" "buffer" "callable" "chr" "classmethod"
|
|
"cmp" "coerce" "compile" "complex" "copyright"
|
|
"delattr" "dict" "dir" "divmod"
|
|
"enumerate" "eval" "execfile" "exit" "file"
|
|
"filter" "float" "getattr" "globals" "hasattr"
|
|
"hash" "hex" "id" "input" "int" "intern"
|
|
"isinstance" "issubclass" "iter" "len" "license"
|
|
"list" "locals" "long" "map" "max" "min" "object"
|
|
"oct" "open" "ord" "pow" "property" "range"
|
|
"raw_input" "reduce" "reload" "repr" "round"
|
|
"setattr" "slice" "staticmethod" "str" "sum"
|
|
"super" "tuple" "type" "unichr" "unicode" "vars"
|
|
"xrange" "zip")
|
|
"\\|"))
|
|
(pseudokw (mapconcat 'identity
|
|
'("self" "None" "True" "False" "Ellipsis")
|
|
"\\|"))
|
|
(string (concat "'\\(?:\\\\[^\n]\\|[^\n']*\\)'" "\\|"
|
|
"\"\\(?:\\\\[^\n]\\|[^\n\"]*\\)\""))
|
|
(brk "\\(?:[ \t(]\\|$\\)"))
|
|
(concat
|
|
;; Comments (group 1)
|
|
"\\(#.*\\)"
|
|
;; Function & Class Definitions (groups 2-3)
|
|
"\\|\\b\\(class\\|def\\)"
|
|
"[ \t]+\\([a-zA-Z_][a-zA-Z0-9_]*\\)"
|
|
;; Builtins preceeded by '.'(group 4)
|
|
"\\|[.][\t ]*\\(" kw3 "\\)"
|
|
;; Keywords & builtins (group 5)
|
|
"\\|\\b\\(" kw1 "\\|" kw2 "\\|"
|
|
kw3 "\\|" pseudokw "\\)" brk
|
|
;; Decorators (group 6)
|
|
"\\|\\(@[a-zA-Z_][a-zA-Z0-9_]*\\)"
|
|
))
|
|
"A regular expression used for syntax highlighting of Python
|
|
source code.")
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Syntax Highlighting (font-lock mode)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; Define the font-lock keyword table.
|
|
(defconst doctest-font-lock-keywords
|
|
`(
|
|
;; The following pattern colorizes source lines. In particular,
|
|
;; it first matches prompts, and then looks for any of the
|
|
;; following matches *on the same line* as the prompt. It uses
|
|
;; the form:
|
|
;;
|
|
;; (MATCHER MATCH-HIGHLIGHT
|
|
;; (ANCHOR-MATCHER nil nil MATCH-HIGHLIGHT))
|
|
;;
|
|
;; See the variable documentation for font-lock-keywords for a
|
|
;; description of what each of those means.
|
|
("^[ \t]*\\(>>>\\|\\.\\.\\.\\)"
|
|
(1 'doctest-prompt-face)
|
|
(doctest-source-matcher
|
|
nil nil
|
|
(1 'font-lock-comment-face t t) ; comments
|
|
(2 'font-lock-keyword-face t t) ; def/class
|
|
(3 'font-lock-type-face t t) ; func/class name
|
|
;; group 4 (builtins preceeded by '.') gets no colorization.
|
|
(5 'font-lock-keyword-face t t) ; keywords & builtins
|
|
(6 'font-lock-preprocessor-face t t) ; decorators
|
|
(7 'font-lock-string-face t t) ; strings
|
|
))
|
|
|
|
;; The following pattern colorizes output lines. In particular,
|
|
;; it uses doctest-output-line-matcher to check if this is an
|
|
;; output line, and if so, it colorizes it, and any special
|
|
;; markers it contains.
|
|
(doctest-output-line-matcher
|
|
(0 'doctest-output-face t)
|
|
("\\.\\.\\." (beginning-of-line) (end-of-line)
|
|
(0 'doctest-output-marker-face t))
|
|
(,doctest-blankline-re (beginning-of-line) (end-of-line)
|
|
(0 'doctest-output-marker-face t))
|
|
(doctest-traceback-line-matcher (beginning-of-line) (end-of-line)
|
|
(0 'doctest-output-traceback-face t))
|
|
(,doctest-traceback-header-re (beginning-of-line) (end-of-line)
|
|
(0 'doctest-output-traceback-face t))
|
|
)
|
|
|
|
;; A PS1 prompt followed by a non-space is an error.
|
|
("^[ \t]*\\(>>>[^ \t\n][^\n]*\\)" (1 'font-lock-warning-face t))
|
|
)
|
|
"Expressions to highlight in doctest-mode.")
|
|
|
|
(defconst doctest-results-font-lock-keywords
|
|
`((,doctest-results-divider-re
|
|
(0 'doctest-results-divider-face))
|
|
(,doctest-py24-results-loc-re
|
|
(0 'doctest-results-loc-face))
|
|
(,doctest-results-header-re
|
|
(0 'doctest-results-header-face))
|
|
(doctest-results-selection-matcher
|
|
(0 'doctest-results-selection-face t)))
|
|
"Expressions to highlight in doctest-results-mode.")
|
|
|
|
(defun doctest-output-line-matcher (limit)
|
|
"A `font-lock-keyword' MATCHER that returns t if the current
|
|
line is the expected output for a doctest example, and if so,
|
|
sets `match-data' so that group 0 spans the current line."
|
|
;; The real work is done by doctest-find-output-line.
|
|
(when (doctest-find-output-line limit)
|
|
;; If we found one, then mark the entire line.
|
|
(beginning-of-line)
|
|
(re-search-forward "[^\n]*" limit)))
|
|
|
|
(defun doctest-traceback-line-matcher (limit)
|
|
"A `font-lock-keyword' MATCHER that returns t if the current line is
|
|
the beginning of a traceback, and if so, sets `match-data' so that
|
|
group 0 spans the entire traceback. n.b.: limit is ignored."
|
|
(beginning-of-line)
|
|
(when (looking-at doctest-traceback-re)
|
|
(goto-char (match-end 0))
|
|
t))
|
|
|
|
(defun doctest-source-matcher (limit)
|
|
"A `font-lock-keyword' MATCHER that returns t if the current line
|
|
contains a Python source expression that should be highlighted
|
|
after the point. If so, it sets `match-data' to cover the string
|
|
literal. The groups in `match-data' should be interpreted as follows:
|
|
|
|
Group 1: comments
|
|
Group 2: def/class
|
|
Group 3: function/class name
|
|
Group 4: builtins preceeded by '.'
|
|
Group 5: keywords & builtins
|
|
Group 6: decorators
|
|
Group 7: strings
|
|
"
|
|
(let ((matchdata nil))
|
|
;; First, look for string literals.
|
|
(when doctest-highlight-strings
|
|
(save-excursion
|
|
(when (doctest-string-literal-matcher limit)
|
|
(setq matchdata
|
|
(list (match-beginning 0) (match-end 0)
|
|
nil nil nil nil nil nil nil nil nil nil nil nil
|
|
(match-beginning 0) (match-end 0))))))
|
|
;; Then, look for other keywords. If they occur before the
|
|
;; string literal, then they take precedence.
|
|
(save-excursion
|
|
(when (and (re-search-forward doctest-keyword-re limit t)
|
|
(or (null matchdata)
|
|
(< (match-beginning 0) (car matchdata))))
|
|
(setq matchdata (match-data))))
|
|
(when matchdata
|
|
(set-match-data matchdata)
|
|
(goto-char (match-end 0))
|
|
t)))
|
|
|
|
(defun doctest-string-literal-matcher (limit &optional debug)
|
|
"A `font-lock-keyword' MATCHER that returns t if the current line
|
|
contains a string literal starting at or after the point. If so, it
|
|
expands `match-data' to cover the string literal. This matcher uses
|
|
`doctest-statement-info' to collect information about strings that
|
|
continue over multiple lines. It therefore might be a little slow for
|
|
very large statements."
|
|
(let* ((stmt-info (doctest-statement-info))
|
|
(quotes (reverse (nth 5 stmt-info)))
|
|
(result nil))
|
|
(if debug (doctest-debug "quotes %s" quotes))
|
|
(while (and quotes (null result))
|
|
(let* ((quote (pop quotes))
|
|
(start (car quote))
|
|
(end (min limit (or (cdr quote) limit))))
|
|
(if debug (doctest-debug "quote %s-%s pt=%s lim=%s"
|
|
start end (point) limit))
|
|
(when (or (and (<= (point) start) (< start limit))
|
|
(and (< start (point)) (< (point) end)))
|
|
(setq start (max start (point)))
|
|
(set-match-data (list start end))
|
|
(if debug (doctest-debug "marking string %s" (match-data)))
|
|
(goto-char end)
|
|
(setq result t))))
|
|
result))
|
|
|
|
(defun doctest-results-selection-matcher (limit)
|
|
"Matches from `doctest-selected-failure' to the end of the
|
|
line. This is used to highlight the currently selected failure."
|
|
(when (and doctest-selected-failure
|
|
(<= (point) doctest-selected-failure)
|
|
(< doctest-selected-failure limit))
|
|
(goto-char doctest-selected-failure)
|
|
(re-search-forward "[^\n]+" limit)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Source code editing & indentation
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defun doctest-indent-source-line (&optional dedent-only)
|
|
"Re-indent the current line, as doctest source code. I.e., add a
|
|
prompt to the current line if it doesn't have one, and re-indent the
|
|
source code (to the right of the prompt). If `dedent-only' is true,
|
|
then don't increase the indentation level any."
|
|
(interactive "*")
|
|
(let ((indent-end nil))
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
(let ((new-indent (doctest-current-source-line-indentation dedent-only))
|
|
(new-margin (doctest-current-source-line-margin))
|
|
(line-had-prompt (looking-at doctest-prompt-re)))
|
|
;; Delete the old prompt (if any).
|
|
(when line-had-prompt
|
|
(goto-char (match-beginning 2))
|
|
(delete-char (- (match-end 2) (match-beginning 2))))
|
|
;; Delete the old indentation.
|
|
(delete-backward-char (skip-chars-forward " \t"))
|
|
;; If it's a continuation line, or a new PS1 prompt,
|
|
;; then copy the margin.
|
|
(when (or new-indent (not line-had-prompt))
|
|
(beginning-of-line)
|
|
(delete-backward-char (skip-chars-forward " \t"))
|
|
(insert-char ?\ new-margin))
|
|
;; Add the new prompt.
|
|
(insert-string (if new-indent "... " ">>> "))
|
|
;; Add the new indentation
|
|
(if new-indent (insert-char ?\ new-indent))
|
|
(setq indent-end (point))))
|
|
;; If we're left of the indentation end, then move up to the
|
|
;; indentation end.
|
|
(if (< (point) indent-end) (goto-char indent-end))))
|
|
|
|
(defun doctest-current-source-line-indentation (&optional dedent-only)
|
|
"Return the post-prompt indent to use for this line. This is an
|
|
integer for a continuation lines, and nil for non-continuation lines."
|
|
(save-excursion
|
|
;; Examine the previous doctest line (if present).
|
|
(let* ((prev-stmt-info (doctest-prev-statement-info))
|
|
(prev-stmt-indent (nth 0 prev-stmt-info))
|
|
(continuation-indent (nth 1 prev-stmt-info))
|
|
(prev-stmt-opens-block (nth 2 prev-stmt-info))
|
|
(prev-stmt-closes-block (nth 3 prev-stmt-info))
|
|
(prev-stmt-blocks-outdent (nth 4 prev-stmt-info))
|
|
)
|
|
;; Examine this doctest line.
|
|
(let* ((curr-line-indent 0)
|
|
(curr-line-outdented nil))
|
|
(beginning-of-line)
|
|
(when (looking-at doctest-prompt-re)
|
|
(setq curr-line-indent (- (match-end 3) (match-beginning 3)))
|
|
(goto-char (match-end 3)))
|
|
(setq curr-line-outdented (and (looking-at doctest-outdent-re)
|
|
(not prev-stmt-blocks-outdent)))
|
|
;; Compute the overall indent.
|
|
(let ((indent (or continuation-indent
|
|
(+ prev-stmt-indent
|
|
(if curr-line-outdented -4 0)
|
|
(if prev-stmt-opens-block 4 0)
|
|
(if prev-stmt-closes-block -4 0)))))
|
|
;; If dedent-only is true, then make sure we don't indent.
|
|
(when dedent-only
|
|
(setq indent (min indent curr-line-indent)))
|
|
;; If indent=0 and we're not outdented, then set indent to
|
|
;; nil (to signify the start of a new source example).
|
|
(when (and (= indent 0)
|
|
(not (or curr-line-outdented continuation-indent)))
|
|
(setq indent nil))
|
|
;; Return the indentation.
|
|
indent)))))
|
|
|
|
(defun doctest-prev-statement-info (&optional debug)
|
|
(save-excursion
|
|
(forward-line -1)
|
|
(doctest-statement-info debug)))
|
|
|
|
(defun doctest-statement-info (&optional debug)
|
|
"Collect info about the previous statement, and return it as a list:
|
|
|
|
(INDENT, CONTINUATION, OPENS-BLOCK, CLOSES-BLOCK, BLOCKS-OUTDENT,
|
|
QUOTES)
|
|
|
|
INDENT -- The indentation of the previous statement (after the prompt)
|
|
|
|
CONTINUATION -- If the previous statement is incomplete (e.g., has an
|
|
open paren or quote), then this is the appropriate indentation
|
|
level; otherwise, it's nil.
|
|
|
|
OPENS-BLOCK -- True if the previous statement opens a Python control
|
|
block.
|
|
|
|
CLOSES-BLOCK -- True if the previous statement closes a Python control
|
|
block.
|
|
|
|
BLOCKS-OUTDENT -- True if the previous statement should 'block the
|
|
next statement from being considered an outdent.
|
|
|
|
QUOTES -- A list of (START . END) pairs for all quotation strings.
|
|
"
|
|
(save-excursion
|
|
(end-of-line)
|
|
(let ((end (point)))
|
|
(while (and (doctest-on-source-line-p "...") (= (forward-line -1) 0)))
|
|
(cond
|
|
;; If there's no previous >>> line, then give up.
|
|
((not (doctest-on-source-line-p ">>>"))
|
|
'(0 nil nil nil nil))
|
|
|
|
;; If there is a previous statement, walk through the source
|
|
;; code, checking for operators that may be of interest.
|
|
(t
|
|
(beginning-of-line)
|
|
(let* ((quote-mark nil) (nesting 0) (indent-stack '())
|
|
(stmt-indent 0)
|
|
(stmt-opens-block nil)
|
|
(stmt-closes-block nil)
|
|
(stmt-blocks-outdent nil)
|
|
(quotes '())
|
|
(elt-re (concat "\\\\[^\n]\\|"
|
|
"(\\|)\\|\\[\\|\\]\\|{\\|}\\|"
|
|
"\"\"\"\\|\'\'\'\\|\"\\|\'\\|"
|
|
"#[^\n]*\\|" doctest-prompt-re)))
|
|
(while (re-search-forward elt-re end t)
|
|
(let* ((elt (match-string 0))
|
|
(elt-first-char (substring elt 0 1)))
|
|
(if debug (doctest-debug "Debug: %s" elt))
|
|
(cond
|
|
;; Close quote -- set quote-mark back to nil. The
|
|
;; second case is for cases like: ' '''
|
|
(quote-mark
|
|
(cond
|
|
((equal quote-mark elt)
|
|
(setq quote-mark nil)
|
|
(setcdr (car quotes) (point)))
|
|
((equal quote-mark elt-first-char)
|
|
(setq quote-mark nil)
|
|
(setcdr (car quotes) (point))
|
|
(backward-char 2))))
|
|
;; Prompt -- check if we're starting a new stmt. If so,
|
|
;; then collect various info about it.
|
|
((string-match doctest-prompt-re elt)
|
|
(when (and (null quote-mark) (= nesting 0))
|
|
(let ((indent (- (match-end 3) (match-end 2))))
|
|
(unless (looking-at "[ \t]*\n")
|
|
(setq stmt-indent indent)
|
|
(setq stmt-opens-block
|
|
(looking-at doctest-open-block-re))
|
|
(setq stmt-closes-block
|
|
(looking-at doctest-close-block-re))
|
|
(setq stmt-blocks-outdent
|
|
(looking-at doctest-no-outdent-re))))))
|
|
;; Open paren -- increment nesting, and update indent-stack.
|
|
((string-match "(\\|\\[\\|{" elt-first-char)
|
|
(let ((elt-pos (point))
|
|
(at-eol (looking-at "[ \t]*\n"))
|
|
(indent 0))
|
|
(save-excursion
|
|
(re-search-backward doctest-prompt-re)
|
|
(if at-eol
|
|
(setq indent (+ 4 (- (match-end 3) (match-end 2))))
|
|
(setq indent (- elt-pos (match-end 2))))
|
|
(push indent indent-stack)))
|
|
(setq nesting (+ nesting 1)))
|
|
;; Close paren -- decrement nesting, and pop indent-stack.
|
|
((string-match ")\\|\\]\\|}" elt-first-char)
|
|
(setq indent-stack (cdr indent-stack))
|
|
(setq nesting (max 0 (- nesting 1))))
|
|
;; Open quote -- set quote-mark.
|
|
((string-match "\"\\|\'" elt-first-char)
|
|
(push (cons (- (point) (length elt)) nil) quotes)
|
|
(setq quote-mark elt)))))
|
|
|
|
(let* ((continuation-indent
|
|
(cond
|
|
(quote-mark 0)
|
|
((> nesting 0) (if (null indent-stack) 0 (car indent-stack)))
|
|
(t nil)))
|
|
(result
|
|
(list stmt-indent continuation-indent
|
|
stmt-opens-block stmt-closes-block
|
|
stmt-blocks-outdent quotes)))
|
|
(if debug (doctest-debug "Debug: %s" result))
|
|
result)))))))
|
|
|
|
(defun doctest-current-source-line-margin ()
|
|
"Return the pre-prompt margin to use for this source line. This is
|
|
copied from the most recent source line, or set to
|
|
`doctest-default-margin' if there are no preceeding source lines."
|
|
(save-excursion
|
|
(save-restriction
|
|
(when (doctest-in-mmm-docstring-overlay)
|
|
(doctest-narrow-to-mmm-overlay))
|
|
(beginning-of-line)
|
|
(forward-line -1)
|
|
(while (and (not (doctest-on-source-line-p))
|
|
(re-search-backward doctest-prompt-re nil t))))
|
|
(cond ((looking-at doctest-prompt-re)
|
|
(- (match-end 1) (match-beginning 1)))
|
|
((doctest-in-mmm-docstring-overlay)
|
|
(doctest-default-margin-in-mmm-docstring-overlay))
|
|
(t
|
|
doctest-default-margin))))
|
|
|
|
(defun doctest-electric-backspace ()
|
|
"Delete the preceeding character, level of indentation, or
|
|
prompt.
|
|
|
|
If point is at the leftmost column, delete the preceding newline.
|
|
|
|
Otherwise, if point is at the first non-whitespace character
|
|
following an indented source line's prompt, then reduce the
|
|
indentation to the next multiple of 4; and update the source line's
|
|
prompt, when necessary.
|
|
|
|
Otherwise, if point is at the first non-whitespace character
|
|
following an unindented source line's prompt, then remove the
|
|
prompt (converting the line to an output line or text line).
|
|
|
|
Otherwise, if point is at the first non-whitespace character of a
|
|
line, the delete the line's indentation.
|
|
|
|
Otherwise, delete the preceeding character.
|
|
"
|
|
(interactive "*")
|
|
(cond
|
|
;; Beginning of line: delete preceeding newline.
|
|
((bolp) (backward-delete-char 1))
|
|
|
|
;; First non-ws char following prompt: dedent or remove prompt.
|
|
((and (looking-at "[^ \t\n]\\|$") (doctest-looking-back doctest-prompt-re))
|
|
(let* ((prompt-beg (match-beginning 2))
|
|
(indent-beg (match-beginning 3)) (indent-end (match-end 3))
|
|
(old-indent (- indent-end indent-beg))
|
|
(new-indent (* (/ (- old-indent 1) 4) 4)))
|
|
(cond
|
|
;; Indented source line: dedent it.
|
|
((> old-indent 0)
|
|
(goto-char indent-beg)
|
|
(delete-region indent-beg indent-end)
|
|
(insert-char ?\ new-indent)
|
|
;; Change prompt to PS1, when appropriate.
|
|
(when (and (= new-indent 0) (not (looking-at doctest-outdent-re)))
|
|
(delete-backward-char 4)
|
|
(insert-string ">>> ")))
|
|
;; Non-indented source line: remove prompt.
|
|
(t
|
|
(goto-char indent-end)
|
|
(delete-region prompt-beg indent-end)))))
|
|
|
|
;; First non-ws char of a line: delete all indentation.
|
|
((and (looking-at "[^ \n\t]\\|$") (doctest-looking-back "^[ \t]+"))
|
|
(delete-region (match-beginning 0) (match-end 0)))
|
|
|
|
;; Otherwise: delete a character.
|
|
(t
|
|
(backward-delete-char 1))))
|
|
|
|
(defun doctest-newline-and-indent ()
|
|
"Insert a newline, and indent the new line appropriately.
|
|
|
|
If the current line is a source line containing a bare prompt,
|
|
then clear the current line, and insert a newline.
|
|
|
|
Otherwise, if the current line is a source line, then insert a
|
|
newline, and add an appropriately indented prompt to the new
|
|
line.
|
|
|
|
Otherwise, if the current line is an output line, then insert a
|
|
newline and indent the new line to match the example's margin.
|
|
|
|
Otherwise, insert a newline.
|
|
|
|
If `doctest-avoid-trailing-whitespace' is true, then clear any
|
|
whitespace to the left of the point before inserting a newline.
|
|
"
|
|
(interactive "*")
|
|
;; If we're avoiding trailing spaces, then delete WS before point.
|
|
(if doctest-avoid-trailing-whitespace
|
|
(delete-char (- (skip-chars-backward " \t"))))
|
|
(cond
|
|
;; If we're on an empty prompt, delete it.
|
|
((doctest-on-empty-source-line-p)
|
|
(delete-region (match-beginning 0) (match-end 0))
|
|
(insert-char ?\n 1))
|
|
;; If we're on a doctest line, add a new prompt.
|
|
((doctest-on-source-line-p)
|
|
(insert-char ?\n 1)
|
|
(doctest-indent-source-line))
|
|
;; If we're in doctest output, indent to the margin.
|
|
((doctest-on-output-line-p)
|
|
(insert-char ?\n 1)
|
|
(insert-char ?\ (doctest-current-source-line-margin)))
|
|
;; Otherwise, just add a newline.
|
|
(t (insert-char ?\n 1))))
|
|
|
|
(defun doctest-electric-colon ()
|
|
"Insert a colon, and dedent the line when appropriate."
|
|
(interactive "*")
|
|
(insert-char ?: 1)
|
|
(when (doctest-on-source-line-p)
|
|
(doctest-indent-source-line t)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Code Execution
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defun doctest-execute ()
|
|
"Run doctest on the current buffer, or on the current docstring
|
|
if the point is inside an `mmm-mode' `doctest-docstring' region.
|
|
Display the results in the *doctest-output* buffer."
|
|
(interactive)
|
|
(doctest-execute-region (point-min) (point-max) nil t))
|
|
|
|
(defun doctest-execute-with-diff ()
|
|
"Run doctest on the current buffer, or on the current docstring
|
|
if the point is inside an `mmm-mode' `doctest-docstring' region.
|
|
Display the results in the *doctest-output* buffer, using diff format."
|
|
(interactive)
|
|
(doctest-execute-region (point-min) (point-max) t t))
|
|
|
|
(defun doctest-execute-buffer-with-diff ()
|
|
"Run doctest on the current buffer, and display the results in the
|
|
*doctest-output* buffer, using the diff format."
|
|
(interactive)
|
|
(doctest-execute-region (point-min) (point-max) t nil))
|
|
|
|
(defun doctest-execute-buffer ()
|
|
"Run doctest on the current buffer, and display the results in the
|
|
*doctest-output* buffer."
|
|
(interactive)
|
|
(doctest-execute-region (point-min) (point-max) nil nil))
|
|
|
|
(defun doctest-execute-region (start end &optional diff
|
|
check-for-mmm-docstring-overlay)
|
|
"Run doctest on the current buffer, and display the results in the
|
|
*doctest-output* buffer."
|
|
(interactive "r")
|
|
;; If it's already running, give the user a chance to restart it.
|
|
(when (doctest-process-live-p doctest-async-process)
|
|
(when (y-or-n-p "Doctest is already running. Restart it? ")
|
|
(doctest-cancel-async-process)
|
|
(message "Killing doctest...")))
|
|
(cond
|
|
((and doctest-async (doctest-process-live-p doctest-async-process))
|
|
(message "Can't run two doctest processes at once!"))
|
|
(t
|
|
(let* ((results-buf-name (doctest-results-buffer-name))
|
|
(in-docstring (and check-for-mmm-docstring-overlay
|
|
(doctest-in-mmm-docstring-overlay)))
|
|
(temp (doctest-temp-name)) (dir doctest-temp-directory)
|
|
(input-file (expand-file-name (concat temp ".py") dir))
|
|
(globs-file (when in-docstring
|
|
(expand-file-name (concat temp "-globs.py") dir)))
|
|
(cur-buf (current-buffer))
|
|
(in-buf (get-buffer-create "*doctest-input*"))
|
|
(script (doctest-script input-file globs-file diff)))
|
|
;; If we're in a docstring, narrow start & end.
|
|
(when in-docstring
|
|
(let ((bounds (doctest-mmm-overlay-bounds)))
|
|
(setq start (max start (car bounds))
|
|
end (min end (cdr bounds)))))
|
|
;; Write the doctests to a file.
|
|
(save-excursion
|
|
(goto-char (min start end))
|
|
(let ((lineno (doctest-line-number)))
|
|
(set-buffer in-buf)
|
|
;; Add blank lines, to keep line numbers the same:
|
|
(dotimes (n (- lineno 1)) (insert-string "\n"))
|
|
;; Add the selected region
|
|
(insert-buffer-substring cur-buf start end)
|
|
;; Write it to a file
|
|
(write-file input-file)))
|
|
;; If requested, write the buffer to a file for use as globs.
|
|
(when globs-file
|
|
(let ((cur-buf-start (point-min)) (cur-buf-end (point-max)))
|
|
(save-excursion
|
|
(set-buffer in-buf)
|
|
(delete-region (point-min) (point-max))
|
|
(insert-buffer-substring cur-buf cur-buf-start cur-buf-end)
|
|
(write-file globs-file))))
|
|
;; Dispose of in-buf (we're done with it now.
|
|
(kill-buffer in-buf)
|
|
;; Prepare the results buffer. Clear it, if it contains
|
|
;; anything, and set its mode.
|
|
(setq doctest-results-buffer (get-buffer-create results-buf-name))
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
(toggle-read-only 0)
|
|
(delete-region (point-min) (point-max))
|
|
(doctest-results-mode)
|
|
(setq doctest-source-buffer cur-buf)
|
|
)
|
|
;; Add markers to examples, and record what line number each one
|
|
;; starts at. That way, if the input buffer is edited, we can
|
|
;; still find corresponding examples in the output.
|
|
(doctest-mark-examples)
|
|
|
|
;; Run doctest
|
|
(cond (doctest-async
|
|
;; Asynchronous mode:
|
|
(let ((process (start-process "*doctest*" doctest-results-buffer
|
|
doctest-python-command
|
|
"-c" script)))
|
|
;; Store some information about the process.
|
|
(setq doctest-async-process-buffer cur-buf)
|
|
(setq doctest-async-process process)
|
|
(push input-file doctest-async-process-tempfiles)
|
|
(when globs-file
|
|
(push globs-file doctest-async-process-tempfiles))
|
|
;; Set up a sentinel to respond when it's done running.
|
|
(set-process-sentinel process 'doctest-async-process-sentinel)
|
|
|
|
;; Show the output window.
|
|
(let ((w (display-buffer doctest-results-buffer)))
|
|
(when doctest-follow-output
|
|
;; Insert a newline, which will move the buffer's
|
|
;; point past the process's mark -- this causes the
|
|
;; window to scroll as new output is generated.
|
|
(save-current-buffer
|
|
(set-buffer doctest-results-buffer)
|
|
(insert-string "\n")
|
|
(set-window-point w (point)))))
|
|
|
|
;; Let the user know the process is running.
|
|
(doctest-update-mode-line ":running")
|
|
(message "Running doctest...")))
|
|
(t
|
|
;; Synchronous mode:
|
|
(call-process doctest-python-command nil
|
|
doctest-results-buffer t "-c" script)
|
|
(doctest-handle-output)
|
|
(delete-file input-file)
|
|
(when globs-file
|
|
(delete-file globs-file))))))))
|
|
|
|
(defun doctest-handle-output ()
|
|
"This function, which is called after the 'doctest' process spawned
|
|
by doctest-execute-buffer has finished, checks the doctest results
|
|
buffer. If that buffer is empty, it reports no errors and hides it;
|
|
if that buffer is not empty, it reports that errors occured, displays
|
|
the buffer, and runs doctest-postprocess-results."
|
|
;; If any tests failed, display them.
|
|
(cond ((not (buffer-live-p doctest-results-buffer))
|
|
(doctest-warn "Results buffer not found!"))
|
|
((> (buffer-size doctest-results-buffer) 1)
|
|
(display-buffer doctest-results-buffer)
|
|
(doctest-postprocess-results)
|
|
(let ((num (length doctest-example-markers)))
|
|
(message "%d doctest example%s failed!" num
|
|
(if (= num 1) "" "s"))))
|
|
(t
|
|
(display-buffer doctest-results-buffer)
|
|
(delete-windows-on doctest-results-buffer)
|
|
(message "All doctest examples passed!"))))
|
|
|
|
(defun doctest-async-process-sentinel (process state)
|
|
"A process sentinel, called when the asynchronous doctest process
|
|
completes, which calls doctest-handle-output."
|
|
;; Check to make sure we got the process we're expecting. On
|
|
;; some operating systems, this will end up getting called twice
|
|
;; when we use doctest-cancel-async-process; this check keeps us
|
|
;; from trying to clean up after the same process twice (since we
|
|
;; set doctest-async-process to nil when we're done).
|
|
(when (and (equal process doctest-async-process)
|
|
(buffer-live-p doctest-async-process-buffer))
|
|
(save-current-buffer
|
|
(set-buffer doctest-async-process-buffer)
|
|
(cond ((not (buffer-live-p doctest-results-buffer))
|
|
(doctest-warn "Results buffer not found!"))
|
|
((equal state "finished\n")
|
|
(doctest-handle-output)
|
|
(let ((window (get-buffer-window
|
|
doctest-async-process-buffer t)))
|
|
(when window (set-window-point window (point)))))
|
|
((equal state "killed\n")
|
|
(message "Doctest killed."))
|
|
(t
|
|
(message "Doctest failed -- %s" state)
|
|
(display-buffer doctest-results-buffer)))
|
|
(doctest-update-mode-line "")
|
|
(while doctest-async-process-tempfiles
|
|
(delete-file (pop doctest-async-process-tempfiles)))
|
|
(setq doctest-async-process nil))))
|
|
|
|
(defun doctest-cancel-async-process ()
|
|
"If a doctest process is running, then kill it."
|
|
(interactive "")
|
|
(when (doctest-process-live-p doctest-async-process)
|
|
;; Update the modeline
|
|
(doctest-update-mode-line ":killing")
|
|
;; Kill the process.
|
|
(kill-process doctest-async-process)
|
|
;; Run the sentinel. (Depending on what OS we're on, the sentinel
|
|
;; may end up getting called once or twice.)
|
|
(doctest-async-process-sentinel doctest-async-process "killed\n")
|
|
))
|
|
|
|
(defun doctest-postprocess-results ()
|
|
"Post-process the doctest results buffer: check what version of
|
|
doctest was used, and set doctest-results-py-version accordingly;
|
|
turn on read-only mode; filter the example markers; hide the example
|
|
source (if `doctest-hide-example-source' is non-nil); and select the
|
|
first failure."
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
;; Check if we're using an old doctest version.
|
|
(goto-char (point-min))
|
|
(if (re-search-forward doctest-py21-results-re nil t)
|
|
(setq doctest-results-py-version 'py21)
|
|
(setq doctest-results-py-version 'py24))
|
|
;; Turn on read-only mode.
|
|
(toggle-read-only t))
|
|
|
|
(doctest-filter-example-markers)
|
|
(if doctest-hide-example-source
|
|
(doctest-hide-example-source))
|
|
(doctest-next-failure 1))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Markers
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defun doctest-mark-examples ()
|
|
"Add a marker at the beginning of every (likely) example in the
|
|
input buffer; and create a list, `doctest-example-markers',
|
|
which maps from markers to the line numbers they originally occured
|
|
on. This will allow us to find the corresponding example in the
|
|
doctest output, even if the input buffer is edited."
|
|
(dolist (marker-info doctest-example-markers)
|
|
(set-marker (car marker-info) nil))
|
|
(setq doctest-example-markers '())
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(while (re-search-forward "^ *>>> " nil t)
|
|
(backward-char 4)
|
|
(push (cons (point-marker) (doctest-line-number))
|
|
doctest-example-markers)
|
|
(forward-char 4))))
|
|
|
|
(defun doctest-filter-example-markers ()
|
|
"Remove any entries from `doctest-example-markers' that do not
|
|
correspond to a failed example."
|
|
(let ((filtered nil) (markers doctest-example-markers))
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
(goto-char (point-max))
|
|
(while (re-search-backward (doctest-results-loc-re) nil t)
|
|
(let ((lineno (string-to-int (match-string 1))))
|
|
(when (equal doctest-results-py-version 'py21)
|
|
(setq lineno (+ lineno 1)))
|
|
(while (and markers (< lineno (cdar markers)))
|
|
(set-marker (caar markers) nil)
|
|
(setq markers (cdr markers)))
|
|
(if (and markers (= lineno (cdar markers)))
|
|
(push (pop markers) filtered)
|
|
(doctest-warn "Example expected on line %d but not found %s"
|
|
lineno markers)))))
|
|
(dolist (marker-info markers)
|
|
(set-marker (car marker-info) nil))
|
|
(setq doctest-example-markers filtered)))
|
|
|
|
(defun doctest-prev-example-marker ()
|
|
"Helper for doctest-replace-output: move to the preceeding example
|
|
marker, and return the corresponding 'original' lineno. If none is
|
|
found, return nil."
|
|
(let ((lineno nil)
|
|
(pos nil))
|
|
(save-excursion
|
|
(end-of-line)
|
|
(when (re-search-backward "^\\( *\\)>>> " nil t)
|
|
(goto-char (match-end 1))
|
|
(dolist (marker-info doctest-example-markers)
|
|
(when (= (marker-position (car marker-info)) (point))
|
|
(setq lineno (cdr marker-info))
|
|
(setq pos (point))))))
|
|
(unless (null lineno)
|
|
(goto-char pos)
|
|
lineno)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Navigation
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defun doctest-next-failure (count)
|
|
"Move to the top of the next failing example, and highlight the
|
|
example's failure description in *doctest-output*."
|
|
(interactive "p")
|
|
(cond
|
|
((and doctest-async (doctest-process-live-p doctest-async-process))
|
|
(message "Wait for doctest to finish running!"))
|
|
((not (doctest-results-buffer-valid-p))
|
|
(message "Run doctest first! (C-c C-c)"))
|
|
((equal count 0)
|
|
t)
|
|
(t
|
|
(let ((marker nil) (example-markers doctest-example-markers)
|
|
(results-window (display-buffer doctest-results-buffer)))
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
;; Pick up where we left off.
|
|
;; (nb: doctest-selected-failure is buffer-local)
|
|
(goto-char (or doctest-selected-failure (point-min)))
|
|
;; Skip past anything on *this* line.
|
|
(if (>= count 0) (end-of-line) (beginning-of-line))
|
|
;; Look for the next failure
|
|
(when
|
|
(if (>= count 0)
|
|
(re-search-forward (doctest-results-loc-re) nil t count)
|
|
(re-search-backward (doctest-results-loc-re) nil t (- count)))
|
|
;; We found a failure:
|
|
(let ((old-selected-failure doctest-selected-failure))
|
|
(beginning-of-line)
|
|
;; Extract the line number for the doctest file.
|
|
(let ((orig-lineno (string-to-int (match-string 1))))
|
|
(when (equal doctest-results-py-version 'py21)
|
|
(setq orig-lineno (+ orig-lineno 1)))
|
|
(dolist (marker-info example-markers)
|
|
(when (= orig-lineno (cdr marker-info))
|
|
(setq marker (car marker-info)))))
|
|
|
|
;; Update the window cursor.
|
|
(beginning-of-line)
|
|
(set-window-point results-window (point))
|
|
;; Store our position for next time.
|
|
(setq doctest-selected-failure (point))
|
|
;; Update selection.
|
|
(doctest-fontify-line old-selected-failure)
|
|
(doctest-fontify-line doctest-selected-failure))))
|
|
|
|
(cond
|
|
;; We found a failure -- move point to the selected failure.
|
|
(marker
|
|
(goto-char (marker-position marker))
|
|
(beginning-of-line))
|
|
;; We didn't find a failure, but there is one -- wrap.
|
|
((> (length doctest-example-markers) 0)
|
|
(if (>= count 0) (doctest-first-failure) (doctest-last-failure)))
|
|
;; We didn't find a failure -- alert the user.
|
|
(t (message "No failures found!")))))))
|
|
|
|
(defun doctest-prev-failure (count)
|
|
"Move to the top of the previous failing example, and highlight
|
|
the example's failure description in *doctest-output*."
|
|
(interactive "p")
|
|
(doctest-next-failure (- count)))
|
|
|
|
(defun doctest-first-failure ()
|
|
"Move to the top of the first failing example, and highlight
|
|
the example's failure description in *doctest-output*."
|
|
(interactive)
|
|
(if (buffer-live-p doctest-results-buffer)
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
(let ((old-selected-failure doctest-selected-failure))
|
|
(setq doctest-selected-failure (point-min))
|
|
(doctest-fontify-line old-selected-failure))))
|
|
(doctest-next-failure 1))
|
|
|
|
(defun doctest-last-failure ()
|
|
"Move to the top of the last failing example, and highlight
|
|
the example's failure description in *doctest-output*."
|
|
(interactive)
|
|
(if (buffer-live-p doctest-results-buffer)
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
(let ((old-selected-failure doctest-selected-failure))
|
|
(setq doctest-selected-failure (point-max))
|
|
(doctest-fontify-line old-selected-failure))))
|
|
(doctest-next-failure -1))
|
|
|
|
(defun doctest-select-failure ()
|
|
"Move to the top of the currently selected example, and select that
|
|
example in the source buffer. Intended for use in the results
|
|
buffer."
|
|
(interactive)
|
|
(re-search-backward doctest-results-divider-re)
|
|
(let ((old-selected-failure doctest-selected-failure))
|
|
(setq doctest-selected-failure (point))
|
|
(doctest-fontify-line doctest-selected-failure)
|
|
(doctest-fontify-line old-selected-failure))
|
|
(pop-to-buffer doctest-source-buffer)
|
|
(doctest-next-failure 1))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Replace Output
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defun doctest-replace-output ()
|
|
"Move to the top of the closest example, and replace its output
|
|
with the 'got' output from the *doctest-output* buffer. An error is
|
|
displayed if the chosen example is not listed in *doctest-output*, or
|
|
if the 'expected' output for the example does not exactly match the
|
|
output listed in the source buffer. The user is asked to confirm the
|
|
replacement."
|
|
(interactive)
|
|
;; Move to the beginning of the example.
|
|
(cond
|
|
((and doctest-async (doctest-process-live-p doctest-async-process))
|
|
(message "Wait for doctest to finish running!"))
|
|
((not (doctest-results-buffer-valid-p))
|
|
(message "Run doctest first! (C-c C-c)"))
|
|
((save-excursion (set-buffer doctest-results-buffer)
|
|
(equal doctest-results-py-version 'py21))
|
|
(error "doctest-replace-output requires python 2.4+"))
|
|
(t
|
|
(save-excursion
|
|
(save-restriction
|
|
(when (doctest-in-mmm-docstring-overlay)
|
|
(doctest-narrow-to-mmm-overlay))
|
|
|
|
(let* ((orig-buffer (current-buffer))
|
|
;; Find an example, and look up its original lineno.
|
|
(lineno (doctest-prev-example-marker))
|
|
;; Find the example's indentation.
|
|
(prompt-indent (doctest-line-indentation)))
|
|
|
|
;; Switch to the output buffer, and look for the example.
|
|
;; If we don't find one, complain.
|
|
(cond
|
|
((null lineno) (message "Doctest example not found"))
|
|
(t
|
|
(set-buffer doctest-results-buffer)
|
|
(goto-char (point-min))
|
|
(let ((output-re (format "^File .*, line %s," lineno)))
|
|
(when (not (re-search-forward output-re nil t))
|
|
(message "This doctest example did not fail")
|
|
(setq lineno nil)))))
|
|
|
|
;; If we didn't find an example, give up.
|
|
(when (not (null lineno))
|
|
;; Get the output's 'expected' & 'got' texts.
|
|
(let ((doctest-got nil) (doctest-expected nil) (header nil))
|
|
(while (setq header (doctest-results-next-header))
|
|
(cond
|
|
((equal header "Failed example:")
|
|
t)
|
|
((equal header "Expected nothing")
|
|
(setq doctest-expected ""))
|
|
((equal header "Expected:")
|
|
(unless (re-search-forward "^\\(\\( \\).*\n\\)*" nil t)
|
|
(error "Error parsing doctest output"))
|
|
(setq doctest-expected (doctest-replace-regexp-in-string
|
|
"^ " prompt-indent
|
|
(match-string 0))))
|
|
((equal header "Got nothing")
|
|
(setq doctest-got ""))
|
|
((or (equal header "Got:") (equal header "Exception raised:"))
|
|
(unless (re-search-forward "^\\(\\( \\).*\n\\)*" nil t)
|
|
(error "Error parsing doctest output"))
|
|
(setq doctest-got (doctest-replace-regexp-in-string
|
|
"^ " prompt-indent (match-string 0))))
|
|
((string-match "^Differences" header)
|
|
(error (concat "doctest-replace-output can not be used "
|
|
"with diff style output")))
|
|
(t (error "Unexpected header %s" header))))
|
|
|
|
;; Go back to the source buffer.
|
|
(set-buffer orig-buffer)
|
|
|
|
;; Skip ahead to the output.
|
|
(beginning-of-line)
|
|
(unless (re-search-forward "^ *>>>.*")
|
|
(error "Error parsing doctest output"))
|
|
(re-search-forward "\\(\n *\\.\\.\\..*\\)*\n?")
|
|
(when (looking-at "\\'") (insert-char ?\n))
|
|
|
|
;; Check that the output matches.
|
|
(let ((start (point)) end)
|
|
(cond ((re-search-forward "^ *\\(>>>.*\\|$\\)" nil t)
|
|
(setq end (match-beginning 0)))
|
|
(t
|
|
(goto-char (point-max))
|
|
(insert-string "\n")
|
|
(setq end (point-max))))
|
|
(when (and doctest-expected
|
|
(not (equal (buffer-substring start end)
|
|
doctest-expected)))
|
|
(warn "{%s} {%s}" (buffer-substring start end)
|
|
doctest-expected)
|
|
(error (concat "This example's output has been modified "
|
|
"since doctest was last run")))
|
|
(setq doctest-expected (buffer-substring start end))
|
|
(goto-char end))
|
|
|
|
;; Trim exceptions
|
|
(when (and doctest-trim-exceptions
|
|
(string-match doctest-traceback-re
|
|
doctest-got))
|
|
(let ((s1 0) (e1 (match-end 1))
|
|
(s2 (match-beginning 2)) (e2 (match-end 2))
|
|
(s3 (match-beginning 3)) (e3 (length doctest-got)))
|
|
(setq doctest-got
|
|
(concat (substring doctest-got s1 e1)
|
|
(substring doctest-got s2 e2) " . . .\n"
|
|
(substring doctest-got s3 e3)))))
|
|
|
|
;; Confirm it with the user.
|
|
(let ((confirm-buffer (get-buffer-create "*doctest-confirm*")))
|
|
(set-buffer confirm-buffer)
|
|
;; Erase anything left over in the buffer.
|
|
(delete-region (point-min) (point-max))
|
|
;; Write a confirmation message
|
|
(if (equal doctest-expected "")
|
|
(insert-string "Replace nothing\n")
|
|
(insert-string (concat "Replace:\n" doctest-expected)))
|
|
(if (equal doctest-got "")
|
|
(insert-string "With nothing\n")
|
|
(insert-string (concat "With:\n" doctest-got)))
|
|
(let ((confirm-window (display-buffer confirm-buffer)))
|
|
;; Shrink the confirm window.
|
|
(shrink-window-if-larger-than-buffer confirm-window)
|
|
;; Return to the original buffer.
|
|
(set-buffer orig-buffer)
|
|
;; Match the old expected region.
|
|
(when doctest-expected
|
|
(search-backward doctest-expected))
|
|
(when (equal doctest-expected "") (backward-char 1))
|
|
;; Get confirmation & do the replacement
|
|
(widen)
|
|
(cond ((y-or-n-p "Ok to replace? ")
|
|
(when (equal doctest-expected "") (forward-char 1))
|
|
(replace-match doctest-got t t)
|
|
(message "Replaced."))
|
|
(t
|
|
(message "Replace cancelled.")))
|
|
;; Clean up our confirm window
|
|
(kill-buffer confirm-buffer)
|
|
(delete-window confirm-window)))))))))))
|
|
|
|
(defun doctest-results-next-header ()
|
|
"Move to the next header in the doctest results buffer, and return
|
|
the string contents of that header. If no header is found, return
|
|
nil."
|
|
(if (re-search-forward (concat doctest-results-header-re "\\|"
|
|
doctest-results-divider-re) nil t)
|
|
(let ((result (match-string 0)))
|
|
(if (string-match doctest-results-header-re result)
|
|
result
|
|
nil))
|
|
nil))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; mmm-mode support
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; MMM Mode is a minor mode for Emacs which allows Multiple Major
|
|
;; Modes to coexist in a single buffer.
|
|
|
|
;;;###autoload
|
|
(defun doctest-register-mmm-classes (&optional add-mode-ext-classes
|
|
fix-mmm-fontify-region-bug)
|
|
"Register doctest's mmm classes, allowing doctest to be used as a
|
|
submode region in other major modes, such as python-mode and rst-mode.
|
|
Two classes are registered:
|
|
|
|
`doctest-docstring'
|
|
|
|
Used to edit docstrings containing doctest examples in python-
|
|
mode. Docstring submode regions start and end with triple-quoted
|
|
strings (\"\"\"). In order to avoid confusing start-string
|
|
markers and end-string markers, all triple-quote strings in the
|
|
buffer are treated as submode regions (even if they're not
|
|
actually docstrings). Use (C-c % C-d) to insert a new doctest-
|
|
docstring region. When `doctest-execute' (C-c C-c) is called
|
|
inside a doctest-docstring region, it executes just the current
|
|
docstring. The globals for this execution are constructed by
|
|
importing the current buffer's contents in Python.
|
|
|
|
`doctest-example'
|
|
|
|
Used to edit doctest examples in text-editing modes, such as
|
|
`rst-mode' or `text-mode'. Docstring submode regions start with
|
|
optionally indented prompts (>>>) and end with blank lines. Use
|
|
(C-c % C-e) to insert a new doctest-example region. When
|
|
`doctest-execute' (C-c C-c) is called inside a doctest-example
|
|
region, it executes all examples in the buffer.
|
|
|
|
If ADD-MODE-EXT-CLASSES is true, then register the new classes in
|
|
`mmm-mode-ext-classes-alist', which will cause them to be used by
|
|
default in the following modes:
|
|
|
|
doctest-docstring: python-mode
|
|
doctest-example: rst-mode
|
|
|
|
If FIX-MMM-FONTIFY-REGION-BUG is true, then register a hook that will
|
|
fix a bug in `mmm-fontify-region' that affects some (but not all)
|
|
versions of emacs. (See `doctest-fixed-mmm-fontify-region' for more
|
|
info.)"
|
|
(interactive)
|
|
(require 'mmm-auto)
|
|
(mmm-add-classes
|
|
'(
|
|
;; === doctest-docstring ===
|
|
(doctest-docstring :submode doctest-mode
|
|
|
|
;; The front is any triple-quote. Include it in the submode region,
|
|
;; to prevent clashes between the two syntax tables over quotes.
|
|
:front "\\(\"\"\"\\|'''\\)" :include-front t
|
|
|
|
;; The back matches the front. Include just the first character
|
|
;; of the quote. If we didn't include at least one quote, then
|
|
;; the outer modes quote-counting would be thrown off. But if
|
|
;; we include all three, we run into a bug in mmm-mode. See
|
|
;; <http://tinyurl.com/2fa83w> for more info about the bug.
|
|
:save-matches t :back "~1" :back-offset 1 :end-not-begin t
|
|
|
|
;; Define a skeleton for entering new docstrings.
|
|
:insert ((?d docstring nil @ "\"\"" @ "\"" \n
|
|
_ \n "\"" @ "\"\"" @)))
|
|
|
|
;; === doctest-example ===
|
|
(doctest-example
|
|
:submode doctest-mode
|
|
;; The front is an optionally indented prompt.
|
|
:front "^[ \t]*>>>" :include-front t
|
|
;; The back is a blank line.
|
|
:back "^[ \t]*$"
|
|
;; Define a skeleton for entering new docstrings.
|
|
:insert ((?e doctest-example nil
|
|
@ @ " >>> " _ "\n\n" @ @)))))
|
|
|
|
;; Register some local variables that need to be saved.
|
|
(add-to-list 'mmm-save-local-variables
|
|
'(doctest-results-buffer buffer))
|
|
(add-to-list 'mmm-save-local-variables
|
|
'(doctest-example-markers buffer))
|
|
|
|
;; Register association with modes, if requested.
|
|
(when add-mode-ext-classes
|
|
(mmm-add-mode-ext-class 'python-mode nil 'doctest-docstring)
|
|
(mmm-add-mode-ext-class 'rst-mode nil 'doctest-example))
|
|
|
|
;; Fix the buggy mmm-fontify-region, if requested.
|
|
(when fix-mmm-fontify-region-bug
|
|
(add-hook 'mmm-mode-hook 'doctest-fix-mmm-fontify-region-bug)))
|
|
|
|
(defvar doctest-old-mmm-fontify-region 'nil
|
|
"Used to hold the original definition of `mmm-fontify-region' when it
|
|
is rebound by `doctest-fix-mmm-fontify-region-bug'.")
|
|
|
|
(defun doctest-fix-mmm-fontify-region-bug ()
|
|
"A function for `mmm-mode-hook' which fixes a potential bug in
|
|
`mmm-fontify-region' by using `doctest-fixed-mmm-fontify-region'
|
|
instead. (See `doctest-fixed-mmm-fontify-region' for more info.)"
|
|
(setq font-lock-fontify-region-function
|
|
'doctest-fixed-mmm-fontify-region))
|
|
|
|
(defun doctest-fixed-mmm-fontify-region (start stop &optional loudly)
|
|
"A replacement for `mmm-fontify-region', which fixes a bug caused by
|
|
versions of emacs where post-command-hooks are run *before*
|
|
fontification. `mmm-mode' assumes that its post-command-hook will be
|
|
run after fontification; and if it's not, then mmm-mode can end up
|
|
with the wrong local variables, keymap, etc. after fontification. We
|
|
fix that here by redefining `mmm-fontify-region' to remember what
|
|
submode overlay it started in; and to return to that overlay after
|
|
fontification is complete. The original definition of
|
|
`mmm-fontify-region' is stored in `doctest-old-mmm-fontify-region'."
|
|
(let ((overlay mmm-current-overlay))
|
|
(mmm-fontify-region start stop loudly)
|
|
(if (and overlay (or (< (point) (overlay-start overlay))
|
|
(> (point) (overlay-end overlay))))
|
|
(goto-char (overlay-start overlay)))
|
|
(mmm-update-submode-region)))
|
|
|
|
(defun doctest-in-mmm-docstring-overlay ()
|
|
(and (featurep 'mmm-auto)
|
|
(mmm-overlay-at (point))
|
|
(save-excursion
|
|
(goto-char (overlay-start (mmm-overlay-at (point))))
|
|
(looking-at "\"\"\"\\|\'\'\'"))))
|
|
|
|
(defun doctest-narrow-to-mmm-overlay ()
|
|
"If we're in an mmm-mode overlay, then narrow to that overlay.
|
|
This is useful, e.g., to keep from interpreting the close-quote of a
|
|
docstring as part of the example's output."
|
|
(let ((bounds (doctest-mmm-overlay-bounds)))
|
|
(when bounds (narrow-to-region (car bounds) (cdr bounds)))))
|
|
|
|
(defun doctest-default-margin-in-mmm-docstring-overlay ()
|
|
(save-excursion
|
|
(let ((pos (car (doctest-mmm-overlay-bounds))))
|
|
(goto-char pos)
|
|
(when (doctest-looking-back "\"\"\"\\|\'\'\'")
|
|
(setq pos (- pos 3)))
|
|
(beginning-of-line)
|
|
(- pos (point)))))
|
|
|
|
(defun doctest-mmm-overlay-bounds ()
|
|
(when (featurep 'mmm-auto)
|
|
(let ((overlay (mmm-overlay-at (point))))
|
|
(when overlay
|
|
(let ((start (overlay-start overlay))
|
|
(end (overlay-end overlay)))
|
|
(when (doctest-in-mmm-docstring-overlay)
|
|
(save-excursion
|
|
(goto-char start)
|
|
(re-search-forward "[\"\']*")
|
|
(setq start (point))
|
|
(goto-char end)
|
|
(while (doctest-looking-back "[\"\']")
|
|
(backward-char 1))
|
|
(setq end (point))))
|
|
(cons start end))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Helper functions
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defun doctest-on-source-line-p (&optional prompt)
|
|
"Return true if the current line is a source line. The optional
|
|
argument prompt can be used to specify which type of source
|
|
line (... or >>>)."
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
;; Check if we're looking at a prompt (of the right type).
|
|
(when (and (looking-at doctest-prompt-re)
|
|
(or (null prompt)
|
|
(equal prompt (substring (match-string 2) 0 3))))
|
|
;; Scan backwards to make sure there's a >>> somewhere. Otherwise,
|
|
;; this might be a '...' in the text or in an example's output.
|
|
(while (looking-at "^[ \t]*[.][.][.]")
|
|
(forward-line -1))
|
|
(looking-at "^[ \t]*>>>"))))
|
|
|
|
(defun doctest-on-empty-source-line-p ()
|
|
"Return true if the current line contains a bare prompt."
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
(and (doctest-on-source-line-p)
|
|
(looking-at (concat doctest-prompt-re "$")))))
|
|
|
|
(defun doctest-on-output-line-p ()
|
|
"Return true if the current line is an output line."
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
;; The line must not be blank or a source line.
|
|
(when (not (or (doctest-on-source-line-p) (looking-at "[ \t]*$")))
|
|
;; The line must follow a source line, with no intervening blank
|
|
;; lines.
|
|
(while (not (or (doctest-on-source-line-p) (looking-at "[ \t]*$")
|
|
(= (point) (point-min))))
|
|
(forward-line -1))
|
|
(doctest-on-source-line-p))))
|
|
|
|
(defun doctest-find-output-line (&optional limit)
|
|
"Move forward to the next doctest output line (staying within
|
|
the given bounds). Return the character position of the doctest
|
|
output line if one was found, and false otherwise."
|
|
(let ((found-it nil) ; point where we found an output line
|
|
(limit (or limit (point-max)))) ; default value for limit
|
|
(save-excursion
|
|
;; Keep moving forward, one line at a time, until we find a
|
|
;; doctest output line.
|
|
(while (and (not found-it) (< (point) limit) (not (eobp)))
|
|
(if (and (not (eolp)) (doctest-on-output-line-p))
|
|
(setq found-it (point))
|
|
(forward-line))))
|
|
;; If we found a doctest output line, then go to it.
|
|
(if found-it (goto-char found-it))))
|
|
|
|
(defun doctest-line-indentation ()
|
|
"Helper for doctest-replace-output: return the whitespace indentation
|
|
at the beginning of this line."
|
|
(save-excursion
|
|
(end-of-line)
|
|
(re-search-backward "^\\( *\\)" nil t)
|
|
(match-string 1)))
|
|
|
|
(defun doctest-optionflags (&optional diff)
|
|
"Return a string describing the optionflags that should be used
|
|
by doctest. If DIFF is non-nil, then add the REPORT_UDIFF flag."
|
|
(let ((flags "0"))
|
|
(dolist (flag doctest-optionflags)
|
|
(setq flags (concat flags "|" flag)))
|
|
(if diff (concat flags "|" "REPORT_UDIFF") flags)))
|
|
|
|
(defun doctest-results-loc-re ()
|
|
"Return the regexp that should be used to look for doctest example
|
|
location markers in doctest's output (based on which version of
|
|
doctest was used"
|
|
(cond
|
|
((equal doctest-results-py-version 'py21)
|
|
doctest-py21-results-loc-re)
|
|
((equal doctest-results-py-version 'py24)
|
|
doctest-py24-results-loc-re)
|
|
(t (error "bad value for doctest-results-py-version"))))
|
|
|
|
(defun doctest-results-buffer-name ()
|
|
"Return the buffer name that should be used for the doctest results
|
|
buffer. This is computed from the variable
|
|
`doctest-results-buffer-name'."
|
|
(doctest-replace-regexp-in-string
|
|
"%[nfN]"
|
|
(lambda (sym)
|
|
(cond ((equal sym "%n") (buffer-name))
|
|
((equal sym "%N") (doctest-replace-regexp-in-string
|
|
"[.]doctest$" "" (buffer-name) t))
|
|
((equal sym "%f") (buffer-file-name))))
|
|
doctest-results-buffer-name t))
|
|
|
|
(defun doctest-script (input-file globs-file diff)
|
|
"..."
|
|
(doctest-replace-regexp-in-string
|
|
"%[tnflm]"
|
|
(lambda (sym)
|
|
(cond ((equal sym "%n") (buffer-name))
|
|
((equal sym "%f") (buffer-file-name))
|
|
((equal sym "%l") (doctest-optionflags diff))
|
|
((equal sym "%t") input-file)
|
|
((equal sym "%m") (or globs-file ""))))
|
|
doctest-script t))
|
|
|
|
(defun doctest-hide-example-source ()
|
|
"Delete the source code listings from the results buffer (since it's
|
|
easy enough to see them in the original buffer)"
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
(toggle-read-only 0)
|
|
(goto-char (point-min))
|
|
(while (re-search-forward doctest-example-source-re nil t)
|
|
(replace-match "" nil nil))
|
|
(toggle-read-only t)))
|
|
|
|
(defun doctest-results-buffer-valid-p ()
|
|
"Return true if this buffer has a live results buffer; and that
|
|
results buffer reports this buffer as its source buffer. (Two
|
|
buffers in doctest-mode might point to the same results buffer;
|
|
but only one of them will be equal to that results buffer's
|
|
source buffer."
|
|
(let ((cur-buf (current-buffer)))
|
|
(and (buffer-live-p doctest-results-buffer)
|
|
(save-excursion
|
|
(set-buffer doctest-results-buffer)
|
|
(equal cur-buf doctest-source-buffer)))))
|
|
|
|
(defun doctest-update-mode-line (value)
|
|
"Update the doctest mode line with the given string value. This
|
|
is used to display information about asynchronous processes that
|
|
are run by doctest-mode."
|
|
(setq doctest-mode-line-process
|
|
value)
|
|
(force-mode-line-update t))
|
|
|
|
(defun doctest-version ()
|
|
"Echo the current version of `doctest-mode' in the minibuffer."
|
|
(interactive)
|
|
(message "Using `doctest-mode' version %s" doctest-version))
|
|
|
|
(defun doctest-warn (msg &rest args)
|
|
"Display a doctest warning message."
|
|
(if (fboundp 'display-warning)
|
|
(display-warning 'doctest (apply 'format msg args))
|
|
(apply 'message msg args)))
|
|
|
|
(defun doctest-debug (msg &rest args)
|
|
"Display a doctest debug message."
|
|
(if (fboundp 'display-warning)
|
|
(display-warning 'doctest (apply 'format msg args) 'debug)
|
|
(apply 'message msg args)))
|
|
|
|
(defvar doctest-serial-number 0) ;used if broken-temp-names.
|
|
(defun doctest-temp-name ()
|
|
"Return a new temporary filename, for use in calling doctest."
|
|
(if (memq 'broken-temp-names features)
|
|
(let
|
|
((sn doctest-serial-number)
|
|
(pid (and (fboundp 'emacs-pid) (emacs-pid))))
|
|
(setq doctest-serial-number (1+ doctest-serial-number))
|
|
(if pid
|
|
(format "doctest-%d-%d" sn pid)
|
|
(format "doctest-%d" sn)))
|
|
(make-temp-name "doctest-")))
|
|
|
|
(defun doctest-fontify-line (charpos)
|
|
"Run font-lock-fontify-region on the line containing the given
|
|
position."
|
|
(if (and charpos (functionp 'font-lock-fontify-region))
|
|
(save-excursion
|
|
(goto-char charpos)
|
|
(let ((beg (progn (beginning-of-line) (point)))
|
|
(end (progn (end-of-line) (point))))
|
|
(font-lock-fontify-region beg end)))))
|
|
|
|
(defun doctest-do-auto-fill ()
|
|
"If the current line is a soucre line or an output line, do nothing.
|
|
Otherwise, call (do-auto-fill)."
|
|
(cond
|
|
;; Don't wrap source lines.
|
|
((doctest-on-source-line-p) nil)
|
|
;; Don't wrap output lines
|
|
((doctest-on-output-line-p) nil)
|
|
;; Wrap all other lines
|
|
(t (do-auto-fill))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Emacs Compatibility Functions
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Define compatible versions of functions that are defined
|
|
;; for some versions of emacs but not others.
|
|
|
|
;; Backwards compatibility: looking-back
|
|
(cond ((fboundp 'looking-back) ;; Emacs 22.x
|
|
(defalias 'doctest-looking-back 'looking-back))
|
|
(t
|
|
(defun doctest-looking-back (regexp)
|
|
"Return true if text before point matches REGEXP."
|
|
(save-excursion
|
|
(let ((orig-pos (point)))
|
|
;; Search backwards for the regexp.
|
|
(if (re-search-backward regexp nil t)
|
|
;; Check if it ends at the original point.
|
|
(= orig-pos (match-end 0))))))))
|
|
|
|
;; Backwards compatibility: replace-regexp-in-string
|
|
(cond ((fboundp 'replace-regexp-in-string)
|
|
(defalias 'doctest-replace-regexp-in-string 'replace-regexp-in-string))
|
|
(t ;; XEmacs 21.x or Emacs 20.x
|
|
(defun doctest-replace-regexp-in-string
|
|
(regexp rep string &optional fixedcase literal)
|
|
"Replace all matches for REGEXP with REP in STRING."
|
|
(let ((start 0))
|
|
(while (and (< start (length string))
|
|
(string-match regexp string start))
|
|
(setq start (+ (match-end 0) 1))
|
|
(let ((newtext (if (functionp rep)
|
|
(save-match-data
|
|
(funcall rep (match-string 0 string)))
|
|
rep)))
|
|
(setq string (replace-match newtext fixedcase
|
|
literal string)))))
|
|
string)))
|
|
|
|
;; Backwards compatibility: line-number
|
|
(cond ((fboundp 'line-number) ;; XEmacs
|
|
(defalias 'doctest-line-number 'line-number))
|
|
((fboundp 'line-number-at-pos) ;; Emacs 22.x
|
|
(defalias 'doctest-line-number 'line-number-at-pos))
|
|
(t ;; Emacs 21.x
|
|
(defun doctest-line-number (&optional pos)
|
|
"Return the line number of POS (default=point)."
|
|
(1+ (count-lines 1
|
|
(save-excursion (progn (beginning-of-line)
|
|
(or pos (point)))))))))
|
|
|
|
;; Backwards compatibility: process-live-p
|
|
(cond ((fboundp 'process-live-p) ;; XEmacs
|
|
(defalias 'doctest-process-live-p 'process-live-p))
|
|
(t ;; Emacs
|
|
(defun doctest-process-live-p (process)
|
|
(and (processp process)
|
|
(equal (process-status process) 'run)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Doctest Results Mode (output of doctest-execute-buffer)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; Register the font-lock keywords (xemacs)
|
|
(put 'doctest-results-mode 'font-lock-defaults
|
|
'(doctest-results-font-lock-keywords))
|
|
|
|
;; Register the font-lock keywords (older versions of gnu emacs)
|
|
(when (boundp 'font-lock-defaults-alist)
|
|
(add-to-list 'font-lock-defaults-alist
|
|
'(doctest-results-mode doctest-results-font-lock-keywords
|
|
nil nil nil nil)))
|
|
|
|
(defvar doctest-selected-failure nil
|
|
"The location of the currently selected failure.
|
|
This variable is uffer-local to doctest-results-mode buffers.")
|
|
|
|
(defvar doctest-source-buffer nil
|
|
"The buffer that spawned this one.
|
|
This variable is uffer-local to doctest-results-mode buffers.")
|
|
|
|
(defvar doctest-results-py-version nil
|
|
"A symbol indicating which version of Python was used to generate
|
|
the results in a doctest-results-mode buffer. Can be either the
|
|
symbol `py21' or the symbol `py24'.
|
|
This variable is uffer-local to doctest-results-mode buffers.")
|
|
|
|
;; Keymap for doctest-results-mode.
|
|
(defconst doctest-results-mode-map
|
|
(let ((map (make-keymap)))
|
|
(define-key map [return] 'doctest-select-failure)
|
|
map)
|
|
"Keymap for doctest-results-mode.")
|
|
|
|
;; Syntax table for doctest-results-mode.
|
|
(defvar doctest-results-mode-syntax-table nil
|
|
"Syntax table used in `doctest-results-mode' buffers.")
|
|
(when (not doctest-results-mode-syntax-table)
|
|
(setq doctest-results-mode-syntax-table (make-syntax-table))
|
|
(dolist (entry '(("(" . "()") ("[" . "(]") ("{" . "(}")
|
|
(")" . ")(") ("]" . ")[") ("}" . "){")
|
|
("$%&*+-/<=>|'\"`" . ".") ("_" . "w")))
|
|
(dolist (char (string-to-list (car entry)))
|
|
(modify-syntax-entry char (cdr entry)
|
|
doctest-results-mode-syntax-table))))
|
|
|
|
;; Define the mode
|
|
(defun doctest-results-mode ()
|
|
"A major mode used to display the results of running doctest.
|
|
See `doctest-mode'.
|
|
|
|
\\{doctest-results-mode-map}"
|
|
(interactive)
|
|
|
|
;; Declare local variables.
|
|
(kill-all-local-variables)
|
|
(make-local-variable 'font-lock-defaults)
|
|
(make-local-variable 'doctest-selected-failure)
|
|
(make-local-variable 'doctest-source-buffer)
|
|
(make-local-variable 'doctest-results-py-version)
|
|
|
|
;; Define local variables.
|
|
(setq major-mode 'doctest-results-mode
|
|
mode-name "Doctest-Results"
|
|
mode-line-process 'doctest-mode-line-process
|
|
font-lock-defaults '(doctest-results-font-lock-keywords
|
|
nil nil nil nil))
|
|
;; Define keymap.
|
|
(use-local-map doctest-results-mode-map)
|
|
|
|
;; Define the syntax table.
|
|
(set-syntax-table doctest-results-mode-syntax-table)
|
|
|
|
;; Enable font-lock mode.
|
|
(if (featurep 'font-lock) (font-lock-mode 1)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Doctest Mode
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; Register the font-lock keywords (xemacs)
|
|
(put 'doctest-mode 'font-lock-defaults '(doctest-font-lock-keywords
|
|
nil nil nil nil))
|
|
|
|
;; Register the font-lock keywords (older versions of gnu emacs)
|
|
(when (boundp 'font-lock-defaults-alist)
|
|
(add-to-list 'font-lock-defaults-alist
|
|
'(doctest-mode doctest-font-lock-keywords
|
|
nil nil nil nil)))
|
|
|
|
(defvar doctest-results-buffer nil
|
|
"The output buffer for doctest-mode.
|
|
This variable is buffer-local to doctest-mode buffers.")
|
|
|
|
(defvar doctest-example-markers nil
|
|
"A list mapping markers to the line numbers at which they appeared
|
|
in the buffer at the time doctest was last run. This is used to find
|
|
'original' line numbers, which can be used to search the doctest
|
|
output buffer. It's encoded as a list of (MARKER . POS) tuples, in
|
|
reverse POS order.
|
|
This variable is buffer-local to doctest-mode buffers.")
|
|
|
|
;; These are global, since we only one run process at a time:
|
|
(defvar doctest-async-process nil
|
|
"The process object created by the asynchronous doctest process")
|
|
(defvar doctest-async-process-tempfiles nil
|
|
"A list of tempfile names created by the asynchronous doctest process")
|
|
(defvar doctest-async-process-buffer nil
|
|
"The source buffer for the asynchronous doctest process")
|
|
(defvar doctest-mode-line-process ""
|
|
"A string displayed on the modeline, to indicate when doctest is
|
|
running asynchronously.")
|
|
|
|
;; Keymap for doctest-mode. n.b.: we intentionally define [tab]
|
|
;; rather than overriding indent-line-function, since we don't want
|
|
;; doctest-indent-source-line to be called by do-auto-fill.
|
|
(defconst doctest-mode-map
|
|
(let ((map (make-keymap)))
|
|
(define-key map [backspace] 'doctest-electric-backspace)
|
|
(define-key map [return] 'doctest-newline-and-indent)
|
|
(define-key map [tab] 'doctest-indent-source-line)
|
|
(define-key map ":" 'doctest-electric-colon)
|
|
(define-key map "\C-c\C-v" 'doctest-version)
|
|
(define-key map "\C-c\C-c" 'doctest-execute)
|
|
(define-key map "\C-c\C-d" 'doctest-execute-with-diff)
|
|
(define-key map "\C-c\C-n" 'doctest-next-failure)
|
|
(define-key map "\C-c\C-p" 'doctest-prev-failure)
|
|
(define-key map "\C-c\C-a" 'doctest-first-failure)
|
|
(define-key map "\C-c\C-e" 'doctest-last-failure)
|
|
(define-key map "\C-c\C-z" 'doctest-last-failure)
|
|
(define-key map "\C-c\C-r" 'doctest-replace-output)
|
|
(define-key map "\C-c|" 'doctest-execute-region)
|
|
map)
|
|
"Keymap for doctest-mode.")
|
|
|
|
;; Syntax table for doctest-mode.
|
|
(defvar doctest-mode-syntax-table nil
|
|
"Syntax table used in `doctest-mode' buffers.")
|
|
(when (not doctest-mode-syntax-table)
|
|
(setq doctest-mode-syntax-table (make-syntax-table))
|
|
(dolist (entry '(("(" . "()") ("[" . "(]") ("{" . "(}")
|
|
(")" . ")(") ("]" . ")[") ("}" . "){")
|
|
("$%&*+-/<=>|'\"`" . ".") ("_" . "w")))
|
|
(dolist (char (string-to-list (car entry)))
|
|
(modify-syntax-entry char (cdr entry) doctest-mode-syntax-table))))
|
|
|
|
;; Use doctest mode for files ending in .doctest
|
|
;;;###autoload
|
|
(add-to-list 'auto-mode-alist '("\\.doctest$" . doctest-mode))
|
|
|
|
;;;###autoload
|
|
(defun doctest-mode ()
|
|
"A major mode for editing text files that contain Python
|
|
doctest examples. Doctest is a testing framework for Python that
|
|
emulates an interactive session, and checks the result of each
|
|
command. For more information, see the Python library reference:
|
|
<http://docs.python.org/lib/module-doctest.html>
|
|
|
|
`doctest-mode' defines three kinds of line, each of which is
|
|
treated differently:
|
|
|
|
- 'Source lines' are lines consisting of a Python prompt
|
|
('>>>' or '...'), followed by source code. Source lines are
|
|
colored (similarly to `python-mode') and auto-indented.
|
|
|
|
- 'Output lines' are non-blank lines immediately following
|
|
source lines. They are colored using several doctest-
|
|
specific output faces.
|
|
|
|
- 'Text lines' are any other lines. They are not processed in
|
|
any special way.
|
|
|
|
\\{doctest-mode-map}"
|
|
(interactive)
|
|
|
|
;; Declare local variables.
|
|
(kill-all-local-variables)
|
|
(make-local-variable 'font-lock-defaults)
|
|
(make-local-variable 'doctest-results-buffer)
|
|
(make-local-variable 'doctest-example-markers)
|
|
|
|
;; Define local variables.
|
|
(setq major-mode 'doctest-mode
|
|
mode-name "Doctest"
|
|
mode-line-process 'doctest-mode-line-process
|
|
font-lock-defaults '(doctest-font-lock-keywords
|
|
nil nil nil nil))
|
|
|
|
;; Define keymap.
|
|
(use-local-map doctest-mode-map)
|
|
|
|
;; Define the syntax table.
|
|
(set-syntax-table doctest-mode-syntax-table)
|
|
|
|
;; Enable auto-fill mode.
|
|
(auto-fill-mode 1)
|
|
(setq auto-fill-function 'doctest-do-auto-fill)
|
|
|
|
;; Enable font-lock mode.
|
|
(if (featurep 'font-lock) (font-lock-mode 1))
|
|
|
|
;; Run the mode hook.
|
|
(run-hooks 'doctest-mode-hook))
|
|
|
|
(provide 'doctest-mode)
|
|
;;; doctest-mode.el ends here
|