PyBase.jl

PyBase.jl

TL;DR

It takes just one line (per type) to define a Python interface:

using MyModule: MyType
using PyCall
using PyBase

PyCall.PyObject(self::MyType) = PyBase.UFCS.wrap(self)
# PyCall.PyObject(self::MyType) = PyBase.Plain.wrap(self)  # alternative

It defines an interface for MyType which can be imported from Python by from julia.MyModule import MyType via PyJulia.

Overview

Suppose you have a Julia type and want to expose its API to the Python world:

julia> mutable struct MyType
           x::Number
       end

julia> add1!(self::MyType) = self.x += 1;

julia> add2(self::MyType) = self.x + 2;

julia> add3(self::MyType) = self.x + 3
       add3!(self::MyType) = self.x += 3;

Using PyBase, it is just a one line to wrap it into a Python interface:

julia> using PyCall
       using PyBase

julia> PyCall.PyObject(self::MyType) = PyBase.UFCS.wrap(self);

Now MyType is usable from Python. Here, for a demonstration purpose, let's send it to Python namespace using PyCall's @py_str macro:

julia> py"""
       MyType = $MyType  # emulating "from julia.MyModule import MyType"
       """

Python users can now use MyType as if it is a Python function.

julia> py"""
       obj = MyType(0)
       assert obj.x == 0
       """

Since MyType uses PyBase.UFCS.wrap, Julia functions taking MyType as the first argument can be called as if it was a Python method of MyType:

julia> py"""
       obj.add1()
       assert obj.x == 1
       """

julia> py"""
       assert obj.add2() == 3
       assert obj.x == 1
       """

julia> py"""
       assert obj.add3(inplace=False) == 4
       assert obj.x == 1
       """

julia> py"""
       obj.add3()  # default to inplace=True
       assert obj.x == 4
       """

It may be useful to provide custom Python methods. This can be done by overloading PyBase.__getattr__:

julia> PyBase.__getattr__(self::MyType, ::Val{:explicit_method}) =
           (arg) -> "explicit method call with $arg"

julia> py"""
       assert obj.explicit_method(123) == "explicit method call with 123"
       """

Note that various Julia interfaces are automatically usable from Python. For example, indexing just works by translating indices as the offsets from Base.firstindex (which keeps 0-origin in Python side):

julia> Base.firstindex(obj::MyType) = 1;

julia> Base.getindex(obj::MyType, i::Integer) = i;

julia> py"""
       assert obj[0] == 1
       """

Pre-defined wrapper factories

"Uniform Function Call Syntax"

PyBase.UFCSModule.

Uniform Function Call Syntax [UFCS] wrapper.

Usage:

PyCall.PyObject(self::MyModule.MyType) = PyBase.UFCS.wrap(self)

It then translates Python method call self.NAME(*args, **kwargs) to

MyModule.MyType.NAME(self, args...; kwargs...)  # or
MyModule.MyType.NAME!(self, args...; kwargs...)

in Julia where NAME is a placeholder (i.e., it can be a method named solve or plot). If both NAME and NAME! exist, NAME! is used unless False is passed to the special keyword argument inplace, i.e.,

# in Python:                               #    in Julia:
self.NAME(*args, **kwargs)                 # => NAME(self, args...; kwargs...)
                                           # or NAME!(self, args...; kwargs...)
self.NAME(*args, inplace=False, **kwargs)  # => NAME(self, args...; kwargs...)
self.NAME(*args, inplace=True, **kwargs)   # => NAME!(self, args...; kwargs...)

The modules in which method NAME is searched can be specified as a keyword argument to PyBase.UFCS.wrap:

using MyBase, MyModule
PyCall.PyObject(self::MyType) = PyBase.UFCS.wrap(self,
                                                 modules = [MyBase, MyModule])

The first match in the list of modules is used.

Usual method overloading of

PyBase.__getattr__(self::MyModule.MyType, name::Symbol)  # and/or
PyBase.__getattr__(self::MyModule.MyType, ::Val{name})

can still control the behavior of Python's __getattr__ if name is not already handled by the above UFCS mechanism.

source
PyBase.UFCS.wrapFunction.
PyBase.UFCS.wrap(self; methods, modules) :: PyObject

"Uniform Function Call Syntax" wrapper. See PyBase.UFCS for details.

