Skip to content

Commit 6e8911d

Browse files
committed
Merge pull request #43 from JuliaIO/sd/error_handling
rewrote error handling
2 parents 5e9107f + 6ce8245 commit 6e8911d

File tree

10 files changed

+255
-165
lines changed

10 files changed

+255
-165
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ os:
55
julia:
66
- 0.3
77
- 0.4
8-
- nightly
98
notifications:
109
email: false
1110
script:

src/FileIO.jl

Lines changed: 1 addition & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -39,120 +39,10 @@ export DataFormat,
3939
import Base.showerror
4040

4141
include("query.jl")
42+
include("error_handling.jl")
4243
include("loadsave.jl")
4344
include("registry.jl")
4445

45-
@doc """
46-
`LoaderError` should be thrown when loader library code fails, and other libraries should
47-
be given the chance to recover from the error. Reports the library name and an error message:
48-
LoaderError("ImageMagick", "Foo not available")
49-
""" ->
50-
type LoaderError <: Exception
51-
library::UTF8String
52-
msg::UTF8String
53-
end
54-
Base.showerror(io::IO, e::LoaderError) = println(io, e.library, " load error: ",
55-
msg, "\n Will try next loader.")
56-
57-
@doc """
58-
`WriterError` should be thrown when writer library code fails, and other libraries should
59-
be given the chance to recover from the error. Reports the library name and an error message:
60-
WriterError("ImageMagick", "Foo not available")
61-
""" ->
62-
type WriterError <: Exception
63-
library::UTF8String
64-
msg::UTF8String
65-
end
66-
Base.showerror(io::IO, e::WriterError) = println(io, e.library, " writer error: ",
67-
msg, "\n Will try next writer.")
68-
69-
@doc """
70-
- `load(filename)` loads the contents of a formatted file, trying to infer
71-
the format from `filename` and/or magic bytes in the file.
72-
- `load(strm)` loads from an `IOStream` or similar object. In this case,
73-
the magic bytes are essential.
74-
- `load(File(format"PNG",filename))` specifies the format directly, and bypasses inference.
75-
- `load(f; options...)` passes keyword arguments on to the loader.
76-
""" ->
77-
function load(s::@compat(Union{AbstractString,IO}), args...; options...)
78-
q = query(s)
79-
libraries = applicable_loaders(q)
80-
last_exception = ErrorException("No library available to load $s")
81-
for library in libraries
82-
try
83-
Library = check_loader(library)
84-
return Library.load(q, args...; options...)
85-
catch e
86-
if isa(e, LoaderError)
87-
info(e.library, " failed. ", e.msg)
88-
info(" Will try next loader, if available")
89-
else
90-
rethrow(e)
91-
end
92-
end
93-
end
94-
end
95-
96-
@doc """
97-
- `save(filename, data...)` saves the contents of a formatted file,
98-
trying to infer the format from `filename`.
99-
- `save(Stream(format"PNG",io), data...)` specifies the format directly, and bypasses inference.
100-
- `save(f, data...; options...)` passes keyword arguments on to the saver.
101-
""" ->
102-
function save(s::@compat(Union{AbstractString,IO}), data...; options...)
103-
q = query(s)
104-
libraries = applicable_savers(q)
105-
last_exception = ErrorException("No library available to save $s")
106-
for library in libraries
107-
try
108-
Library = check_saver(library)
109-
return Library.save(q, data...; options...)
110-
catch e
111-
if isa(e, WriterError)
112-
info(e.library, " failed. ", e.msg)
113-
info(" Will try next writer, if available")
114-
else
115-
rethrow(e)
116-
end
117-
end
118-
end
119-
end
120-
121-
# Forced format
122-
function save{sym}(::Type{DataFormat{sym}}, f::AbstractString, data...; options...)
123-
libraries = sym2saver[sym]
124-
check_saver(libraries[1])
125-
save(File(DataFormat{sym}, f), data...; options...)
126-
end
127-
128-
function save{sym}(::Type{DataFormat{sym}}, s::IO, data...; options...)
129-
libraries = sym2saver[sym]
130-
check_saver(libraries[1])
131-
save(Stream(DataFormat{sym}, s), data...; options...)
132-
end
133-
134-
function Base.writemime(io::IO, mime::MIME, x)
135-
handlers = applicable_mime(mime)
136-
last_exception = ErrorException("No package available to writemime $mime")
137-
for (T,pkg) in handlers
138-
isa(x, T) || continue
139-
try
140-
check_mime(pkg)
141-
return writemime(Stream(DataFormat{pkg}, io), mime, x)
142-
catch e
143-
if isa(e, WriterError)
144-
info(e.library, " failed. ", e.msg)
145-
info(" Will try next writer, if available")
146-
else
147-
rethrow(e)
148-
end
149-
end
150-
end
151-
end
152-
153-
# Fallbacks
154-
load{F}(f::Formatted{F}, args...; options...) = error("No load function defined for format ", F, " with filename ", filename(f))
155-
save{F}(f::Formatted{F}, data...; options...) = error("No save function defined for format ", F, " with filename ", filename(f))
15646

