Stream: helpdesk (published)

Topic: Temporary namespace


view this post on Zulip Timothy (Nov 21 2022 at 05:32):

I'm looking to drop the user into a "prototyping" REPL where they can do a few things and then the result is saved in a script. I'd like to create a new temporary namespace for this and then drop back to old-Main later. From https://github.com/JuliaLang/julia/issues/2385#issuecomment-1261400850 I get the impression this might be possible, but I have no idea how.

view this post on Zulip Mason Protter (Nov 21 2022 at 05:44):

In version 1.9, you can do

using REPL
REPL.activate(::Module)

view this post on Zulip Mason Protter (Nov 21 2022 at 05:45):

https://github.com/JuliaLang/julia/blob/master/stdlib/REPL/docs/src/index.md#changing-the-contextual-module-which-is-active-at-the-repl

view this post on Zulip Mason Protter (Nov 21 2022 at 05:45):

But that won't help you with the "save input to a script" part

view this post on Zulip Mason Protter (Nov 21 2022 at 05:47):

Well, I guess you could save the REPL history from the logs

view this post on Zulip Mason Protter (Nov 21 2022 at 06:05):

Here's how I'd approach it:

using ReplMaker

function script_mode(filename, mod=(@eval module $(gensym(:Scratch)) end))
    initrepl(;prompt_text="Script> ", start_key=')', mode_name="Script mode", valid_input_checker=complete_julia) do s
        ex = Meta.parse(s)
        if ex.head == :block
            ex.head = :toplevel
        end
        open(filename, "a+") do io
            write(io, s*"\n\n")
        end
        ex
    end
    nothing
end

view this post on Zulip Mason Protter (Nov 21 2022 at 06:07):

Now at the REPL,

julia> script_mode("my_script.jl")
REPL mode Script mode initialized. Press ) to enter and backspace to exit.

Script> x = 1
1

Script> y = 2
2

Script> z = x + y
3

Script> function f(x)
            x + 1
        end
f (generic function with 1 method)

And here's the generated script:

shell> cat my_script.jl
x = 1

y = 2

z = x + y

function f(x)
    x + 1
end

view this post on Zulip Mason Protter (Nov 21 2022 at 06:11):

And everything is properly namespaced, so the Main repl isn't polluted:

julia> x
ERROR: UndefVarError: x not defined

julia> y
ERROR: UndefVarError: y not defined

julia> z
ERROR: UndefVarError: z not defined

julia> f
ERROR: UndefVarError: f not defined

view this post on Zulip Syx Pek (Nov 21 2022 at 06:14):

Mason Protter said:

Here's how I'd approach it:

using ReplMaker

...

There should be a github of just useful scripts like this

view this post on Zulip Timothy (Nov 21 2022 at 06:20):

Thanks Mason, I'll have a look at REPL.activate.

For reference, this is what I've got so far

subrepl = REPL.LineEditREPL(REPL.TerminalMenus.terminal,
                            get(stdout, :color, false))
if subrepl.hascolor
    subrepl.prompt_color = Base.text_colors[:light_magenta]
end
subrepl.interface = REPL.setup_interface(subrepl)
(; hp) = first(subrepl.interface.modes[isa.(subrepl.interface.modes, REPL.LineEdit.HistoryPrompt)])
hp.mode_mapping[:julia].prompt = "(data) julia> "
hp.mode_mapping[:shell].prompt = "(data) shell> "
REPL.run_repl(subrepl)
lines = map(zip(hp.history, hp.modes)) do (line, mode)
    if mode == :julia
        line
        # Identify data""-y commands and extract to variables
    elseif mode == :shell
        "run(`$line`)"
    elseif mode == :help
        nothing
    else
        @warn "Mode $mode unrecognised"
        nothing
    end
end
content = string("# Formed by wrangle command\n",
                    "function ()\n",
                    join(filter(!isnothing, lines), "\n  "),
                    "\nend")

view this post on Zulip Mason Protter (Nov 21 2022 at 06:23):

Syx Pek said:

Mason Protter said:

Here's how I'd approach it:

using ReplMaker

...

There should be a github of just useful scripts like this

There's the ReplMaker.jl readme :grinning_face_with_smiling_eyes:

view this post on Zulip Mason Protter (Nov 21 2022 at 06:24):

@Timothy does the ReplMaker.jl thing I posted not work for you?

