[ANN] PythonCall and JuliaCall

Yea I think that’s basically it. I made a tiny package to do this and also to make IJulia figures work right: https://github.com/marius311/PyPlotCall.jl I have no plans to develop this much further, but if someone wanted to claim ownership or build it out, please feel free to.

4 Likes

PythonCall used to have a very similar integration with IJulia which got lost in a code reorg. Would be great to reintroduce it.

2 Likes

Setting environment variables in PyCall is just one-off. However, for JULIA_PYTHONCALL_EXE in PythonCall, I need to set it to @PyCall or the Python path each time when I open the REPL. Is it normal?

That’s right, PythonCall intentionally doesn’t persist these settings. You can always set that env var in your .profile or startup.jl or etc…

1 Like

Thanks for the reply. Though I would prefer to have an argument for us to determine whether it memorizes the last setting.

Say I have a Julia function Main.f with two methods f(v::Array{Float64}) and f(v:Array{String}). With pyjulia (the Python module julia) I can do these calls: Main.f(['a', 'b']) and Main.f([1.0, 2.0]). In this case the Python lists will be converted to Array{String} and Array{Float64} and the correct method will be called. That’s the end of the story, I don’t need to do anything more. (The disadvantage is that I do need to do something more if I want to suppress automatic conversion.)

How can achieve the same thing with juliacall? For example Main.f([1.0, 2.0]) will convert the Python list to PythonCall.PyList{PythonCall.Py}, which is IIUC a wrapper around the Python object. And there is no matching method for f.

Because PyList <: AbstractVector, I could write a method f(v::AbstractVector). But, I would still need to look at the elements to find what the eltypes are and then convert and call f again. What would that code look like ? Or another option, perhaps better, would be to convert the Python list ['x', 'y'] to a Vector{String} from the Python side. This would be wrapped in the Python type juliacall.VectorValue. If I then call Main.f, I see that the correct method, one of f(v::Array{Float64}) or f(v:Array{String}) is indeed called. How can I do this; i.e. convert a Python list to a VectorValue ? I see that there is a function PythonCall.pytype. I could perhaps write some functions using these tools to do the conversions. Are there any facilities like this already written ?

EDIT: There is a function PythonCall.pyconvert(T, x) that converts a (python) object x to an object of Julia type T. I’d like to write a function pyconvert(x) that converts x to the most natural type. For example strString. What is the most efficient way to write such a function?

The intended way to do this is

from juliacall import Main as jl, As as jlas
jl.f(jlas(["foo", "bar"], jl.Vector))

The object jlas(x, T) is viewed on the Julia side as pyconvert(T, x).

There was some discussion here with @icweaver about creating a function pyconvertnative(x) to convert only to Julia “native” types.

That doesn’t appear to work for me with juliacall 0.6.1
I get the following:

In [1]: from juliacall import Main as jl, As as jlas

In [2]: jl.seval('f(x) = nothing')
Out[2]: f (generic function with 1 method)

In [3]: jl.f(jlas(["foo", "bar"], jl.Vector))
---------------------------------------------------------------------------
JuliaError                                Traceback (most recent call last)
Input In [3], in <cell line: 1>()
----> 1 jl.f(jlas(["foo", "bar"], jl.Vector))

File ~/.julia/packages/PythonCall/Z6DIG/src/jlwrap/any.jl:167:30, in __call__(self, *args, **kwargs)
     28     module = Base
     29 elif mname == 'Core':
---> 30     module = Core
     31 elif mname == 'Main':
     32     module = Main