15747
end # module
15848

src/error_handling.jl

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
2+
@doc """
3+
`LoaderError` should be thrown when loader library code fails, and other libraries should
4+
be given the chance to recover from the error. Reports the library name and an error message:
5+
LoaderError("ImageMagick", "Foo not available")
6+
""" ->
7+
immutable LoaderError <: Exception
8+
library::UTF8String
9+
msg::UTF8String
10+
end
11+
Base.showerror(io::IO, e::LoaderError) = println(io, e.library, " load error: ",
12+
msg, "\n Will try next loader.")
13+
14+
@doc """
15+
`WriterError` should be thrown when writer library code fails, and other libraries should
16+
be given the chance to recover from the error. Reports the library name and an error message:
17+
WriterError("ImageMagick", "Foo not available")
18+
""" ->
19+
immutable WriterError <: Exception
20+
library::UTF8String
21+
msg::UTF8String
22+
end
23+
Base.showerror(io::IO, e::WriterError) = println(
24+
io, e.library, " writer error: ",
25+
msg, "\n Will try next writer."
26+
)
27+
28+
@doc """
29+
`NotInstalledError` should be thrown when a library is currently not installed.
30+
""" ->
31+
immutable NotInstalledError <: Exception
32+
library::Symbol
33+
message::UTF8String
34+
end
35+
Base.showerror(io::IO, e::NotInstalledError) = println(io, e.library, " is not installed.")
36+
37+
@doc """
38+
`UnknownFormat` gets thrown when FileIO can't recognize the format of a file.
39+
""" ->
40+
immutable UnknownFormat{T <: Formatted} <: Exception
41+
format::T
42+
end
43+
Base.showerror(io::IO, e::UnknownFormat) = println(io, e.format, " couldn't be recognized by FileIO.")
44+
45+
46+
@doc """
47+
Handles error as soon as they get thrown while doing IO
48+
""" ->
49+
function handle_current_error(e, islast)
50+
bt = catch_backtrace()
51+
bts = sprint(io->Base.show_backtrace(io, bt))
52+
message = islast ? "" : "\nTrying next loading library! Please report this issue on Github"
53+
warn(string(e, bts, message))
54+
end
55+
handle_current_error(e::NotInstalledError) = warn(string("lib ", e.library, " not installed, trying next library"))
56+
57+
@doc """
58+
Handles a list of thrown errors after no IO library was found working
59+
""" ->
60+
function handle_error(exceptions::Vector)
61+
for exception in exceptions
62+
continue_ = handle_error(exception...)
63+
continue_ || break
64+
end
65+
end
66+
67+
handle_error(e, q) = rethrow(e)
68+
69+
function handle_error(e::NotInstalledError, q)
70+
println("Library ", e.library, " is not installed but can load format: ", q)
71+
!isinteractive() && rethrow(e) # if we're not in interactive mode just throw
72+
while true
73+
println("should we install ", e.library, " for you? (y/n):")
74+
input = lowercase(chomp(strip(readline(STDIN))))
75+
if input == "y"
76+
info(string("Start installing ", e.library, "..."))
77+
Pkg.add(string(e.library))
78+
return false # don't continue
79+
elseif input == "n"
80+
info(string("Not installing ", e.library))
81+
return true # User does not install, continue going through errors.
82+
else
83+
println("$input is not a valid choice. Try typing y or n")
84+
end
85+
end
86+
true # User does not install, continue going through errors.
87+
end
88+
89+

src/loadsave.jl

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,99 @@
11
const sym2loader = Dict{Symbol,Vector{Symbol}}()
22
const sym2saver = Dict{Symbol,Vector{Symbol}}()
3-
const mimedict = Dict{Symbol,Vector{Any}}()
43

