Skip to content

Commit 8f3c10a

Browse files
author
Christopher Doris
committed
dev
1 parent 896864d commit 8f3c10a

File tree

12 files changed

+443
-210
lines changed

12 files changed

+443
-210
lines changed

src/PyArray.jl

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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)

src/PyObject.jl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@ pylazyobject(mk) = PyObject(Val(:lazy), mk)
2929

3030
C.PyObject_TryConvert__initial(o, ::Type{PyObject}) = C.putresult(pyborrowedobject(o))
3131

32-
Base.convert(::Type{Any}, x::PyObject) = x
3332
Base.convert(::Type{PyObject}, x::PyObject) = x
33+
Base.convert(::Type{Any}, x::PyObject) = x
34+
Base.convert(::Type{T}, x::PyObject) where {T} = x isa T ? x : pyconvert(T, x)
3435
Base.convert(::Type{PyObject}, x) = PyObject(x)
3536

36-
Base.convert(::Type{T}, x::PyObject) where {T} = pyconvert(T, x)
37-
3837
### Cache some common values
3938

4039
const _pynone = pylazyobject(() -> pynone(PyRef))
@@ -137,8 +136,8 @@ function Base.show(io::IO, ::MIME"text/plain", o::PyObject)
137136
end
138137
end
139138

140-
Base.show(io::IO, mime::MIME, o::PyObject) = _py_mime_show(io, mime, o)
141-
Base.showable(mime::MIME, o::PyObject) = _py_mime_showable(mime, o)
139+
Base.show(io::IO, mime::_py_mimetype, o::PyObject) = _py_mime_show(io, mime, o)
140+
Base.showable(mime::_py_mimetype, o::PyObject) = _py_mime_showable(mime, o)
142141

143142
### PROPERTIES
144143

src/PyPandasDataFrame.jl

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
1-
# const pypandas = pylazyobject(() -> pyimport("pandas"))
2-
# const pypandasdataframetype = pylazyobject(() -> pypandas.DataFrame)
1+
asvector(x::AbstractVector) = x
2+
asvector(x) = collect(x)
33

4-
# asvector(x::AbstractVector) = x
5-
# asvector(x) = collect(x)
6-
7-
# """
8-
# pycolumntable(src)
4+
"""
5+
pycolumntable([T=PyObject,] src) :: T
96
10-
# Construct a "column table" from the `Tables.jl`-compatible table `src`, namely a Python `dict` mapping column names to column vectors.
11-
# """
12-
# function pycolumntable(src)
13-
# cols = Tables.columns(src)
14-
# pydict_fromiter(pystr(String(n)) => asvector(Tables.getcolumn(cols, n)) for n in Tables.columnnames(cols))
15-
# end
16-
# pycolumntable(; cols...) = pycolumntable(cols)
17-
# export pycolumntable
7+
Construct a "column table" from the `Tables.jl`-compatible table `src`, namely a Python `dict` mapping column names to column vectors.
8+
"""
9+
function pycolumntable(::Type{T}, src) where {T}
10+
cols = Tables.columns(src)
11+
pydict(T, pystr(String(n)) => asvector(Tables.getcolumn(cols, n)) for n in Tables.columnnames(cols))
12+
end
13+
pycolumntable(::Type{T}; cols...) where {T} = pycolumntable(T, cols)
14+
pycolumntable(src) = pycolumntable(PyObject, src)
15+
pycolumntable(; opts...) = pycolumntable(PyObject, opts)
16+
export pycolumntable
1817

19-
# """
20-
# pyrowtable(src)
18+
"""
19+
pyrowtable([T=PyObject,] src) :: T
2120
22-
# Construct a "row table" from the `Tables.jl`-compatible table `src`, namely a Python `list` of rows, each row being a Python `dict` mapping column names to values.
23-
# """
24-
# function pyrowtable(src)
25-
# rows = Tables.rows(src)
26-
# names = Tables.columnnames(rows)
27-
# pynames = [pystr(String(n)) for n in names]
28-
# pylist_fromiter(pydict_fromiter(pn => Tables.getcolumn(row, n) for (n,pn) in zip(names, pynames)) for row in rows)
29-
# end
30-
# pyrowtable(; cols...) = pyrowtable(cols)
31-
# export pyrowtable
21+
Construct a "row table" from the `Tables.jl`-compatible table `src`, namely a Python `list` of rows, each row being a Python `dict` mapping column names to values.
22+
"""
23+
function pyrowtable(::Type{T}, src) where {T}
24+
rows = Tables.rows(src)
25+
names = Tables.columnnames(rows)
26+
pynames = [pystr(String(n)) for n in names]
27+
pylist(T, pydict(pn => Tables.getcolumn(row, n) for (n,pn) in zip(names, pynames)) for row in rows)
28+
end
29+
pyrowtable(::Type{T}; cols...) where {T} = pyrowtable(T, cols)
30+
pyrowtable(src) = pyrowtable(PyObject, src)
31+
pyrowtable(; opts...) = pyrowtable(PyObject, opts)
32+
export pyrowtable
3233

3334
# """
34-
# pypandasdataframe([src]; ...)
35+
# pypandasdataframe([T=PyObject,] [src]; ...) :: T
3536

