Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,13 @@ jobs:
with:
python-version: ${{ matrix.pyversion }}

- name: Check Python OpenSSL version (see setup_julia)
shell: python
run: |
import ssl
assert ssl.OPENSSL_VERSION_INFO < (3, 5)
# We previously had to restrict to Julia 1.11 because Julia 1.12 requires OpenSSL
# 3.5 but the GitHub runners only ship Python with OpenSSL 3.0.
# - name: Check Python OpenSSL version (see setup_julia)
# shell: python
# run: |
# import ssl
# assert ssl.OPENSSL_VERSION_INFO < (3, 5)

- name: Set up uv
uses: astral-sh/setup-uv@v7
Expand All @@ -120,9 +122,7 @@ jobs:
id: setup_julia
uses: julia-actions/setup-julia@v3
with:
# Python in the GitHub runners ships with OpenSSL 3.0. Julia 1.12 requires
# OpenSSL 3.5. Therefore juliapkg requires Julia 1.11 or lower.
version: '1.11'
version: '1'

- name: Set up test Julia project
if: ${{ matrix.juliaexe == 'julia' }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased
* Add `pyrepl` and `@pyrepl` to launch a Python REPL.
* Add `repl_style` preference.

## 0.9.35 (2026-06-08)
* Add option `lib` to JuliaCall. Setting this will skip the discovery subprocess.
* Add support for using a system image in `juliacall` that has `PythonCall` baked in.
Expand Down
4 changes: 3 additions & 1 deletion docs/src/pythoncall-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,13 @@ ispy(x::MyType) = true
Py(x::MyType) = x.py
```

## `@py` and `@pyconst`
## `@py`, `@pyconst`, `pyrepl` and `@pyrepl`

```@docs
@py
@pyconst
pyrepl
@pyrepl
```

## Multi-threading
Expand Down
1 change: 1 addition & 0 deletions docs/src/pythoncall.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ variables.
| `exe` | `JULIA_PYTHONCALL_EXE` | Path to the Python executable, or special values (see below). |
| `lib` | `JULIA_PYTHONCALL_LIB` | Path to the Python library (usually inferred automatically). |
| `pickle` | `JULIA_PYTHONCALL_PICKLE` | Pickle module to use for serialization (`pickle` or `dill`). |
| `repl_style` | `JULIA_PYTHONCALL_REPL_STYLE` | Style of the Python REPL in [`pyrepl`](@ref). |

The easiest way to set these preferences is with the
[`PreferenceTools`](https://github.com/cjdoris/PreferenceTools.jl)
Expand Down
2 changes: 2 additions & 0 deletions src/API/exports.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Core
export @py
export @pyconst
export @pyrepl
export @pyeval
export @pyexec
export ispy
Expand Down Expand Up @@ -82,6 +83,7 @@ export pypos
export pypow
export pyprint
export pyrange
export pyrepl
export pyrepr
export pyrowlist
export pyrshift
Expand Down
1 change: 1 addition & 0 deletions src/API/functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function pypos end
function pypow end
function pyprint end
function pyrange end
function pyrepl end
function pyrepr end
function pyrowlist end
function pyrshift end
Expand Down
1 change: 1 addition & 0 deletions src/API/macros.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Core
macro pyconst end
macro pyrepl end
macro pyeval end
macro pyexec end

Expand Down
2 changes: 2 additions & 0 deletions src/Core/Core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ using Markdown: Markdown

import ..PythonCall:
@pyconst,
@pyrepl,
@pyeval,
@pyexec,
getptr,
Expand Down Expand Up @@ -119,6 +120,7 @@ import ..PythonCall:
pypow,
pyprint,
pyrange,
pyrepl,
pyrepr,
pyrowlist,
pyrshift,
Expand Down
126 changes: 125 additions & 1 deletion src/Core/builtins.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,10 @@ _pyexec_ans(::Type{Nothing}, globals, locals) = nothing
:(
$v = pyconvert(
$(types.parameters[i]),
pygetitem(locals, $(string(names[i]))),
@something(
pygetitem(locals, $(string(names[i])), nothing),
pygetitem(globals, $(string(names[i]))),
),
)
),
)
Expand Down Expand Up @@ -1427,6 +1430,127 @@ macro pyexec(arg)
end
end

"""
pyrepl(locals; [style=nothing])

Run a Python REPL, for interacting directly with Python.

Press Ctrl-D to terminate the REPL and return to Julia.

Runs in a scope defined by `locals`. As with [`pyeval`](@ref), if you pass a module,
then a persistent scope for that module is used. Otherwise you must pass a Python
`dict`.

The `style` keyword argument selects the REPL implementation:
- `:code` (default): Standard library `code.interact()`.
- `:ipython`: IPython REPL via `IPython.embed()` (requires `IPython` to be installed).
- `:bpython`: bpython REPL via `bpython.embed()` (requires `bpython` to be installed).
- `:ptpython`: ptpython REPL via `ptpython.embed()` (requires `ptpython` to be installed).

The default style is `:code`, which is always available. You can set
[the `repl_style` preference](@ref pythoncall-config) to override this default.

For most uses, [`@pyrepl`](@ref) is preferred. It is equivalent to `pyrepl(@__MODULE__)`.

You can use `pyeval("varname", locals)` to retrieve a variable computed during the REPL
session.
"""
function pyrepl(locals; style=nothing)
if ispy(locals)
locals = Py(locals)
elseif locals isa Module
locals = get!(pydict, MODULE_GLOBALS, locals)
else
error("locals must be a Module or a Python dict")
end
if style === nothing
style = Utils.getpref_repl_style()
end
sys = pyimport("sys")
ps1 = pygetattr(sys, "ps1", nothing)
ps2 = pygetattr(sys, "ps2", nothing)
try
if style == :code
pyimport("code").interact(banner="", exitmsg="", var"local"=locals)
elseif style == :ipython
config = pyimport("traitlets.config").Config()
config.InteractiveShell.banner1 = ""
config.InteractiveShell.banner2 = ""
config.InteractiveShell.enable_tip = false
locid = "$(@__FILE__):$(@__LINE__)"
mod = pyimport("sys").__class__("temp")
pyimport("IPython.terminal.embed").InteractiveShellEmbed(
_init_location_id=locid,
config=config,
)(
user_ns=locals,
local_ns=locals,
var"module"=mod,
_call_location_id=locid,
compile_flags=0,
)
elseif style == :bpython
pyimport("bpython").embed(locals_=locals)
elseif style == :ptpython
pyimport("ptpython").embed(globals=locals)
else
error("Unknown REPL style: $style. Supported styles are :code, :ipython, :bpython, :ptpython")
end
finally
for (k, v) in (("ps1", ps1), ("ps2", ps2))
if v === nothing
if pyhasattr(sys, k)
pydelattr(sys, k)
end
else
pysetattr(sys, k, v)
end
end
end
return
end

"""
@pyrepl ...

Run a Python REPL, for interacting directly with Python.

Press Ctrl-D to terminate the REPL and return to Julia.

Runs in a persistent scope tied to the Julia module that this was called from, the same
as for [`@pyeval`](@ref) and [`@pyexec`](@ref).

Equivalent to `pyrepl(@__MODULE__; ...)`. Keyword arguments are as for [`pyrepl`](@ref).

# Examples

Launch a REPL and use `@pyeval` to retrieve a value.

```julia-repl
julia> @pyrepl
>>> x = 12
>>> # press Ctrl-D to quit

julia> @pyeval "x"
Python: 12
```

Launch a REPL and directly set a value in the Main module.

```julia-repl
julia> @pyrepl
>>> from juliacall import Main as jl
>>> jl.x = 123
>>> # press Ctrl-D to quit

julia> x
123
```
"""
macro pyrepl(args...)
esc(:($pyrepl($__module__; $(args...))))
end

### with

"""
Expand Down
14 changes: 14 additions & 0 deletions src/Utils/Utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ checkpref(::Type{String}, x::AbstractString) = convert(String, x)
getpref_exe() = getpref(String, "exe", "JULIA_PYTHONCALL_EXE", "")
getpref_lib() = getpref(String, "lib", "JULIA_PYTHONCALL_LIB", nothing)
getpref_pickle() = getpref(String, "pickle", "JULIA_PYTHONCALL_PICKLE", "pickle")
function getpref_repl_style()
pref = getpref(String, "repl_style", "JULIA_PYTHONCALL_REPL_STYLE", "code")
if pref == "code"
:code
elseif pref == "ipython"
:ipython
elseif pref == "bpython"
:bpython
elseif pref == "ptpython"
:ptpython
else
error("invalid repl_style preference, expecting `code`, `ipython`, `bpython` or `ptpython`")
end
end

function explode_union(T)
@nospecialize T
Expand Down
Loading