5-
for (appl,fchk,fadd,dct) in (
6-
(:applicable_loaders, :check_loader, :add_loader, :sym2loader),
7-
(:applicable_savers, :check_saver, :add_saver, :sym2saver))
4+
function is_installed(pkg::Symbol)
5+
isdefined(pkg) && isa(Main.(pkg), Module) && return true # is a module defined in Main scope
6+
path = Base.find_in_path(string(pkg)) # hacky way to determine if a Package is installed
7+
path == nothing && return false
8+
return isfile(path)
9+
end
10+
11+
function checked_import(pkg::Symbol)
12+
!is_installed(pkg) && throw(NotInstalledError(pkg, ""))
13+
!isdefined(Main, pkg) && eval(Main, Expr(:import, pkg))
14+
return Main.(pkg)
15+
end
16+
17+
18+
for (applicable_, add_, dict_) in (
19+
(:applicable_loaders, :add_loader, :sym2loader),
20+
(:applicable_savers, :add_saver, :sym2saver))
821
@eval begin
9-
$appl{sym}(::Formatted{DataFormat{sym}}) = get($dct, sym, [:FileIO]) # if no loader is declared, fallback to FileIO
10-
function $fchk(pkg::Symbol)
11-
!isdefined(Main, pkg) && eval(Main, Expr(:import, pkg))
12-
return Main.(pkg)
13-
end
14-
function $fadd{sym}(::Type{DataFormat{sym}}, pkg::Symbol)
15-
list = get($dct, sym, Symbol[])
16-
$dct[sym] = push!(list, pkg)
22+
$applicable_{sym}(::@compat(Union{Type{DataFormat{sym}}, Formatted{DataFormat{sym}}})) = get($dict_, sym, [:FileIO]) # if no loader is declared, fallback to FileIO
23+
function $add_{sym}(::Type{DataFormat{sym}}, pkg::Symbol)
24+
list = get($dict_, sym, Symbol[])
25+
$dict_[sym] = push!(list, pkg)
1726
end
1827
end
1928
end
2029

21-
applicable_mime{sym}(::MIME{sym}) = get(mimedict, sym, [:nothing])
22-
function check_mime(pkg::Symbol)
23-
pkg == :nothing && error("No MIME package available")
24-
!isdefined(Main, pkg) && eval(Main, Expr(:import, pkg))
25-
return pkg
26-
end
30+
31+
@doc "`add_loader(fmt, :Package)` triggers `using Package` before loading format `fmt`" -> add_loader
32+
@doc "`add_saver(fmt, :Package)` triggers `using Package` before saving format `fmt`" -> add_saver
33+
34+
35+
@doc """
36+
- `load(filename)` loads the contents of a formatted file, trying to infer
37+
the format from `filename` and/or magic bytes in the file.
38+
- `load(strm)` loads from an `IOStream` or similar object. In this case,
39+
the magic bytes are essential.
40+
- `load(File(format"PNG",filename))` specifies the format directly, and bypasses inference.
41+
- `load(f; options...)` passes keyword arguments on to the loader.
42+
""" ->
43+
load(s::@compat(Union{AbstractString,IO}), args...; options...) =
44+
load(query(s), args...; options...)
2745

2846
@doc """
29-
`add_mime(mime, T, :Package)` triggers `using Package` before attempting to write object of type `T` in format `mime`.
47+
- `save(filename, data...)` saves the contents of a formatted file,
48+
trying to infer the format from `filename`.
49+
- `save(Stream(format"PNG",io), data...)` specifies the format directly, and bypasses inference.
50+
- `save(f, data...; options...)` passes keyword arguments on to the saver.
3051
""" ->
31-
function add_mime{sym,T}(::MIME{sym}, ::Type{T}, pkg::Symbol)
32-
list = get(mimedict, sym, Any[])
33-
mimedict[sym] = push!(list, (T,pkg))
52+
save(s::@compat(Union{AbstractString,IO}), data...; options...) =
53+
save(query(s), data...; options...)
54+
55+
# Forced format
56+
function save{sym}(df::Type{DataFormat{sym}}, f::AbstractString, data...; options...)
57+
libraries = applicable_savers(df)
58+
checked_import(libraries[1])
59+
save(File(DataFormat{sym}, f), data...; options...)
3460
end
3561

