(Emacs) Python mode, version 1.02

Tim Peters (tim@ksr.com)
Thu, 20 Feb 92 01:20:27 EST

> [jack jansen]
> if you're looking for more things to do to the python mode (grin:-):

Yes, actually -- of the spare time I've been able to make for Python
lately, I've spent 95% of it indenting Python instead of actually using
it <grin/sigh>. Seriously, I can use all the input from the trenches I
can get.

> Guido and myself are having eternal battles about indent level: he
> uses 8 and I use 4. Needless to say, neither of us will give up,

Well, of course *you* shouldn't give up, because you're right <grin>.

> I often edit code by him (and the other way around). I find that I keep
> using set-variable to change the indent, so I wonder wether it would
> be possible to have a command that more-or-less automatically guesses
> the indent of the current file and sets py-indent-offset accordingly...

A patch to change python-mode.el from version 1.01 to 1.02 is attached
below. The new `C-c :' command should (I think) be close to what you
want. Just hit `C-c :'; if it doesn't do what you wanted, read the docs
& try again.

Note that, by default, it makes py-indent-offset local to the buffer:
that way you can have Guido's code in one buffer, yours in another, and
when moving stuff between them `C-c TAB' will magically reindent
correctly for the buffer the stuff ends up in. `C-c C-h m' has also
been changed to show both the local and the global values of a variable
(when two versions exist).

For those feeling nervous, the easiest way to use 'patch' is to

1) Read this whole mail msg into a file, say some_file.

2) Go to the directory that contains python-mode.el, and enter

patch < some_file

That will rename the original python-mode.el to python-mode.el.orig,
and upgrade python-mode.el to version 1.02.

a-fellow-indenter-by-4's-ly y'rs - tim

Tim Peters Kendall Square Research Corp
tim@ksr.com, ksr!tim@uunet.uu.net

Change log:

Thu Feb 20 00:13:34 1992 tim
version 1.02
added hint on entering multiple CTRL keys
added back-to-indentation to the "obscure Emacs cmds" section
added 'indentation' to the short blurbs saying what py-mode knows about
tossed progn in py-continuation-line-p (a tad faster)
ditto in py-goto-block-up
changed `x' to `_' in the "why indentation is up to you" blurb
for clarity (sez guido; i agree)
several changes to py-dump-help-string
replaced read-from-string by `intern'
ditto (princ "\n") -> (terpri)
renamed 'kind' -> 'funckind'
signal error if funckind unknown
display both local & global vrbl values (if appropriate)
added new function py-guess-indent-offset (a la jack jansen)
bound to `C-c :'; updated docs

Patch:

*** python-mode.el Wed Feb 19 23:21:58 1992
--- python-mode.102.el Wed Feb 19 23:34:05 1992
***************
*** 1,4 ****
! ;;; Major mode for editing Python programs, version 1.01
;; by: Michael A. Guravage
;; Guido van Rossum <guido@cwi.nl>
;; Tim Peters <tim@ksr.com>
--- 1,4 ----
! ;;; Major mode for editing Python programs, version 1.02
;; by: Michael A. Guravage
;; Guido van Rossum <guido@cwi.nl>
;; Tim Peters <tim@ksr.com>
***************
*** 28,34 ****
"*Shell command used to start Python interpreter.")

(defvar py-indent-offset 8 ; argue with Guido <grin>
! "*Indentation increment.")

(defvar py-continuation-offset 2
"*Indentation (in addition to py-indent-offset) for continued lines.
--- 28,36 ----
"*Shell command used to start Python interpreter.")

