From 37788b249dea32af223acdc402d936a6d2b93d28 Mon Sep 17 00:00:00 2001 From: Stephan Sahm Date: Mon, 12 Aug 2024 11:56:04 +0200 Subject: [PATCH] a couple of cleanups --- Project.toml | 6 +- ext/AWSExt.jl | 95 ------------------------------ ext/PlotsExt.jl | 34 +++++------ ext/PythonCallExt.jl | 8 ++- ext/RCallExt.jl | 5 +- src/JolinPluto.jl | 8 +-- src/authorize.jl | 121 --------------------------------------- src/frontend.jl | 45 +++------------ src/languages.jl | 64 +++++---------------- src/plutohooks_basics.jl | 61 +++----------------- src/possibly_useful.jl | 84 --------------------------- src/setter.jl | 13 +++-- src/tasks.jl | 10 ++-- src/viewof.jl | 46 +++++++++++++++ 14 files changed, 120 insertions(+), 480 deletions(-) delete mode 100644 ext/AWSExt.jl delete mode 100644 src/authorize.jl delete mode 100644 src/possibly_useful.jl create mode 100644 src/viewof.jl diff --git a/Project.toml b/Project.toml index 1af2fcf..96c21a5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "JolinPluto" uuid = "5b0b4ef8-f4e6-4363-b674-3f031f7b9530" authors = ["Stephan Sahm and contributors"] -version = "0.1.79" +version = "0.1.80" [deps] AbstractPlutoDingetjes = "6e696c72-6542-2067-7265-42206c756150" @@ -18,15 +18,11 @@ JWTs = "d850fbd6-035d-5a70-a269-1ca2e636ac6c" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [weakdeps] -AWS = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" RCall = "6f49c342-dc21-5d91-9882-a32aef131414" CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" [extensions] -AWSExt = "AWS" -PlotsExt = "Plots" PythonCallExt = "PythonCall" RCallExt = ["RCall", "CondaPkg"] diff --git a/ext/AWSExt.jl b/ext/AWSExt.jl deleted file mode 100644 index a911037..0000000 --- a/ext/AWSExt.jl +++ /dev/null @@ -1,95 +0,0 @@ -module AWSExt -import JolinPluto, AWS -using Dates - -# this way works to overload a macro https://github.com/JuliaLang/julia/issues/15838 -import JolinPluto.@authorize_aws -import JolinPluto.authorize_aws - - -macro authorize_aws(args...) - mydateformat = Dates.dateformat"yyyymmdd\THHMMSS\Z" - # we define a function in a macro, so that we can use @get_jwt macro (which needs the location) - # as well as use `renew`` argument, which requires a function - @gensym _authorize_aws - esc(quote - function $_authorize_aws(role_arn; audience="", role_session::Union{AbstractString,Nothing}=nothing) - if isnothing(role_session) - role_session = $AWS._role_session_name( - "jolincloud-role-", - basename(role_arn), - "-" * $Dates.format($Dates.now($Dates.UTC), $mydateformat), - ) - end - # we need to be cautious that @get_jwt is called with the same __source__ - web_identity = $(Expr(:macrocall, JolinPluto.var"@get_jwt", __source__, :audience)) - - response = $AWS.AWSServices.sts( - "AssumeRoleWithWebIdentity", - Dict( - "RoleArn" => role_arn, - "RoleSessionName" => role_session, # Required by AssumeRoleWithWebIdentity - "WebIdentityToken" => web_identity, - ); - aws_config=$AWS.AWSConfig(; creds=nothing), - feature_set=$AWS.FeatureSet(; use_response_type=true), - ) - dict = $JolinPluto.parse(response) - role_creds = dict["AssumeRoleWithWebIdentityResult"]["Credentials"] - assumed_role_user = dict["AssumeRoleWithWebIdentityResult"]["AssumedRoleUser"] - - return $AWS.global_aws_config(creds=$AWS.AWSCredentials( - role_creds["AccessKeyId"], - role_creds["SecretAccessKey"], - role_creds["SessionToken"], - assumed_role_user["Arn"]; - expiry=$Dates.DateTime(rstrip(role_creds["Expiration"], 'Z')), - renew=() -> $_authorize_aws(role_arn; audience, role_session).credentials, - )) - end - $(Expr(:call, _authorize_aws, args...)) - end) -end - -function authorize_aws(args...) - mydateformat = Dates.dateformat"yyyymmdd\THHMMSS\Z" - # we define a function in a macro, so that we can use @get_jwt macro (which needs the location) - # as well as use `renew`` argument, which requires a function - function _authorize_aws(role_arn; audience="", role_session::Union{AbstractString,Nothing}=nothing) - if isnothing(role_session) - role_session = AWS._role_session_name( - "jolincloud-role-", - basename(role_arn), - "-" * Dates.format(Dates.now(Dates.UTC), mydateformat), - ) - end - # we need to be cautious that @get_jwt is called with the same __source__ - web_identity = JolinPluto.get_jwt(audience) - - response = AWS.AWSServices.sts( - "AssumeRoleWithWebIdentity", - Dict( - "RoleArn" => role_arn, - "RoleSessionName" => role_session, # Required by AssumeRoleWithWebIdentity - "WebIdentityToken" => web_identity, - ); - aws_config=AWS.AWSConfig(; creds=nothing), - feature_set=AWS.FeatureSet(; use_response_type=true), - ) - dict = JolinPluto.parse(response) - role_creds = dict["AssumeRoleWithWebIdentityResult"]["Credentials"] - assumed_role_user = dict["AssumeRoleWithWebIdentityResult"]["AssumedRoleUser"] - - return AWS.global_aws_config(creds=AWS.AWSCredentials( - role_creds["AccessKeyId"], - role_creds["SecretAccessKey"], - role_creds["SessionToken"], - assumed_role_user["Arn"]; - expiry=Dates.DateTime(rstrip(role_creds["Expiration"], 'Z')), - renew=() -> _authorize_aws(role_arn; audience, role_session).credentials, - )) - end - _authorize_aws(args...) -end - -end # module \ No newline at end of file diff --git a/ext/PlotsExt.jl b/ext/PlotsExt.jl index 9522130..285d3dd 100644 --- a/ext/PlotsExt.jl +++ b/ext/PlotsExt.jl @@ -1,21 +1,23 @@ module PlotsExt import JolinPluto, Plots -# little helper to support plotly responsiveness -# see this issue for updates https://github.com/JuliaPlots/Plots.jl/issues/4775 -function JolinPluto.plotly_responsive(plt=Plots.current()) - HTML("
" * replace( - Plots.embeddable_html(plt), - # adapt margin as it interfers with responsive - r"\"margin\": \{[^\}]*\},"s => """ - "margin": {"l": 40,"b": 40,"r": 0,"t": 22},""", - # delete extra outer style attribute - not needed at all - r"style=\"[^\"]*\""s => "", - # delete layout width as this interfers with responsiveness - r"\"width\":[^,}]*"s => "", - # add extra config json at the end of the call to Plotly.newPlot - ");" => ", {\"responsive\": true});" - ) * "
") -end +# TODO use PlutoPlotly.jl instead + +# # little helper to support plotly responsiveness +# # see this issue for updates https://github.com/JuliaPlots/Plots.jl/issues/4775 +# function JolinPluto.plotly_responsive(plt=Plots.current()) +# HTML("
" * replace( +# Plots.embeddable_html(plt), +# # adapt margin as it interfers with responsive +# r"\"margin\": \{[^\}]*\},"s => """ +# "margin": {"l": 40,"b": 40,"r": 0,"t": 22},""", +# # delete extra outer style attribute - not needed at all +# r"style=\"[^\"]*\""s => "", +# # delete layout width as this interfers with responsiveness +# r"\"width\":[^,}]*"s => "", +# # add extra config json at the end of the call to Plotly.newPlot +# ");" => ", {\"responsive\": true});" +# ) * "
") +# end end diff --git a/ext/PythonCallExt.jl b/ext/PythonCallExt.jl index 98b843e..d5d68ae 100644 --- a/ext/PythonCallExt.jl +++ b/ext/PythonCallExt.jl @@ -17,7 +17,7 @@ end function JolinPluto.start_python_thread(func) threading = @pyconst(pyimport("threading")) - if !JolinPluto.is_running_in_pluto_process() + if !JolinPluto.is_running_in_jolinpluto_process() # just start a plain thread without cleanup stop_event = threading.Event() threading.Thread(target=func, daemon=true, args=(stop_event,)).start() @@ -58,8 +58,12 @@ end pyglobals() = get!(PythonCall.pydict, PythonCall.Core.MODULE_GLOBALS, Main) JolinPluto.lang_enabled(::Val{:py}) = true -function JolinPluto.lang_copy_bind(::Val{:py}, def, value) +function JolinPluto.lang_set_global(::Val{:py}, def, value) pyglobals()[string(def)] = value end +function JolinPluto.lang_get_global(::Val{:py}, def) + pyglobals()[string(def)] +end + end \ No newline at end of file diff --git a/ext/RCallExt.jl b/ext/RCallExt.jl index 054aec2..460e873 100644 --- a/ext/RCallExt.jl +++ b/ext/RCallExt.jl @@ -16,9 +16,12 @@ end # c(MD, HTML, format_html, viewof) %<-% julia_eval("Jolin.MD, Jolin._HTML, Jolin.format_html, Jolin.viewof") JolinPluto.lang_enabled(::Val{:r}) = true -function JolinPluto.lang_copy_bind(::Val{:r}, def::Symbol, value) +function JolinPluto.lang_set_global(::Val{:r}, def::Symbol, value) RCall.Const.GlobalEnv[def] = value end +function JolinPluto.lang_get_global(::Val{:r}, def::Symbol) + RCall.Const.GlobalEnv[def] +end function __init__() # this is crucial so that the path is set correctly diff --git a/src/JolinPluto.jl b/src/JolinPluto.jl index ddb6a03..fbc407a 100644 --- a/src/JolinPluto.jl +++ b/src/JolinPluto.jl @@ -1,11 +1,7 @@ module JolinPluto -# we use macros for everything, releasing mental load -export @get_jwt, @authorize_aws -export get_jwt, authorize_aws export @repeat_take!, @repeat_at, @repeat_run, @Channel export repeat_take!, repeat_take, repeat_at, repeat_run, ChannelPluto, repeat_queueget, ChannelWithRepeatedFill, NoPut, start_python_thread -export @output_below, @clipboard_image_to_clipboard_html export output_below, clipboard_image_to_clipboard_html, embedLargeHTML, plotly_responsive export Setter, @get, @cell_ids_create_wrapper, @cell_ids_push! export cell_ids_create_wrapper, cell_ids_push!, cell_ids_push @@ -17,10 +13,10 @@ using HypertextLiteral, Continuables import AbstractPlutoDingetjes include("plutohooks_basics.jl") -include("authorize.jl") +include("viewof.jl") include("tasks.jl") -include("frontend.jl") include("setter.jl") +include("frontend.jl") include("languages.jl") end # module diff --git a/src/authorize.jl b/src/authorize.jl deleted file mode 100644 index 877f4ae..0000000 --- a/src/authorize.jl +++ /dev/null @@ -1,121 +0,0 @@ -""" - @get_jwt - @get_jwt "exampleaudience" - -Creates a JSON Web Token which can be used for authentication at common cloud providers. - -On cloud.jolin.io the token will be issued and signed by cloud.jolin.io, -on Github Actions (used for automated tests), a respective github token is returned. -""" -macro get_jwt(audience="") - # Jolin Cloud - if parse(Bool, get(ENV, "JOLIN_CLOUD", "false")) - serviceaccount_token = readchomp("/var/run/secrets/kubernetes.io/serviceaccount/token") - path = split(String(__source__.file),"#==#")[1] - project_dir = readchomp(`$(git()) -C $(dirname(path)) rev-parse --show-toplevel`) - @assert startswith(path, project_dir) "invalid workflow location" - workflowpath = path[length(project_dir)+2:end] - quote - response = $HTTP.get("http://jolin-workspace-server-jwts.jolin-workspace-server/request_jwt", - query=["serviceaccount_token" => $serviceaccount_token, - "workflowpath" => $workflowpath, - "audience" => $(esc(audience))]) - $JSON3.read(response.body).token - end - # Github Actions - elseif (parse(Bool, get(ENV, "CI", "false")) - && haskey(ENV, "ACTIONS_ID_TOKEN_REQUEST_TOKEN") - && haskey(ENV, "ACTIONS_ID_TOKEN_REQUEST_URL")) - quote - response = $HTTP.get($(ENV["ACTIONS_ID_TOKEN_REQUEST_URL"]), - query=["audience" => $(esc(audience))], - headers=["Authorization" => "bearer " * $(ENV["ACTIONS_ID_TOKEN_REQUEST_TOKEN"])]) - # the token is in subfield value https://blog.alexellis.io/deploy-without-credentials-using-oidc-and-github-actions/ - $JSON3.read(response.body).value - end - # Fallback with Dummy Value - else - quote - payload = Dict( - "iss" => "http://www.example.com/", - "sub" => "/env/YOUR_ENV/github.com/YOUR_ORGANIZATION/YOUR_REPO/PATH/TO/WORKFLOW", - "aud" => $(esc(audience)), - "exp" => 1536080651, - "iat" => 1535994251, - ) - # for details see https://github.com/tanmaykm/JWTs.jl/issues/22 - ".$(JWT(; payload))." - end - end -end - - -""" - get_jwt() - get_jwt("exampleaudience") - -Creates a JSON Web Token which can be used for authentication at common cloud providers. - -On cloud.jolin.io the token will be issued and signed by cloud.jolin.io, -on Github Actions (used for automated tests), a respective github token is returned. -""" -function get_jwt(audience="") - # Jolin Cloud - if parse(Bool, get(ENV, "JOLIN_CLOUD", "false")) - serviceaccount_token = readchomp("/var/run/secrets/kubernetes.io/serviceaccount/token") - path = Main.PlutoRunner.notebook_path[] - project_dir = readchomp(`$(git()) -C $(dirname(path)) rev-parse --show-toplevel`) - @assert startswith(path, project_dir) "invalid workflow location" - workflowpath = path[length(project_dir)+2:end] - - response = HTTP.get("http://jolin-workspace-server-jwts.jolin-workspace-server/request_jwt", - query=["serviceaccount_token" => serviceaccount_token, - "workflowpath" => workflowpath, - "audience" => audience]) - JSON3.read(response.body).token - # Github Actions - elseif (parse(Bool, get(ENV, "CI", "false")) - && haskey(ENV, "ACTIONS_ID_TOKEN_REQUEST_TOKEN") - && haskey(ENV, "ACTIONS_ID_TOKEN_REQUEST_URL")) - response = HTTP.get(ENV["ACTIONS_ID_TOKEN_REQUEST_URL"], - query=["audience" => audience], - headers=["Authorization" => "bearer " * ENV["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]]) - # the token is in subfield value https://blog.alexellis.io/deploy-without-credentials-using-oidc-and-github-actions/ - JSON3.read(response.body).value - # Fallback with Dummy Value - else - payload = Dict( - "iss" => "http://www.example.com/", - "sub" => "/env/YOUR_ENV/github.com/YOUR_ORGANIZATION/YOUR_REPO/PATH/TO/WORKFLOW", - "aud" => audience, - "exp" => 1536080651, - "iat" => 1535994251, - ) - # for details see https://github.com/tanmaykm/JWTs.jl/issues/22 - jwt = JWT(; payload) - ".$jwt." - end -end - - -""" - @authorize_aws(role_arn; audience="") - -Assume role via web identity. How to define such a role can be found here -https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc - -CAUTION: Please note that the semicolon is really important! `@authorize_aws(role_arn, audience="myaudience")` won't work as of now. -""" -macro authorize_aws end - -""" - authorize_aws(role_arn; audience="") - -Assume role via web identity. How to define such a role can be found here -https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc - -CAUTION: Please note that the semicolon is really important! `@authorize_aws(role_arn, audience="myaudience")` won't work as of now. -""" -function authorize_aws end - -# TODO add Azure, Google Cloud and HashiCorp diff --git a/src/frontend.jl b/src/frontend.jl index 860ace5..fe8d666 100644 --- a/src/frontend.jl +++ b/src/frontend.jl @@ -1,15 +1,5 @@ using EzXML - -""" - @output_below - -Reverse input output, first input then output. When removing the cell with -`@output_below`, the order is reversed again. -""" -macro output_below() - result = output_below() - QuoteNode(result) -end +import AbstractPlutoDingetjes """ output_below() @@ -44,16 +34,6 @@ output_below() = @htl """ """ -""" - @clipboard_image_to_clipboard_html - -Creates a little textfield where you can paste images. These images are then transformed -to self-containing html img tags and copied back to the clipboard to be entered -somewhere in Pluto. -""" -macro clipboard_image_to_clipboard_html() - QuoteNode(clipboard_image_to_clipboard_html()) -end """ clipboard_image_to_clipboard_html() @@ -117,20 +97,6 @@ function embedLargeHTML(rawpagedata; kwargs...) end -""" - plotly_responsive() - plotly_responsive(plot_object) - -IMPORTANT: Works only if `plotly()` backend is activated - -Makes the plotly plot responsive and returns the new plot. -""" -function plotly_responsive end # See this issue for updates https://github.com/JuliaPlots/Plots.jl/issues/4775 - - - - - # We build an individual CommonMark parser with a special inline `<> ... ` component. import CommonMark @@ -166,6 +132,9 @@ CommonMark.enable!(_MD_parser, HtmlFragmentInlineRule()) """ MD("# Markdown String") + +Markdown parser with special support for inline html fenced by `<>...`. +As `format_html` returns single line html, this makes it possible to interpolate arbitrary html into markdown tables. """ function MD(args...; kwargs...) _MD_parser(args...; kwargs...) @@ -174,7 +143,11 @@ end # RCall's calling syntax does not support arbitrary types, but is good with functions """ - HTML("

HTML String

") + _HTML("

HTML String

") + +Just like HTML, but a function. + +Background: RCall's calling syntax does not support arbitrary types, but is good with functions. """ function _HTML(args...; kwargs...) HTML(args...; kwargs...) diff --git a/src/languages.jl b/src/languages.jl index 6ffb968..c652ec5 100644 --- a/src/languages.jl +++ b/src/languages.jl @@ -1,60 +1,22 @@ # common things for both R and Python -lang_enabled(lang) = false -function lang_copy_bind end - -function copy_bind_to_registered_languages(def::Symbol, value) - lang_enabled(Val{:py}()) && lang_copy_bind(Val{:py}(), def, value) - lang_enabled(Val{:r}()) && lang_copy_bind(Val{:r}(), def, value) -end - """ -```julia -viewof(:symbol, element) -viewof("symbol", element) -``` + lang_enabled(Val(:py)) -Return the HTML `element`, and use its latest JavaScript value as the definition of `symbol`. +Checks whether the language support is activated. +""" +lang_enabled(lang) = false -# Example +""" + lang_set_global(Val(:py), symbol, value) -```julia -viewof(:x, html"") -``` -and in another cell: -```julia -x^2 -``` +Sets the given variable on the respective language side. +""" +function lang_set_global end -The first cell will show a slider as the cell's output, ranging from 0 until 100. -The second cell will show the square of `x`, and is updated in real-time as the slider is moved. """ -function viewof(def, ui) - if !isa(def, Symbol) - throw(ArgumentError("""\nMacro example usage: \n\n\t@bind my_number html""\n\n""")) - elseif !isdefined(Main, :PlutoRunner) - initial_value_getter = try - Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value - catch - b -> missing - end - initial_value = Core.applicable(Base.get, ui) ? Base.get(ui) : initial_value_getter(ui) - # setproperty(_module_where_plutoscript_is_included[], def, ) - # copy_bind_to_registered_languages(def, initial_value) - return initial_value, ui - else - Main.PlutoRunner.load_integrations_if_needed() - initial_value_getter = Main.PlutoRunner.initial_value_getter_ref[](ui) - initial_value = Core.applicable(Base.get, ui) ? Base.get(ui) : initial_value_getter(ui) - # setproperty!(Main.PlutoRunner.currently_running_module[], def, initial_value) - # copy_bind_to_registered_languages(def, initial_value) - return initial_value, Main.PlutoRunner.create_bond(ui, def, Main.PlutoRunner.currently_running_cell_id[]) - end -end + lang_get_global(Val(:py), symbol) -# for python and R especially -# (python strings are automatically transformed to Julia strings in JuliaCall when calling julia functions from python) -# (same for R strings) -function viewof(def::AbstractString, ui) - viewof(Symbol(def), ui) -end +Gets the given variable from the respective language side. +""" +function lang_get_global end diff --git a/src/plutohooks_basics.jl b/src/plutohooks_basics.jl index c2b300f..2b52ef0 100644 --- a/src/plutohooks_basics.jl +++ b/src/plutohooks_basics.jl @@ -12,61 +12,18 @@ function is_running_in_pluto_process() isdefined(Main.PlutoRunner, :GiveMeRegisterCleanupFunction) end - -Base.@kwdef struct NotRunningInPlutoCellException <: Exception end - -function Base.showerror(io::IO, expr::NotRunningInPlutoCellException) - print(io, "NotRunningInPlutoCell: Expected to run in a Pluto cell, but wasn't! We'll try to get these hooks to work transparently when switching from Pluto to a script.. but not yet, so just as a precaution: this error!") -end - - - """ - @give_me_the_pluto_cell_id() - -> ⚠️ Don't use this directly!! if you think you need it, you might actually need [`@use_did_deps_change([])`](@ref) but even that is unlikely. - -Used inside a Pluto cell this will resolve to the current cell UUID. -Outside a Pluto cell it will throw an error. -""" -macro give_me_the_pluto_cell_id() - if is_running_in_pluto_process() - :($(Main.PlutoRunner.GiveMeCellID())) - else - :(throw(NotRunningInPlutoCellException())) - end -end - + is_running_in_pluto_process() +This doesn't mean we're in a Pluto cell, e.g. can use @bind and hooks goodies. +It only means PlutoRunner is available (and at a version that technically supports hooks) """ - @give_me_rerun_cell_function() +function is_running_in_jolinpluto_process() + ispluto = is_running_in_pluto_process() + isjolin = ispluto && isdefined(Main.PlutoRunner, :currently_running_user_requested_run) -> ⚠️ Don't use this directly!! if you think you need it, you need [`@use_state`](@ref). - -Used inside a Pluto cell this will resolve to a function that, when called, will cause the cell to be re-run (in turn re-running all dependent cells). -Outside a Pluto cell it will throw an error. -""" -macro give_me_rerun_cell_function() - if is_running_in_pluto_process() - :($(Main.PlutoRunner.GiveMeRerunCellFunction())) - else - :(throw(NotRunningInPlutoCellException())) + if ispluto && !isjolin + @warn "You are using functionality which is as of now only available inside Jolin's reactive notebooks. Falling back to no reactivity." end + return ispluto && isjolin end - -# ╔═╡ cf55239c-526b-48fe-933e-9e8d56161fd6 -""" - @give_me_register_cleanup_function() - -> ⚠️ Don't use this directly!! if you think you need it, you need [`@use_effect`](@ref). - -Used inside a Pluto cell this will resolve to a function that call be called with yet another function, and then will call that function when the cell gets explicitly re-run. ("Explicitly re-run" meaning all `@use_ref`s get cleared, for example). -Outside a Pluto cell it will throw an error. -""" -macro give_me_register_cleanup_function() - if is_running_in_pluto_process() - :($(Main.PlutoRunner.GiveMeRegisterCleanupFunction())) - else - :(throw(NotRunningInPlutoCellException())) - end -end \ No newline at end of file diff --git a/src/possibly_useful.jl b/src/possibly_useful.jl deleted file mode 100644 index 34027d1..0000000 --- a/src/possibly_useful.jl +++ /dev/null @@ -1,84 +0,0 @@ - - -_free_symbols(sym::Symbol) = @cont isdefined(Main, sym) || cont(sym) -_free_symbols(other) = @cont () - -get_where_symbol(sym::Symbol) = sym -function get_where_symbol(expr::Expr) - if expr.head === :comparison - expr.args[3] - elseif expr.head in (:(<:), :(>:)) - expr.args[1] - else - error("should not happen") - end -end - -@cont function _free_symbols(expr::Expr) - if expr.head ∈ (:function, :->) - call = expr.args[1] - body = expr.args[2] - - func_args = if isa(call, Symbol) - (call,) - elseif call.head === :tuple - call.args - elseif call.head === :call - call.args[2:end] - end - - foreach(_free_symbols(body)) do sym - sym ∈ func_args || cont(sym) - end - elseif expr.head === :ref - # this is indexing, where the symbols :end and :begin have special meaning - for arg in expr.args - foreach(_free_symbols(arg)) do sym - sym ∈ (:begin, :end) || cont(sym) - end - end - elseif expr.head === :where - where_symbols = get_where_symbol.(expr.args[2:end]) - for arg in expr.args - foreach(_free_symbols(arg)) do sym - sym ∈ where_symbols || cont(sym) - end - end - else - defs = [] - for arg in expr.args - if Meta.isexpr(arg, :(=)) - push!(defs, arg.args[1]) - end - - foreach(_free_symbols(arg)) do sym - sym in defs || cont(sym) - end - end - end -end - - - - - -macro use_state_reinit(init) - updated_by_hooks_ref = Ref(false) - upgrade_set_update(set_update) = function set_update_upgraded(arg) - updated_by_hooks_ref[] = true - set_update(arg) - end - :(let - update, set_update = @use_state(nothing) - hooked = $updated_by_hooks_ref[] - if !hooked - # if we are not updated by hooks, but by other triggers, we also want to reinitiate things - # this is also for the first time, which is why we do not need the standard init value above - update = $(esc(init)) - end - # reset to false for a new round - $updated_by_hooks_ref[] = false - update, $upgrade_set_update(set_update), hooked - end) -end - diff --git a/src/setter.jl b/src/setter.jl index 406cba8..41bd9bd 100644 --- a/src/setter.jl +++ b/src/setter.jl @@ -61,7 +61,7 @@ macro get(setter) quote setter = getsetter($setter) if $firsttime[] || setter.just_created - rerun = @give_me_rerun_cell_function + rerun = $(Main.PlutoRunner.GiveMeRerunCellFunction()) if setter.rerun !== nothing @error "`@get` was already called on the setter. Only use one invocation of `@get` per setter." end @@ -69,7 +69,7 @@ macro get(setter) if $firsttime[] $firsttime[] = false - cleanup = @give_me_register_cleanup_function + cleanup = $(Main.PlutoRunner.GiveMeRegisterCleanupFunction) cleanup() do if setter.rerun === rerun setter.rerun = nothing @@ -84,7 +84,7 @@ end function Base.get(setter::Setter) - is_running_in_pluto_process() || return setter.value + is_running_in_jolinpluto_process() || return setter.value firsttime = Main.PlutoRunner.currently_running_user_requested_run[] if firsttime || setter.just_created @@ -168,11 +168,11 @@ macro cell_ids_push!(setter) is_running_in_pluto_process() || return QuoteNode(nothing) quote setter = getsetter($setter) - cell_id = @give_me_the_pluto_cell_id + cell_id = $(Main.PlutoRunner.GiveMeCellID()) setter() do cell_ids push!(cell_ids, cell_id) end - cleanup = @give_me_register_cleanup_function + cleanup = $(Main.PlutoRunner.GiveMeRegisterCleanupFunction) cleanup() do setter() do cell_ids delete!(cell_ids, cell_id) @@ -193,7 +193,7 @@ Also cleanup is handled, i.e. that the cell-id is removed again if this cell is """ function cell_ids_push!(setter::Setter) # if this is not run inside Pluto, we just don't add a cell_id - is_running_in_pluto_process() || return nothing + is_running_in_jolinpluto_process() || return nothing firsttime = Main.PlutoRunner.currently_running_user_requested_run[] cell_id = Main.PlutoRunner.currently_running_cell_id[] @@ -212,4 +212,5 @@ function cell_ids_push!(setter::Setter) nothing end +# useful for use in Python and R, where exclamation mark is not a valid symbol const cell_ids_push = cell_ids_push! \ No newline at end of file diff --git a/src/tasks.jl b/src/tasks.jl index 696e163..aab05d2 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -112,8 +112,8 @@ macro repeat_run(init, repeatme=init) firsttime = Ref(true) :(let if $firsttime[] - $rerun[] = $JolinPluto.@give_me_rerun_cell_function - register_cleanup_fn = $JolinPluto.@give_me_register_cleanup_function() + $rerun[] = $(Main.PlutoRunner.GiveMeRerunCellFunction()) + register_cleanup_fn = $(Main.PlutoRunner.GiveMeRegisterCleanupFunction) register_cleanup_fn($(create_taskref_cleanup(task))) $firsttime[] = false end @@ -158,7 +158,7 @@ also be triggered if the cell is re-evaluated because some dependent cell or bon changed. """ function repeat_run(init, repeatme=init) - is_running_in_pluto_process() || return init() + is_running_in_jolinpluto_process() || return init() firsttime = Main.PlutoRunner.currently_running_user_requested_run[] cell_id = Main.PlutoRunner.currently_running_cell_id[] @@ -427,7 +427,7 @@ macro Channel(args...) chnl = Channel($(map(esc, args)...); taskref=$task) if $firsttime[] - register_cleanup_fn = $JolinPluto.@give_me_register_cleanup_function() + register_cleanup_fn = $(Main.PlutoRunner.GiveMeRegisterCleanupFunction) register_cleanup_fn($(create_taskref_cleanup(task))) $firsttime[] = false end @@ -448,7 +448,7 @@ Like normal `Channel`, with the underlying task being interrupted as soon as the Pluto cell is deleted. """ function ChannelPluto(args...; kwargs...) - is_running_in_pluto_process() || return Channel(args...; kwargs...) # just create a plain channel without cleanup + is_running_in_jolinpluto_process() || return Channel(args...; kwargs...) # just create a plain channel without cleanup firsttime = Main.PlutoRunner.currently_running_user_requested_run[] cell_id = Main.PlutoRunner.currently_running_cell_id[] diff --git a/src/viewof.jl b/src/viewof.jl new file mode 100644 index 0000000..ebf79a9 --- /dev/null +++ b/src/viewof.jl @@ -0,0 +1,46 @@ +""" +```julia +viewof(:symbol, element) +viewof("symbol", element) +``` + +Return the HTML `element`, and use its latest JavaScript value as the definition of `symbol`. + +# Example + +```julia +viewof(:x, html"") +``` +and in another cell: +```julia +x^2 +``` + +The first cell will show a slider as the cell's output, ranging from 0 until 100. +The second cell will show the square of `x`, and is updated in real-time as the slider is moved. +""" +function viewof(def, ui) + if !isa(def, Symbol) + throw(ArgumentError("""\nMacro example usage: \n\n\t@bind my_number html""\n\n""")) + elseif !isdefined(Main, :PlutoRunner) + initial_value_getter = try + Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value + catch + b -> missing + end + initial_value = Core.applicable(Base.get, ui) ? Base.get(ui) : initial_value_getter(ui) + return initial_value, ui + else + Main.PlutoRunner.load_integrations_if_needed() + initial_value_getter = Main.PlutoRunner.initial_value_getter_ref[](ui) + initial_value = Core.applicable(Base.get, ui) ? Base.get(ui) : initial_value_getter(ui) + return initial_value, Main.PlutoRunner.create_bond(ui, def, Main.PlutoRunner.currently_running_cell_id[]) + end +end + +# for python and R especially +# (python strings are automatically transformed to Julia strings in JuliaCall when calling julia functions from python) +# (same for R strings) +function viewof(def::AbstractString, ui) + viewof(Symbol(def), ui) +end