Skip to content

Commit

Permalink
Merge pull request #13 from timholy/teh/module_docstring
Browse files Browse the repository at this point in the history
Support docstrings for modules
  • Loading branch information
timholy authored Jul 4, 2017
2 parents 083797f + 095f276 commit 8cb8b19
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 39 deletions.
103 changes: 65 additions & 38 deletions src/Revise.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,14 @@ mutable struct RelocatableExpr
RelocatableExpr(head::Symbol, args...) = new(head, [args...])
end

# Works in-place and hence is unsafe. Only for internal use.
Base.convert(::Type{RelocatableExpr}, ex::Expr) = relocatable!(ex)

# RelocatableExpr <---> Expr. Mutating in-place, so only for internal use.
function relocatable!(ex::Expr)
rex = RelocatableExpr(ex.head, relocatable!(ex.args))
rex.typ = ex.typ
rex
end
function unrelocatable!(rex::RelocatableExpr)
ex = Expr(rex.head, unrelocatable!(rex.args)...)
ex.typ = rex.typ
ex
end

# A copying transformation. Because the above mutate in-place, to
# eval RelocatableExprs without changing their underlying
# representation, we have to make a copy.
# We only call this when we've detected a new or changed expression.
function unrelocatable(rex::RelocatableExpr)
ex = Expr(rex.head, unrelocatable!(deepcopy(rex.args))...)
ex.typ = rex.typ
ex
end

function relocatable!(args::Vector{Any})
for (i, a) in enumerate(args)
Expand All @@ -65,14 +50,16 @@ function relocatable!(args::Vector{Any})
end
args
end
function unrelocatable!(args::Vector{Any})
for (i, a) in enumerate(args)
if isa(a, RelocatableExpr)
args[i] = unrelocatable!(a::RelocatableExpr)
end
end
args

function Base.convert(::Type{Expr}, rex::RelocatableExpr)
# This makes a copy. Used for `eval`, where we don't want to
# mutate the cached represetation.
ex = Expr(rex.head)
ex.args = Base.copy_exprargs(rex.args)
ex.typ = rex.typ
ex
end
Base.copy_exprs(rex::RelocatableExpr) = convert(Expr, rex)

# Implement the required comparison functions. `hash` is needed for Dicts.
function Base.:(==)(a::RelocatableExpr, b::RelocatableExpr)
Expand Down Expand Up @@ -249,12 +236,13 @@ end

function eval_revised(revmd::ModDict)
for (mod, exprs) in revmd
for ex in exprs
for rex in exprs
ex = convert(Expr, rex)
try
eval(mod, unrelocatable(ex))
eval(mod, ex)
catch err
warn("failure to evaluate changes in ", mod)
println(STDERR, unrelocatable(ex))
println(STDERR, ex)
end
end
end
Expand Down Expand Up @@ -360,9 +348,14 @@ function parse_expr!(md::ModDict, ex::Expr, file::Symbol, mod::Module, path)
elseif ex.head == :line
return md
elseif ex.head == :module
newmod = getfield(mod, _module_name(ex))
md[newmod] = Set{RelocatableExpr}()
parse_source!(md, ex.args[3], file, newmod, path)
parse_module!(md, ex, file, mod, path)
elseif isdocexpr(ex) && isa(ex.args[nargs_docexpr], Expr) && ex.args[nargs_docexpr].head == :module
# Module with a docstring (issue #8)
# Split into two expressions, a module definition followed by
# `"docstring" newmodule`
newmod = parse_module!(md, ex.args[nargs_docexpr], file, mod, path)
ex.args[nargs_docexpr] = Symbol(newmod)
push!(md[mod], convert(RelocatableExpr, ex))
elseif ex.head == :call && ex.args[1] == :include
if path != nothing
filename = ex.args[2]
Expand Down Expand Up @@ -399,6 +392,13 @@ function parse_expr!(md::ModDict, ex::Expr, file::Symbol, mod::Module, path)
md
end

function parse_module!(md::ModDict, ex::Expr, file::Symbol, mod::Module, path)
newmod = getfield(mod, _module_name(ex))
md[newmod] = Set{RelocatableExpr}()
parse_source!(md, ex.args[3], file, newmod, path)
newmod
end

function watch_package(modsym::Symbol)
files = parse_pkg_files(modsym)
for file in files
Expand Down Expand Up @@ -441,6 +441,11 @@ function revise_file_now(file0)
nothing
end

"""
revise()
`eval` any changes in tracked files in the appropriate modules.
"""
function revise()
for file in revision_queue
revise_file_now(file)
Expand All @@ -449,6 +454,33 @@ function revise()
nothing
end