(defvar py-indent-offset 8 ; argue with Guido <grin>
! "*Indentation increment.
! Note that `\\[py-guess-indent-offset]' can usually guess a good value when you're
! editing someone else's Python code.")

(defvar py-continuation-offset 2
"*Indentation (in addition to py-indent-offset) for continued lines.
***************
*** 73,78 ****
--- 75,81 ----
(define-key py-mode-map "\C-c!" 'py-shell)
(define-key py-mode-map "\177" 'py-delete-char)
(define-key py-mode-map "\n" 'py-newline-and-indent)
+ (define-key py-mode-map "\C-c:" 'py-guess-indent-offset)
(define-key py-mode-map "\C-c\t" 'py-indent-region)
(define-key py-mode-map "\C-c<" 'py-shift-region-left)
(define-key py-mode-map "\C-c>" 'py-shift-region-right)
***************
*** 140,146 ****
(defun python-mode ()
"Major mode for editing Python files.
Do `\\[py-describe-mode]' for detailed documentation.
! Knows about Python tokens, comments and continuation lines.
Paragraphs are separated by blank lines only.

COMMANDS
--- 143,149 ----
(defun python-mode ()
"Major mode for editing Python files.
Do `\\[py-describe-mode]' for detailed documentation.
! Knows about Python indentation, tokens, comments and continuation lines.
Paragraphs are separated by blank lines only.

COMMANDS
***************
*** 324,329 ****
--- 327,387 ----
(+ (current-indentation) py-indent-offset)
(current-indentation))))))

+ (defun py-guess-indent-offset (&optional global)
+ "Guess a good value for, and change, py-indent-offset.
+ By default (without a prefix arg), makes a buffer-local copy of
+ py-indent-offset with the new value. This will not affect any other
+ Python buffers. With a prefix arg, changes the global value of
+ py-indent-offset. This affects all Python buffers (that don't have
+ their own buffer-local copy), both those currently existing and those
+ created later in the Emacs session.
+
+ Some people use a different value for py-indent-offset than you use.
+ There's no excuse for such foolishness, but sometimes you have to deal
+ with their ugly code anyway. This function examines the file and sets
+ py-indent-offset to what it thinks it was when they created the mess.
+
+ Specifically, it searches forward from the statement containing point,
+ looking for a line that opens a block of code. py-indent-offset is set
+ to the difference in indentation between that line and the Python
+ statement following it. If the search doesn't succeed going forward,
+ it's tried again going backward."
+ (interactive "P") ; raw prefix arg
+ (let ( new-value
+ (start (point))
+ (found nil)
+ colon-indent)
+ (py-goto-initial-line)
+ (while (not (or found (eobp)))
+ (setq found
+ (and
+ (re-search-forward ":[ \t]*\\($\\|[#\\]\\)" nil 'move)
+ (or (py-goto-initial-line) t) ; always true -- side effect
+ (looking-at py-colon-line-re))))
+ (if found
+ ()
+ (goto-char start)
+ (py-goto-initial-line)
+ (while (not (or found (bobp)))
+ (setq found
+ (and
+ (re-search-backward ":[ \t]*\\($\\|[#\\]\\)" nil 'move)
+ (or (py-goto-initial-line) t) ; always true -- side effect
+ (looking-at py-colon-line-re)))))
+ (setq colon-indent (current-indentation)
+ found (and found (zerop (py-next-statement 1)))
+ new-value (- (current-indentation) colon-indent))
+ (goto-char start)
+ (if found
+ (progn
+ (funcall (if global 'kill-local-variable 'make-local-variable)
+ 'py-indent-offset)
+ (setq py-indent-offset new-value)
+ (message "%s value of py-indent-offset set to %d"
+ (if global "Global" "Local")
+ py-indent-offset))
+ (error "Sorry, couldn't guess a value for py-indent-offset"))))
+
(defun py-shift-region (start end count)
(save-excursion
(goto-char end) (beginning-of-line) (setq end (point))
***************
*** 500,510 ****
(setq found
(and
(re-search-backward ":[ \t]*\\($\\|[#\\]\\)" nil 'move)
! (progn
! (py-goto-initial-line)
! (and
! (< (current-indentation) initial-indent)
! (looking-at py-colon-line-re))))))
(if found
(progn
(or nomark (push-mark start))
--- 558,566 ----
(setq found
(and
(re-search-backward ":[ \t]*\\($\\|[#\\]\\)" nil 'move)
! (or (py-goto-initial-line) t) ; always true -- side effect
! (< (current-indentation) initial-indent)
! (looking-at py-colon-line-re))))
(if found
(progn
(or nomark (push-mark start))
***************
*** 618,650 ****
;; current values
(defun py-dump-help-string (str)
(with-output-to-temp-buffer "*Help*"
! (let ( kind
! funcname func funcdoc
(start 0) mstart end
keys )
(while (string-match "^%\\([vc]\\):\\(.+\\)\n" str start)
(setq mstart (match-beginning 0) end (match-end 0)
! kind (substring str (match-beginning 1) (match-end 1))
funcname (substring str (match-beginning 2) (match-end 2))
! func (car (read-from-string funcname)))
(princ (substitute-command-keys (substring str start mstart)))
! (if (equal kind "c")
! (setq funcdoc (documentation func)
! keys (concat
! "Key(s): "
! (mapconcat 'key-description
! (where-is-internal func py-mode-map)
! ", ")))
! (setq funcdoc (substitute-command-keys
! (get func 'variable-documentation))
! keys (concat
! "Value: "
! (prin1-to-string (symbol-value func)))))
(princ (format "\n-> %s:\t%s\t%s\n\n"
! (if (equal kind "c") "Command" "Variable")
funcname keys))
(princ funcdoc)
! (princ "\n")
(setq start end))
(princ (substitute-command-keys (substring str start))))
(print-help-return-message)))
--- 674,716 ----
;; current values
(defun py-dump-help-string (str)
(with-output-to-temp-buffer "*Help*"
! (let ( (locals (buffer-local-variables))
! funckind funcname func funcdoc
(start 0) mstart end
keys )
(while (string-match "^%\\([vc]\\):\\(.+\\)\n" str start)
(setq mstart (match-beginning 0) end (match-end 0)
! funckind (substring str (match-beginning 1) (match-end 1))
funcname (substring str (match-beginning 2) (match-end 2))
! func (intern funcname))
(princ (substitute-command-keys (substring str start mstart)))
! (cond
! ( (equal funckind "c") ; command
! (setq funcdoc (documentation func)
! keys (concat
! "Key(s): "
! (mapconcat 'key-description
! (where-is-internal func py-mode-map)
! ", "))))
! ( (equal funckind "v") ; variable
! (setq funcdoc (substitute-command-keys
! (get func 'variable-documentation))
! keys (if (assq func locals)
! (concat
! "Local/Global values: "
! (prin1-to-string (symbol-value func))
! " / "
! (prin1-to-string (default-value func)))
! (concat
! "Value: "
! (prin1-to-string (symbol-value func))))))
! ( t ; unexpected
! (error "Error in py-dump-help-string, tag `%s'" funckind)))
(princ (format "\n-> %s:\t%s\t%s\n\n"
! (if (equal funckind "c") "Command" "Variable")
funcname keys))
(princ funcdoc)
! (terpri)
(setq start end))
(princ (substitute-command-keys (substring str start))))
(print-help-return-message)))
***************
*** 653,659 ****
"Dump long form of Python-mode docs."
(interactive)
(py-dump-help-string "Major mode for editing Python files.
! Knows about Python tokens, comments and continuation lines.
Paragraphs are separated by blank lines only.

Major sections below begin with the string `@'; specific function and
--- 719,725 ----
"Dump long form of Python-mode docs."
(interactive)
(py-dump-help-string "Major mode for editing Python files.
! Knows about Python indentation, tokens, comments and continuation lines.
Paragraphs are separated by blank lines only.

Major sections below begin with the string `@'; specific function and
***************
*** 749,754 ****
--- 815,823 ----
\t\\[py-delete-char]\t reduce indentation, or delete single character

Primarily for reindenting existing code:
+ \t\\[py-guess-indent-offset]\t guess py-indent-offset from file content; change locally
+ \t\\[universal-argument] \\[py-guess-indent-offset]\t ditto, but change globally
+
\t\\[py-indent-region]\t reindent region to match its context
\t\\[py-shift-region-left]\t shift region left by py-indent-offset
\t\\[py-shift-region-right]\t shift region right by py-indent-offset
***************
*** 763,780 ****
the indentation of preceding statements. E.g., assuming
py-indent-offset is 4, after you enter
\tif a > 0: \\[py-newline-and-indent]
! the cursor will be moved to the position of the `x':
\tif a > 0:
! \t x
If you then enter `c = d' \\[py-newline-and-indent], the cursor will move
to
\tif a > 0:
\t c = d
! \t x
Python-mode cannot know whether that's what you intended, or whether
\tif a > 0:
\t c = d
! \tx
was your intent. In general, Python-mode either reproduces the
indentation of the (closest code or indenting-comment) preceding
statement, or adds an extra py-indent-offset blanks if the preceding
--- 832,851 ----
the indentation of preceding statements. E.g., assuming
py-indent-offset is 4, after you enter
\tif a > 0: \\[py-newline-and-indent]
! the cursor will be moved to the position of the `_' (_ is not a
! character in the file, it's just used here to indicate the location of
! the cursor):
\tif a > 0:
! \t _
If you then enter `c = d' \\[py-newline-and-indent], the cursor will move
to
\tif a > 0:
\t c = d
! \t _
Python-mode cannot know whether that's what you intended, or whether
\tif a > 0:
\t c = d
! \t_
was your intent. In general, Python-mode either reproduces the
indentation of the (closest code or indenting-comment) preceding
statement, or adds an extra py-indent-offset blanks if the preceding
***************
*** 789,794 ****
--- 860,870 ----
%c:py-newline-and-indent
%c:py-delete-char

+
+ The next function may be handy when editing code you didn't write:
+ %c:py-guess-indent-offset
+
+
The remaining `indent' functions apply to a region of Python code. They
assume the block structure (equals indentation, in Python) of the region
is correct, and alter the indentation in various ways while preserving
***************
*** 827,835 ****

`\\[indent-new-comment-line]' is handy for entering a multi-line comment.

! `\\[set-selective-display]' with a `small' prefix arg is ideally suited for viewing
! the overall class and def structure of a module.

`\\[indent-relative]' is handy for creating odd indentation.

@OTHER EMACS HINTS
--- 903,913 ----

`\\[indent-new-comment-line]' is handy for entering a multi-line comment.

! `\\[set-selective-display]' with a `small' prefix arg is ideally suited for viewing the
! overall class and def structure of a module.

+ `\\[back-to-indentation]' moves point to a line's first non-blank character.
+
`\\[indent-relative]' is handy for creating odd indentation.

@OTHER EMACS HINTS
***************
*** 842,847 ****
--- 920,931 ----
To see the value of a variable, do `\\[describe-variable]' and enter the variable
name at the prompt.

+ When entering a key sequence like `C-c C-n', it is not necessary to
+ release the CONTROL key after doing the `C-c' part -- it suffices to
+ press the CONTROL key, press and release `c' (while still holding down
+ CONTROL), press and release `n' (while still holding down CONTROL), &
+ then release CONTROL.
+
Entering Python mode calls with no arguments the value of the variable
`py-mode-hook', if that value exists and is not nil; see the `Hooks'
section of the Elisp manual for details.
***************
*** 878,886 ****
;; use a cheap test first to avoid the regexp if possible
;; use 'eq' because char-after may return nil
(eq (char-after (- (point) 2)) ?\\ )
! (progn
! (forward-line -1) ; since eq test passed, there is a line above
! (looking-at py-continued-re)))))

;; go to start of first statement (not blank or comment or continuation
;; line) at or preceding point
--- 962,970 ----
;; use a cheap test first to avoid the regexp if possible
;; use 'eq' because char-after may return nil
(eq (char-after (- (point) 2)) ?\\ )
! ;; make sure; since eq test passed, there is a preceding line
! (forward-line -1) ; always true -- side effect
! (looking-at py-continued-re))))

;; go to start of first statement (not blank or comment or continuation
;; line) at or preceding point

>>> END OF MSG