36-
@doc "`add_loader(fmt, :Package)` triggers `using Package` before loading format `fmt`" -> add_loader
37-
@doc "`add_saver(fmt, :Package)` triggers `using Package` before saving format `fmt`" -> add_saver
62+
function save{sym}(df::Type{DataFormat{sym}}, s::IO, data...; options...)
63+
libraries = applicable_savers(df)
64+
checked_import(libraries[1])
65+
save(Stream(DataFormat{sym}, s), data...; options...)
66+
end
67+
68+
69+
# Fallbacks
70+
function load{F}(q::Formatted{F}, args...; options...)
71+
unknown(q) && throw(UnknownFormat(q))
72+
libraries = applicable_loaders(q)
73+
failures = Any[]
74+
for library in libraries
75+
try
76+
Library = checked_import(library)
77+
return Library.load(q, args...; options...)
78+
catch e
79+
handle_current_error(e, library == last(libraries))
80+
push!(failures, (e, q))
81+
end
82+
end
83+
handle_error(failures)
84+
end
85+
function save{F}(q::Formatted{F}, data...; options...)
86+
unknown(q) && throw(UnknownFormat(q))
87+
libraries = applicable_savers(q)
88+
failures = Any[]
89+
for library in libraries
90+
try
91+
Library = checked_import(library)
92+
return Library.save(q, data...; options...)
93+
catch e
94+
handle_current_error(e, library == last(libraries))
95+
push!(failures, (e, q))
96+
end
97+
end
98+
handle_error(failures)
99+
end

src/query.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ immutable SAVE end
1010

1111
split_predicates(list) = filter(x-> x <: OS, list), filter(x-> !(x <: OS), list)
1212
applies_to_os(os::Vector) = isempty(os) || any(applies_to_os, os)
13-
applies_to_os{O <: OS}(os::Type{O}) = false
13+
applies_to_os{O <: OS}(os::Type{O}) = false
1414
@unix_only applies_to_os{U <: Unix}(os::Type{U}) = true
1515
@windows_only applies_to_os(os::Type{Windows}) = true
1616
@linux_only applies_to_os(os::Type{OSX}) = false
@@ -46,12 +46,12 @@ const unknown_df = DataFormat{:UNKNOWN}
4646

4747
@doc """
4848
`unknown(f)` returns true if the format of `f` is unknown.""" ->
49-
unknown(::Type{format"UNKNOWN"}) = true
49+
unknown(::Type{format"UNKNOWN"}) = true
5050
unknown{sym}(::Type{DataFormat{sym}}) = false
5151

52-
const ext2sym = Dict{ASCIIString, @compat(Union{Symbol,Vector{Symbol}})}()
52+
const ext2sym = Dict{ASCIIString, @compat(Union{Symbol,Vector{Symbol}})}()
5353
const magic_list = Array(Pair, 0) # sorted, see magic_cmp below
54-
const sym2info = Dict{Symbol,Any}() # Symbol=>(magic, extension)
54+
const sym2info = Dict{Symbol,Any}() # Symbol=>(magic, extension)
5555
const magic_func = Array(Pair, 0) # for formats with complex magic #s
5656

5757

test/REQUIRE

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
FactCheck
2-
Images
3-
Netpbm
4-
ImageMagick
1+
FactCheck

test/error_handling.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
context("Not installed") do
2+
eval(Base, :(is_interactive = true)) # for interactive error handling
3+
4+
add_format(format"NotInstalled", (), ".not_installed", [:NotInstalled])
5+
stdin_copy = STDIN
6+
rs, wr = redirect_stdin()
7+
ref = @async save("test.not_installed")
8+
println(wr, "y")
9+
if VERSION < v"0.4.0-dev"
10+
@fact_throws ErrorException wait(ref) #("unknown package NotInstalled")
11+
else
12+
@fact_throws CompositeException wait(ref) #("unknown package NotInstalled")
13+
end
14+
ref = @async save("test.not_installed")
15+
println(wr, "invalid") #test invalid input
16+
println(wr, "n") # don't install
17+
wait(ref)
18+
@fact istaskdone(ref) --> true
19+
redirect_stdin(stdin_copy)
20+
close(rs);close(wr);
21+
eval(Base, :(is_interactive = false)) # for interactive error handling
22+
23+
end

0 commit comments

Comments
 (0)