diff --git a/Project.toml b/Project.toml index 14afb67..ccc9f90 100644 --- a/Project.toml +++ b/Project.toml @@ -12,7 +12,7 @@ DashTable = "1b08a953-4be3-4667-9a23-f0e2ba4deb9a" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" MD5 = "6ac74813-4b46-53a4-afec-0b5dc9d7885c" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5" @@ -30,7 +30,7 @@ DashTable = "5.0.0" DataStructures = "0.17, 0.18" HTTP = "0.8.10, 0.9" JSON = "0.21" -JSON2 = "0.3" +JSON3 = "1.9" MD5 = "0.2" PlotlyBase = "0.8.5, 0.8.6" YAML = "0.4.7" diff --git a/README.md b/README.md index 74ced38..78c0bcb 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,5 @@ Be careful - in Dash.jl states come first in an arguments list. ### JSON: -I use JSON2.jl for JSON serialization/deserialization, so in callbacks all JSON objects are `NamedTuples` rather than dictionaries. Within component properties you can use both `Dict` and `NamedTuple` for JSON objects. - +I use JSON3.jl for JSON serialization/deserialization. Note when declaring elements with a single properly that `layout = (title = "Test graph")` is not interpreted as a `NamedTuple` by Julia - you'll need to add a comma when declaring the layout, e.g. `layout = (title = "Test graph",)` diff --git a/docs/src/index.md b/docs/src/index.md index d1129a4..b03d337 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -174,4 +174,4 @@ Be careful - in Dashboards states came first in arguments list ### json: -I use JSON2 for json serialization/deserialization, so in callbacks all json objects are NamedTuples not Dicts. In component props you can use both Dicts and NamedTuples for json objects. But be careful with single property objects: `layout = (title = "Test graph")` is not interpreted as NamedTuple by Julia - you need add comma at the end `layout = (title = "Test graph",)` +I use JSON3 for json serialization/deserialization. In component props you can use both Dicts and NamedTuples for json objects. But be careful with single property objects: `layout = (title = "Test graph")` is not interpreted as NamedTuple by Julia - you need add comma at the end `layout = (title = "Test graph",)` diff --git a/src/Dash.jl b/src/Dash.jl index 7b4f907..542bd9c 100644 --- a/src/Dash.jl +++ b/src/Dash.jl @@ -1,8 +1,9 @@ module Dash using DashBase -import HTTP, JSON2, CodecZlib, MD5 +import HTTP, JSON3, CodecZlib, MD5 using Sockets using Pkg.Artifacts + const ROOT_PATH = realpath(joinpath(@__DIR__, "..")) #const RESOURCE_PATH = realpath(joinpath(ROOT_PATH, "resources")) include("exceptions.jl") @@ -25,6 +26,7 @@ include("resources/application.jl") include("handlers.jl") include("server.jl") include("init.jl") +include("plotly_base.jl") @doc """ module Dash @@ -107,6 +109,8 @@ function __init__() end +JSON3.StructTypes.StructType(::Type{DashBase.Component}) = JSON3.StructTypes.Struct() +JSON3.StructTypes.excludes(::Type{DashBase.Component}) = (:name, :available_props, :wildcard_regex) -end # module +end # module \ No newline at end of file diff --git a/src/app/supporttypes.jl b/src/app/supporttypes.jl index db20b76..14fbcaa 100644 --- a/src/app/supporttypes.jl +++ b/src/app/supporttypes.jl @@ -2,7 +2,9 @@ struct Wildcard type ::Symbol end -JSON2.write(io::IO, wild::Wildcard; kwargs...) = Base.write(io, "[\"", wild.type, "\"]") +JSON3.StructTypes.StructType(::Type{Wildcard}) = JSON3.RawType() + +JSON3.rawbytes(wild::Wildcard) = string("[\"", wild.type, "\"]") const MATCH = Wildcard(:MATCH) const ALL = Wildcard(:ALL) @@ -30,6 +32,8 @@ const Input = Dependency{TraitInput} const State = Dependency{TraitState} const Output = Dependency{TraitOutput} +JSON3.StructTypes.StructType(::Type{<:Dependency}) = JSON3.StructTypes.Struct() + """ Base.==(a::Dependency, b::Dependency) @@ -97,6 +101,9 @@ struct ClientsideFunction namespace ::String function_name ::String end + +JSON3.StructTypes.StructType(::Type{ClientsideFunction}) = JSON3.StructTypes.Struct() + struct Callback func ::Union{Function, ClientsideFunction} dependencies ::CallbackDeps diff --git a/src/handler/callback_context.jl b/src/handler/callback_context.jl index eeef59a..d261372 100644 --- a/src/handler/callback_context.jl +++ b/src/handler/callback_context.jl @@ -35,16 +35,16 @@ function callback_context() return get_context(_callback_context_storage) end -function inputs_list_to_dict(list::Vector{Any}) +function inputs_list_to_dict(list::AbstractVector) result = Dict{String, Any}() _item_to_dict!.(Ref(result), list) return result end -dep_id_string(id::NamedTuple) = sorted_json(id) +dep_id_string(id::AbstractDict) = sorted_json(id) dep_id_string(id::AbstractString) = String(id) function _item_to_dict!(target::Dict{String, Any}, item) target["$(dep_id_string(item.id)).$(item.property)"] = get(item, :value, nothing) end -_item_to_dict!(target::Dict{String, Any}, item::Vector) = _item_to_dict!.(Ref(target), item) \ No newline at end of file +_item_to_dict!(target::Dict{String, Any}, item::AbstractVector) = _item_to_dict!.(Ref(target), item) \ No newline at end of file diff --git a/src/handler/index_page.jl b/src/handler/index_page.jl index fcaf300..039df6e 100644 --- a/src/handler/index_page.jl +++ b/src/handler/index_page.jl @@ -93,7 +93,7 @@ function config_html(app::DashApp) max_retry = get_devsetting(app, :hot_reload_max_retry) ) end - return """""" + return """""" end diff --git a/src/handler/processors/callback.jl b/src/handler/processors/callback.jl index d8a5378..b0824e3 100644 --- a/src/handler/processors/callback.jl +++ b/src/handler/processors/callback.jl @@ -12,14 +12,14 @@ function split_callback_id(callback_id::AbstractString) end input_to_arg(input) = get(input, :value, nothing) -input_to_arg(input::Vector) = input_to_arg.(input) +input_to_arg(input::AbstractVector) = input_to_arg.(input) make_args(inputs, state) = vcat(input_to_arg(inputs), input_to_arg(state)) res_to_vector(res) = res res_to_vector(res::Vector) = res -function _push_to_res!(res, value, out::Vector) +function _push_to_res!(res, value, out::AbstractVector) _push_to_res!.(Ref(res), value, out) end function _push_to_res!(res, value, out) @@ -47,6 +47,7 @@ function process_callback_call(app, callback_id, outputs, inputs, state) _push_to_res!(response, res_vector, outputs) + if length(response) == 0 throw(PreventUpdate()) end @@ -59,7 +60,7 @@ function process_callback(request::HTTP.Request, state::HandlerState) app = state.app response = HTTP.Response(200, ["Content-Type" => "application/json"]) - params = JSON2.read(String(request.body)) + params = JSON3.read(String(request.body)) inputs = get(params, :inputs, []) state = get(params, :state, []) output = Symbol(params[:output]) @@ -74,7 +75,7 @@ function process_callback(request::HTTP.Request, state::HandlerState) cb_result = with_callback_context(context) do process_callback_call(app, output, outputs_list, inputs, state) end - response.body = Vector{UInt8}(JSON2.write(cb_result)) + response.body = Vector{UInt8}(JSON3.write(cb_result)) catch e if isa(e,PreventUpdate) return HTTP.Response(204) diff --git a/src/handler/processors/layout.jl b/src/handler/processors/layout.jl index 584bbfb..bca65ab 100644 --- a/src/handler/processors/layout.jl +++ b/src/handler/processors/layout.jl @@ -4,6 +4,6 @@ function process_layout(request::HTTP.Request, state::HandlerState) return HTTP.Response( 200, ["Content-Type" => "application/json"], - body = JSON2.write(layout_data(state.app.layout)) + body = JSON3.write(layout_data(state.app.layout)) ) end \ No newline at end of file diff --git a/src/handler/processors/reload_hash.jl b/src/handler/processors/reload_hash.jl index 45fd5d6..38952c1 100644 --- a/src/handler/processors/reload_hash.jl +++ b/src/handler/processors/reload_hash.jl @@ -4,9 +4,9 @@ function process_reload_hash(request::HTTP.Request, state::HandlerState) hard = state.reload.hard, packages = keys(state.cache.resources.files), files = state.reload.changed_assets - ) + ) state.reload.hard = false state.reload.changed_assets = [] - return HTTP.Response(200, ["Content-Type" => "application/json"], body = JSON2.write(reload_tuple)) + return HTTP.Response(200, ["Content-Type" => "application/json"], body = JSON3.write(reload_tuple)) end \ No newline at end of file diff --git a/src/handler/state.jl b/src/handler/state.jl index e25ba5e..0e033e3 100644 --- a/src/handler/state.jl +++ b/src/handler/state.jl @@ -31,7 +31,7 @@ function _dependencies_json(app::DashApp) prevent_initial_call = callback.prevent_initial_call ) end - return JSON2.write(result) + return JSON3.write(result) end function _cache_tuple(app::DashApp, registry::ResourcesRegistry) diff --git a/src/plotly_base.jl b/src/plotly_base.jl new file mode 100644 index 0000000..9f89483 --- /dev/null +++ b/src/plotly_base.jl @@ -0,0 +1,8 @@ +import PlotlyBase +import JSON + +function DashBase.to_dash(p::PlotlyBase.Plot) + data = JSON.lower(p) + pop!(data, :config, nothing) + return data +end \ No newline at end of file diff --git a/src/utils/misc.jl b/src/utils/misc.jl index 7e41f27..585353d 100644 --- a/src/utils/misc.jl +++ b/src/utils/misc.jl @@ -10,7 +10,7 @@ function format_tag(name ::String, attributes::Dict{String, String}, inner::Stri tag *= "/>" elseif opened tag *= ">" - else + else tag *= ">$inner" end end @@ -32,7 +32,7 @@ function validate_index(name::AbstractString, index::AbstractString, checks::Vec string( "Missing item", (length(missings)>1 ? "s" : ""), " ", join(getindex.(missings, 1), ", "), - " in ", name + " in ", name ) ) end @@ -43,26 +43,26 @@ macro var_str(s) end function parse_props(s) - function make_prop(part) + function make_prop(part) m = match(r"^(?[A-Za-z]+[\w\-\:\.]*)\.(?[A-Za-z]+[\w\-\:\.]*)$", strip(part)) if isnothing(m) error("expected .[,....] in $(part)") end - + return (Symbol(m[:id]), Symbol(m[:prop])) - end + end props_parts = split(s, ",", keepempty = false) - + return map(props_parts) do part return make_prop(part) - end + end end function generate_hash() - return strip(string(UUIDs.uuid4()), '-') + return strip(string(UUIDs.uuid4()), '-') end -sort_by_keys(data::NamedTuple) = (;sort!(collect(pairs(data)), by = (x)->x[1])...,) +sort_by_keys(data) = (;sort!(collect(pairs(data)), by = (x)->x[1])...,) -sorted_json(data::NamedTuple) = JSON2.write(sort_by_keys(data)) \ No newline at end of file +sorted_json(data) = JSON3.write(sort_by_keys(data)) \ No newline at end of file diff --git a/test/callbacks.jl b/test/callbacks.jl index e92dcb9..be66eb8 100644 --- a/test/callbacks.jl +++ b/test/callbacks.jl @@ -1,4 +1,4 @@ -import HTTP, JSON2 +import HTTP, JSON3 using Test using Dash @@ -22,7 +22,7 @@ using Dash handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") resp = HTTP.handle(handler, request) - deps = JSON2.read(String(resp.body)) + deps = JSON3.read(String(resp.body)) @test deps[1].prevent_initial_call == false @test deps[2].prevent_initial_call == false @@ -44,7 +44,7 @@ using Dash handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") resp = HTTP.handle(handler, request) - deps = JSON2.read(String(resp.body)) + deps = JSON3.read(String(resp.body)) @test deps[1].prevent_initial_call == true @test deps[2].prevent_initial_call == false @@ -66,7 +66,7 @@ using Dash handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") resp = HTTP.handle(handler, request) - deps = JSON2.read(String(resp.body)) + deps = JSON3.read(String(resp.body)) @test deps[1].prevent_initial_call == true @test deps[2].prevent_initial_call == true @@ -88,7 +88,7 @@ using Dash handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") resp = HTTP.handle(handler, request) - deps = JSON2.read(String(resp.body)) + deps = JSON3.read(String(resp.body)) @test deps[1].prevent_initial_call == true @test deps[2].prevent_initial_call == false @@ -111,7 +111,7 @@ end handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") resp = HTTP.handle(handler, request) - deps = JSON2.read(String(resp.body)) + deps = JSON3.read(String(resp.body)) @test length(deps) == 1 cb = deps[1] @@ -126,7 +126,7 @@ end request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) response = HTTP.handle(handler, request) @test response.status == 200 - resp_obj = JSON2.read(String(response.body)) + resp_obj = JSON3.read(String(response.body)) @test in(:multi, keys(resp_obj)) @test resp_obj.response.var"my-div".children == "test" @@ -151,7 +151,7 @@ end request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) response = HTTP.handle(handler, request) @test response.status == 200 - resp_obj = JSON2.read(String(response.body)) + resp_obj = JSON3.read(String(response.body)) @test in(:multi, keys(resp_obj)) @test resp_obj.response[Symbol("my-div")].children == "test" @test resp_obj.response[Symbol("my-div2")].children == "state" @@ -174,7 +174,7 @@ end request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) response = HTTP.handle(handler, request) @test response.status == 200 - resp_obj = JSON2.read(String(response.body)) + resp_obj = JSON3.read(String(response.body)) @test in(:multi, keys(resp_obj)) @test resp_obj.response[Symbol("my-div")].children == "test" @@ -329,7 +329,7 @@ end (id = "test-in", property = "value", value = "test") ] ) - test_json = JSON2.write(request) + test_json = JSON3.write(request) request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) response = HTTP.handle(handler, request) @@ -368,12 +368,12 @@ end (id = (index=2, type="test"), property = "value", value = "test") ] ) - test_json = JSON2.write(request) + test_json = JSON3.write(request) request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) response = HTTP.handle(handler, request) @test response.status == 200 - resp_obj = JSON2.read(String(response.body)) + resp_obj = JSON3.read(String(response.body)) @test in(:multi, keys(resp_obj)) @test resp_obj.response.var"{\"index\":1,\"type\":\"test_out\"}".children == "test" @@ -420,11 +420,12 @@ end ] ] ) - test_json = JSON2.write(request) + test_json = JSON3.write(request) request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) response = HTTP.handle(handler, request) @test response.status == 200 - resp_obj = JSON2.read(String(response.body)) + s = String(response.body) + resp_obj = JSON3.read(s) @test in(:multi, keys(resp_obj)) @test resp_obj.response.var"{\"index\":1,\"type\":\"test-out\"}".children =="test 1" @@ -472,11 +473,11 @@ end ] ] ) - test_json = JSON2.write(request) + test_json = JSON3.write(request) request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) response = HTTP.handle(handler, request) @test response.status == 200 - resp_obj = JSON2.read(String(response.body)) + resp_obj = JSON3.read(String(response.body)) @test in(:multi, keys(resp_obj)) @test resp_obj.response.var"{\"index\":1,\"type\":\"test-out\"}".children =="test 1" @@ -578,7 +579,7 @@ end handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") resp = HTTP.handle(handler, request) - deps = JSON2.read(String(resp.body)) + deps = JSON3.read(String(resp.body)) @test length(deps) == 1 cb = deps[1] @@ -621,7 +622,7 @@ end handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") resp = HTTP.handle(handler, request) - deps = JSON2.read(String(resp.body)) + deps = JSON3.read(String(resp.body)) @test length(deps) == 1 cb = deps[1] diff --git a/test/core.jl b/test/core.jl index d6d4d9a..00d4689 100644 --- a/test/core.jl +++ b/test/core.jl @@ -1,4 +1,4 @@ -import HTTP, JSON2 +import HTTP, JSON3 using Test using DashBase using Dash @@ -55,15 +55,15 @@ end response = HTTP.handle(handler, request) @test response.status == 200 body_str = String(response.body) - @test body_str == JSON2.write(app.layout) + @test body_str == JSON3.write(app.layout) request = HTTP.Request("GET", "/_dash-dependencies") response = HTTP.handle(handler, request) @test response.status == 200 body_str = String(response.body) - resp_json = JSON2.read(body_str) + resp_json = JSON3.read(body_str) - @test resp_json isa Vector + @test resp_json isa AbstractVector @test length(resp_json) == 2 @test haskey(resp_json[1], :inputs) @test haskey(resp_json[1], :state) @@ -100,14 +100,14 @@ end response = HTTP.handle(handler, request) @test response.status == 200 body_str = String(response.body) - @test body_str == JSON2.write(layout_func()) + @test body_str == JSON3.write(layout_func()) @test occursin("my_div2", body_str) global_id = "my_div3" response = HTTP.handle(handler, request) @test response.status == 200 body_str = String(response.body) - @test body_str == JSON2.write(layout_func()) + @test body_str == JSON3.write(layout_func()) @test occursin("my_div3", body_str) end @@ -168,7 +168,7 @@ end response = HTTP.handle(handler, request) @test response.status == 200 - result = JSON2.read(String(response.body)) + result = JSON3.read(String(response.body)) @test length(result[:response]) == 1 @test haskey(result[:response], Symbol("my-div2")) diff --git a/test/dev.jl b/test/dev.jl index 0aab9f3..3e0d4c7 100644 --- a/test/dev.jl +++ b/test/dev.jl @@ -5,7 +5,7 @@ after the overall architecture emerges, I disable dev.jl and start writing full- =# using Test using Dash -using JSON2 +using JSON3 using HTTP @testset "dev" begin r = Dash.load_meta("dash_core_components") diff --git a/test/integration/base/jl_plotly_graph/jlpg001_plotly_graph.jl b/test/integration/base/jl_plotly_graph/jlpg001_plotly_graph.jl new file mode 100644 index 0000000..244b64e --- /dev/null +++ b/test/integration/base/jl_plotly_graph/jlpg001_plotly_graph.jl @@ -0,0 +1,20 @@ +using Dash +using PlotlyBase +app = dash() +app.layout = html_div() do + dcc_graph(id = "graph", + figure = Plot(scatter(x=1:10, y = 1:10)) + ), + html_button("draw", id = "draw"), + html_div("", id = "status") +end + +callback!(app, Output("graph", "figure"), Output("status", "children"), Input("draw", "n_clicks")) do nclicks + plot = isnothing(nclicks) ? + no_update() : + Plot([scatter(x=1:10, y = 1:10), scatter(x=1:10, y = 1:2:20)]) + status = isnothing(nclicks) ? "first" : "second" + return (plot, status) +end + +run_server(app) \ No newline at end of file diff --git a/test/integration/base/test_plotly_graph.py b/test/integration/base/test_plotly_graph.py new file mode 100644 index 0000000..a6529e9 --- /dev/null +++ b/test/integration/base/test_plotly_graph.py @@ -0,0 +1,27 @@ +import pathlib +import os.path +import logging +logger = logging.getLogger(__name__) + +curr_path = pathlib.Path(__file__).parent.absolute() +def jl_test_file_path(filename): + return os.path.join(curr_path, "jl_plotly_graph", filename) + + + + +def test_jlpg001_plotly_graph(dashjl): + fp = jl_test_file_path("jlpg001_plotly_graph.jl") + dashjl.start_server(fp) + dashjl.wait_for_element_by_css_selector( + "#graph", timeout=20 + ) + + dashjl.wait_for_text_to_equal("#status", "first", timeout=10) + + dashjl.percy_snapshot(name="PlotlyBase figure layout") + + dashjl.find_element("#draw").click() + dashjl.wait_for_text_to_equal("#status", "second", timeout=10) + + dashjl.percy_snapshot(name="PlotlyBase figure callback") \ No newline at end of file diff --git a/test/misc.jl b/test/misc.jl index 406da80..4aa116f 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -1,22 +1,22 @@ -import HTTP, JSON2 +import HTTP, JSON3 using Test using Dash @testset "deps to json" begin - @test JSON2.write(ALL) == "[\"ALL\"]" - @test JSON2.write(MATCH) == "[\"MATCH\"]" - @test JSON2.write(ALLSMALLER) == "[\"ALLSMALLER\"]" + @test JSON3.write(ALL) == "[\"ALL\"]" + @test JSON3.write(MATCH) == "[\"MATCH\"]" + @test JSON3.write(ALLSMALLER) == "[\"ALLSMALLER\"]" test_dep = Input((type = "test", index = MATCH), "value") - @test JSON2.write(test_dep) == """{"id":{"type":"test","index":["MATCH"]},"property":"value"}""" + @test JSON3.write(test_dep) == """{"id":{"type":"test","index":["MATCH"]},"property":"value"}""" end @testset "json keys sorted" begin test_dep = (type = "test", index = 1) test_dep2 = (index = 1, type = "test") test_json = Dash.sorted_json(test_dep) - test_json2 = Dash.sorted_json(test_dep2) - @test test_json == test_json2 + test_JSON3 = Dash.sorted_json(test_dep2) + @test test_json == test_JSON3 @test test_json == """{"index":1,"type":"test"}""" end @@ -35,9 +35,9 @@ end @test Input((a=1, b=2), "c") == Output((a=1, b=2), "c") @test Input((a=1, b=2), "c") != Input((a=1, b=2), "e") - @test Input((a=1, b=2), "c") != Output((a=1, e=2), "c") + @test Input((a=1, b=2), "c") != Output((a=1, e=2), "c") - @test Input((a=1, b=2), "c") != Output((a=1, b=3), "c") + @test Input((a=1, b=2), "c") != Output((a=1, b=3), "c") @test Input((a=ALL, b=2), "c") == Output((a=1, b=2), "c") @test Input((a=ALL, b=3), "c") != Output((a=1, b=2), "c") @@ -50,7 +50,7 @@ end @test Input((a=ALL, b=2), "c") in [Output((a=1, b="ssss"), "c"), Output((a=1, b=2), "c")] @test isequal(Output((a = ALL, b=2), "c"), Output((a = 1, b=2), "c")) - @test Output((a = ALL, b=2), "c") in [Output((a = 1, b=2), "c"), Output((a=2, b=2), "c")] + @test Output((a = ALL, b=2), "c") in [Output((a = 1, b=2), "c"), Output((a=2, b=2), "c")] test = [Output((a = ALL, b=2), "c"), Output((a=2, b=2), "c"), Output((a = 1, b=2), "c")] @test !Dash.check_unique(test)