Skip to content

Commit 761cefa

Browse files
committed
TOML: fix converted tabular data printing
Previously the printing pass for converted data only works for non-tabular data (like simple number literals), and it doesn't work for `Dict` or `Array`s. Rather it leads to runtime error because we don't pass over the same `by` keyword argument through recursive calls and it may not be assigned: ```julia julia> struct MyStruct a::Int end julia> data = Dict(:foo => MyStruct(1)) Dict{Symbol, MyStruct} with 1 entry: :foo => MyStruct(1) julia> TOML.print(data; softed=true) do x x isa MyStruct && return Dict(:bar => x.a) end ERROR: MethodError: no method matching print(::var"#38#39", ::Dict{Symbol, MyStruct}; softed=true) You may have intended to import Base.print Closest candidates are: print(::Union{Nothing, Function}, ::AbstractDict; sorted, by) at /Users/aviatesk/julia/julia/stdlib/TOML/src/print.jl:130 got unsupported keyword argument "softed" print(::Union{Nothing, Function}, ::IO, ::AbstractDict; sorted, by) at /Users/aviatesk/julia/julia/stdlib/TOML/src/print.jl:129 got unsupported keyword argument "softed" print(::IO, ::AbstractDict; sorted, by) at /Users/aviatesk/julia/julia/stdlib/TOML/src/print.jl:131 got unsupported keyword argument "softed" Stacktrace: [1] kwerr(::NamedTuple{(:softed,), Tuple{Bool}}, ::Function, ::Function, ::Dict{Symbol, MyStruct}) @ Base ./error.jl:163 [2] top-level scope @ none:1 ``` <details><summary>Originally reported by JET:</summary> julia> using JET, Pkg julia> @report_call Pkg.project() ═════ 3 possible errors found ═════ ┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/API.jl:103 Pkg.API.EnvCache() │┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/Types.jl:285 #self#(Pkg.Types.nothing) ││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/Types.jl:288 Pkg.Types.read_project(project_file) │││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/project.jl:138 Pkg.Types.sprint(Pkg.Types.showerror, e) ││││┌ @ strings/io.jl:106 Base.#sprint#412(Core.tuple(Base.nothing, 0, #self#, f), args...) │││││┌ @ strings/io.jl:112 f(Core.tuple(s), args...) ││││││┌ @ toml_parser.jl:326 Base.TOML.point_to_line(Base.getproperty(err, :str), pos, pos, io) │││││││ for 1 of union split cases, no matching method found for call signatures (Tuple{typeof(Base.TOML.point_to_line), Nothing, Int64, Int64, IOBuffer})): Base.TOML.point_to_line(Base.getproperty(err::Base.TOML.ParserError, :str::Symbol)::Union{Nothing, String}, pos::Int64, pos::Int64, io::IOBuffer) ││││││└────────────────────── │││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/project.jl:142 Pkg.Types.Project(raw) ││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/project.jl:124 Base.setproperty!(project, :targets, Pkg.Types.read_project_targets(Pkg.Types.get(raw, "targets", Pkg.Types.nothing), project)) │││││┌ @ Base.jl:35 Base.convert(Base.fieldtype(Base.typeof(x), f), v) ││││││┌ @ abstractdict.jl:523 _(x) │││││││┌ @ dict.jl:104 Base.setindex!(h, v, k) ││││││││┌ @ dict.jl:382 Base.convert(_, v0) │││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/LinearAlgebra/src/factorization.jl:58 _(f) ││││││││││ no matching method found for call signature (Tuple{Type{Vector{String}}, LinearAlgebra.Factorization}): _::Type{Vector{String}}(f::LinearAlgebra.Factorization) │││││││││└───────────────────────────────────────────────────────────────────────────────────────────────── ││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/Types.jl:305 Pkg.Types.write_env_usage(manifest_file, "manifest_usage.toml") │││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/Types.jl:436 Pkg.Types.sprint(#35) ││││┌ @ strings/io.jl:106 Base.#sprint#412(Core.tuple(Base.nothing, 0, #self#, f), args...) │││││┌ @ strings/io.jl:112 f(Core.tuple(s), args...) ││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/Pkg/src/Types.jl:437 TOML.print(io, Pkg.Types.Dict(Pkg.Types.=>(Core.getfield(#self#, :source_file), Base.vect(Pkg.Types.Dict(Pkg.Types.=>("time", Pkg.Types.now())))))) │││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:131 TOML.Internals.Printer.#print#16(false, TOML.Internals.Printer.identity, #self#, io, a) ││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:131 Core.kwfunc(TOML.Internals.Printer._print)(Core.apply_type(Core.NamedTuple, (:sorted, :by))(Core.tuple(sorted, by)), TOML.Internals.Printer._print, TOML.Internals.Printer.nothing, io, a) │││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:76 #s848(_2, _3, f, io, a, Base.getindex(TOML.Internals.Printer.String)) ││││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:76 TOML.Internals.Printer.#_print#11(indent, first_block, sorted, by, _3, f, io, a, ks) │││││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:88 Core.kwfunc(TOML.Internals.Printer.printvalue)(Core.apply_type(Core.NamedTuple, (:sorted,))(Core.tuple(sorted)), TOML.Internals.Printer.printvalue, f, io, value) ││││││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:25 TOML.Internals.Printer.#printvalue#1(sorted, _3, f, io, value) │││││││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:29 Core.kwfunc(TOML.Internals.Printer._print)(Core.apply_type(Core.NamedTuple, (:sorted,))(Core.tuple(sorted)), TOML.Internals.Printer._print, f, io, x) ││││││││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:76 #s848(_2, _3, f, io, a, Base.getindex(TOML.Internals.Printer.String)) │││││││││││││││┌ @ /Users/aviatesk/julia/julia/usr/share/julia/stdlib/v1.7/TOML/src/print.jl:76 Core.throw(Core.UndefKeywordError(:by)) ││││││││││││││││ UndefKeywordError: keyword argument by not assigned │││││││││││││││└──────────────────────────────────────────────────────────────────────────────── Pkg.API.ProjectInfo </details> With this PR, everything should work: > After ```julia julia> TOML.print(data; sorted=true) do x x isa MyStruct && return Dict(:bar => x.a) end [foo] bar = 1 ```
1 parent 2280efe commit 761cefa