JuliaError: StackOverflowError:
Stacktrace:
     [1] pyconvert_rule_jlas(#unused#::Type{Vector}, x::PythonCall.Py)
       @ PythonCall ~/.julia/packages/PythonCall/Z6DIG/src/juliacall.jl:52
     [2] (::PythonCall.var"#52#53"{Vector, typeof(PythonCall.pyconvert_rule_jlas)})(x::PythonCall.Py)
       @ PythonCall ~/.julia/packages/PythonCall/Z6DIG/src/convert.jl:207
     [3] macro expansion
       @ ~/.julia/packages/PythonCall/Z6DIG/src/convert.jl:238 [inlined]
     [4] macro expansion
       @ ~/.julia/packages/PythonCall/Z6DIG/src/Py.jl:129 [inlined]
     [5] pytryconvert(#unused#::Type{Vector}, x::PythonCall.Py)
       @ PythonCall ~/.julia/packages/PythonCall/Z6DIG/src/convert.jl:221
--- the last 5 lines are repeated 11424 more times ---
 [57126] pyconvert_rule_jlas(#unused#::Type{Vector}, x::PythonCall.Py)
       @ PythonCall ~/.julia/packages/PythonCall/Z6DIG/src/juliacall.jl:52

The following seems to work, and doesn’t seem grossly inefficient. Does this look reasonable? Am I am reproducing something that already exists, or is there a more idiomatic solution? (perhaps juliacall.As is convenient and efficient, once I get it working.) This converts list to Vector assuming the pyconvert succeeds for every element.

function pyconvert_list(::Type{T}, list) where T
    vec = Vector{T}(undef, length(list))
    for i in eachindex(list)
        vec[i] = PythonCall.pyconvert(T, list[i])
    end
    return vec
end

That’s a perfectly reasonable function, but I don’t think it’s any different from pyconvert(Vector{T}, list)?

Thanks for the bug report, there was a very silly mistake in the conversion code. Fixed on main. I think I’ll make a release today too, so try again in an hour.

1 Like

pyconvert(Vector{T}, list)

I didn’t try that. I think this is exactly what I’m looking for. A minor problem is, from the python side, jl.Vector works, but jl.Vector{String} is not valid python syntax. I suppose you could do jl.seval('Vector{String}').

You can do jl.Vector[jl.String] instead. :slight_smile:

I really should work on the JuliaCall docs, they aren’t great!

I’m sure it will improve. On the upside, the code is pretty readable and discoverable. And getting it in the discourse thread is a start.

OK I have just released PythonCall/JuliaCall 0.8.0. To reduce complexity I got rid of juliacall.As and replaced it with juliacall.convert which you can use like so:

>>> from juliacall import Main as jl, convert as jlconvert
>>> jlconvert(jl.Vector[jl.String], ["foo", "bar"])
<jl ["foo", "bar"]>
1 Like

This looks like an improvement.

However, compared to v0.6.1, it introduced some problems.

First if I do juliacall.Main.bogussymbol from the ipython cli, I get a segfault (Because bogussymbol is not defined). This doesn’t appear to be a bug in PythonCall per se, because the segfault doesn’t occur if I just use the crude python cli. I haven’t yet tried to track down the problem.

Second, I was able to compile a system image of a project and modify the initialization of juliacall to load the system image. After updating juliacall, I can still do this, but am getting frequent segfaults when I try to use it. I haven’t yet located the problem(s).

from juliacall import Main as jl
import numpy as np
jl.seval("using Plots")
jl.plot(np.random.randn(100))

How to make the graph show?

somewhat refactored, but this works:

using PythonCall
plt    = pyimport("matplotlib.pyplot")
plt.plot(randn(100))
plt.show()

You may also want to consider PythonPlot.jl

Edit: this is not an answer to the question (which I misunderstood)

I take this example from JuliaCall is a corresponding Python package to interoperate with Julia, so for example
the code runs without errors within python only that it does not show the graph
But I can’t get the graph to show up!!!

You probably want jl.display() at the end?

Traceback (most recent call last):
  File "F:/Julia software/Julia Scripts/Julia Flux_getting_started/Example_2a_Calling_Julia_from_Python list type.py", line 21, in <module>
    jl.display()
  File "C:\Users\user\.julia\packages\PythonCall\4eoCM\src\jlwrap\any.jl", line 201, in __call__
    return self._jl_callmethod($(pyjl_methodnum(pyjlany_call)), args, kwargs)
TypeError: Julia: MethodError: no method matching display()
Closest candidates are:
  display(!Matched::REPL.REPLDisplay, !Matched::MIME{Symbol("text/plain")}, !Matched::Any) at C:\Users\user\AppData\Local\Programs\Julia-1.7.3\share\julia\stdlib\v1.7\REPL\src\REPL.jl:257
  display(!Matched::REPL.REPLDisplay, !Matched::Any) at C:\Users\user\AppData\Local\Programs\Julia-1.7.3\share\julia\stdlib\v1.7\REPL\src\REPL.jl:271
  display(!Matched::AbstractDisplay, !Matched::AbstractString, !Matched::Any) at C:\Users\user\AppData\Local\Programs\Julia-1.7.3\share\julia\base\multimedia.jl:216
  ...