|
| 1 | +""" |
| 2 | + PyArray{T,N,R,M,L}(o) |
| 3 | +
|
| 4 | +Interpret the Python array `o` as a Julia array. |
| 5 | +
|
| 6 | +The input may be anything supporting the buffer protocol or the numpy array interface. |
| 7 | +This includes, `bytes`, `bytearray`, `array.array`, `numpy.ndarray`, `pandas.Series`. |
| 8 | +
|
| 9 | +- `T` is the (Julia) element type. |
| 10 | +- `N` is the number of dimensions. |
| 11 | +- `R` is the type of elements of the underlying buffer (which may be different from `T` to allow some basic conversion). |
| 12 | +- `M` is true if the array is mutable. |
| 13 | +- `L` is true if the array supports fast linear indexing. |
| 14 | +""" |
| 15 | +mutable struct PyArray{T,N,R,M,L} <: AbstractArray{T,N} |
| 16 | + ref :: PyRef |
| 17 | + ptr :: Ptr{R} |
| 18 | + size :: NTuple{N,Int} |
| 19 | + length :: Int |
| 20 | + bytestrides :: NTuple{N,Int} |
| 21 | + handle :: Any |
| 22 | +end |
| 23 | +const PyVector{T,R,M,L} = PyArray{T,1,R,M,L} |
| 24 | +const PyMatrix{T,R,M,L} = PyArray{T,2,R,M,L} |
| 25 | +export PyArray, PyVector, PyMatrix |
| 26 | + |
| 27 | +ispyreftype(::Type{<:PyArray}) = true |
| 28 | +pyptr(x::PyArray) = pyptr(x.ref) |
| 29 | +Base.unsafe_convert(::Type{CPyPtr}, x::PyArray) = pyptr(x.ref) |
| 30 | +C.PyObject_TryConvert__initial(o, ::Type{T}) where {T<:PyArray} = CTryConvertRule_trywrapref(o, T) |
| 31 | + |
| 32 | +function PyArray{T,N,R,M,L}(o::PyRef, info) where {T,N,R,M,L} |
| 33 | + # T - array element type |
| 34 | + T isa Type || error("T must be a type, got T=$T") |
| 35 | + |
| 36 | + # N - number of dimensions |
| 37 | + N isa Integer || error("N must be an integer, got N=$N") |
| 38 | + N isa Int || return PyArray{T, Int(N), R, M, L}(o, info) |
| 39 | + N == info.ndims || error("source dimension is $(info.ndims), got N=$N") |
| 40 | + |
| 41 | + # R - buffer element type |
| 42 | + R isa Type || error("R must be a type, got R=$R") |
| 43 | + Base.allocatedinline(R) || error("source elements must be allocated inline, got R=$R") |
| 44 | + Base.aligned_sizeof(R) == info.elsize || error("source elements must have size $(info.elsize), got R=$R") |
| 45 | + |
| 46 | + # M - mutable |
| 47 | + M isa Bool || error("M must be true or false, got M=$M") |
| 48 | + !M || info.mutable || error("source is immutable, got M=$M") |
| 49 | + |
| 50 | + bytestrides = info.bytestrides |
| 51 | + size = info.size |
| 52 | + |
| 53 | + # L - linear indexable |
| 54 | + L isa Bool || error("L must be true or false, got L=$L") |
| 55 | + !L || N ≤ 1 || size_to_fstrides(bytestrides[1], size...) == bytestrides || error("not linearly indexable, got L=$L") |
| 56 | + |
| 57 | + PyArray{T, N, R, M, L}(PyRef(o), Ptr{R}(info.ptr), size, N==0 ? 1 : prod(size), bytestrides, info.handle) |
| 58 | +end |
| 59 | +PyArray{T,N,R,M}(o::PyRef, info) where {T,N,R,M} = PyArray{T,N,R,M, N≤1 || size_to_fstrides(info.bytestrides[1], info.size...) == info.bytestrides}(o, info) |
| 60 | +PyArray{T,N,R}(o::PyRef, info) where {T,N,R} = PyArray{T,N,R, info.mutable}(o, info) |
| 61 | +PyArray{T,N}(o::PyRef, info) where {T,N} = PyArray{T,N, info.eltype}(o, info) |
| 62 | +PyArray{T}(o::PyRef, info) where {T} = PyArray{T, info.ndims}(o, info) |
| 63 | +PyArray{<:Any,N}(o::PyRef, info) where {N} = PyArray{pyarray_default_T(info.eltype), N}(o, info) |
| 64 | +PyArray(o::PyRef, info) = PyArray{pyarray_default_T(info.eltype)}(o, info) |
| 65 | + |
| 66 | +(::Type{A})(o; opts...) where {A<:PyArray} = begin |
| 67 | + ref = PyRef(o) |
| 68 | + info = pyarray_info(ref; opts...) |
| 69 | + info = ( |
| 70 | + ndims = Int(info.ndims), |
| 71 | + eltype = info.eltype :: Type, |
| 72 | + elsize = Int(info.elsize), |
| 73 | + mutable = info.mutable :: Bool, |
| 74 | + bytestrides = NTuple{Int(info.ndims), Int}(info.bytestrides), |
| 75 | + size = NTuple{Int(info.ndims), Int}(info.size), |
| 76 | + ptr = Ptr{Cvoid}(info.ptr), |
| 77 | + handle = info.handle, |
| 78 | + ) |
| 79 | + A(ref, info) |
| 80 | +end |
| 81 | + |
| 82 | +function pyarray_info(ref; buffer=true, array=true, copy=true) |
| 83 | + if array && pyhasattr(ref, "__array_interface__") |
| 84 | + pyconvertdescr(x) = begin |
| 85 | + @py ``` |
| 86 | + def convert(x): |
| 87 | + def fix(x): |
| 88 | + a = x[0] |
| 89 | + a = (a, a) if isinstance(a, str) else (a[0], a[1]) |
| 90 | + b = x[1] |
| 91 | + c = x[2] if len(x)>2 else 1 |
| 92 | + return (a, b, c) |
| 93 | + if x is None or isinstance(x, str): |
| 94 | + return x |
| 95 | + else: |
| 96 | + return [fix(y) for y in x] |
| 97 | + $(r::Union{Nothing,String,Vector{Tuple{Tuple{String,String}, PyObject, Int}}}) = convert($x) |
| 98 | + ``` |
| 99 | + r isa Vector ? [(a, pyconvertdescr(b), c) for (a,b,c) in r] : r |
| 100 | + end |
| 101 | + ai = pygetattr(ref, "__array_interface__") |
| 102 | + pyconvert(Int, ai["version"]) == 3 || error("wrong version") |
| 103 | + size = pyconvert(Tuple{Vararg{Int}}, ai["shape"]) |
| 104 | + ndims = length(size) |
| 105 | + typestr = pyconvert(String, ai["typestr"]) |
| 106 | + descr = pyconvertdescr(ai.get("descr")) |
| 107 | + eltype = pytypestrdescr_to_type(typestr, descr) |
| 108 | + elsize = Base.aligned_sizeof(eltype) |
| 109 | + strides = pyconvert(Union{Nothing, Tuple{Vararg{Int}}}, ai.get("strides")) |
| 110 | + strides === nothing && (strides = size_to_cstrides(elsize, size...)) |
| 111 | + pyis(ai.get("mask"), pynone()) || error("mask not supported") |
| 112 | + offset = pyconvert(Union{Nothing, Int}, ai.get("offset")) |
| 113 | + offset === nothing && (offset = 0) |
| 114 | + data = pyconvert(Union{PyObject, Tuple{UInt, Bool}, Nothing}, ai.get("data")) |
| 115 | + if data isa Tuple |
| 116 | + ptr = Ptr{Cvoid}(data[1]) |
| 117 | + mutable = !data[2] |
| 118 | + handle = (ref, ai) |
| 119 | + else |
| 120 | + buf = PyBuffer(data === nothing ? ref : data) |
| 121 | + ptr = buf.buf |
| 122 | + mutable = !buf.readonly |
| 123 | + handle = (ref, ai, buf) |
| 124 | + end |
| 125 | + return (ndims=ndims, eltype=eltype, elsize=elsize, mutable=mutable, bytestrides=strides, size=size, ptr=ptr, handle=handle) |
| 126 | + end |
| 127 | + if array && pyhasattr(ref, "__array_struct__") |
| 128 | + # TODO |
| 129 | + end |
| 130 | + if buffer && C.PyObject_CheckBuffer(ref) |
| 131 | + try |
| 132 | + b = PyBuffer(ref, C.PyBUF_RECORDS_RO) |
| 133 | + return (ndims=b.ndim, eltype=b.eltype, elsize=b.itemsize, mutable=!b.readonly, bytestrides=b.strides, size=b.shape, ptr=b.buf, handle=b) |
| 134 | + catch |
| 135 | + end |
| 136 | + end |
| 137 | + if array && copy && pyhasattr(ref, "__array__") |
| 138 | + try |
| 139 | + return pyarray_info(pycall(PyRef, pygetattr(PyRef, ref, "__array__")); buffer=buffer, array=array, copy=false) |
| 140 | + catch |
| 141 | + end |
| 142 | + end |
| 143 | + error("given object does not support the buffer protocol or array interface") |
| 144 | +end |
| 145 | + |
| 146 | +Base.isimmutable(x::PyArray{T,N,R,M,L}) where {T,N,R,M,L} = !M |
| 147 | +Base.size(x::PyArray) = x.size |
| 148 | +Base.length(x::PyArray) = x.length |
| 149 | +Base.IndexStyle(::Type{PyArray{T,N,R,M,L}}) where {T,N,R,M,L} = L ? Base.IndexLinear() : Base.IndexCartesian() |
| 150 | + |
| 151 | +Base.@propagate_inbounds Base.getindex(x::PyArray{T,N,R,M,L}, i::Vararg{Int,N2}) where {T,N,R,M,L,N2} = |
| 152 | + if (N2==N) || (L && N2==1) |
| 153 | + @boundscheck checkbounds(x, i...) |
| 154 | + pyarray_load(T, x.ptr + pyarray_offset(x, i...)) |
| 155 | + else |
| 156 | + invoke(getindex, Tuple{AbstractArray{T,N}, Vararg{Int,N2}}, x, i...) |
| 157 | + end |
| 158 | + |
| 159 | +Base.@propagate_inbounds Base.setindex!(x::PyArray{T,N,R,true,L}, v, i::Vararg{Int,N2}) where {T,N,R,L,N2} = |
| 160 | + if (N2==N) || (L && N2==1) |
| 161 | + @boundscheck checkbounds(x, i...) |
| 162 | + pyarray_store!(x.ptr + pyarray_offset(x, i...), convert(T, v)) |
| 163 | + x |
| 164 | + else |
| 165 | + invoke(setindex!, Tuple{AbstractArray{T,N}, typeof(v), Vararg{Int,N2}}, x, v, i...) |
| 166 | + end |
| 167 | + |
| 168 | +pyarray_default_T(::Type{R}) where {R} = R |
| 169 | +pyarray_default_T(::Type{C.PyObjectRef}) = PyObject |
| 170 | + |
| 171 | +pyarray_load(::Type{T}, p::Ptr{T}) where {T} = unsafe_load(p) |
| 172 | +pyarray_load(::Type{T}, p::Ptr{C.PyObjectRef}) where {T} = begin |
| 173 | + o = unsafe_load(p).ptr |
| 174 | + isnull(o) && throw(UndefRefError()) |
| 175 | + ism1(C.PyObject_Convert(o, T)) && pythrow() |
| 176 | + takeresult(T) |
| 177 | +end |
| 178 | + |
| 179 | +pyarray_store!(p::Ptr{T}, v::T) where {T} = unsafe_store!(p, v) |
| 180 | +pyarray_store!(p::Ptr{C.PyObjectRef}, v::T) where {T} = begin |
| 181 | + o = C.PyObject_From(v) |
| 182 | + isnull(o) && pythrow() |
| 183 | + C.Py_DecRef(unsafe_load(p).ptr) |
| 184 | + unsafe_store!(p, C.PyObjectRef(o)) |
| 185 | +end |
| 186 | + |
| 187 | +pyarray_offset(x::PyArray{T,N,R,M,true}, i::Int) where {T,N,R,M} = |
| 188 | + N==0 ? 0 : (i-1) * x.bytestrides[1] |
| 189 | + |
| 190 | +pyarray_offset(x::PyArray{T,1,R,M,true}, i::Int) where {T,R,M} = |
| 191 | + (i-1) .* x.bytestrides[1] |
| 192 | + |
| 193 | +pyarray_offset(x::PyArray{T,N}, i::Vararg{Int,N}) where {T,N} = |
| 194 | + sum((i .- 1) .* x.bytestrides) |
0 commit comments