File tree

2 files changed

+59
-32
lines changed

2 files changed

+59
-32
lines changed

stdlib/TOML/src/print.jl

+37-32
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import Dates
44

5+
import Base: @invokelatest
56
import ..isvalid_barekey_char
67

78
function printkey(io::IO, keys::Vector{String})
@@ -20,46 +21,36 @@ function printkey(io::IO, keys::Vector{String})
2021
end
2122

2223
const MbyFunc = Union{Function, Nothing}
23-
const TOMLValue = Union{AbstractVector, AbstractDict, Dates.DateTime, Dates.Time, Dates.Date, Bool, Integer, AbstractFloat, String}
24-
function printvalue(f::MbyFunc, io::IO, value::AbstractVector; sorted=false)
24+
const TOMLValue = Union{AbstractVector, AbstractDict, Dates.DateTime, Dates.Time, Dates.Date, Bool, Integer, AbstractFloat, AbstractString}
25+
function printvalue(f::MbyFunc, io::IO, value::AbstractVector; sorted=false, by=identity)
2526
Base.print(io, "[")
2627
for (i, x) in enumerate(value)
2728
i != 1 && Base.print(io, ", ")
2829
if isa(x, AbstractDict)
29-
_print(f, io, x; sorted)
30+
_print(f, io, x; sorted, by)
3031
else
31-
printvalue(f, io, x; sorted)
32+
printvalue(f, io, x; sorted, by)
3233
end
3334
end
3435
Base.print(io, "]")
3536
end
36-
function printvalue(f::MbyFunc, io::IO, value; sorted)
37-
if f === nothing
38-
error("type `$(typeof(value))` is not a valid TOML type, pass a conversion function to `TOML.print`")
39-
end
40-
toml_value = f(value)
41-
if !(toml_value isa TOMLValue)
42-
error("TOML syntax function for type `$(typeof(value))` did not return a valid TOML type but a `$(typeof(toml_value))`")
43-
end
44-
Base.invokelatest(printvalue, f, io, toml_value; sorted)
45-
end
46-
printvalue(f::MbyFunc, io::IO, value::AbstractDict; sorted) =
47-
_print(f, io, value; sorted)
48-
printvalue(f::MbyFunc, io::IO, value::Dates.DateTime; sorted) =
37+
printvalue(f::MbyFunc, io::IO, value::AbstractDict; sorted=false, by=identity) =
38+
_print(f, io, value; sorted, by)
39+
printvalue(f::MbyFunc, io::IO, value::Dates.DateTime; _...) =
4940
Base.print(io, Dates.format(value, Dates.dateformat"YYYY-mm-dd\THH:MM:SS.sss\Z"))
50-
printvalue(f::MbyFunc, io::IO, value::Dates.Time; sorted) =
41+
printvalue(f::MbyFunc, io::IO, value::Dates.Time; _...) =
5142
Base.print(io, Dates.format(value, Dates.dateformat"HH:MM:SS.sss"))
52-
printvalue(f::MbyFunc, io::IO, value::Dates.Date; sorted) =
43+
printvalue(f::MbyFunc, io::IO, value::Dates.Date; _...) =
5344
Base.print(io, Dates.format(value, Dates.dateformat"YYYY-mm-dd"))
54-
printvalue(f::MbyFunc, io::IO, value::Bool; sorted) =
45+
printvalue(f::MbyFunc, io::IO, value::Bool; _...) =
5546
Base.print(io, value ? "true" : "false")
56-
printvalue(f::MbyFunc, io::IO, value::Integer; sorted) =
47+
printvalue(f::MbyFunc, io::IO, value::Integer; _...) =
5748
Base.print(io, Int64(value)) # TOML specifies 64-bit signed long range for integer
58-
printvalue(f::MbyFunc, io::IO, value::AbstractFloat; sorted) =
49+
printvalue(f::MbyFunc, io::IO, value::AbstractFloat; _...) =
5950
Base.print(io, isnan(value) ? "nan" :
6051
isinf(value) ? string(value > 0 ? "+" : "-", "inf") :
6152
Float64(value)) # TOML specifies IEEE 754 binary64 for float
62-
printvalue(f::MbyFunc, io::IO, value::AbstractString; sorted) = Base.print(io, "\"", escape_string(value), "\"")
53+
printvalue(f::MbyFunc, io::IO, value::AbstractString; _...) = Base.print(io, "\"", escape_string(value), "\"")
6354

