From 62b85aa6a55ba3431d05b0dc7499ceb18196d8c5 Mon Sep 17 00:00:00 2001 From: Travis DePrato <773453+travigd@users.noreply.github.com> Date: Sat, 25 May 2019 14:28:19 -0700 Subject: [PATCH] Fix %APPDATA% directory on Windows (for real this time). (#286) * Fix %APPDATA% directory on Windows (for real this time). * Write to both the .py and .json notebook config files. * Don't use get! macro. --- deps/build.jl | 151 ++++++++++++++++++++++---------------------- deps/jupyterdirs.jl | 37 +++++++++++ 2 files changed, 113 insertions(+), 75 deletions(-) create mode 100644 deps/jupyterdirs.jl diff --git a/deps/build.jl b/deps/build.jl index 8cf1f1db..5af11b2c 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -1,112 +1,113 @@ using JSON +include("./jupyterdirs.jl") + const BEGIN_MARKER = "###JULIA-WEBIO-CONFIG-BEGIN" const END_MARKER = "###JULIA-WEBIO-CONFIG-END" -function install_ijulia_config() - config_file = joinpath(homedir(), ".jupyter", "jupyter_notebook_config.py") - if isfile(config_file) - config_str = String(read(config_file)) - else - mkpath(dirname(config_file)) - config_str = "" - end +""" + install_notebook_config() + +Install necessary configuration for the `jlstaticserve` notebook (server) +extension. This function only configures the notebook extension, not browser +nbextension. +* Adds the path to `./deps` to Python's `sys.path` so that we can load the + `jlstaticserve.py` extension. This is done in `jupyter_notebook_config.py` + because there's no way to add to `sys.path` from the JSON config file. +* Adds `jlstaticserve` to the list of extensions loaded in the notebook server. + This is done in `jupyter_notebook_config.json` because that file has higher + precedence when both the `.py` and `.json` files exist (IPyWidgets, for + example, writes to the JSON file, so if we only wrote to the `.py` file, + that directive would take precedence and the `jlstaticserve` extension would + **not** be loaded). +""" +function install_notebook_config() + config_dir = jupyter_config_dir() + mkpath(config_dir) + config_file_py = joinpath(config_dir, "jupyter_notebook_config.py") + config_py = isfile(config_file_py) ? read(config_file_py, String) : "" - # remove previous config - config_str = replace(config_str, Regex("\n?" * BEGIN_MARKER * ".*" * END_MARKER * "\n?", "s") => "") + # Remove previous config + config_py = replace(config_py, Regex("\n?" * BEGIN_MARKER * ".*" * END_MARKER * "\n?", "s") => "") - config_str *= """ + # We do a repr to make sure that all the necessary characters are escaped + # and the whole path is wrapped in quotes. + deps_dir = dirname(@__FILE__) + deps_dir_esc = repr(deps_dir) + error_msg = strip(""" + Directory $deps_dir could not be found; WebIO will not work as + """) + config_py *= """ $BEGIN_MARKER - import sys, os - if os.path.isfile($(repr(joinpath(dirname(@__FILE__), "jlstaticserve.py")))): - sys.path.append($(repr(dirname(@__FILE__)))) - c = get_config() - c.NotebookApp.nbserver_extensions = { - "jlstaticserve": True - } + # Add the path to WebIO/deps so that we can load the `jlstaticserve` extension. + import os, sys, warnings + webio_deps_dir = $deps_dir_esc + if os.path.isfile(os.path.join(webio_deps_dir, "jlstaticserve.py")): + sys.path.append(webio_deps_dir) else: - print("WebIO config in ~/.jupyter/jupyter_notebook_config.py but WebIO plugin not found") + warning_msg = ( + 'Directory %s could not be found; WebIO.jl will not work as expected. ' + + 'Make sure WebIO.jl is installed correctly (try running ' + + 'Pkg.add("WebIO") and Pkg.build("WebIO") from the Julia console to ' + + 'make sure it is).' + ) % webio_deps_dir + warnings.warn(warning_msg) $END_MARKER """ - write(config_file, config_str) - config_file_json = joinpath(homedir(), ".jupyter", "jupyter_notebook_config.json") - if isfile(config_file_json) - dict = try - JSON.parse(read(config_file_json, String)) - catch err - println(stderr, "Error parsing Jupyter config file $config_file_json - fix it and build again or delete it to enable WebIO") - @goto jsondone - end - app = Base.@get! dict "NotebookApp" Dict() - nbext = Base.@get! app "nbserver_extensions" Dict() - nbext["jlstaticserve"] = true - open(config_file_json, "w") do io - prettyio = JSON.Writer.PrettyContext(io, 4) - JSON.print(prettyio, dict) - end + config_file_json = joinpath(config_dir, "jupyter_notebook_config.json") + config_json = isfile(config_file_json) ? read(config_file_json, String) : "{}" + config_json = try + JSON.parse(config_json) + catch exc + @error "Unable to parse Jupyter notebook config file ($config_file_json). Please fix it and rebuild WebIO." exception=exc + rethrow() end - @label jsondone -end -function get_jupyter_datadir() - try - return readline(open(`jupyter --data-dir`)) - catch (e) - # Is there a way to use Conda.jl if it's installed? - @warn "Didn't detect Jupyter." - end + # Magic to safely access nested JSON objects and set them to empty objects + # if they don't exist yet. + app_config = get!(config_json, "NotebookApp", Dict()) + extensions_config = get!(app_config, "nbserver_extensions", Dict()) + extensions_config["jlstaticserve"] = true - # Try guessing based on OS - # https://jupyter.readthedocs.io/en/latest/projects/jupyter-directories.html - if Sys.iswindows() - # Is this right? - return joinpath("%APPDATA%", "jupyter") - elseif Sys.isapple() - return joinpath(homedir(), "Library", "Jupyter") - else - # Maybe need to check XDG_DATA_HOME environment variable? - return joinpath(homedir(), ".local", "share", "jupyter") - end + # Defer writing until both files until the end to avoid inconsistent state. + write(config_file_py, config_py) + write(config_file_json, json(config_json, 4)) + return nothing end """ Install the Jupyter WebIO notebook extension. """ function install_webio_nbextension() - extension_dir = joinpath(get_jupyter_datadir(), "nbextensions") - mkpath(extension_dir) - - # I think the config dir is always ~/.jupyter, even on Windows. - config_dir = joinpath(homedir(), ".jupyter", "nbconfig") - mkpath(config_dir) - config_file_json = joinpath(config_dir, "notebook.json") + extensions_dir = jupyter_nbextensions_dir() + mkpath(extensions_dir) + extension_dir = joinpath(extensions_dir, "webio") # Copy the nbextension files. @info "Copying WebIO nbextension files to $(extension_dir)." cp( joinpath(@__DIR__, "../packages/jupyter-notebook-provider/dist"), - joinpath(extension_dir, "webio"), + extension_dir, ; force=true ) # Enable the notebook extension. - config_data = Dict() - if isfile(config_file_json) - config_data = try - JSON.parse(read(config_file_json, String)) - catch err - println(stderr, "Error parsing Jupyter config file $config_file_json - fix it and build again or delete it to enable WebIO") - return - end + config_dir = jupyter_nbconfig_dir() + mkpath(config_dir) + config_file = joinpath(config_dir, "notebook.json") + config_data = try + isfile(config_file) ? JSON.parse(read(config_file, String)) : Dict() + catch exc + @error "Error parsing Jupyter config file $config_file - fix it and build again or delete it to enable WebIO." exception=exc + error("Unable to parse Jupyter config file.") end config_data["load_extensions"] = get(config_data, "load_extensions", Dict()) config_data["load_extensions"]["webio/main"] = true - open(config_file_json, "w") do io - prettyio = JSON.Writer.PrettyContext(io, 4) - JSON.print(prettyio, config_data) + open(config_file, "w") do io + JSON.print(JSON.Writer.PrettyContext(io, 4), config_data) end end -install_ijulia_config() +install_notebook_config() install_webio_nbextension() diff --git a/deps/jupyterdirs.jl b/deps/jupyterdirs.jl new file mode 100644 index 00000000..47e2ee11 --- /dev/null +++ b/deps/jupyterdirs.jl @@ -0,0 +1,37 @@ +# This code is "borrowed" from IJulia.jl (MIT license). +# https://github.com/JuliaLang/IJulia.jl/blob/68748c2b88b4b394e2802e911eb154b7008ebdd2/deps/kspec.jl +@static if Sys.iswindows() + function appdata() # return %APPDATA% + path = zeros(UInt16, 300) + CSIDL_APPDATA = 0x001a + result = ccall((:SHGetFolderPathW,:shell32), stdcall, Cint, + (Ptr{Cvoid},Cint,Ptr{Cvoid},Cint,Ptr{UInt16}),C_NULL,CSIDL_APPDATA,C_NULL,0,path) + return result == 0 ? transcode(String, resize!(path, findfirst(iszero, path)-1)) : get(ENV, "APPDATA", "") + end + function default_jupyter_data_dir() + APPDATA = appdata() + return !isempty(APPDATA) ? joinpath(APPDATA, "jupyter") : joinpath(get(ENV, "JUPYTER_CONFIG_DIR", joinpath(homedir(), ".jupyter")), "data") + end +elseif Sys.isapple() + default_jupyter_data_dir() = joinpath(homedir(), "Library/Jupyter") +else + function default_jupyter_data_dir() + xdg_data_home = get(ENV, "XDG_DATA_HOME", "") + data_home = !isempty(xdg_data_home) ? xdg_data_home : joinpath(homedir(), ".local", "share") + joinpath(data_home, "jupyter") + end +end + +function jupyter_data_dir() + env_data_dir = get(ENV, "JUPYTER_DATA_DIR", "") + !isempty(env_data_dir) ? env_data_dir : default_jupyter_data_dir() +end + +### End of borrowed code ### + +function jupyter_config_dir() + env_config_dir = get(ENV, "JUPYTER_CONFIG_DIR", "") + !isempty(env_config_dir) ? env_config_dir : joinpath(homedir(), ".jupyter") +end +jupyter_nbextensions_dir() = joinpath(jupyter_data_dir(), "nbextensions") +jupyter_nbconfig_dir() = joinpath(jupyter_config_dir(), "nbconfig")