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.

467 lines
19 KiB

  1. ;;; ein-pytools.el --- Python tools build on top of kernel
  2. ;; Copyright (C) 2012- Takafumi Arakaki
  3. ;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
  4. ;; This file is NOT part of GNU Emacs.
  5. ;; ein-pytools.el is free software: you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; ein-pytools.el is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with ein-pytools.el. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;;
  17. ;;; Code:
  18. ;; for `ein:pytools-pandas-to-ses'
  19. (declare-function ses-yank-tsf "ses")
  20. (declare-function ses-command-hook "ses")
  21. (require 'ein-kernel)
  22. (require 'ein-notebook)
  23. (require 'ein-shared-output)
  24. (defun ein:goto-file (filename lineno &optional other-window)
  25. "Jump to file FILEAME at line LINENO.
  26. If OTHER-WINDOW is non-`nil', open the file in the other window."
  27. (funcall (if other-window #'find-file-other-window #'find-file) filename)
  28. (goto-char (point-min))
  29. (forward-line (1- lineno)))
  30. (defun ein:goto-marker (marker &optional other-window)
  31. (funcall (if other-window #'pop-to-buffer #'switch-to-buffer)
  32. (marker-buffer marker))
  33. (goto-char marker))
  34. (defcustom ein:propagate-connect t
  35. "Set to `t' to connect to the notebook after jumping to a buffer."
  36. :type '(choice (const :tag "Yes" t)
  37. (const :tag "No" nil))
  38. :group 'ein)
  39. (defun ein:pytools-setup-hooks (kernel notebook)
  40. (push (cons #'ein:pytools-load-safely kernel)
  41. (ein:$kernel-after-start-hook kernel)))
  42. (defun ein:pytools-wrap-hy-code (code)
  43. (format "__import__('hy').eval(__import__('hy').read_str('''%s'''))" code))
  44. (defun ein:pytools-load-safely (kernel)
  45. (with-temp-buffer
  46. (let ((pytools-file (format "%s/%s" ein:source-dir "ein_remote_safe.py")))
  47. (insert-file-contents pytools-file)
  48. (ein:kernel-execute
  49. kernel
  50. (buffer-string)))))
  51. (defun ein:pytools-reinject ()
  52. "Re-send ein's pytools code to the current kernel.
  53. If the kernel is reset by the notebook server then it may become
  54. necessary to call this command to ensure pytools continue
  55. working."
  56. (interactive)
  57. (ein:pytools-load-safely (ein:get-kernel-or-error)))
  58. (defun ein:pytools-add-sys-path (kernel)
  59. (ein:kernel-execute
  60. kernel
  61. (format "__import__('sys').path.append('%s')" ein:source-dir)))
  62. (defun ein:set-buffer-file-name (nb msg-type content -not-used-)
  63. (let ((buf (ein:notebook-buffer nb)))
  64. (ein:case-equal msg-type
  65. (("stream" "output")
  66. (with-current-buffer buf
  67. (setq buffer-file-name
  68. (expand-file-name
  69. (format "%s" (ein:$notebook-notebook-name nb))
  70. (plist-get content :text))))))))
  71. (defun ein:pytools-get-notebook-dir (packed)
  72. (cl-multiple-value-bind (kernel notebook) packed
  73. (ein:kernel-execute
  74. kernel
  75. (format "print(__import__('os').getcwd(),end='')")
  76. (list
  77. :output (cons
  78. #'ein:set-buffer-file-name
  79. notebook)))))
  80. ;;; Tooltip and help
  81. ;; We can probably be more sophisticated than this, but
  82. ;; as a hack it will do.
  83. (defun ein:pytools-magic-func-p (fstr)
  84. (string-prefix-p "%" fstr))
  85. (defun ein:pytools-request-tooltip (kernel func)
  86. (interactive (list (ein:get-kernel-or-error)
  87. (ein:object-at-point-or-error)))
  88. (unless (ein:pytools-magic-func-p func)
  89. (if (>= (ein:$kernel-api-version kernel) 3)
  90. (ein:kernel-execute
  91. kernel
  92. (format "__ein_print_object_info_for(%s)" func)
  93. (list
  94. :output (cons
  95. (lambda (name msg-type content -metadata-not-used-)
  96. (ein:case-equal msg-type
  97. (("stream" "display_data")
  98. (ein:pytools-finish-tooltip name (ein:json-read-from-string (plist-get content :text)) nil))))
  99. func)))
  100. (ein:kernel-object-info-request
  101. kernel func (list :object_info_reply
  102. (cons #'ein:pytools-finish-tooltip nil))))))
  103. (declare-function pos-tip-show "pos-tip")
  104. (declare-function popup-tip "popup")
  105. (defun ein:pytools-finish-tooltip (-ignore- content -metadata-not-used-)
  106. ;; See: Tooltip.prototype._show (tooltip.js)
  107. (let ((tooltip (ein:kernel-construct-help-string content))
  108. (defstring (ein:kernel-construct-defstring content))
  109. (name (plist-get content :name)))
  110. (if tooltip
  111. (cond
  112. ((and window-system (featurep 'pos-tip))
  113. (pos-tip-show tooltip 'ein:pos-tip-face nil nil 0))
  114. ((featurep 'popup)
  115. (popup-tip tooltip))
  116. (t (when (stringp defstring)
  117. (message (ein:trim (ansi-color-apply defstring))))))
  118. (ein:log 'info "no info for %s" name))))
  119. (defun ein:pytools-request-help (kernel func)
  120. (interactive (list (ein:get-kernel-or-error)
  121. (ein:object-at-point-or-error)))
  122. (ein:kernel-execute kernel
  123. (format "%s?" func) ; = code
  124. nil ; = callbacks
  125. ;; It looks like that magic command does
  126. ;; not work in silent mode.
  127. :silent nil))
  128. (defun ein:pytools-request-tooltip-or-help (&optional pager)
  129. "Show the help for the object at point using tooltip.
  130. When the prefix argument ``C-u`` is given, open the help in the
  131. pager buffer. You can explicitly specify the object by selecting it."
  132. (interactive "P")
  133. (call-interactively (if pager
  134. #'ein:pytools-request-help
  135. #'ein:pytools-request-tooltip)))
  136. ;;; Source jump
  137. (defvar ein:pytools-jump-stack nil)
  138. (defvar ein:pytools-jump-to-source-not-found-regexp
  139. (ein:join-str "\\|"
  140. (list "^WARNING: .*"
  141. "^Traceback (most recent call last):\n"
  142. "^.*<ipython-input-[^>\n]+>\n"
  143. "^\n")))
  144. (defun ein:pytools-jump-to-source-1 (packed msg-type content -metadata-not-used-)
  145. (ein:log 'debug "msg-type[[%s]] content[[%s]]" msg-type content)
  146. (cl-destructuring-bind (kernel object other-window notebook) packed
  147. (ein:log 'debug "object[[%s]] other-window[[%s]]" object other-window)
  148. (ein:case-equal msg-type
  149. (("stream" "display_data")
  150. (ein:aif (or (plist-get content :text) (plist-get content :data))
  151. (if (string-match ein:pytools-jump-to-source-not-found-regexp it)
  152. (ein:log 'info
  153. "Jumping to the source of %s...Not found" object)
  154. (cl-destructuring-bind (filename &optional lineno &rest ignore)
  155. (split-string it "\n")
  156. (setq lineno (string-to-number lineno)
  157. filename (ein:kernel-filename-from-python kernel filename))
  158. (ein:log 'debug "filename[[%s]] lineno[[%s]] ignore[[%s]]"
  159. filename lineno ignore)
  160. (if (not (file-exists-p filename))
  161. (ein:log 'info
  162. "Jumping to the source of %s...Not found" object)
  163. (let ((ein:connect-default-notebook nil))
  164. ;; Avoid auto connection to connect to the
  165. ;; NOTEBOOK instead of the default one.
  166. (ein:goto-file filename lineno other-window))
  167. ;; Connect current buffer to NOTEBOOK. No reconnection.
  168. (ein:connect-buffer-to-notebook notebook nil t)
  169. (push (point-marker) ein:pytools-jump-stack)
  170. (ein:log 'info "Jumping to the source of %s...Done" object))))))
  171. (("pyerr" "error")
  172. (ein:log 'info "Jumping to the source of %s...Not found" object)))))
  173. (defun ein:pytools-jump-to-source (kernel object &optional
  174. other-window notebook)
  175. (ein:log 'info "Jumping to the source of %s..." object)
  176. (let ((last (car ein:pytools-jump-stack)))
  177. (if (ein:aand last (eql (current-buffer) (marker-buffer it)))
  178. (unless (equal (point) (marker-position last))
  179. (push (point-marker) ein:pytools-jump-stack))
  180. (setq ein:pytools-jump-stack (list (point-marker)))))
  181. (ein:kernel-execute
  182. kernel
  183. (format "__ein_find_source('%s')" object)
  184. (list
  185. :output
  186. (cons
  187. #'ein:pytools-jump-to-source-1
  188. (list kernel object other-window notebook)))))
  189. (defun ein:pytools-find-source (kernel object &optional callback)
  190. "Find the file and line where object is defined.
  191. This function mostly exists to support company-mode, but might be
  192. useful for other purposes. If the definition for object can be
  193. found and when callback isort specified, the callback will be
  194. called with a cons of the filename and line number where object
  195. is defined."
  196. (ein:kernel-execute
  197. kernel
  198. (format "__ein_find_source('%s')" object)
  199. (list
  200. :output
  201. (cons
  202. #'ein:pytools-finish-find-source
  203. (list kernel object callback)))))
  204. (defun ein:pytools-finish-find-source (packed msg-type content -ignored-)
  205. (cl-destructuring-bind (kernel object callback) packed
  206. (if (or (string= msg-type "stream")
  207. (string= msg-type "display_data"))
  208. (ein:aif (or (plist-get content :text) (plist-get content :data))
  209. (if (string-match ein:pytools-jump-to-source-not-found-regexp it)
  210. (ein:log 'info
  211. "Source of %s not found" object)
  212. (cl-destructuring-bind (filename &optional lineno &rest ignore)
  213. (split-string it "\n")
  214. (if callback
  215. (funcall callback
  216. (cons (ein:kernel-filename-from-python kernel filename)
  217. (string-to-number lineno)))
  218. (cons (ein:kernel-filename-from-python kernel filename)
  219. (string-to-number lineno)))))) ;; FIXME Generator?
  220. (ein:log 'info "Source of %s notebook found" object))))
  221. (defun ein:pytools-jump-to-source-command (&optional other-window)
  222. "Jump to the source code of the object at point.
  223. When the prefix argument ``C-u`` is given, open the source code
  224. in the other window. You can explicitly specify the object by
  225. selecting it."
  226. (interactive "P")
  227. (if poly-ein-mode
  228. (cl-letf (((symbol-function 'xref--prompt-p) #'ignore))
  229. (if other-window
  230. (call-interactively #'xref-find-definitions-other-window)
  231. (call-interactively #'xref-find-definitions)))
  232. (let ((kernel (ein:get-kernel))
  233. (object (ein:object-at-point)))
  234. (cl-assert (ein:kernel-live-p kernel) nil "Kernel is not ready.")
  235. (cl-assert object nil "Object at point not found.")
  236. (ein:pytools-jump-to-source kernel object other-window
  237. (when ein:propagate-connect
  238. (ein:get-notebook))))))
  239. (defun ein:pytools-jump-back-command (&optional other-window)
  240. "Go back to the point where `ein:pytools-jump-to-source-command'
  241. is executed last time. When the prefix argument ``C-u`` is
  242. given, open the last point in the other window."
  243. (interactive "P")
  244. (if poly-ein-mode
  245. (call-interactively #'xref-pop-marker-stack)
  246. (when (ein:aand (car ein:pytools-jump-stack)
  247. (equal (point) (marker-position it)))
  248. (setq ein:pytools-jump-stack (cdr ein:pytools-jump-stack)))
  249. (ein:aif (car ein:pytools-jump-stack)
  250. (ein:goto-marker it other-window)
  251. (ein:log 'info "Nothing on stack."))))
  252. (define-obsolete-function-alias
  253. 'ein:pytools-eval-string-internal
  254. 'ein:shared-output-eval-string "0.1.2")
  255. (defun ein:pytools-doctest ()
  256. "Do the doctest of the object at point."
  257. (interactive)
  258. (let ((object (ein:object-at-point)))
  259. (ein:shared-output-eval-string (ein:get-kernel)
  260. (format "__ein_run_docstring_examples(%s)" object)
  261. t)))
  262. (defun ein:pytools-whos ()
  263. "Execute ``%whos`` magic command and popup the result."
  264. (interactive)
  265. (ein:shared-output-eval-string (ein:get-kernel) "%whos" t))
  266. (defun ein:pytools-hierarchy (&optional ask)
  267. "Draw inheritance graph of the class at point.
  268. hierarchymagic_ extension is needed to be installed.
  269. You can explicitly specify the object by selecting it.
  270. .. _hierarchymagic: https://github.com/tkf/ipython-hierarchymagic"
  271. (interactive "P")
  272. (let ((object (ein:object-at-point)))
  273. (when ask
  274. (setq object (read-from-minibuffer "class or object: " object)))
  275. (cl-assert (and object (not (equal object "")))
  276. nil "Object at point not found.")
  277. (ein:shared-output-eval-string (ein:get-kernel) (format "%%hierarchy %s" object) t)))
  278. (defun ein:pytools-pandas-to-ses (dataframe)
  279. "View pandas_ DataFrame in SES_ (Simple Emacs Spreadsheet).
  280. Open a `ses-mode' buffer and import DataFrame object into it.
  281. SES_ is distributed with Emacs since Emacs 22, so you don't need
  282. to install it if you are using newer Emacs.
  283. .. _pandas: http://pandas.pydata.org
  284. .. _SES: http://www.gnu.org/software/emacs/manual/html_node/ses/index.html"
  285. (interactive (list (read-from-minibuffer "pandas DataFrame "
  286. (ein:object-at-point))))
  287. (let ((buffer (get-buffer-create
  288. (generate-new-buffer-name "*ein:ses pandas*"))))
  289. ;; fetch TSV (tab separated values) via stdout
  290. (ein:kernel-request-stream
  291. (ein:get-kernel)
  292. (concat dataframe ".to_csv(__import__('sys').stdout, sep='\\t')")
  293. (lambda (tsv buffer)
  294. (with-current-buffer buffer
  295. (cl-flet ((y-or-n-p
  296. (prompt)
  297. (if (string-prefix-p "Yank will insert " prompt)
  298. t
  299. (error "Unexpected prompt: %s" prompt))))
  300. ;; Import DataFrame as TSV
  301. (ses-yank-tsf tsv nil))
  302. ;; Force SES to update (equivalent to run `post-command-hook').
  303. (ses-command-hook)))
  304. (list buffer))
  305. ;; Open `ses-mode' buffer
  306. (with-current-buffer buffer
  307. (ses-mode))
  308. (pop-to-buffer buffer)))
  309. (defun ein:pytools-export-buffer (buffer format)
  310. "Export contents of notebook using nbconvert_ to user-specified format
  311. \(options will depend on the version of nbconvert available\) to a new buffer.
  312. Currently EIN/IPython supports exporting to the following formats:
  313. - HTML
  314. - JSON (this is basically the same as opening the ipynb file in a buffer).
  315. - Latex
  316. - Markdown
  317. - Python
  318. - RST
  319. - Slides
  320. .. _nbconvert: http://ipython.org/ipython-doc/stable/notebook/nbconvert.html"
  321. (interactive (list (read-buffer "Buffer: " (current-buffer) t)
  322. (ein:completing-read "Export format: "
  323. (list "html"
  324. "json"
  325. "latex"
  326. "markdown"
  327. "python"
  328. "rst"
  329. "slides"))))
  330. (let* ((nb (car (ein:notebook-opened-notebooks
  331. #'(lambda (nb)
  332. (equal (buffer-name (ein:notebook-buffer nb))
  333. buffer)))))
  334. (json (json-encode (ein:notebook-to-json nb)))
  335. (name (format "*ein %s export: %s*" format (ein:$notebook-notebook-name nb)))
  336. (buffer (get-buffer-create name)))
  337. (if (equal format "json")
  338. (with-current-buffer buffer
  339. (erase-buffer)
  340. (insert json)
  341. (json-pretty-print (point-min) (point-max)))
  342. (ein:kernel-request-stream
  343. (ein:get-kernel)
  344. (format "__ein_export_nb(r'%s', '%s')"
  345. json
  346. format)
  347. (lambda (export buffer)
  348. (with-current-buffer buffer
  349. (erase-buffer)
  350. (insert export)))
  351. (list buffer)))
  352. (switch-to-buffer buffer)))
  353. ;;;; Helper functions for working with matplotlib
  354. (defun ein:pytools-set-figure-size (width height)
  355. "Set the default figure size for matplotlib figures. Works by setting `rcParams['figure.figsize']`."
  356. (interactive "nWidth: \nnHeight: ")
  357. (ein:shared-output-eval-string (ein:get-kernel)
  358. (format "__ein_set_figure_size('[%s, %s]')" width height)
  359. nil))
  360. (defun ein:pytools-set-figure-dpi (dpi)
  361. "Set the default figure dpi for matplotlib figures. Works by setting `rcParams['figure.figsize']`."
  362. (interactive "nFigure DPI: ")
  363. (ein:shared-output-eval-string (ein:get-kernel)
  364. (format "__ein_set_figure_dpi('%s')" dpi)
  365. nil))
  366. (defun ein:pytools-set-matplotlib-parameter (param value)
  367. "Generically set any matplotlib parameter exposed in the matplotlib.pyplot.rcParams variable. Value is evaluated as a Python expression, so be careful of side effects."
  368. (interactive
  369. (list (completing-read "Parameter: " (ein:pytools--get-matplotlib-params) nil t)
  370. (read-string "Value: " nil)))
  371. (let* ((split (cl-position ?. param))
  372. (family (cl-subseq param 0 split))
  373. (setting (cl-subseq param (1+ split))))
  374. (ein:shared-output-eval-string (ein:get-kernel)
  375. (format "__ein_set_matplotlib_param('%s', '%s', '%s')" family setting value)
  376. nil)))
  377. (defun ein:pytools--get-matplotlib-params ()
  378. (ein:shared-output-eval-string (ein:get-kernel)
  379. (format "__ein_get_matplotlib_params()")
  380. nil)
  381. (with-current-buffer (ein:shared-output-create-buffer)
  382. (ein:wait-until #'(lambda ()
  383. (slot-value (slot-value *ein:shared-output* :cell) :outputs))
  384. nil
  385. 5.0)
  386. (let ((outputs (first (slot-value (slot-value *ein:shared-output* :cell) :outputs))))
  387. (ein:json-read-from-string (plist-get outputs :text)))))
  388. (defun ein:pytools--estimate-screen-dpi ()
  389. (let* ((pixel-width (display-pixel-width))
  390. (pixel-height (display-pixel-height))
  391. (in-width (/ (display-mm-width) 25.4))
  392. (in-height (/ (display-mm-height) 25.4)))
  393. (values (/ pixel-width in-width) (/ pixel-height in-height))))
  394. (defun ein:pytools-matplotlib-dpi-correction ()
  395. "Estimate the screen dpi and set the matplotlib rc parameter 'figure.dpi' to that value. Call this command *after* importing matplotlib into your notebook, else this setting will be overwritten after the first call to `import matplotlib' Further testing is needed to see how well this works on high resolution displays."
  396. (interactive)
  397. (multiple-value-bind (dpi-w dpi-h) (ein:pytools--estimate-screen-dpi)
  398. (let ((dpi (floor (/ (+ dpi-w dpi-h) 2.0))))
  399. (ein:log 'info "Setting matplotlib scaling to: %s dpi" dpi)
  400. (ein:pytools-set-figure-dpi dpi))))
  401. (provide 'ein-pytools)
  402. ;;; ein-pytools.el ends here