6455
is_table(value) = isa(value, AbstractDict)
6556
is_array_of_tables(value) = isa(value, AbstractArray) &&
@@ -70,8 +61,8 @@ function _print(f::MbyFunc, io::IO, a::AbstractDict,
7061
ks::Vector{String} = String[];
7162
indent::Int = 0,
7263
first_block::Bool = true,
73-
sorted::Bool,
74-
by::Function,
64+
sorted::Bool = false,
65+
by::Function = identity,
7566
)
7667
akeys = keys(a)
7768
if sorted
@@ -82,11 +73,25 @@ function _print(f::MbyFunc, io::IO, a::AbstractDict,
8273
for key in akeys
8374
value = a[key]
8475
is_tabular(value) && continue
85-
Base.print(io, ' '^4max(0,indent-1))
86-
printkey(io, [String(key)])
87-
Base.print(io, " = ") # print separator
88-
printvalue(f, io, value; sorted)
89-
Base.print(io, "\n") # new line?
76+
if !isa(value, TOMLValue)
77+
if f === nothing
78+
error("type `$(typeof(value))` is not a valid TOML type, pass a conversion function to `TOML.print`")
79+
end
80+
toml_value = f(value)
81+
if !(toml_value isa TOMLValue)
82+
error("TOML syntax function for type `$(typeof(value))` did not return a valid TOML type but a `$(typeof(toml_value))`")
83+
end
84+
value = toml_value
85+
end
86+
if is_tabular(value)
87+
_print(f, io, Dict(key => value); indent, first_block, sorted, by)
88+
else
89+
Base.print(io, ' '^4max(0,indent-1))
90+
printkey(io, [String(key)])
91+
Base.print(io, " = ") # print separator
92+
printvalue(f, io, value; sorted, by)
93+
Base.print(io, "\n") # new line?
94+
end
9095
first_block = false
9196
end
9297

@@ -105,7 +110,7 @@ function _print(f::MbyFunc, io::IO, a::AbstractDict,
105110
Base.print(io,"]\n")
106111
end
107112
# Use runtime dispatch here since the type of value seems not to be enforced other than as AbstractDict
108-
Base.invokelatest(_print, f, io, value, ks; indent = indent + header, first_block = header, sorted, by)
113+
@invokelatest _print(f, io, value, ks; indent = indent + header, first_block = header, sorted, by)
109114
pop!(ks)
110115
elseif is_array_of_tables(value)
111116
# print array of tables
@@ -119,7 +124,7 @@ function _print(f::MbyFunc, io::IO, a::AbstractDict,
119124
Base.print(io,"]]\n")
120125
# TODO, nicer error here
121126
!isa(v, AbstractDict) && error("array should contain only tables")
122-
Base.invokelatest(_print, f, io, v, ks; indent = indent + 1, sorted, by)
127+
@invokelatest _print(f, io, v, ks; indent = indent + 1, sorted, by)
123128
end
124129
pop!(ks)
125130
end

stdlib/TOML/test/print.jl

+22
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,34 @@ struct MyStruct
2121
a::Int
2222
end
2323
@test_throws ErrorException toml_str(Dict("foo" => MyStruct(1)))
24+
# simple value
2425
@test toml_str(Dict("foo" => MyStruct(1))) do x
2526
x isa MyStruct && return x.a
2627
end == """
2728
foo = 1
2829
"""
2930

31+
# tabular values
32+
@test toml_str(Dict("foo" => MyStruct(1)); sorted=true) do x
33+
x isa MyStruct && return [x.a]
34+
end == """
35+
foo = [1]
36+
"""
37+
@test toml_str(Dict("foo" => MyStruct(1)); sorted=true) do x
38+
x isa MyStruct && return Dict(:bar => x.a)
39+
end == """
40+
[foo]
41+
bar = 1
42+
"""
43+
44+
# validation against the usual case
45+
@test toml_str(Dict("foo" => MyStruct(1)); sorted=true) do x
46+
x isa MyStruct && return [x.a]
47+
end == toml_str(Dict("foo" => [1]); sorted=true)
48+
@test toml_str(Dict("foo" => MyStruct(1)); sorted=true) do x
49+
x isa MyStruct && return Dict(:bar => x.a)
50+
end == toml_str(Dict("foo" => Dict(:bar => 1)); sorted=true)
51+
3052
@test toml_str(Dict("b" => SubString("foo"))) == "b = \"foo\"\n"
3153

3254
@testset "empty dict print" begin

0 commit comments

Comments
 (0)