Skip to content

Commit 9acf95f

Browse files
authored
Merge pull request #46 from plotly/callback_context
Callback context
2 parents 68bec5f + eb51d4b commit 9acf95f

40 files changed

+737
-456
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- run:
2929
name: 🔎 Unit tests
3030
command: |
31-
julia -e 'using Pkg; Pkg.update(); Pkg.add(PackageSpec(url="https://github.com/plotly/Dash.jl.git", rev=ENV["CIRCLE_BRANCH"])); Pkg.add(PackageSpec(url="https://github.com/waralex/dash-html-components.git", rev="jl_generator_test")); Pkg.add(PackageSpec(url="https://github.com/waralex/dash-core-components.git", rev="jl_generator_test")); Pkg.build("Dash"); Pkg.build("DashHtmlComponents"); Pkg.build("DashCoreComponents"); Pkg.test("Dash", coverage=true); function precompile_pkgs(); for pkg in collect(keys(Pkg.installed())); if !isdefined(Symbol(pkg), :Symbol) && pkg != "Compat.jl"; @info("Importing $(pkg)..."); try (@eval import $(Symbol(pkg))) catch end; end; end; end; precompile_pkgs()'
31+
julia test/ci_prepare.jl
3232
3333
- run:
3434
name: ⚙️ Integration tests

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ julia = "1.1"
3030
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3131

3232
[targets]
33-
test = ["Test"]
33+
test = ["Test"]

src/Contexts/Contexts.jl

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module Contexts
2+
3+
using DataStructures: Stack
4+
5+
export TaskContextStorage, with_context, has_context, get_context
6+
7+
mutable struct ContextItem{T}
8+
value::T
9+
ContextItem(value::T) where {T} = new{T}(value)
10+
end
11+
12+
struct TaskContextStorage
13+
storage::Dict{UInt64, Stack{ContextItem}}
14+
guard::ReentrantLock
15+
TaskContextStorage() = new(Dict{UInt64, Stack{ContextItem}}(), ReentrantLock())
16+
end
17+
18+
#threads are runing as tasks too, so this id also unique for different threads
19+
curr_task_id() = objectid(current_task())
20+
21+
#thread unsafe should be used under locks
22+
get_curr_stack!(context::TaskContextStorage) = get!(context.storage, curr_task_id(), Stack{ContextItem}())
23+
24+
25+
function Base.push!(context::TaskContextStorage, item::ContextItem)
26+
lock(context.guard) do
27+
push!(get_curr_stack!(context), item)
28+
end
29+
end
30+
31+
function Base.pop!(context::TaskContextStorage)
32+
return lock(context.guard) do
33+
return pop!(get_curr_stack!(context))
34+
end
35+
end
36+
37+
function Base.isempty(context::TaskContextStorage)
38+
return lock(context.guard) do
39+
return isempty(get_curr_stack!(context))
40+
end
41+
end
42+
43+
function with_context(f, context::TaskContextStorage, item::ContextItem)
44+
push!(context, item)
45+
result = f()
46+
pop!(context)
47+
return result
48+
end
49+
50+
with_context(f, context::TaskContextStorage, item) = with_context(f, context, ContextItem(item))
51+
52+
has_context(context::TaskContextStorage) = !isempty(context)
53+
54+
function get_context(context::TaskContextStorage)
55+
return lock(context.guard) do
56+
isempty(context) && throw(ArgumentError("context is not set"))
57+
return first(get_curr_stack!(context)).value
58+
end
59+
end
60+
61+
end

src/Dash.jl

Lines changed: 9 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
module Dash
22
import HTTP, JSON2, CodecZlib, MD5
33
using Sockets
4-
using MacroTools
54
const ROOT_PATH = realpath(joinpath(@__DIR__, ".."))
65
include("Components.jl")
76
include("Front.jl")
7+
include("HttpHelpers/HttpHelpers.jl")
88
import .Front
99
using .Components
10+
using .HttpHelpers
1011

1112
export dash, Component, Front, callback!,
1213
enable_dev_tools!, ClientsideFunction,
1314
run_server, PreventUpdate, no_update, @var_str,
14-
Input, Output, State, make_handler
15+
Input, Output, State, make_handler, callback_context
1516

