diff --git a/pysrc/juliacall/__main__.py b/pysrc/juliacall/__main__.py new file mode 100644 index 00000000..4590e975 --- /dev/null +++ b/pysrc/juliacall/__main__.py @@ -0,0 +1,31 @@ +import argparse + +from juliacall import Main +from juliacall.repl import run_repl, add_repl_args + + +def main(): + parser = argparse.ArgumentParser("JuliaCall REPL (experimental)") + parser.add_argument('-e', '--eval', type=str, default=None, help='Evaluate . If specified, all other arguments are ignored.') + parser.add_argument('-E', '--print', type=str, default=None, help='Evaluate and display the result. If specified, all other arguments are ignored.') + + add_repl_args(parser) + + args = parser.parse_args() + assert not (args.eval is not None and args.print is not None), "Cannot specify both -e/--eval and -E/--print" + + if args.eval is not None: + Main.seval(args.eval) + elif args.print is not None: + result = Main.seval(args.print) + Main.display(result) + else: + run_repl( + banner=args.banner, + quiet=args.quiet, + history_file=args.history_file, + preamble=args.preamble + ) + +if __name__ == '__main__': + main() diff --git a/pysrc/juliacall/banner.jl b/pysrc/juliacall/banner.jl new file mode 100644 index 00000000..82c4093a --- /dev/null +++ b/pysrc/juliacall/banner.jl @@ -0,0 +1,60 @@ +# https://github.com/JuliaLang/julia/blob/fae0d0ad3e5d9804533435fe81f4eaac819895af/stdlib/REPL/src/REPL.jl#L1727C1-L1795C4 + +function __PythonCall_banner(banner_opt::Symbol = :yes) + io = stdout + + if banner_opt == :no + return + end + + short = banner_opt == :short + + if get(io, :color, false)::Bool + c = Base.text_colors + tx = c[:normal] # text + jl = c[:normal] # julia + jc = c[:blue] # juliacall text + jb = c[:bold] * jc # bold blue dot + d1 = c[:bold] * c[:blue] # first dot + d2 = c[:bold] * c[:red] # second dot + d3 = c[:bold] * c[:green] # third dot + d4 = c[:bold] * c[:magenta] # fourth dot + d5 = c[:bold] * c[:yellow] # bold yellow dot + + if short + print(io,""" + $(jb)o$(tx) | Julia $(VERSION) + $(jb)o$(tx) $(d5)o$(tx) | PythonCall $(PythonCall.VERSION) + """) + else + print(io,""" $(d3)_$(tx) + $(d1)_$(tx) $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)$(jc) _ _ $(tx) | Documentation: https://juliapy.github.io/PythonCall.jl/ + $(d1)(_)$(jl) | $(d2)(_)$(tx) $(d4)(_)$(tx)$(jc) | || |$(tx) | + $(jl)_ _ _| |_ __ _$(jc) ___ __ _ | || |$(tx) | Julia: $(VERSION) + $(jl)| | | | | | |/ _` |$(jc)/ __|/ _` || || |$(tx) | PythonCall: $(PythonCall.VERSION) + $(jl)| | |_| | | | (_| |$(jc) |__ (_| || || |$(tx) | + $(jl)_/ |\\__'_|_|_|\\__'_|$(jc)\\___|\\__'_||_||_|$(tx) | The JuliaCall REPL is experimental. + $(jl)|__/$(tx) | + + """) + end + else + if short + print(io,""" + o | Julia $(VERSION) + o o | PythonCall $(PythonCall.VERSION) + """) + else + print(io,""" + _ + _ _ _(_)_ _ _ | Documentation: https://juliapy.github.io/PythonCall.jl/ + (_) | (_) (_) | || | | + _ _ _| |_ __ _ ___ __ _ | || | | Julia: $(VERSION) + | | | | | | |/ _` |/ __|/ _` || || | | PythonCall: $(PythonCall.VERSION) + | | |_| | | | (_| | |__ (_| || || | | + _/ |\\__'_|_|_|\\__'_|\\___|\\__'_||_||_| | The JuliaCall REPL is experimental. + |__/ | + """) + end + end +end diff --git a/pysrc/juliacall/init.py b/pysrc/juliacall/init.py new file mode 100644 index 00000000..7af7bb92 --- /dev/null +++ b/pysrc/juliacall/init.py @@ -0,0 +1,14 @@ +import json +import juliapkg +import sys + +if __name__ == '__main__': + # invoking python -m juliacall.init automatically imports juliacall which + # calls init() which calls juliapkg.executable() which lazily downloads julia + + if "--debug" in sys.argv: + state = juliapkg.state.STATE + state["version"] = str(state["version"]) + print(json.dumps(state, indent=2)) + else: + print("Initialized successfully. Pass --debug to see the full JuliaPkg state.") \ No newline at end of file diff --git a/pysrc/juliacall/repl.py b/pysrc/juliacall/repl.py new file mode 100644 index 00000000..bdd120ce --- /dev/null +++ b/pysrc/juliacall/repl.py @@ -0,0 +1,45 @@ +import argparse +import os +from pathlib import Path + +from juliacall import Main, Base + +def run_repl(banner='yes', quiet=False, history_file='yes', preamble=None): + os.environ.setdefault("PYTHON_JULIACALL_HANDLE_SIGNALS", "yes") + if os.environ.get("PYTHON_JULIACALL_HANDLE_SIGNALS") != "yes": + print("Experimental JuliaCall REPL requires PYTHON_JULIACALL_HANDLE_SIGNALS=yes") + exit(1) + + Base.is_interactive = True + + if not quiet: + Main.include(os.path.join(os.path.dirname(__file__), 'banner.jl')) + Main.__PythonCall_banner(Base.Symbol(banner)) + + if Main.seval(r'VERSION ≥ v"v1.11.0-alpha1"'): + no_banner_opt = Base.Symbol("no") + else: + no_banner_opt = False + + if preamble: + Main.include(str(preamble.resolve())) + + Base.run_main_repl( + Base.is_interactive, + quiet, + no_banner_opt, + history_file == 'yes', + True + ) + +def add_repl_args(parser): + parser.add_argument('--banner', choices=['yes', 'no', 'short'], default='yes', help='Enable or disable startup banner') + parser.add_argument('--quiet', '-q', action='store_true', help='Quiet startup: no banner, suppress REPL warnings') + parser.add_argument('--history-file', choices=['yes', 'no'], default='yes', help='Load or save history') + parser.add_argument('--preamble', type=Path, help='Code to be included before the REPL starts') + +if __name__ == '__main__': + parser = argparse.ArgumentParser("JuliaCall REPL (experimental)") + add_repl_args(parser) + args = parser.parse_args() + run_repl(args.banner, args.quiet, args.history_file, args.preamble) diff --git a/pytest/test_all.py b/pytest/test_all.py index 9cdc8ce4..93305600 100644 --- a/pytest/test_all.py +++ b/pytest/test_all.py @@ -168,3 +168,20 @@ def test_call_nogil(yld, raw): t2 = time() - t0 # executing the tasks should take about 1 second because they happen in parallel assert 0.9 < t2 < 1.5 + + +def test_repl(): + import sys, subprocess + import juliapkg + import juliacall as _ + + output, _ = subprocess.Popen( + [sys.executable, "-m", "juliacall"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ).communicate(input="", timeout=10) + + assert f"Julia: {juliapkg.state.STATE['version']}" in output + assert "julia>" in output