3637
# Construct a pandas dataframe from `src`.
3738

@@ -77,8 +78,8 @@ Base.unsafe_convert(::Type{CPyPtr}, df::PyPandasDataFrame) = checknull(pyptr(df)
7778
C.PyObject_TryConvert__initial(o, ::Type{PyPandasDataFrame}) = C.putresult(PyPandasDataFrame(pyborrowedref(o)))
7879

7980
Base.show(io::IO, x::PyPandasDataFrame) = print(io, pystr(String, x))
80-
Base.show(io::IO, mime::MIME, o::PyPandasDataFrame) = _py_mime_show(io, mime, o)
81-
Base.showable(mime::MIME, o::PyPandasDataFrame) = _py_mime_showable(mime, o)
81+
Base.show(io::IO, mime::_py_mimetype, o::PyPandasDataFrame) = _py_mime_show(io, mime, o)
82+
Base.showable(mime::_py_mimetype, o::PyPandasDataFrame) = _py_mime_showable(mime, o)
8283

8384
### Tables.jl / TableTraits.jl integration
8485

src/Python.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ include("PySet.jl")
9494
include("PyIterable.jl")
9595
include("PyIO.jl")
9696
include("PyBuffer.jl")
97+
include("PyArray.jl")
9798
include("PyObjectArray.jl")
9899
include("PyPandasDataFrame.jl")
99100

src/builtins.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -779,23 +779,25 @@ export pyellipsis
779779

780780
### MULTIMEDIA DISPLAY
781781

782-
for (mime, method) in [
782+
const _py_mimes = [
783783
(MIME"text/html", "_repr_html_"), (MIME"text/markdown", "_repr_markdown_"),
784784
(MIME"text/json", "_repr_json_"), (MIME"application/javascript", "_repr_javascript_"),
785785
(MIME"application/pdf", "_repr_pdf_"), (MIME"image/jpeg", "_repr_jpeg_"),
786786
(MIME"image/png", "_repr_png_"), (MIME"image/svg+xml", "_repr_svg_"),
787787
(MIME"text/latex", "_repr_latex_")
788-
]
788+
]
789+
const _py_mimetype = Union{map(first, _py_mimes)...}
789790

791+
for (mime, method) in _py_mimes
790792
T = istextmime(mime()) ? String : Vector{UInt8}
791793
@eval begin
792794
_py_mime_show(io::IO, mime::$mime, o) = begin
793795
try
794-
x = pycall(PyRef, pygetattr(Ref, o, $method))
796+
x = pycall(PyRef, pygetattr(PyRef, o, $method))
795797
pyis(x, pynone(PyRef)) || return write(io, pyconvert($T, x))
796798
catch
797799
end
798-
throw(MethodError(show, (io, mime, o)))
800+
throw(MethodError(_py_mime_show, (io, mime, o)))
799801
end
800802
_py_mime_showable(::$mime, o) = begin
801803
try

src/cpython/CPython.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ include("juliatype.jl")
6262
include("juliadict.jl")
6363
include("juliaarray.jl")
6464
include("juliavector.jl")
65+
include("juliamodule.jl")
6566
include("arg.jl")
6667

6768
__init__() = begin

0 commit comments

Comments
 (0)