16-
17-
18-
#ComponentPackages.@reg_components()
17+
include("Contexts/Contexts.jl")
1918
include("env.jl")
2019
include("utils.jl")
2120
include("app.jl")
2221
include("resources/registry.jl")
2322
include("resources/application.jl")
2423
include("handlers.jl")
24+
include("server.jl")
2525

2626
@doc """
2727
module Dash
@@ -47,10 +47,10 @@ app = dash(external_stylesheets=["https://codepen.io/chriddyp/pen/bWLwgP.css"])
4747
4848
end
4949
end
50-
callback!(app, callid"{graphTitle.type} graphTitle.value => outputID.children") do type, value
50+
callback!(app, Output("outputID", "children"), Input("graphTitle","value"), State("graphTitle","type")) do value, type
5151
"You've entered: '\$(value)' into a '\$(type)' input control"
5252
end
53-
callback!(app, callid"graphTitle.value => graph.figure") do value
53+
callback!(app, Output("graph", "figure"), Input("graphTitle", "value")) do value
5454
(
5555
data = [
5656
(x = [1,2,3], y = abs.(randn(3)), type="bar"),
@@ -59,108 +59,11 @@ callback!(app, callid"graphTitle.value => graph.figure") do value
5959
layout = (title = value,)
6060
)
6161
end
62-
handle = make_handler(app, debug = true)
63-
run_server(handle, HTTP.Sockets.localhost, 8050)
64-
```
65-
66-
""" Dashboards
67-
68-
69-
"""
70-
run_server(app::DashApp, host = HTTP.Sockets.localhost, port = 8050; debug::Bool = false)
71-
72-
Run Dash server
73-
74-
#Arguments
75-
- `app` - Dash application
76-
- `host` - host
77-
- `port` - port
78-
- `debug::Bool = false` - Enable/disable all the dev tools
79-
80-
#Examples
81-
```jldoctest
82-
julia> app = dash() do
83-
html_div() do
84-
html_h1("Test Dashboard")
85-
end
86-
end
87-
julia>
88-
julia> run_server(handler, HTTP.Sockets.localhost, 8050)
62+
run_server(app, HTTP.Sockets.localhost, 8050)
8963
```
9064
91-
"""
92-
function run_server(app::DashApp, host = HTTP.Sockets.localhost, port = 8050;
93-
debug = nothing,
94-
dev_tools_ui = nothing,
95-
dev_tools_props_check = nothing,
96-
dev_tools_serve_dev_bundles = nothing,
97-
dev_tools_hot_reload = nothing,
98-
dev_tools_hot_reload_interval = nothing,
99-
dev_tools_hot_reload_watch_interval = nothing,
100-
dev_tools_hot_reload_max_retry = nothing,
101-
dev_tools_silence_routes_logging = nothing,
102-
dev_tools_prune_errors = nothing
103-
)
104-
@env_default!(debug, Bool, false)
105-
enable_dev_tools!(app,
106-
debug = debug,
107-
dev_tools_ui = dev_tools_ui,
108-
dev_tools_props_check = dev_tools_props_check,
109-
dev_tools_serve_dev_bundles = dev_tools_serve_dev_bundles,
110-
dev_tools_hot_reload = dev_tools_hot_reload,
111-
dev_tools_hot_reload_interval = dev_tools_hot_reload_interval,
112-
dev_tools_hot_reload_watch_interval = dev_tools_hot_reload_watch_interval,
113-
dev_tools_hot_reload_max_retry = dev_tools_hot_reload_max_retry,
114-
dev_tools_silence_routes_logging = dev_tools_silence_routes_logging,
115-
dev_tools_prune_errors = dev_tools_prune_errors
116-
)
117-
main_func = () -> begin
118-
ccall(:jl_exit_on_sigint, Cvoid, (Cint,), 0)
119-
handler = make_handler(app);
120-
try
121-
task = @async HTTP.serve(handler, host, port)
122-
@info string("Running on http://", host, ":", port)
123-
wait(task)
124-
catch e
125-
if e isa InterruptException
126-
@info "exited"
127-
else
128-
rethrow(e)
129-
end
130-
131-
end
132-
end
133-
start_server = () -> begin
134-
handler = make_handler(app);
135-
server = Sockets.listen(get_inetaddr(host, port))
136-
task = @async HTTP.serve(handler, host, port; server = server)
137-
@info string("Running on http://", host, ":", port)
138-
return (server, task)
139-
end
65+
""" Dash
14066

141-
if get_devsetting(app, :hot_reload) && !is_hot_restart_available()
142-
@warn "Hot reloading is disabled for interactive sessions. Please run your app using julia from the command line to take advantage of this feature."
143-
end
14467

145-
if get_devsetting(app, :hot_reload) && is_hot_restart_available()
146-
hot_restart(start_server, check_interval = get_devsetting(app, :hot_reload_watch_interval))
147-
else
148-
(server, task) = start_server()
149-
try
150-
wait(task)
151-
println(task)
152-
catch e
153-
close(server)
154-
if e isa InterruptException
155-
println("finished")
156-
return
157-
else
158-
rethrow(e)
159-
end
160-
end
161-
end
162-
end
163-
get_inetaddr(host::String, port::Integer) = Sockets.InetAddr(parse(IPAddr, host), port)
164-
get_inetaddr(host::IPAddr, port::Integer) = Sockets.InetAddr(host, port)
16568

16669
end # module

src/HttpHelpers/HttpHelpers.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module HttpHelpers
2+
3+
export state_handler, compress_handler, Route, Router, add_route!
4+
5+
import HTTP, CodecZlib
6+
7+
include("handlers.jl")
8+
include("router.jl")
9+
10+
end
File renamed without changes.
File renamed without changes.

src/app/supporttypes.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ const Input = Dependency{TraitInput}
1111
const State = Dependency{TraitState}
1212
const Output = Dependency{TraitOutput}
1313

14-
const IdProp = Tuple{Symbol, Symbol}
15-
16-
1714

1815
struct CallbackDeps
1916
output ::Vector{Output}
@@ -36,6 +33,11 @@ struct Callback
3633
dependencies ::CallbackDeps
3734
end
3835

36+
is_multi_out(cb::Callback) = cb.dependencies.multi_out == true
37+
get_output(cb::Callback) = cb.dependencies.output
38+
get_output(cb::Callback, i) = cb.dependencies.output[i]
39+
first_output(cb::Callback) = first(cb.dependencies.output)
40+
3941
struct PreventUpdate <: Exception
4042

4143
end

src/handler/callback_context.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using .Contexts
2+
const CallbackContextItems = Union{Nothing, Vector{NamedTuple}}
3+
const TriggeredParam = NamedTuple{(:prop_id, :value)}
4+
mutable struct CallbackContext
5+
response::HTTP.Response
6+
inputs::Dict{String, Any}
7+
states::Dict{String, Any}
8+
outputs_list::Vector{Any}
9+
inputs_list::Vector{Any}
10+
states_list::Vector{Any}
11+
triggered::Vector{TriggeredParam}
12+
function CallbackContext(response, outputs_list, inputs_list, states_list, changed_props)
13+
input_values = inputs_list_to_dict(inputs_list)
14+
state_values = inputs_list_to_dict(states_list)
15+
triggered = TriggeredParam[(prop_id = id, value = input_values[id]) for id in changed_props]
16+
return new(response, input_values, state_values, outputs_list, inputs_list, states_list, triggered)
17+
end
18+
end
19+
20+
const _callback_context_storage = TaskContextStorage()
21+
22+
function with_callback_context(f, context::CallbackContext)
23+
return with_context(f, _callback_context_storage, context)
24+
end
25+
26+
"""
27+
callback_context()::CallbackContext
28+
29+
Get context of current callback, available only inside callback processing function
30+
"""
31+
function callback_context()
32+
!has_context(_callback_context_storage) && error("callback_context() is only available from a callback processing function")
33+
return get_context(_callback_context_storage)
34+
end
35+
36+
function inputs_list_to_dict(list::Vector{Any})
37+
result = Dict{String, Any}()
38+
_item_to_dict!.(Ref(result), list)
39+
return result
40+
end
41+
42+
function _item_to_dict!(target::Dict{String, Any}, item)
43+
target["$(item.id).$(item.property)"] = get(item, :value, nothing)
44+
end
45+
46+
_item_to_dict!(target::Dict{String, Any}, item::Vector) = _item_to_dict!.(Ref(target), item)

0 commit comments

Comments
 (0)