view this post on Zulip Timothy (Nov 21 2022 at 06:24):

I won't be ably to try until later, I just thought I'd share what I started with.

view this post on Zulip Timothy (Nov 21 2022 at 06:26):

That said, I do generally try to avoid dependencies so if I can make do without it I'll probably try that, but I expect it will be useful regardless :)

view this post on Zulip Mason Protter (Nov 21 2022 at 06:32):

I find the REPL code in base really hard to use, and easy to get broken with version changes, so that's why I try to push ReplMaker.

view this post on Zulip Mason Protter (Nov 21 2022 at 06:33):

But yeah, if you can figure out how to do it without ReplMaker then that's nice. Be warned though, it might mean you know enough about the REPL internals that I'll come ask for you help next time a julia update breaks ReplMaker.jl :laughing:

view this post on Zulip Timothy (Nov 21 2022 at 06:34):

That sounds reasonable. If I wasn't "almost there" with my non-ReplMaker approach I'd probably jump on that (I've already got a bunch of other stuff sorted).

view this post on Zulip Timothy (Nov 21 2022 at 06:35):

At this point I've got enough "nice things" on top of REPL (e.g. sub-commands with automatic help and completion), that the little bit of extra here is very likely less effort than rewriting to use ReplMaker.

view this post on Zulip Timothy (Nov 21 2022 at 06:37):

Throwaway comment: I wish the Julia REPL module came with a nice little library of interactive terminal utils (e.g. a better version of print("Question? "); readline(stdin) for prompting the user, y/n confirmation, etc.)

view this post on Zulip Timothy (Nov 21 2022 at 06:38):

Since the REPL experience with Julia is rather important, making it easy for packages to make nice REPL experiences seems like it would be well worth the ~1-200 lines for a few nice extras.

view this post on Zulip Mason Protter (Nov 21 2022 at 06:39):

yeah

view this post on Zulip Mason Protter (Nov 21 2022 at 06:41):

I want ReplMaker.jl to try and provide a good toolkit since REPL.jl itself refuses to, but it's hard.

view this post on Zulip Timothy (Nov 21 2022 at 06:41):

As it is I have some hacky hand-rolled stand-ins, e.g.

"""
    prompt(question::AbstractString, default::AbstractString="",
           allowempty::Bool=false, cleardefault::Bool=true)
Interactively ask `question` and return the response string, optionally
with a `default` value.

Unless `allowempty` is set an empty response is not accepted.
If `cleardefault` is set, then an initial backspace will clear the default value.

The prompt supports the following line-edit-y keys:
- left arrow
- right arrow
- home
- end
- delete forwards
- delete backwards

### Example

```julia-repl
julia> prompt("What colour is the sky? ")
What colour is the sky? Blue
"Blue"
```
"""
function prompt(question::AbstractString, default::AbstractString="";
                allowempty::Bool=false, cleardefault::Bool=true)
    printstyled(question, color=REPL_QUESTION_COLOR)
    get(stdout, :color, false) && print(Base.text_colors[REPL_USER_INPUT_COLOUR])
    REPL.Terminals.raw!(REPL.TerminalMenus.terminal, true)
    response = let response = collect(default)
        point = length(response)
        firstinput = true
        print("\e[s")
        while true
            print("\e[u\e[J")
            if String(response) == default
                print("\e[90m")
            end
            print(String(response))
            if point < length(response)
                print("\e[$(length(response) - point)D")
            end
            next = Char(REPL.TerminalMenus.readkey(REPL.TerminalMenus.terminal.in_stream))
            if next == '\r'  # RET
                if (!isempty(response) || allowempty)
                    print('\n')
                    break
                end
            elseif next == 'Ϭ' # DEL-forward
                if point < length(response)
                    deleteat!(response, point + 1)
                end
            elseif next == '\x03' # ^C
                print("\e[m^C\n")
                throw(InterruptException())
            elseif next == '\x7f' # DEL
                if firstinput && cleardefault
                    response = Char[]
                    point = 0
                elseif point > 0
                    deleteat!(response, point)
                    point -= 1
                end
            elseif next == 'Ϩ' # <left>
                point = max(0, point - 1)
            elseif next == 'ϩ' # <right>
                point = min(length(response), point + 1)
            elseif next == 'ϭ' # HOME
                point = 0
            elseif next == 'Ϯ' # END
                point = length(response)
            else
                point += 1
                insert!(response, point, next)
            end
            firstinput = false
        end
        String(response)
    end
    REPL.Terminals.raw!(REPL.TerminalMenus.terminal, false)
    get(stdout, :color, false) && print("\e[m")
    response
end

"""
    prompt_char(question::AbstractString, options::Vector{Char},
                default::Union{Char, Nothing}=nothing)
Interatively ask `question`, only accepting `options` keys as answers.
All keys are converted to lower case on input. If `default` is not nothing and
'RET' is hit, then `default` will be returned.

Should '^C' be pressed, an InterruptException will be thrown.
"""
function prompt_char(question::AbstractString, options::Vector{Char},
                     default::Union{Char, Nothing}=nothing)
    printstyled(question, color=REPL_QUESTION_COLOR)
    REPL.Terminals.raw!(REPL.TerminalMenus.terminal, true)
    char = '\x01'
    while char  options
        char = lowercase(Char(REPL.TerminalMenus.readkey(REPL.TerminalMenus.terminal.in_stream)))
        if char == '\r' && !isnothing(default)
            char = default
        elseif char == '\x03' # ^C
            print("\e[m^C\n")
            throw(InterruptException())
        end
    end
    REPL.Terminals.raw!(REPL.TerminalMenus.terminal, false)
    get(stdout, :color, false) && print(Base.text_colors[REPL_USER_INPUT_COLOUR])
    print(stdout, char, '\n')
    get(stdout, :color, false) && print("\e[m")
    char
end

"""
    confirm_yn(question::AbstractString, default::Bool=false)
Interactively ask `question` and accept y/Y/n/N as the response.
If any other key is pressed, then `default` will be taken as the response.
A " [y/n]: " string will be appended to the question, with y/n capitalised
to indicate the default value.

### Example

```julia-repl
julia> confirm_yn("Do you like chocolate?", true)
Do you like chocolate? [Y/n]: y
true
```
"""
function confirm_yn(question::AbstractString, default::Bool=false)
    char = prompt_char(question * (" [y/N]: ", " [Y/n]: ")[1+ default],
                       ['y', 'n'], ('n', 'y')[1+default])
    char == 'y'
end

view this post on Zulip Mason Protter (Nov 21 2022 at 06:41):

So if during your development, you find any customizations and stuff you think might make a nice contribution to ReplMaker.jl, I'd be very glad to receive PRs

view this post on Zulip Timothy (Nov 21 2022 at 06:42):

If I do end up going that way, I'll be sure to do so :+1:

view this post on Zulip Timothy (Nov 21 2022 at 06:46):

Hmm, maybe it would be worth having something like TermInteractionUtils.jl which provides the sort of things you see in the gifs of the readme of https://github.com/charmbracelet/gum

view this post on Zulip Timothy (Nov 21 2022 at 06:48):

Actually, I'd be potentially up for collaborating on something like that.

view this post on Zulip Timothy (Nov 21 2022 at 07:11):

Mason Protter said:

Here's how I'd approach it: [snip] @eval module $(gensym(:Scratch)) end) [snip]

This seems to work well, thanks.

Bonus complication: how hard would it be to make packages available in the current Main available in the created module too?

view this post on Zulip Sebastian Pfitzner (Nov 21 2022 at 14:29):

You can take a look at the Infiltrator.jl source code. I'm doing a lot of stuff with anon modules and binding-imports/exports in there

view this post on Zulip Mason Protter (Nov 21 2022 at 23:40):

Timothy said:

Bonus complication: how hard would it be to make packages available in the current Main available in the created module too?

You could do something like

let mod = (@eval module $(gensym(:Scratch)) end)
    for s  names(Main, all=true)
        if getproperty(Main, s) isa Module
            @eval mod using $s
        end
    end
end

But I guess that might cause a problem because there might be a discrepancy if the user did somehting like

using Foo: bar

instead of plain using Foo

view this post on Zulip Mason Protter (Nov 21 2022 at 23:51):

@Timothy you might find https://github.com/NHDaly/DeepcopyModules.jl helpful

view this post on Zulip Timothy (Nov 22 2022 at 01:09):

Thanks Mason, you've been wonderfully helpful to me as of late :hug:


Last updated: Oct 02 2023 at 04:34 UTC