diff --git a/init.jl b/init.jl index 7af40b2..552c436 100644 --- a/init.jl +++ b/init.jl @@ -1,4 +1,4 @@ -const supported_packages = [:DataFrames, :NamedArrays, :Plots] +const supported_packages = [:DataFrames, :NamedArrays, :Plots, :HypothesisTests, :CSVFiles] # Generic fallback orgshow(io::IO, Any, i; kwargs...) = show(io, i) @@ -27,8 +27,17 @@ function orgshow(io::IO, ::MIME"text/csv", i::Array{T,2}; kwargs...) where T <: orgshow(io, MIME("text/org"), i; kwargs...) end +function orgstring(e::Tuple{Exception,Any}) + string("ERROR,", e[1], "\n", + "Stacktrace:\n", + join(e[2], '\n')) +end + # The comma is needed to allow export as table -orgshow(io::IO, ::MIME"text/org", e::Exception; kwargs...) = print(io, "ERROR,", e) +function orgshow(io::IO, ::MIME"text/org", e::Tuple{Exception,Any}; + kwargs...) + print(io, orgstring(e)) +end function orgshow(io::IO, ::MIME"text/org", t::NamedTuple) print(io, join(string.(keys(t)), ',')) @@ -48,6 +57,26 @@ function orgshow(io::IO, ::MIME"text/org", end end +OrgAPApvalue(p) = p < 0.001 ? "< .001" : string("= ", round(p, sigdigits = 2)) + +function define_HypothesisTests() + Main.@eval function orgshow(io::IO, + m::MIME"text/org", + test::PowerDivergenceTest; kwargs...) + println(io, join(["test", "df", "N", "stat", "p-value"], ',')) + print(io, join(["X^2", test.df, test.n, test.stat, pvalue(test)], ',')) + end + Main.@eval function orgshow(io::IO, + m::MIME"text/html", + test::PowerDivergenceTest; kwargs...) + print(io, string("X^2", + " (", test.df, ", N = ", test.n, + ") = ", + round(test.stat; digits = 2), + ", p ", OrgAPApvalue(pvalue(test)))) + end +end + function define_Plots() # Fallback: we will try to plot any image/png or image/svg Main.@eval function orgshow(io::IO, @@ -65,9 +94,10 @@ function define_Plots() p::Plots.Plot; kwargs...) show(io, MIME("image/png"), plot(p; kwargs...)) end - Main.@eval function orgshow(io::IO, ::MIME"image/png", e::Exception; kwargs...) + Main.@eval function orgshow(io::IO, ::MIME"image/png", e::Tuple{Exception,Any}; + kwargs...) let p = plot(showaxis = false, grid = false, bg = :yellow) - annotate!([0.5], [0.5], (string("ERROR: ", e), :red)) + annotate!([0.5], [0.5], (orgstring(e), :red)) orgshow(io, MIME("image/png"), p; kwargs...) end end @@ -78,7 +108,10 @@ function define_Plots() end end Main.@eval function orgshow(io::IO, ::MIME"text/html", p::Plots.Plot; kwargs...) - Plots._show(io, MIME("text/html"), plot(p; kwargs...)) + p = plot(p; kwargs...) + p.attr[:html_output_format] = "png" + # Plots._show(io::IO, MIME("text/html"), p::Plots.Plot; kwargs...) + Plots._show(io, MIME("text/html"), p) end Main.@eval function orgshow(io::IO, ::MIME"image/svg+xml", p::Plots.Plot; kwargs...) show(io, MIME("image/svg+xml"), plot(p; kwargs...)) @@ -116,13 +149,30 @@ function define_DataFrames() end end +function define_CSVFiles() + Main.@eval function orgshow(io::IO, ::MIME"text/csv", d::CSVFiles.CSVFile) + orgshow(io, MIME("text/org"), d) + end + Main.@eval function orgshow(io::IO, ::MIME"text/html", d::CSVFiles.CSVFile) + show(io, MIME("text/html"), RES) + end + Main.@eval function orgshow(io::IO, ::MIME"application/json", d::CSVFiles.CSVFile) + show(io, MIME("application/vnd.dataresource+json"), RES) + end + Main.@eval function orgshow(io::IO, ::MIME"text/org", d::CSVFiles.CSVFile) + orgshow(io, MIME("text/org"), collect(d)) + end +end + function define_NamedArrays() Main.@eval function orgshow(io::IO, ::MIME"text/org", - na::NamedArrays.NamedArray{T,2} where T <: Any) + na::NamedArray{T,2} where T <: Any) n = names(na) a = collect(na) - print(io, join(string.(na.dimnames), "/") * ',') - print(io, join(n[2], ','') * '\n') + # The char used by NamedArrays is '╲' but by default it's not + # shown in pdf export + print(io, join(string.(na.dimnames), " \\ ") * ',') + print(io, join(n[2], ',') * '\n') print(io, join([join([string(n[1][i], ','), join([a[i,j] for j in 1:size(na,2) @@ -130,7 +180,7 @@ function define_NamedArrays() for i in 1:size(na,1) ], '\n')) end - Main.@eval function orgshow(io::IO, ::MIME"text/org", na::NamedArrays.NamedArray{T,1} where T <: Any) + Main.@eval function orgshow(io::IO, ::MIME"text/org", na::NamedArray{T,1} where T <: Any) n = names(na) a = collect(na) print(io, string(na.dimnames[1], ',', '\n')) @@ -141,15 +191,16 @@ end define_package_functions(pkg::Symbol) = (@eval $pkg)() -function OrgBabelImport(_imports; forced = false) +function OrgBabelImport(imports; forced = false) "Load dependencies. Do this before calling OrgBabelReload()" # Reload this module, so that if new packages have been imported, # we can use them to save the output - !forced && isempty(_imports) && return + !forced && isempty(imports) && return + println("$imports") try Main.eval(Meta.parse( """begin - $_imports + $imports end""")) true catch e @@ -183,29 +234,30 @@ function OrgBabelFormat(output_type::Symbol, dir, vars, src_file, silently::Bool, + pure::Bool, kwargs) content = read(src_file, String) # Fake a prompt with the current input try - println(string("\njulia> ", content)) + printstyled(IOContext(stdout, :color => true), + "\njulia> ", color = :blue) + println(content) catch end # Dispatch on output type + code = pure ? "let $vars; $content; end" : "begin $vars; $content end" if output_type == :value # Run the code result = cd(dir) do try - # Variable assignment - Main.eval(Meta.parse(vars)) - # src block evaluation - Main.eval(Meta.parse("begin $content end")) + Main.eval(Meta.parse(code)) catch e - e + (e, stacktrace()) end end if !silently try - print(result) + display(MIME("text/plain"), result) catch e println("Error $e while showing results") end @@ -236,12 +288,10 @@ function OrgBabelFormat(output_type::Symbol, redirect_stderr(f) do cd(dir) do try - # Variable assignment - Main.eval(Meta.parse(vars)) - # src block evaluation - Main.eval(Meta.parse("begin $content end")) + Main.eval(Meta.parse(code)) catch e println(e) + println(join(stacktrace(), '\n')) end end end @@ -260,3 +310,4 @@ function OrgBabelFormat(output_type::Symbol, end OrgBabelReload() + diff --git a/ob-julia.el b/ob-julia.el index 0599162..4e1230c 100644 --- a/ob-julia.el +++ b/ob-julia.el @@ -12,7 +12,7 @@ :version "24.1" :type 'string) -(defcustom org-babel-julia-startup-script +(defcustom ob-julia-startup-script (concat (file-name-directory (or load-file-name (buffer-file-name))) "init.jl") "Julia file path to run at startup. Must be absolute." @@ -52,6 +52,7 @@ There's no effect in non-session evaluations" (height . :any) (size . :any) (inline . :any) + (let . :any) (import . :any) (using . :any) (async . :any) @@ -81,7 +82,14 @@ There's no effect in non-session evaluations" (set (make-local-variable 'org-babel-julia-session-directory) dir) (save-window-excursion (require 'ess) - (julia) + ;; load the julia startup script (defined in ob-julia-startup-script) + ;; pass it along with other arguments defined in inferior-julia-args + (let* ((start-script-arg + (concat (format "--load=%s" ob-julia-startup-script))) + (inferior-julia-args (if inferior-julia-args + (concat inferior-julia-args start-script-arg) + start-script-arg))) + (julia)) (rename-buffer (if (bufferp session) (buffer-name session) @@ -92,14 +100,7 @@ There's no effect in non-session evaluations" ;; running the command (set-process-filter (get-buffer-process (org-babel-comint-buffer-livep session)) - 'org-julia-async-process-filter) - ;; Initialization - (let ((julia-init - (with-temp-buffer - (insert-file-contents org-babel-julia-startup-script) - (buffer-string)))) - (ess-send-string (ess-get-process) julia-init nil)) - (current-buffer)))) + 'org-julia-async-process-filter)))) (defun org-babel-julia-get-session-name (params) "Extract the session name from the PARAMS. @@ -153,8 +154,7 @@ This function is used for all async processing with and without session." ;; This will evaluate the code again ;; (cl-callf org-babel-process-params (nth 2 info)) (setq params (nth 2 info)) - (setq cache (let ((c (cdr (assq :cache params)))) - (and c (string= "yes" c)))) + (setq cache (ob-julia-check-trueness params :cache)) ;; pass info to have a different hash (setq new-hash (if cache (org-babel-sha1-hash) nil)) (org-babel-remove-result) @@ -181,14 +181,14 @@ Does not rely on an ESS session." (make-process :name "*julia-async-process*" :filter #'org-julia-async-process-filter :command `(,org-babel-julia-command - "--load" ,org-babel-julia-startup-script + "--load" ,ob-julia-startup-script "--eval" ,(format "include(%S);%s" tmpfile command))) (concat "julia-async:" uuid ":" outfile)) (progn (shell-command (format "%s --load %s %s" org-babel-julia-command - org-babel-julia-startup-script tmpfile)) + ob-julia-startup-script tmpfile)) outfile)))) (defun org-babel-julia-assign-to-var-or-array (var) @@ -201,7 +201,7 @@ Does not rely on an ESS session." "Create a Matrix (Vector{Any,2} from `MATRIX' and assign it to `NAME'" (format "%s = [%s]" name (mapconcat (lambda (line) (mapconcat (lambda (e) - (format "%s" e)) + (format "%S" e)) line " ")) matrix ";"))) (defun org-babel-julia-assign-to-var (name value) @@ -290,19 +290,22 @@ else OUTFILE is used, and data is _write()_ to it." (format "OrgBabelImport(%S);OrgBabelReload();" (concat (if using (mapconcat (lambda (x) (concat "using " x)) using "\n") "") + "\n" (if import (mapconcat (lambda (x) (concat "import " x)) import "\n") ""))) "") (format - "OrgBabelFormat(%s,%S,%S,%S,%S,%s,%S);" - output-type outfile - dir - (mapconcat 'concat vars ";") srcfile - (if org-babel-julia-silent-repl - "true" "false") - (org-babel-julia-make-kwargs `((width . ,width) - (height . ,height) - (size . ,size))))))) + "OrgBabelFormat(%s,%S,%S,%S,%S,%s,%s,%S);" + output-type outfile + dir + (mapconcat 'concat vars ";") srcfile + (if org-babel-julia-silent-repl + "true" "false") + (if (ob-julia-check-trueness params :let) + "true" "false") + (org-babel-julia-make-kwargs `((width . ,width) + (height . ,height) + (size . ,size))))))) (defun org-babel-execute:julia-async (buffer session body block output params) (let* ((uuid (org-id-uuid)) @@ -361,14 +364,14 @@ If PARAMS is :async, insert a link, unless CALLBACK is true." (if inlined (with-temp-buffer (when (bound-and-true-p org-export-current-backend) - (insert (format "@@%s:" + (insert (format "#+begin_export %s\n" (if org-export-current-backend org-export-current-backend inlined)))) (insert-file-contents results) (when (bound-and-true-p org-export-current-backend) (goto-char (point-max)) - (insert "@@")) + (insert "\n#+end_export")) (buffer-string)) (org-babel-result-cond (if res (split-string res) nil) (with-temp-buffer @@ -391,27 +394,25 @@ table. To force a matrix, use matrix" ((member "raw" results) 'raw) (t 'auto)))) +(defun ob-julia-check-trueness (params param) + "" + (and (assoc param params) + (let ((val (cdr (assoc param params)))) + (or + (not val) + (string= "t" val) + (string= "yes" val))))) + (defun org-babel-julia-async-p (params) "Check whether the session should be async or not." (let* ((res (cdr (assoc :results params))) (async (assoc :async params))) (and async - (or - (not (cdr async)) - (string= "t" (cdr async)) - (string= "yes" (cdr async))) + (ob-julia-check-trueness params :async) + (and (eq org-babel-current-src-block-location (org-babel-where-is-src-block-head))) (not (and res (stringp res) (member "silent" (split-string res))))))) (defun org-babel-julia-really-async-p () - ;; (let* - ;; ((head (org-babel-where-is-src-block-head)) - ;; (async (and (not (bound-and-true-p org-export-current-backend)) - ;; head - ;; org-babel-current-src-block-location - ;; (equal org-babel-current-src-block-location - ;; head)))) - ;; async) - ;; Disable async on export (not (bound-and-true-p org-export-current-backend))) ;; Copied from ob-python @@ -442,7 +443,6 @@ This function is called by `org-babel-execute-src-block'. BODY is the content of the src block PARAMS are the parameter passed to the block" ;; org-babel-current-src-block-location ; this variable does not work >.< - (message (format "body: %s, params: %s" body params)) (save-excursion (let* ((buffer (buffer-name)) (session (org-babel-julia-get-session-name params)) diff --git a/readme.org b/readme.org index 615615b..83717af 100644 --- a/readme.org +++ b/readme.org @@ -19,7 +19,10 @@ See [[Implemented features]] for more details. (customized by =org-babel-julia-default-session=) 3. If session has a values, use it's name 3. Check if we want to use a session or not. Check if the session - exists. Start the session accordingly. + exists. Start the session accordingly. During startup, the file + defined in =ob-julia-startup-script= is loaded. This file must define + functions like OrgBabelFormat, to let emacs send code to + execute. See =init.jl= for details. 4. Is the evaluation async? 1. YES: 1. Register a filter function @@ -100,7 +103,18 @@ sleep(1) Asynchronous evaluation is automatically disabled on export, or when a code block depends on one (=:var=) -** Variables input (=:var=), Standard output +** Variables input (=:var =:let=), Standard output + +As usual, you can set variables with the =:var= header. To make ob-julia +behave like the julia REPL, variables are set in the global scope. If +you want a block to be isolated, you can use the extra =:let= header +argument: with this, the src block is inside a let block: + +#+begin_src julia :exports code :eval never +let vars + src_block +end +#+end_src *** Inputs