"""
Revise.track(mod::Module, file::AbstractString)
Revise.track(file::AbstractString)
Watch `file` for updates and [`revise`](@ref) loaded code with any
changes. If `mod` is omitted it defaults to `Main`.
"""
function track(mod::Module, file::AbstractString)
isfile(file) || error(file, " is not a file")
empty!(new_files)
parse_source(file, mod, dirname(file))
for fl in new_files
@schedule revise_file_queued(fl)
end
nothing
end
track(file::AbstractString) = track(Main, file)

"""
Revise.track(Base)
Track the code in Julia's `base` directory for updates. This
facilitates making changes to Julia itself and testing them
immediately (without rebuilding).
At present some files in Base are not trackable, see the README.
"""
function track(mod::Module)
if mod == Base
empty!(new_files)
Expand All @@ -463,15 +495,6 @@ function track(mod::Module)
nothing
end

function track(file::AbstractString)
isfile(file) || error(file, " is not a file")
parse_source(file, Main, dirname(file))
for fl in new_files
@schedule revise_file_queued(fl)
end
nothing
end

## Utilities

_module_name(ex::Expr) = ex.args[2]
Expand Down Expand Up @@ -521,6 +544,10 @@ function macroreplace(ex::Expr, filename)
end
macroreplace(s, filename) = s

const nargs_docexpr = VERSION < v"0.7.0-DEV.328" ? 3 : 4
isdocexpr(ex) = ex.head == :macrocall && ex.args[1] == GlobalRef(Core, Symbol("@doc")) &&
length(ex.args) >= nargs_docexpr

function steal_repl_backend(backend = Base.active_repl_backend)
# terminate the current backend
put!(backend.repl_channel, (nothing, -1))
Expand Down
72 changes: 71 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ to_remove = String[]
exs
end

yry() = (sleep(0.1); revise(); sleep(0.1))

@testset "LineSkipping" begin
ex = Revise.relocatable!(quote
f(x) = x^2
Expand Down Expand Up @@ -83,7 +85,6 @@ to_remove = String[]
mkdir(testdir)
push!(to_remove, testdir)
push!(LOAD_PATH, testdir)
yry() = (sleep(0.1); revise(); sleep(0.1))
for (pcflag, fbase) in ((true, "pc"), (false, "npc")) # precompiled & not
modname = uppercase(fbase)
# Create a package with the following structure:
Expand Down Expand Up @@ -195,6 +196,8 @@ end
@eval @test $(fn4)() == -4
@eval @test $(fn5)() == -5
end
# Remove the precompiled file
rm(joinpath(Base.LOAD_CACHE_PATH[1], "PC.ji"))

# Test files paths that can't be statically parsed
dn = joinpath(testdir, "LoopInclude", "src")
Expand All @@ -221,13 +224,80 @@ end
@eval using LoopInclude
@test li_f() == 1
@test li_g() == 2
sleep(0.1) # ensure watching is set up
open(joinpath(dn, "file1.jl"), "w") do io
println(io, "li_f() = -1")
end
@test li_f() == 1 # unless the include is at toplevel it is not found

pop!(LOAD_PATH)
end

# issue #8
@testset "Module docstring" begin
testdir = joinpath(tempdir(), randstring(10))
mkdir(testdir)
push!(to_remove, testdir)
push!(LOAD_PATH, testdir)
dn = joinpath(testdir, "ModDocstring", "src")
mkpath(dn)
open(joinpath(dn, "ModDocstring.jl"), "w") do io
println(io, """
" Ahoy! "
module ModDocstring
include("dependency.jl")
f() = 1
end
""")
end
open(joinpath(dn, "dependency.jl"), "w") do io
println(io, "")
end
@eval using ModDocstring
@test ModDocstring.f() == 1
ds = @doc ModDocstring
@test ds.content[1].content[1].content[1] == "Ahoy! "

sleep(0.1) # ensure watching is set up
open(joinpath(dn, "ModDocstring.jl"), "w") do io
println(io, """
" Ahoy! "
module ModDocstring
include("dependency.jl")
f() = 2
end
""")
end
yry()
@test ModDocstring.f() == 2
ds = @doc ModDocstring
@test ds.content[1].content[1].content[1] == "Ahoy! "

open(joinpath(dn, "ModDocstring.jl"), "w") do io
println(io, """
" Hello! "
module ModDocstring
include("dependency.jl")
f() = 3
end
""")
end
yry()
@test ModDocstring.f() == 3
ds = @doc ModDocstring
@test ds.content[2].content[1].content[1] == "Hello! "

pop!(LOAD_PATH)
end
end

# These may cause warning messages about "not an existing file", but that's fine
Expand Down

0 comments on commit 8cb8b19

Please sign in to comment.