Library of Assembled Shared Sources
 
Loading...
Searching...
No Matches
subscript.h
Go to the documentation of this file.
1/** @file
2 * @author Bram de Greve (bram@cocamware.com)
3 * @author Tom De Muer (tom@cocamware.com)
4 *
5 * *** BEGIN LICENSE INFORMATION ***
6 *
7 * The contents of this file are subject to the Common Public Attribution License
8 * Version 1.0 (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
10 * http://lass.sourceforge.net/cpal-license. The License is based on the
11 * Mozilla Public License Version 1.1 but Sections 14 and 15 have been added to cover
12 * use of software over a computer network and provide for limited attribution for
13 * the Original Developer. In addition, Exhibit A has been modified to be consistent
14 * with Exhibit B.
15 *
16 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
17 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
18 * language governing rights and limitations under the License.
19 *
20 * The Original Code is LASS - Library of Assembled Shared Sources.
21 *
22 * The Initial Developer of the Original Code is Bram de Greve and Tom De Muer.
23 * The Original Developer is the Initial Developer.
24 *
25 * All portions of the code written by the Initial Developer are:
26 * Copyright (C) 2026 the Initial Developer.
27 * All Rights Reserved.
28 *
29 * Contributor(s):
30 *
31 * Alternatively, the contents of this file may be used under the terms of the
32 * GNU General Public License Version 2 or later (the GPL), in which case the
33 * provisions of GPL are applicable instead of those above. If you wish to allow use
34 * of your version of this file only under the terms of the GPL and not to allow
35 * others to use your version of this file under the CPAL, indicate your decision by
36 * deleting the provisions above and replace them with the notice and other
37 * provisions required by the GPL License. If you do not delete the provisions above,
38 * a recipient may use your version of this file under either the CPAL or the GPL.
39 *
40 * *** END LICENSE INFORMATION ***
41 */
42
43#pragma once
44
45#include "python_common.h"
46
47namespace lass::python
48{
49
50/** @defgroup PythonSubscript Subscript Protocol
51 * @brief Support for index- and slice-based access, assignment and deletion.
52 *
53 * To support `obj[key]` subscription on an exported class, assign your `__getitem__`, `__setitem__` and `__delitem__`
54 * overloads to one of two families of special slots:
55 *
56 * | operation | sequence protocol (adjusted index) | mapping protocol (raw key) |
57 * |--------------|--------------------------------------------------|----------------------------|
58 * | `v = obj[i]` | `methods::seq_getitem_` (= `methods::_getitem_`) | `methods::map_getitem_` |
59 * | `obj[i] = v` | `methods::seq_setitem_` (= `methods::_setitem_`) | `methods::map_setitem_` |
60 * | `del obj[i]` | n/a | `methods::map_delitem_` |
61 *
62 *
63 * ### Integer indices only
64 *
65 * Assign a `getitem(Py_ssize_t)` method to `methods::_getitem_`. Python adjusts negative indices automatically before
66 * calling your method, so that `obj[-1]` will be adjusted to `size() - 1`. Your method only needs to check bounds,
67 * and raise an `IndexError` if the index is out of range.
68 *
69 * Default iteration `for x in obj` works automatically through this slot.
70 *
71 * ```
72 * std::string myvector_seq_getitem(const MyVector& self, Py_ssize_t index)
73 * {
74 * if (index < 0 || std::cmp_greater_equal(index, self.size()))
75 * {
76 * throw lass::python::PythonException(PyExc_IndexError, "index out of range");
77 * }
78 * return self.at(static_cast<size_t>(index));
79 * }
80 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_seq_getitem, methods::seq_getitem_)
81 * ```
82 *
83 * If your method already throws a `std::out_of_range` exception for out of bound indices, like `std::vector<T>::at`,
84 * this will be automatically translated to an `IndexError` in Python.
85 *
86 * If the index uses a type other than `Py_ssize_t`, you may get an `OverflowError` instead of `IndexError` if an
87 * index doesn't fit the type. In particular, with `size_t` you will get an `OverflowError` for negative numbers.
88 * To be fully compliant, roll your own bounds checks as above. But if the `OverflowError` is acceptable, the
89 * method shrinks to:
90 *
91 * ```
92 * std::string myvector_seq_getitem(const MyVector& self, size_t index)
93 * {
94 * return self.at(index);
95 * }
96 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_seq_getitem, methods::seq_getitem_)
97 * ```
98 *
99 *
100 * ### Slices
101 *
102 * `slice` arguments can only be received through the mapping protocol, as a Slice parameter.
103 * Assign a `getslice(Slice)` method to `methods::map_getitem_`.
104 *
105 * ```
106 * std::vector<std::string> myvector_getslice(const MyVector& self, Slice slice)
107 * {
108 * auto sliceLength = slice.adjustIndices(self.size());
109 * auto index = slice.start;
110 * std::vector<std::string> result;
111 * result.reserve(sliceLength);
112 * for (Py_ssize_t i = 0; i < sliceLength; ++i)
113 * {
114 * result.push_back(self.at(static_cast<size_t>(index)));
115 * index += slice.step;
116 * }
117 * return result;
118 * }
119 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_getslice, methods::map_getitem_)
120 * ```
121 *
122 * @note This has an important consequence: once a class has any `__getitem__` method on the `methods::map_getitem_`
123 * slot, Python ignores the `methods::seq_getitem_` for subscription. Move the integer overload to
124 * `methods::map_getitem_` too, but Python will no longer automatically adjust negative indices! Use
125 * `adjustIndex()` to manually adjust the negative indices; as a bonus, it will also raise `IndexError` for
126 * out-of-range indices.
127 *
128 * ```
129 * std::string myvector_map_getitem(const MyVector& self, Py_ssize_t index)
130 * {
131 * index = adjustIndex(index, self.size());
132 * return self.at(static_cast<size_t>(index));
133 * }
134 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_map_getitem, methods::map_getitem_)
135 * ```
136 *
137 *
138 * ### Iteration
139 *
140 * Without a `__getitem__` on the `methods::seq_getitem_` slot, default iteration no longer works. You have two options:
141 *
142 * 1. Register a proper `__iter__` function. See @ref PythonIterators for more details:
143 *
144 * ```
145 * PY_CLASS_FREE_METHOD_NAME(MyVector, makeMemberRangeViewFactory<MyVector>(&MyVector::begin, &MyVector::end), methods::_iter_)
146 * ```
147 *
148 * 2. **Also** register a `__getitem__` on `methods::seq_getitem_`. **But don't use adjustIndex() for that one:**
149 * for callers of `PySequence_GetItem(obj, index)` with `index < -len(obj)`, the index would be adjusted **twice**,
150 * potentially bringing the index in-range while it still should have been out-of-range.
151 *
152 *
153 * ### Assignment and Deletion
154 *
155 * Assignment and deletion follow the same rules through `methods::map_setitem_` and `methods::map_delitem_`. Under the
156 * hood, they share the same `Py_mp_ass_subscript` slot, but a `__setitem__` overload has key and value parameters,
157 * while a `__delitem__` overload takes only a key parameter.
158 *
159 * Putting it all together:
160 *
161 * ```
162 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_getitem, methods::map_getitem_) // (Py_ssize_t) -> T, uses adjustIndex()
163 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_getslice, methods::map_getitem_) // (Slice) -> std::vector<T>
164 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_setitem, methods::map_setitem_) // (Py_ssize_t, T), uses adjustIndex()
165 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_setslice, methods::map_setitem_) // (Slice, std::vector<T>)
166 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_delitem, methods::map_delitem_) // (Py_ssize_t), uses adjustIndex()
167 * PY_CLASS_FREE_METHOD_NAME(MyVector, myvector_delslice, methods::map_delitem_) // (Slice)
168 * PY_CLASS_FREE_METHOD_NAME(MyVector, makeMemberRangeViewFactory<MyVector>(&MyVector::begin, &MyVector::end), methods::_iter_)
169 * ```
170 *
171 * @ingroup Python
172 * @sa PythonIterators
173 */
174
175
176/** Helper type to get or return Python slice objects
177 *
178 * This helper can be used as C++ parameter type to overload `__getitem__` to support Python slice objects.
179 *
180 * See @ref PythonSubscript for more details and examples.
181 *
182 * @note Before you can use the start/stop indices, you **must** adjust them for the actual sequence length by
183 * calling adjustIndices(). This will also return the actual number of elements in the slice.
184 *
185 *
186 * @ingroup PythonSubscript
187 * @sa PythonSubscript
188 * @sa adjustIndex
189 * @sa adjustIndexEx
190 * @sa PyExportTraits<Slice>
191 */
192struct LASS_PYTHON_DLL Slice
193{
194 Py_ssize_t start; ///< start index of slice. Always the index of a valid iterator after calling adjustIndices()
195 Py_ssize_t stop; ///< stop index of slice. Can be -1 if step < 0! It's not guaranteed that `stop == start + sliceLength * step`
196 Py_ssize_t step; ///< step size of slice.
197
198 /** Adjust start and stop for actual sequence length, returning slice length
199 *
200 * @param sequenceLength actual length of the sequence to be sliced
201 * @return `sliceLength`, actual number of elements in slice
202 *
203 * @note Unlike `PySlice_AdjustIndices`, `start` will never be -1 (that case, which only happens when
204 * `sliceLength == 0` and `step < 0`, is normalized to `start = stop = 0`).
205 *
206 * @pre `sequenceLength >= 0`
207 * @pre `step != 0`
208 * @post `start >= 0 && start <= sequenceLength`
209 * @post `stop >= -1 && stop <= sequenceLength`
210 */
211 Py_ssize_t adjustIndices(Py_ssize_t sequenceLength);
212};
213
214
215
216/** Helper to adjust negative sequence indices
217 *
218 * Use adjustIndex() to adjust the raw index argument of functions that are assigned to `methods::map_getitem_`,
219 * `methods::map_setitem_`, or `methods::map_delitem_`. Negative indices will be adjusted to start counting from the
220 * end of the sequence.
221 *
222 * If the adjusted index is out of range of the sequence, a C++ PythonException exception will
223 * be thrown, containing a Python `IndexError` exception.
224 *
225 * See @ref PythonSubscript for more details.
226 *
227 * @note Only use in methods on the `methods::map_*` slots: the sequence slots receive already-adjusted indices.
228 *
229 * @param[in] index the raw index to be adjusted
230 * @param[in] sequenceLength size of the sequence to adjust the index for
231 * @return the adjusted index
232 * @throw PythonException with an `IndexError` if adjusted index is outside [0, sequenceLength)
233 *
234 * @pre `sequenceLength >= 0`
235 *
236 * @ingroup PythonSubscript
237 * @sa PythonSubscript
238 * @sa Slice
239 * @sa adjustIndexEx
240 */
241LASS_PYTHON_DLL Py_ssize_t adjustIndex(Py_ssize_t index, Py_ssize_t sequenceLength);
242
243
244
245/** Helper to adjust negative sequence indices
246 *
247 * Similar to `adjustIndex()`, but modifies `index` in-place, and returns false if the adjusted
248 * index is out of range.
249 *
250 * See @ref PythonSubscript for more details.
251 *
252 * @note Most user code should use adjustIndex().
253 *
254 * @note Only use in methods on the `methods::map_*` slots: the sequence slots receive already-adjusted indices.
255 *
256 * @param[in,out] index pointer to the raw index to be adjusted
257 * @param[in] sequenceLength size of the sequence to adjust the index for
258 * @return
259 * - true if the adjusted index is within range;
260 * - false if the adjusted index is out of range: an `IndexError` Python exception will be set.
261 *
262 * @pre `index != nullptr`
263 * @pre `sequenceLength >= 0`
264 * @post On failure, `*index` is unchanged
265 *
266 * @ingroup PythonSubscript
267 * @sa PythonSubscript
268 * @sa Slice
269 * @sa adjustIndex
270 */
271LASS_PYTHON_DLL bool adjustIndexEx(Py_ssize_t *index, Py_ssize_t sequenceLength);
272
273}
274
275// EOF
Py_ssize_t adjustIndex(Py_ssize_t index, Py_ssize_t sequenceLength)
Helper to adjust negative sequence indices.
Definition subscript.cpp:90
bool adjustIndexEx(Py_ssize_t *index, Py_ssize_t sequenceLength)
Helper to adjust negative sequence indices.
Definition subscript.cpp:69
Comprehensive C++ to Python binding library.
Helper type to get or return Python slice objects.
Definition subscript.h:193
Py_ssize_t adjustIndices(Py_ssize_t sequenceLength)
Adjust start and stop for actual sequence length, returning slice length.
Definition subscript.cpp:50
Py_ssize_t step
step size of slice.
Definition subscript.h:196
Py_ssize_t start
start index of slice. Always the index of a valid iterator after calling adjustIndices()
Definition subscript.h:194
Py_ssize_t stop
stop index of slice. Can be -1 if step < 0! It's not guaranteed that stop == start + sliceLength * st...
Definition subscript.h:195