Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ os:
- linux
- osx
julia:
- 0.4
- 0.5
- 0.6
- nightly
notifications:
email: false
Expand Down
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
NBInclude is a package for the [Julia language](http://julialang.org/) which allows you to include and execute [IJulia](https://github.com/JuliaLang/IJulia.jl) (Julia-language [Jupyter](https://jupyter.org/)) notebook files just as you would include an ordinary Julia file. That is, analogous to doing [`include("myfile.jl")`](http://docs.julialang.org/en/latest/stdlib/base/#Base.include) in Julia to execute `myfile.jl`, you can do
```jl
using NBInclude
nbinclude("myfile.ipynb")
@nbinclude("myfile.ipynb")
```
to execute all of the code cells in the IJulia notebook `myfile.ipynb`. Similar to `include`, the value of the last evaluated expression in the last evaluated code cell is returned.

Expand All @@ -18,14 +18,10 @@ Key features of NBInclude are:
and nested inclusions can use paths relative to the notebook, just as for `include`.
* In a module, included notebooks work fine with [precompilation](http://docs.julialang.org/en/latest/manual/modules/#module-initialization-and-precompilation) in Julia 0.4 (and re-compilation is automatically triggered if the notebook changes).
* Code is associated with accurate line numbers (e.g. for backtraces when exceptions are thrown), in the form of `myfile.ipynb:In[N]:M` for line `M` in input cell `N` of the `myfile.ipynb` notebook. Un-numbered cells (e.g. unevaluated cells) are given a number
`+N` for the `N`-th nonempty cell in the notebook. You can use `nbinclude("myfile.ipynb", renumber=true)` to automatically renumber the cells in sequence (as if you had selected *Run All* from the Jupyter *Cell* menu), without altering the file.
`+N` for the `N`-th nonempty cell in the notebook. You can use `@nbinclude("myfile.ipynb", renumber=true)` to automatically renumber the cells in sequence (as if you had selected *Run All* from the Jupyter *Cell* menu), without altering the file.
* The Julia `@__FILE__` macro returns `/path/to/myfile.ipynb:In[N]` for input cell `N`.
* Like `include`, `nbinclude` works fine with parallel Julia processes, even for
worker processes (from Julia's [`addprocs`](http://docs.julialang.org/en/latest/stdlib/parallel/#Base.addprocs)) that may not have filesystem access.
(Do `import NBInclude; @everywhere using NBInclude` to use `nbinclude` on
all processes.)
* In IJulia, cells beginning with `;` or `?` are interpreted as shell commands or help requests, respectively. Such cells are ignored by `nbinclude`.
* `counters` and `regex` keywords can be used to include a subset of notebook cells to those for which `counter ∈ counters` and the cell text matches `regex`. For example, `nbinclude("notebook.ipynb"; counters=1:10, regex=r"#\s*EXECUTE")`
* In IJulia, cells beginning with `;` or `?` are interpreted as shell commands or help requests, respectively. Such cells are ignored by `@nbinclude`.
* `counters` and `regex` keywords can be used to include a subset of notebook cells to those for which `counter ∈ counters` and the cell text matches `regex`. For example, `@nbinclude("notebook.ipynb"; counters=1:10, regex=r"#\s*EXECUTE")`
would include cells 1 to 10 from `notebook.ipynb` that contain comments like `# EXECUTE`.
* A keyword `anshook` can be used to run a passed function on the return value of all the cells.
* No Python or Jupyter dependency.
Expand Down
4 changes: 2 additions & 2 deletions REQUIRE
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
julia 0.4
julia 0.6
JSON
Compat 0.7.9
Compat 0.62.0
6 changes: 2 additions & 4 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
environment:
matrix:
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.4/julia-0.4-latest-win32.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.4/julia-0.4-latest-win64.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.5/julia-0.5-latest-win32.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.5/julia-0.5-latest-win64.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe"
- JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe"
- JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe"

Expand Down
145 changes: 92 additions & 53 deletions src/NBInclude.jl
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
VERSION >= v"0.4.0-dev+6521" && __precompile__()
__precompile__()

"""
The NBInclude module allow you to include and execute Julia code from
IJulia Jupyter notebooks. Analogous to `include("myfile.jl")`, just do

using NBInclude
nbinclude("myfile.ipynb")
@nbinclude("myfile.ipynb")

to include the Julia code from the notebook `myfile.ipynb`. Like `include`,
the value of the last evaluated expression is returned.
"""
module NBInclude
export nbinclude, @nbinclude

using Compat, JSON
export nbinclude

"""
my_include_string(s::AbstractString, path::AbstractString, prev)
my_include_string(m::Module, s::AbstractString, path::AbstractString, prev)

Like include_string, but also change the current source path just
as `include(filename)` would do. We are hacking undocumented internals
of Julia here (see `base/loading.jl:include_from_node1`), but it hasn't
changed from Julia 0.2 to Julia 0.4 so it's not too crazy. `prev`
of Julia here (see `base/loading.jl:include_relative`), but it hasn't
changed from Julia 0.2 to Julia 0.7 so it's not too crazy. `prev`
should be the previous path returned by `Base.source_path`.
"""
function my_include_string(s::AbstractString, path::AbstractString, prev)
function my_include_string(m::Module, s::AbstractString, path::AbstractString, prev)
tls = task_local_storage()
tls[:SOURCE_PATH] = path
try
return include_string(s, path)
return include_string(m, s, path)
finally
if prev === nothing
delete!(tls, :SOURCE_PATH)
Expand All @@ -39,10 +39,70 @@ function my_include_string(s::AbstractString, path::AbstractString, prev)
end

"""
nbinclude(path::AbstractString; renumber::Bool=false, counters=1:typemax(Int), regex::Regex=r"", anshook = identity)
nbinclude(m::Module, path; ...)

Like `@nbinclude(path; ...)` but allows you to specify a module
to evaluate in, similar to `include(m, path)`.
"""
function nbinclude(m::Module, path::AbstractString;
renumber::Bool=false,
counters = 1:typemax(Int),
regex::Regex = r"",
anshook = identity)
# act like include(path), in that path is relative to current file:
# for precompilation, invalidate the cache if the notebook changes:
path, prev = @static if VERSION >= v"0.7.0-DEV.3483" # julia#25455
Base._include_dependency(m, path)
else
Base._include_dependency(path)
end

# similar to julia#22588, we assume that all nodes
# where you are running nbinclude can access the filesystem
nb = open(JSON.parse, path, "r")

# check for an acceptable notebook:
nb["nbformat"] == 4 || error("unrecognized notebook format ", nb["nbformat"])
lang = lowercase(nb["metadata"]["language_info"]["name"])
lang == "julia" || error("notebook is for unregognized language $lang")

shell_or_help = r"^\s*[;?]" # pattern for shell command or help

ret = nothing
counter = 0 # keep our own cell counter to handle un-executed notebooks.
for cell in nb["cells"]
if cell["cell_type"] == "code" && !isempty(cell["source"])
s = join(cell["source"])
isempty(strip(s)) && continue # Jupyter doesn't number empty cells
counter += 1
occursin(shell_or_help, s) && continue
cellnum = renumber ? string(counter) :
cell["execution_count"] == nothing ? string('+',counter) :
string(cell["execution_count"])
counter in counters && occursin(regex, s) || continue
ret = my_include_string(m, s, string(path, ":In[", cellnum, "]"), prev)
anshook(ret)
end
end
return ret
end

# nbinclude(path) must be a macro because of #22064 — in 1.0, current_module() is disappearing
# so there is no way to get the caller's module without being a macro.

@noinline function nbinclude(path::AbstractString; kws...)
Base.depwarn("`nbinclude(path)` is deprecated, use `@nbinclude(path)` instead.", :nbinclude)
return nbinclude(isdefined(Base, :_current_module) ? Base._current_module() : current_module(),
path; kws...)
end

const curmod_expr = VERSION >= v"0.7.0-DEV.481" ? :(@__MODULE__) : :(current_module())

"""
@nbinclude(path::AbstractString; renumber::Bool=false, counters=1:typemax(Int), regex::Regex=r"", anshook = identity)

Include the IJulia Jupyter notebook at `path` and execute the code
cells (in the order that they appear in the file), returning the
cells (in the order that they appear in the file) in `m`, returning the
result of the last expression in the last code cell.

Similarly to `include(path)` for `.jl` files, the `path` is relative
Expand All @@ -61,59 +121,38 @@ and each cell is assigned a consecutive number `N`.
Only cells for which `counter ∈ counters` holds and the cell text matches `regex`
are executed. E.g.

nbinclude("notebook.ipynb"; counters = 1:10, regex=r"# *exec"i)
@nbinclude("notebook.ipynb"; counters = 1:10, regex=r"# *exec"i)

would include cells 1 to 10 from "notebook.ipynb" that contain comments like
`# exec` or `# ExecuteMe` in the cell text.

`anshook` can be used to execute a function on all the values returned in the cells.

See also `nbinclude(module, path; ...)` to include a notebook in a specified module.
"""
function nbinclude(path::AbstractString; renumber::Bool=false,
counters = 1:typemax(Int),
regex::Regex = r"",
anshook = identity)
# act like include(path), in that path is relative to current file:
prev = Base.source_path(nothing)
path = (prev == nothing) ? abspath(path) : joinpath(dirname(prev),path)

# for precompilation, invalidate the cache if the notebook changes:
include_dependency(path)

# similar to base/loading.jl, handle nbinclude calls from worker
# nodes that may not have filesystem access by fetching the file
# contents from node 1.
nb = if myid() == 1
# sleep a bit to process file requests from other nodes
nprocs()>1 && sleep(0.005)
open(JSON.parse, path, "r")
macro nbinclude(args...)
args = collect(args) # need a mutable collection, not a tuple
# (extracting keyword arguments in macro calls is a pain since we want to handle
# the cases both with and without a semicolon.)
if !isempty(args) && Meta.isexpr(args[1], :parameters)
kws = map(esc, popfirst!(args).args)
else
JSON.parse(remotecall_fetch(readstring, 1, path))
kws = Any[]
end

# check for an acceptable notebook:
nb["nbformat"] == 4 || error("unrecognized notebook format ", nb["nbformat"])
lang = lowercase(nb["metadata"]["language_info"]["name"])
lang == "julia" || error("notebook is for unregognized language $lang")

shell_or_help = r"^\s*[;?]" # pattern for shell command or help

ret = nothing
counter = 0 # keep our own cell counter to handle un-executed notebooks.
for cell in nb["cells"]
if cell["cell_type"] == "code" && !isempty(cell["source"])
s = join(cell["source"])
isempty(strip(s)) && continue # Jupyter doesn't number empty cells
counter += 1
ismatch(shell_or_help, s) && continue
cellnum = renumber ? string(counter) :
cell["execution_count"] == nothing ? string('+',counter) :
string(cell["execution_count"])
counter in counters && ismatch(regex, s) || continue
ret = my_include_string(s, string(path, ":In[", cellnum, "]"), prev)
anshook(ret)
while !isempty(args)
if Meta.isexpr(args[end], :(=))
pushfirst!(kws, esc(Expr(:kw, args[end].args)))
pop!(args)
else
break
end
end
return ret
# remaining args: path or module,path
args = esc.(args)
if length(args) == 1
pushfirst!(args, curmod_expr) # use current module
end
return Expr(:call, :nbinclude, Expr(:parameters, kws...), args...)
end

end # module
2 changes: 1 addition & 1 deletion test/includes/test1.jl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nbinclude("test2.ipynb")
@nbinclude("test2.ipynb")
2 changes: 1 addition & 1 deletion test/includes/test3.jl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nbinclude("test4.ipynb")
@nbinclude("test4.ipynb")
2 changes: 1 addition & 1 deletion test/includes/test4.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"outputs": [],
"source": [
"nbinclude(joinpath(\"..\", \"test.ipynb\"))"
"@nbinclude(joinpath(\"..\", \"test.ipynb\"))"
]
}
],
Expand Down
19 changes: 6 additions & 13 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
using Base.Test, Compat

addprocs(1)

import NBInclude; @everywhere using NBInclude
using NBInclude, Compat, Compat.Test

@test include(joinpath("includes", "test1.jl")) == 314159
@test f(5) == 6
@test normpath(myfile) == abspath("test.ipynb") * ":In[6]"
@test normpath(myfile2) == abspath("test.ipynb") * ":In[+7]"

@test remotecall_fetch(nbinclude, 2, "test.ipynb") == 314159


x=[]; nbinclude("test2.ipynb")
x=[]; @nbinclude("test2.ipynb")
@test x == [1, 2, 3, 4, 5, 6]

x=[]; nbinclude("test2.ipynb"; counters = [1, 4, 5])
x=[]; @nbinclude("test2.ipynb"; counters = [1, 4, 5])
@test x == [1, 4, 5]

x=[]; nbinclude("test2.ipynb"; regex=r"#.*executeme")
x=[]; @nbinclude("test2.ipynb"; regex=r"#.*executeme")
@test x == [2, 4]

x=[]; nbinclude("test2.ipynb"; counters = [1, 4, 5], regex=r"#.*executeme")
x=[]; @nbinclude("test2.ipynb"; counters = [1, 4, 5], regex=r"#.*executeme")
@test x == [4]

z = 0; nbinclude("test2.ipynb"; anshook = x -> (global z += 1))
z = 0; @nbinclude("test2.ipynb"; anshook = x -> (global z += 1))
@test z == 6