Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

628 lines
24 KiB

;;; le-python.el --- lispy support for Python. -*- lexical-binding: t -*-
;; Copyright (C) 2016 Oleh Krehel
;; This file is not part of GNU Emacs
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; For a full copy of the GNU General Public License
;; see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'python)
(require 'json)
(defun lispy-trim-python (str)
"Trim extra Python indentation from STR.
STR is a string copied from Python code. It can be that each line
of STR is prefixed by e.g. 4 or 8 or 12 spaces.
Stripping them will produce code that's valid for an eval."
(if (string-match "\\`\\( +\\)" str)
(let* ((indent (match-string 1 str))
(re (concat "^" indent)))
(apply #'concat
(split-string str re t)))
str))
(defun lispy-eval-python-bnd ()
(let (bnd)
(save-excursion
(cond ((region-active-p)
(cons
(if (> (count-lines (region-beginning) (region-end)) 1)
(save-excursion
(goto-char (region-beginning))
(skip-chars-backward " ")
(point))
(region-beginning))
(region-end)))
((and (looking-at lispy-outline)
(looking-at lispy-outline-header))
(lispy--bounds-outline))
((looking-at "@")
(setq bnd (cons (point)
(save-excursion
(forward-sexp)
(skip-chars-forward "[ \t\n]")
(cdr (lispy-bounds-python-block))))))
((setq bnd (lispy-bounds-python-block)))
((bolp)
(lispy--bounds-c-toplevel))
((lispy-bolp)
(lispy--bounds-c-toplevel))
(t
(cond ((lispy-left-p))
((lispy-right-p)
(backward-list))
(t
(error "Unexpected")))
(setq bnd (lispy--bounds-dwim))
(ignore-errors (backward-sexp))
(while (or (eq (char-before) ?.)
(eq (char-after) ?\())
(backward-sexp))
(setcar bnd (point))
bnd)))))
(defun lispy-extended-eval-str (bnd)
(let* ((str (lispy--string-dwim bnd))
(lp (cl-count ?\( str))
(rp (cl-count ?\) str)))
(save-excursion
(goto-char (cdr bnd))
(while (< rp lp)
(re-search-forward "[()]" nil t)
(cond ((string= (match-string 0) "(")
(cl-incf lp))
((string= (match-string 0) ")")
(cl-incf rp))
(t
(error "Unexpected"))))
(if (lispy-after-string-p ")")
(let ((end (point)))
(save-excursion
(forward-sexp -1)
(concat (buffer-substring-no-properties
(car bnd) (point))
(replace-regexp-in-string
"[\\]*\n[\t ]*" " "
(buffer-substring-no-properties
(point) end)))))
(buffer-substring-no-properties (car bnd) (point))))))
(defun lispy-eval-python-str ()
(let* ((bnd (lispy-eval-python-bnd))
(str1 (lispy-trim-python
(lispy-extended-eval-str bnd)))
(str1.5 (replace-regexp-in-string "^ *#[^\n]+\n" "" str1))
;; (str2 (replace-regexp-in-string "\\\\\n +" "" str1.5))
;; (str3 (replace-regexp-in-string "\n *\\([])}]\\)" "\\1" str2))
;; (str4 (replace-regexp-in-string "\\([({[,]\\)\n +" "\\1" str3))
;; (str5 (replace-regexp-in-string "\"\n *\"" "\" \"" str4))
)
str1.5))
(defun lispy-bounds-python-block ()
(if (save-excursion
(when (looking-at " ")
(forward-char))
(python-info-beginning-of-block-p))
(let ((indent (if (bolp)
0
(1+ (- (point) (line-beginning-position))))))
(cons
(line-beginning-position)
(save-excursion
(python-nav-end-of-block)
(while (let ((pt (point))
bnd)
(skip-chars-forward "\n ")
(when (setq bnd (lispy--bounds-comment))
(goto-char (cdr bnd)))
(beginning-of-line)
(if (looking-at (format "[\n ]\\{%d,\\}\\(except\\|else\\|elif\\)" indent))
t
(goto-char pt)
nil))
(goto-char (match-beginning 1))
(python-nav-end-of-block))
(point))))
(cons (if (looking-at " ")
(1+ (point))
(point))
(save-excursion
(end-of-line)
(let (bnd)
(when (setq bnd (lispy--bounds-string))
(goto-char (cdr bnd))))
(end-of-line)
(while (member (char-before) '(?\\ ?\( ?\, ?\[ ?\{))
(if (member (char-before) '(?\( ?\[ ?\{))
(progn
(up-list)
(end-of-line))
(end-of-line 2)))
(point)))))
(defun lispy-eval-python (&optional plain)
(let ((res (lispy--eval-python
(lispy-eval-python-str)
plain)))
(if (and res (not (equal res "")))
(lispy-message
(replace-regexp-in-string
"%" "%%" res))
(lispy-message
(replace-regexp-in-string
"%" "%%" lispy-eval-error)))))
(defvar-local lispy-python-proc nil)
(declare-function mash-make-shell "ext:mash")
(defun lispy-set-python-process-action (x)
(setq lispy-python-proc
(cond ((consp x)
(cdr x))
((require 'mash-python nil t)
(save-window-excursion
(get-buffer-process
(mash-make-shell x 'mash-new-lispy-python))))
(t
(lispy--python-proc (concat "lispy-python-" x))))))
(defun lispy-short-process-name (x)
(when (string-match "^lispy-python-\\(.*\\)" (process-name x))
(match-string 1 (process-name x))))
(defun lispy-set-python-process ()
"Associate a (possibly new) Python process to the current buffer.
Each buffer can have only a single Python process associated with
it at one time."
(interactive)
(let* ((process-names
(delq nil
(mapcar
(lambda (x)
(let ((name (lispy-short-process-name x)))
(when name
(cons name x))))
(process-list)))))
(ivy-read "Process: " process-names
:action #'lispy-set-python-process-action
:preselect (when (process-live-p lispy-python-proc)
(lispy-short-process-name lispy-python-proc))
:caller 'lispy-set-python-process)))
(defvar lispy--python-middleware-loaded-p nil
"Nil if the Python middleware in \"lispy-python.py\" wasn't loaded yet.")
(defun lispy--python-proc (&optional name)
(let* ((proc-name (or name
(and (process-live-p lispy-python-proc)
lispy-python-proc)
"lispy-python-default"))
(process (get-process proc-name)))
(if (process-live-p process)
process
(let* ((python-shell-font-lock-enable nil)
(inferior-python-mode-hook nil)
(python-shell-interpreter
(cond
((save-excursion
(goto-char (point-min))
(looking-at "#!\\(?:/usr/bin/env \\)\\(.*\\)$"))
(match-string-no-properties 1))
((file-exists-p python-shell-interpreter)
(expand-file-name python-shell-interpreter))
(t
python-shell-interpreter)))
(python-binary-name (python-shell-calculate-command)))
(setq process (get-buffer-process
(python-shell-make-comint
python-binary-name proc-name nil nil))))
(setq lispy--python-middleware-loaded-p nil)
(lispy--python-middleware-load)
process)))
(defun lispy--python-eval-string-dwim (str)
(setq str (string-trim str))
(let ((single-line-p (= (cl-count ?\n str) 0)))
(cond ((and (or (string-match "\\`\\(\\(?:[., ]\\|\\sw\\|\\s_\\|[][]\\)+\\) += " str)
(string-match "\\`\\(([^)]+)\\) *=[^=]" str))
(save-match-data
(or single-line-p
(and (not (string-match-p "lp\\." str))
(equal (lispy--eval-python
(format "x=lp.is_assignment(\"\"\"%s\"\"\")\nprint (x)" str)
t)
"True")))))
(concat str (format "\nprint (repr ((%s)))" (match-string 1 str))))
;; match e.g. "x in array" part of "for x in array:"
((and single-line-p
(string-match "\\`\\([A-Z_a-z0-9]+\\|\\(?:([^)]+)\\)\\) in \\(.*\\)\\'" str))
(let ((vars (match-string 1 str))
(val (match-string 2 str)))
(format "%s = list (%s)[0]\nprint ((%s))" vars val vars)))
((string-match "\\`def \\([a-zA-Z_0-9]+\\)\\s-*(\\s-*self" str)
(let ((fname (match-string 1 str))
(cname (car (split-string (python-info-current-defun) "\\."))))
(concat str
"\n"
(format "lp.rebind('%s', '%s')" cname fname))))
(t
str))))
(defun lispy--eval-python (str &optional plain)
"Eval STR as Python code."
(let ((single-line-p (= (cl-count ?\n str) 0)))
(unless plain
(setq str (lispy--python-eval-string-dwim str))
(when (string-match "__file__" str)
(lispy--eval-python (format "__file__ = '%s'\n" (buffer-file-name)) t))
(when (and single-line-p (string-match "\\`return \\(.*\\)\\'" str))
(setq str (match-string 1 str))))
(let ((res
(cond ((or single-line-p
(string-match "\n .*\\'" str)
(string-match "\"\"\"" str))
(python-shell-send-string-no-output
str (lispy--python-proc)))
((string-match "\\`\\([\0-\377[:nonascii:]]*\\)\n\\([^\n]*\\)\\'" str)
(let* ((p1 (match-string 1 str))
(p2 (match-string 2 str))
(p1-output (python-shell-send-string-no-output
p1 (lispy--python-proc)))
p2-output)
(cond
((string-match-p "SyntaxError:\\|error:" p1-output)
(python-shell-send-string-no-output
str (lispy--python-proc)))
((null p1-output)
(lispy-message lispy-eval-error))
((null (setq p2-output (lispy--eval-python p2)))
(lispy-message lispy-eval-error))
(t
(concat
(if (string= p1-output "")
""
(concat p1-output "\n"))
p2-output)))))
(t
(error "unexpected")))))
(cond
((string-match "SyntaxError: 'return' outside function\\'" res)
(lispy--eval-python
(concat "__return__ = None\n"
(replace-regexp-in-string
"\\(^ *\\)return"
(lambda (x) (concat (match-string 1 x) "__return__ ="))
str)
"\nprint (repr(__return__))")
t))
((string-match "^Traceback.*:" res)
(set-text-properties
(match-beginning 0)
(match-end 0)
'(face error)
res)
(setq lispy-eval-error res)
nil)
((equal res "")
(setq lispy-eval-error "(ok)")
"")
((string-match-p "^<\\(?:map\\|filter\\|generator\\) object" res)
(let ((last (car (last (split-string str "\n")))))
(when (string-match "\\`print (repr ((\\(.*\\))))\\'" last)
(setq str (match-string 1 last))))
(lispy--eval-python (format "list(%s)" str) t))
((string-match-p "SyntaxError:" res)
(setq lispy-eval-error res)
nil)
(t
(replace-regexp-in-string "\\\\n" "\n" res))))))
(defun lispy--python-array-to-elisp (array-str)
"Transform a Python string ARRAY-STR to an Elisp string array."
(when (and (stringp array-str)
(not (string= array-str "")))
(let ((parts (with-temp-buffer
(python-mode)
(insert (substring array-str 1 -1))
(goto-char (point-min))
(let (beg res)
(while (< (point) (point-max))
(setq beg (point))
(forward-sexp)
(push (buffer-substring-no-properties beg (point)) res)
(skip-chars-forward ", "))
(nreverse res)))))
(mapcar (lambda (s)
(if (string-match "\\`\"" s)
(read s)
(if (string-match "\\`'\\(.*\\)'\\'" s)
(match-string 1 s)
s)))
parts))))
(defun lispy-python-symbol-bnd ()
(let ((bnd (or (bounds-of-thing-at-point 'symbol)
(cons (point) (point)))))
(save-excursion
(goto-char (car bnd))
(while (progn
(skip-chars-backward " ")
(lispy-after-string-p "."))
(backward-char 1)
(skip-chars-backward " ")
(if (lispy-after-string-p ")")
(backward-sexp 2)
(backward-sexp)))
(skip-chars-forward " ")
(setcar bnd (point)))
bnd))
(defun lispy-python-completion-at-point ()
(cond ((looking-back "^\\(import\\|from\\) .*" (line-beginning-position))
(let* ((line (buffer-substring-no-properties
(line-beginning-position)
(point)))
(str
(format
"import jedi; script=jedi.Script(\"%s\",1,%d); [_x_.name for _x_ in script.completions()]"
line (length line)))
(cands
(lispy--python-array-to-elisp
(lispy--eval-python str)))
(bnd (bounds-of-thing-at-point 'symbol))
(beg (if bnd (car bnd) (point)))
(end (if bnd (cdr bnd) (point))))
(list beg end cands)))
((lispy-complete-fname-at-point))
(t
(let* ((bnd (lispy-python-symbol-bnd))
(str (buffer-substring-no-properties
(car bnd) (cdr bnd))))
(when (string-match "\\()\\)[^)]*\\'" str)
(let ((expr (format "__t__ = %s" (substring str 0 (match-end 1)))))
(setq str (concat "__t__" (substring str (match-end 1))))
(cl-incf (car bnd) (match-end 1))
(lispy--eval-python expr t)))
(list (car bnd)
(cdr bnd)
(mapcar (lambda (s)
(replace-regexp-in-string
"__t__" ""
(if (string-match "(\\'" s)
(substring s 0 (match-beginning 0))
s)))
(python-shell-completion-get-completions
(lispy--python-proc)
nil str)))))))
(defvar lispy--python-arg-key-re "\\`\\(\\(?:\\sw\\|\\s_\\)+\\) ?= ?\\(.*\\)\\'"
"Constant regexp for matching function keyword spec.")
(defun lispy--python-args (beg end)
(let (res)
(save-excursion
(goto-char beg)
(skip-chars-forward "\n\t ")
(setq beg (point))
(while (< (point) end)
(forward-sexp)
(while (and (< (point) end)
(not (looking-at ",")))
(forward-sexp))
(push (buffer-substring-no-properties
beg (point))
res)
(skip-chars-forward ", \n")
(setq beg (point))))
(nreverse res)))
(defun lispy--python-step-in-loop ()
(when (looking-at " ?for \\([A-Z_a-z,0-9 ()]+\\) in \\(.*\\):")
(let* ((vars (match-string-no-properties 1))
(val (match-string-no-properties 2))
(res (lispy--eval-python
(format "lp.list_step(\"%s\",%s)" vars val)
t)))
(lispy-message res))))
(defun lispy--python-debug-step-in ()
(unless (lispy--python-step-in-loop)
(when (looking-at " *(")
;; tuple assignment
(forward-list 1))
(re-search-forward "(" (line-end-position))
(backward-char)
(let* ((p-ar-beg (point))
(p-ar-end (save-excursion
(forward-list)
(point)))
(p-fn-end (progn
(skip-chars-backward " ")
(point)))
(method-p nil)
(p-fn-beg (progn
(backward-sexp)
(while (eq (char-before) ?.)
(setq method-p t)
(backward-sexp))
(point)))
(fn (buffer-substring-no-properties
p-fn-beg p-fn-end))
(args
(lispy--python-args (1+ p-ar-beg) (1- p-ar-end)))
(args (if (and method-p
(string-match "\\`\\(.*?\\)\\.\\([^.]+\\)\\'" fn))
(cons (match-string 1 fn)
args)
args))
(args-key (cl-remove-if-not
(lambda (s)
(string-match lispy--python-arg-key-re s))
args))
(args-normal (cl-set-difference args args-key))
(fn-data
(json-read-from-string
(substring
(lispy--eval-python
(format "import inspect, json; json.dumps (inspect.getargspec (%s))"
fn))
1 -1)))
(fn-args
(append (mapcar #'identity (elt fn-data 0))
(if (elt fn-data 1)
(list (elt fn-data 1)))))
(fn-defaults
(mapcar
(lambda (x)
(cond ((null x)
"None")
((eq x t)
"True")
(t
(prin1-to-string x))))
(elt fn-data 3)))
(fn-alist
(cl-mapcar #'cons
fn-args
(append (make-list (- (length fn-args)
(length fn-defaults))
nil)
fn-defaults)))
fn-alist-x dbg-cmd)
(if method-p
(unless (member '("self") fn-alist)
(push '("self") fn-alist))
(setq fn-alist (delete '("self") fn-alist)))
(setq fn-alist-x fn-alist)
(dolist (arg args-normal)
(setcdr (pop fn-alist-x) arg))
(dolist (arg args-key)
(if (string-match lispy--python-arg-key-re arg)
(let ((arg-name (match-string 1 arg))
(arg-val (match-string 2 arg))
arg-cell)
(if (setq arg-cell (assoc arg-name fn-alist))
(setcdr arg-cell arg-val)
(error "\"%s\" is not in %s" arg-name fn-alist)))
(error "\"%s\" does not match the regex spec" arg)))
(when (memq nil (mapcar #'cdr fn-alist))
(error "Not all args were provided: %s" fn-alist))
(setq dbg-cmd
(mapconcat (lambda (x)
(format "%s = %s" (car x) (cdr x)))
fn-alist
"; "))
(if (lispy--eval-python dbg-cmd t)
(progn
(goto-char p-fn-end)
(lispy-goto-symbol fn))
(goto-char p-ar-beg)
(message lispy-eval-error)))))
(declare-function deferred:sync! "ext:deferred")
(declare-function jedi:goto-definition "ext:jedi-core")
(declare-function jedi:call-deferred "ext:jedi-core")
(defun lispy-goto-symbol-python (_symbol)
(save-restriction
(widen)
(let ((res (ignore-errors
(or
(deferred:sync!
(jedi:goto-definition))
t))))
(if (member res '(nil "Definition not found."))
(let* ((symbol (python-info-current-symbol))
(symbol-re (concat "^\\(?:def\\|class\\).*" (car (last (split-string symbol "\\." t)))))
(file (lispy--eval-python
(format
"import inspect\nprint(inspect.getsourcefile(%s))" symbol))))
(cond ((and (equal file "None")
(re-search-backward symbol-re nil t)))
(file
(find-file file)
(goto-char (point-min))
(re-search-forward symbol-re)
(beginning-of-line))
(t
(error "Both jedi and inspect failed"))))
(unless (looking-back "def " (line-beginning-position))
(jedi:goto-definition))))))
(defun lispy--python-docstring (symbol)
"Look up the docstring for SYMBOL.
First, try to see if SYMBOL.__doc__ returns a string in the
current REPL session (dynamic).
Otherwise, fall back to Jedi (static)."
(let ((dynamic-result (lispy--eval-python (concat symbol ".__doc__"))))
(if (> (length dynamic-result) 0)
(mapconcat #'string-trim-left
(split-string (substring dynamic-result 1 -1) "\\\\n")
"\n")
(require 'jedi)
(plist-get (car (deferred:sync!
(jedi:call-deferred 'get_definition)))
:doc))))
(defun lispy-python-middleware-reload ()
(interactive)
(setq lispy--python-middleware-loaded-p nil)
(lispy--python-middleware-load))
(defvar lispy-python-init-file "~/git/site-python/init.py")
(defun lispy--python-middleware-load ()
"Load the custom Python code in \"lispy-python.py\"."
(unless lispy--python-middleware-loaded-p
(let ((r (lispy--eval-python
(format "import imp;lp=imp.load_source('lispy-python','%s');__name__='__repl__'"
(expand-file-name "lispy-python.py" lispy-site-directory)))))
(if r
(progn
(when (file-exists-p lispy-python-init-file)
(lispy--eval-python
(format "exec (open ('%s').read(), globals ())"
(expand-file-name lispy-python-init-file))))
(setq lispy--python-middleware-loaded-p t))
(lispy-message lispy-eval-error)))))
(defun lispy--python-arglist (symbol filename line column)
(lispy--python-middleware-load)
(let* ((boundp (lispy--eval-python symbol))
(code (if boundp
(format "lp.arglist(%s)" symbol)
(format "lp.arglist_jedi(%d, %d, '%s')" line column filename)))
(args (lispy--python-array-to-elisp
(lispy--eval-python
code))))
(format "%s (%s)"
symbol
(mapconcat #'identity
(delete "self" args)
", "))))
(provide 'le-python)
;;; le-python.el ends here