Skip to content

Commit 82742f2

Browse files
committed
Support range and step range for index access
1 parent ad52143 commit 82742f2

File tree

6 files changed

+180
-15
lines changed

6 files changed

+180
-15
lines changed

ext/pycall/libpython.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ pycall_init_libpython_api_table(VALUE libpython_handle)
150150
INIT_API_TABLE_ENTRY(PyTuple_GetItem, required);
151151
INIT_API_TABLE_ENTRY(PyTuple_SetItem, required);
152152

153+
INIT_API_TABLE_ENTRY(PySlice_New, required);
154+
153155
INIT_API_TABLE_ENTRY(PyIter_Next, required);
154156

155157
INIT_API_TABLE_ENTRY(PyErr_Occurred, required);

ext/pycall/pycall.c

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,35 @@ pycall_libpython_helpers_m_define_wrapper_method(VALUE mod, VALUE wrapper, VALUE
10071007
return Qnil;
10081008
}
10091009

1010+
static PyObject *
1011+
pycall_convert_index(VALUE index)
1012+
{
1013+
PyObject *pyobj;
1014+
1015+
if (RB_TYPE_P(index, T_ARRAY) && RARRAY_LEN(index) == 1) {
1016+
index = RARRAY_AREF(index, 0);
1017+
}
1018+
if (RB_TYPE_P(index, T_ARRAY)) {
1019+
long i, n = RARRAY_LEN(index);
1020+
pyobj = Py_API(PyTuple_New)(n);
1021+
for (i = 0; i < n; ++i) {
1022+
PyObject *pytem = pycall_convert_index(RARRAY_AREF(index, i)); /* New reference */
1023+
Py_API(PyTuple_SetItem)(pyobj, i, pytem); /* Steal reference */
1024+
}
1025+
}
1026+
else if (rb_obj_is_kind_of(index, rb_cRange)) {
1027+
pyobj = pycall_pyslice_from_ruby(index); /* New refrence */
1028+
}
1029+
else if (pycall_obj_is_step_range(index)) {
1030+
pyobj = pycall_pyslice_from_ruby(index); /* New refrence */
1031+
}
1032+
else {
1033+
pyobj = pycall_pyobject_from_ruby(index); /* New reference */
1034+
}
1035+
1036+
return pyobj;
1037+
}
1038+
10101039
static VALUE
10111040
pycall_libpython_helpers_m_getitem(VALUE mod, VALUE pyptr, VALUE key)
10121041
{
@@ -1019,7 +1048,7 @@ pycall_libpython_helpers_m_getitem(VALUE mod, VALUE pyptr, VALUE key)
10191048

10201049
pyobj = get_pyobj_ptr(pyptr);
10211050

1022-
pyobj_key = pycall_pyobject_from_ruby(key);
1051+
pyobj_key = pycall_convert_index(key);
10231052

10241053
pyobj_v = Py_API(PyObject_GetItem)(pyobj, pyobj_key);
10251054
if (!pyobj_v) {
@@ -1038,7 +1067,7 @@ pycall_libpython_helpers_m_setitem(VALUE mod, VALUE pyptr, VALUE key, VALUE v)
10381067
int res;
10391068

10401069
pyobj = check_get_pyobj_ptr(pyptr, NULL);
1041-
pyobj_key = pycall_pyobject_from_ruby(key);
1070+
pyobj_key = pycall_convert_index(key);
10421071
pyobj_value = pycall_pyobject_from_ruby(v);
10431072

10441073
res = Py_API(PyObject_SetItem)(pyobj, pyobj_key, pyobj_value);
@@ -1058,7 +1087,7 @@ pycall_libpython_helpers_m_delitem(VALUE mod, VALUE pyptr, VALUE key, VALUE v)
10581087
int res;
10591088

10601089
pyobj = check_get_pyobj_ptr(pyptr, NULL);
1061-
pyobj_key = pycall_pyobject_from_ruby(key);
1090+
pyobj_key = pycall_convert_index(key);
10621091

10631092
res = Py_API(PyObject_DelItem)(pyobj, pyobj_key);
10641093
if (res == -1) {
@@ -1731,6 +1760,40 @@ pycall_pydict_from_ruby(VALUE obj)
17311760
return pydictobj;
17321761
}
17331762

1763+
PyObject *
1764+
pycall_pyslice_from_ruby(VALUE obj)
1765+
{
1766+
VALUE begin, end, step = Qnil;
1767+
int exclude_end;
1768+
PyObject *pystart, *pystop, *pystep, *pyslice;
1769+
1770+
if (rb_obj_is_kind_of(obj, rb_cRange)) {
1771+
pycall_extract_range(obj, &begin, &end, &exclude_end, NULL);
1772+
}
1773+
else if (pycall_obj_is_step_range(obj)) {
1774+
pycall_extract_range(obj, &begin, &end, &exclude_end, &step);
1775+
}
1776+
else {
1777+
rb_raise(rb_eTypeError, "unexpected argument type %s (expected Range or Enumerator generated by Range#step)", rb_class2name(CLASS_OF(obj)));
1778+
}
1779+
1780+
if (!exclude_end) {
1781+
end = SSIZET2NUM(NUM2SSIZET(end) + 1); /* TODO: limit check */
1782+
}
1783+
1784+
pystart = pycall_pyobject_from_ruby(begin);
1785+
pystop = pycall_pyobject_from_ruby(end);
1786+
pystep = pycall_pyobject_from_ruby(step);
1787+
1788+
pyslice = Py_API(PySlice_New)(pystart, pystop, pystep);
1789+
/* PySlice_New increments reference counts of pystart, pystop, and pystep */
1790+
pycall_Py_DecRef(pystart);
1791+
pycall_Py_DecRef(pystop);
1792+
pycall_Py_DecRef(pystep);
1793+
1794+
return pyslice;
1795+
}
1796+
17341797
VALUE
17351798
pycall_pyerror_new(PyObject *type, PyObject *value, PyObject *traceback)
17361799
{

ext/pycall/pycall_internal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,8 @@ typedef struct {
577577
PyObject * (* PyTuple_GetItem)(PyObject *, Py_ssize_t);
578578
int (* PyTuple_SetItem)(PyObject *, Py_ssize_t, PyObject *);
579579

580+
PyObject * (* PySlice_New)(PyObject *, PyObject *, PyObject *);
581+
580582
PyObject * (* PyIter_Next)(PyObject *);
581583

582584
PyObject * (* PyErr_Occurred)(void);
@@ -653,6 +655,7 @@ PyObject *pycall_pystring_from_ruby(VALUE);
653655
PyObject *pycall_pytuple_from_ruby(VALUE);
654656
PyObject *pycall_pylist_from_ruby(VALUE);
655657
PyObject *pycall_pydict_from_ruby(VALUE);
658+
PyObject *pycall_pyslice_from_ruby(VALUE);
656659

657660
NORETURN(void pycall_pyerror_fetch_and_raise(char const *format, ...));
658661

@@ -662,6 +665,9 @@ PyObject *pycall_pystring_from_formatv(char const *format, va_list vargs);
662665

663666
VALUE pycall_pyrubyptr_new(PyObject *pyrubyobj);
664667

668+
int pycall_obj_is_step_range(VALUE obj);
669+
int pycall_extract_range(VALUE obj, VALUE *pbegin, VALUE *pend, int *pexclude_end, VALUE *pstep);
670+
665671
void pycall_gcguard_register(PyObject *, VALUE);
666672
void pycall_gcguard_delete(PyObject *);
667673
void pycall_gcguard_register_pyrubyobj(PyObject *);

ext/pycall/range.c

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#include "pycall_internal.h"
2+
3+
struct enumerator_head {
4+
VALUE obj;
5+
ID meth;
6+
VALUE args;
7+
};
8+
9+
int
10+
pycall_obj_is_step_range(VALUE obj)
11+
{
12+
struct enumerator_head *eh;
13+
14+
if (!RB_TYPE_P(obj, T_DATA)) {
15+
return 0;
16+
}
17+
18+
if (!rb_obj_is_kind_of(obj, rb_cEnumerator)) {
19+
return 0;
20+
}
21+
22+
eh = (struct enumerator_head *)DATA_PTR(obj);
23+
24+
if (!rb_obj_is_kind_of(eh->obj, rb_cRange)) {
25+
return 0;
26+
}
27+
if (eh->meth == rb_intern("step")) {
28+
if (!RB_TYPE_P(eh->args, T_ARRAY)) {
29+
return 0;
30+
}
31+
return (RARRAY_LEN(eh->args) == 1);
32+
}
33+
34+
return 0;
35+
}
36+
37+
int
38+
pycall_extract_range(VALUE obj, VALUE *pbegin, VALUE *pend, int *pexclude_end, VALUE *pstep)
39+
{
40+
ID id_begin, id_end, id_exclude_end;
41+
VALUE begin, end, exclude_end, step = Qnil;
42+
43+
CONST_ID(id_begin, "begin");
44+
CONST_ID(id_end, "end");
45+
CONST_ID(id_exclude_end, "exclude_end?");
46+
47+
if (rb_obj_is_kind_of(obj, rb_cRange)) {
48+
begin = rb_funcallv(obj, id_begin, 0, 0);
49+
end = rb_funcallv(obj, id_end, 0, 0);
50+
exclude_end = rb_funcallv(obj, id_exclude_end, 0, 0);
51+
}
52+
else if (pycall_obj_is_step_range(obj)) {
53+
struct enumerator_head *eh = (struct enumerator_head *)DATA_PTR(obj);
54+
begin = rb_funcallv(eh->obj, id_begin, 0, 0);
55+
end = rb_funcallv(eh->obj, id_end, 0, 0);
56+
exclude_end = rb_funcallv(eh->obj, id_exclude_end, 0, 0);
57+
step = RARRAY_AREF(eh->args, 0);
58+
}
59+
else {
60+
return 0;
61+
}
62+
63+
if (pbegin) *pbegin = begin;
64+
if (pend) *pend = end;
65+
if (pexclude_end) *pexclude_end = RTEST(exclude_end);
66+
if (pstep) *pstep = step;
67+
68+
return 1;
69+
}

lib/pycall/pyobject_wrapper.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,10 @@ def #{op}(other)
7676
end
7777

7878
def [](*key)
79-
if key.length == 1
80-
key = key[0]
81-
else
82-
keys = PyCall::Tuple.new(key)
83-
end
8479
LibPython::Helpers.getitem(__pyptr__, key)
8580
end
8681

8782
def []=(*key, value)
88-
if key.length == 1
89-
key = key[0]
90-
else
91-
key = PyCall::Tuple.new(key)
92-
end
9383
LibPython::Helpers.setitem(__pyptr__, key, value)
9484
end
9585

spec/pycall/pyobject_wrapper_spec.rb

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,46 @@ module PyCall
143143
end
144144

145145
describe '#[]' do
146-
pending
146+
context 'when the given index is a Range' do
147+
specify do
148+
list = PyCall::List.new([*1..10])
149+
expect(list[1..-2]).to eq(PyCall::List.new([*2..9]))
150+
expect(list[1...-2]).to eq(PyCall::List.new([*2..8]))
151+
end
152+
end
153+
154+
context 'when the given index is an Enumerable that is created by Range#step' do
155+
specify do
156+
list = PyCall::List.new([*1..10])
157+
expect(list[(1..-2).step(2)]).to eq(PyCall::List.new([2, 4, 6, 8]))
158+
end
159+
end
147160
end
148161

149162
describe '#[]=' do
150-
pending
163+
context 'when the given index is a Range' do
164+
specify do
165+
list = PyCall::List.new([*1..10])
166+
list[1..-2] = [100, 200, 300]
167+
expect(list).to eq(PyCall::List.new([1, 100, 200, 300, 10]))
168+
169+
list = PyCall::List.new([*1..10])
170+
list[1...-2] = [100, 200, 300]
171+
expect(list).to eq(PyCall::List.new([1, 100, 200, 300, 9, 10]))
172+
end
173+
end
174+
175+
context 'when the given index is an Enumerable that is created by Range#step' do
176+
specify do
177+
list = PyCall::List.new([*1..10])
178+
list[(1..-3).step(2)] = [100, 200, 300, 400]
179+
expect(list).to eq(PyCall::List.new([1, 100, 3, 200, 5, 300, 7, 400, 9, 10]))
180+
181+
list = PyCall::List.new([*1..10])
182+
list[(1...-3).step(2)] = [100, 200, 300]
183+
expect(list).to eq(PyCall::List.new([1, 100, 3, 200, 5, 300, 7, 8, 9, 10]))
184+
end
185+
end
151186
end
152187

153188
describe '#call' do

0 commit comments

Comments
 (0)