source

Plain

PyBase.PlainModule.

Plain wrapper.

Usage:

PyCall.PyObject(self::MyModule.MyType) = PyBase.Plain.wrap(self)
source
PyBase.Plain.wrapFunction.
PyBase.Plain.wrap(self) :: PyObject
source

Python interface methods

PyBase provides interface for defining special methods for Python data model. Note that these methods do not follow Julia's convention that mutating functions have to end with ! to avoid extra complication of naming transformation.

PyBase.__getattr__Function.
PyBase.__getattr__(self, ::Val{name::Symbol})
PyBase.__getattr__(self, name::Symbol)

Python's attribute getter interface __getattr__.

Roughly speaking, the default implementation is:

__getattr__(self, name::Symbol) = __getattr__(self, Val(name))
__getattr__(self, ::Val{name}) = getproperty(self, name)

To add a specific Python property to self, overload __getattr__(::MyType, ::Val{name}). Use __getattr__(::MyType, ::Symbol) to fully control the attribute access in Python.

At the lowest level (which is invoked directly by Python), PyBase defines __getattr__(self, name::String) = __getattr__(self, Symbol(name)) this can be used for advanced control when using predefined wrappers. However, __getattr__(::Shim{MyType}, ::String) has to be overloaded instead of __getattr__(::MyType, ::String).

source
PyBase.__setattr__Function.
PyBase.__setattr__(self, ::Val{name::Symbol}, value)
PyBase.__setattr__(self, name::Symbol, value)

Python's attribute setter interface __setattr__. Default implementation invokes setproperty!. See PyBase.__getattr__.

source
PyBase.__delattr__Function.
PyBase.delattr(self, ::Val{name::Symbol})
PyBase.delattr(self, name::Symbol)

Python's attribute deletion interface __delattr__. Default implementation raises AttributeError in Python. See PyBase.__getattr__.

source
PyBase.__dir__Function.

Python's __dir__

source
PyBase.convert_itemkey(self, key::Tuple)

Often __getitem__, __setitem__ and __delitem__ can be directly mapped to getindex, setindex! and delete! respectively, provided that the key/(multi-)index is mapped correctly. This mapping is done by convert_itemkey. The default implementation does a lot of magics (= run-time introspection) to convert Python/Numpy-like semantics to Julia-like semantics.

source
PyBase.__getitem__Function.
__getitem__(self, key)

Python's __getitem__. Default implementation is:

getindex(self, convert_itemkey(self, key)...)
source
PyBase.__setitem__Function.
__setitem__(self, key, value)

Python's __setitem__. Default implementation is:

setindex!(self, value, convert_itemkey(self, key)...)

Note that the order of key and value for __setitem__ is the opposite of setindex!.

source
PyBase.__delitem__Function.
__delitem__(self, key)

Python's __delitem__. Default implementation is:

delete!(self, convert_itemkey(self, key)...)
source
PyBase.__add__Function.
__add__(self, other)

Python's __add__. Default implementation is roughly: broadcast(+, self, other).

source
PyBase.__radd__Function.
__radd__(self, other)

Python's __radd__. Default implementation: PyBase.__add__(other, self).

source
PyBase.__iadd__Function.
__iadd__(self, other)

Python's __iadd__. If self is mutated, PyBase.mutated must be returned. Default implementation:

function __iadd__(self, other)
    broadcast!(+, self, self, other)
    return PyBase.mutated
end
source

List of supported methods

Supported method
__abs__
__add__
__and__
__ceil__
__complex__
__delattr__
__delitem__
__dir__
__divmod__
__float__
__floor__
__floordiv__
__getattr__
__getitem__
__iadd__
__iand__
__ifloordiv__
__ilshift__
__imatmul__
__imod__
__imul__
__int__
__invert__
__ior__
__ipow__
__irshift__
__isub__
__itruediv__
__ixor__
__lshift__
__matmul__
__mod__
__mul__
__neg__
__or__
__pos__
__pow__
__radd__
__rand__
__rdivmod__
__rfloordiv__
__rlshift__
__rmatmul__
__rmod__
__rmul__
__ror__
__round__
__rpow__
__rrshift__
__rshift__
__rsub__
__rtruediv__
__rxor__
__setattr__
__setitem__
__sub__
__truediv__
__trunc__
__xor__