Skip to content

Commit 005871f

Browse files
committed
Finished on parsing mutable arguments.
1 parent f88669f commit 005871f

File tree

4 files changed

+372
-24
lines changed

4 files changed

+372
-24
lines changed

doc/sphinx/source/cpp/cpp_and_cpython.rst

Lines changed: 234 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ This new reference wrapper can be used as follows:
114114
.. _cpp_and_cpython.handling_default_arguments:
115115

116116
.. index::
117-
single: C++; Default Mutable Arguments
117+
single: Parsing Arguments Example; Default Mutable Arguments
118+
single: Default Mutable Arguments; C++
118119

119120
============================================
120121
Handling Default Arguments
@@ -123,49 +124,261 @@ Handling Default Arguments
123124
Handling default, possibly mutable, arguments in a pythonic way is described here:
124125
:ref:`cpython_default_mutable_arguments`.
125126
It is quite complicated to get it right but C++ can ease the pain with a generic class to simplify handling default
126-
arguments in CPython functions:
127+
arguments in CPython functions.
128+
129+
The actual code is in ``src/cpy/ParseArgs/cParseArgsHelper.cpp`` but here it is, simplified to its essentials:
127130

128131
.. code-block:: cpp
129132
130133
class DefaultArg {
131134
public:
132-
DefaultArg(PyObject *new_ref) : m_arg { NULL }, m_default { new_ref } {}
133-
// Allow setting of the (optional) argument with PyArg_ParseTupleAndKeywords
134-
PyObject **operator&() { m_arg = NULL; return &m_arg; }
135-
// Access the argument or the default if default.
136-
operator PyObject*() const { return m_arg ? m_arg : m_default; }
137-
// Test if constructed successfully from the new reference.
135+
DefaultArg(PyObject *new_ref) : m_arg(NULL), m_default(new_ref) {}
136+
/// Allow setting of the (optional) argument with
137+
/// PyArg_ParseTupleAndKeywords
138+
PyObject **operator&() {
139+
m_arg = NULL;
140+
return &m_arg;
141+
}
142+
/// Access the argument or the default if default.
143+
operator PyObject *() const {
144+
return m_arg ? m_arg : m_default;
145+
}
146+
PyObject *obj() const {
147+
return m_arg ? m_arg : m_default;
148+
}
149+
/// Test if constructed successfully from the new reference.
138150
explicit operator bool() { return m_default != NULL; }
139151
protected:
140152
PyObject *m_arg;
141153
PyObject *m_default;
142154
};
143155
144-
Suppose we have the Python function signature of ``def function(encoding='utf8', cache={}):`` then in C/C++ we can do this:
156+
---------------------------
157+
Immutable Default Arguments
158+
---------------------------
159+
160+
Suppose we have the Python function equivalent to the Python function:
161+
162+
.. code-block:: python
163+
164+
def parse_defaults_with_helper_class(
165+
encoding_m: str = "utf-8",
166+
the_id_m: int = 1024,
167+
log_interval_m: float = 8.0):
168+
return encoding_m, the_id_m, log_interval_m
169+
170+
Here it is in C:
145171

146172
.. code-block:: cpp
147173
148-
PyObject *
149-
function(PyObject * /* module */, PyObject *args, PyObject *kwargs) {
150-
/* ... */
151-
static DefaultArg encoding(PyUnicode_FromString("utf8"));
152-
static DefaultArg cache(PyDict_New());
153-
/* Check constructed OK. */
154-
if (! encoding || ! cache) {
174+
static PyObject *
175+
parse_defaults_with_helper_class(PyObject *Py_UNUSED(module), PyObject *args, PyObject *kwds) {
176+
PyObject *ret = NULL;
177+
/* Initialise default arguments. */
178+
static DefaultArg encoding_c(PyUnicode_FromString("utf-8"));
179+
static DefaultArg the_id_c(PyLong_FromLong(DEFAULT_ID));
180+
static DefaultArg log_interval_c(PyFloat_FromDouble(DEFAULT_FLOAT));
181+
182+
/* Check that the defaults are non-NULL i.e. succesful. */
183+
if (!encoding_c || !the_id_c || !log_interval_c) {
155184
return NULL;
156185
}
157-
static const char *kwlist[] = { "encoding", "cache", NULL };
158-
if (! PyArg_ParseTupleAndKeywords(args, kwargs, "|OO", const_cast<char**>(kwlist), &encoding, &cache)) {
186+
187+
static const char *kwlist[] = {"encoding", "the_id", "log_interval", NULL};
188+
/* &encoding etc. accesses &m_arg in DefaultArg because of PyObject **operator&() */
189+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO",
190+
const_cast<char **>(kwlist),
191+
&encoding_c, &the_id_c, &log_interval_c)) {
159192
return NULL;
160193
}
161-
/* Then just use encoding, cache as if they were a PyObject* (possibly
162-
* might need to be cast to some specific PyObject*). */
163-
194+
195+
PY_DEFAULT_CHECK(encoding_c, PyUnicode_Check, "str");
196+
PY_DEFAULT_CHECK(the_id_c, PyLong_Check, "int");
197+
PY_DEFAULT_CHECK(log_interval_c, PyFloat_Check, "float");
198+
199+
/*
200+
* Use encoding, the_id, must_log from here on as PyObject* since we have
201+
* operator PyObject*() const ...
202+
*
203+
* So if we have a function:
204+
* set_encoding(PyObject *obj) { ... }
205+
*/
206+
// set_encoding(encoding);
164207
/* ... */
208+
209+
/* Py_BuildValue("O") increments the reference count. */
210+
ret = Py_BuildValue("OOO", encoding_c.obj(), the_id_c.obj(), log_interval_c.obj());
211+
return ret;
165212
}
166213
167214
The full code is in ``src/cpy/cParseArgsHelper.cpp`` and the tests in ``tests/unit/test_c_parse_args_helper.py``.
168215

216+
Here is an example test:
217+
218+
.. code-block:: python
219+
220+
@pytest.mark.parametrize(
221+
'args, expected',
222+
(
223+
(
224+
(),
225+
('utf-8', 1024, 8.0),
226+
),
227+
(
228+
('Encoding', 4219, 16.0),
229+
('Encoding', 4219, 16.0),
230+
),
231+
),
232+
)
233+
def test_parse_defaults_with_helper_class(args, expected):
234+
assert cParseArgsHelper.parse_defaults_with_helper_class(*args) == expected
235+
236+
-------------------------
237+
Mutable Default Arguments
238+
-------------------------
239+
240+
The same class can be used for mutable arguments.
241+
The following emulates this Python function:
242+
243+
.. code-block:: python
244+
245+
def parse_mutable_defaults_with_helper_class(obj, default_list=[]):
246+
default_list.append(obj)
247+
return default_list
248+
249+
Here it is in C:
250+
251+
.. code-block:: c
252+
253+
/** Parse the args where we are simulating mutable default of an empty list.
254+
* This uses the helper class.
255+
*
256+
* This is equivalent to:
257+
*
258+
* def parse_mutable_defaults_with_helper_class(obj, default_list=[]):
259+
* default_list.append(obj)
260+
* return default_list
261+
*
262+
* This adds the object to the list and returns None.
263+
*
264+
* This imitates the Python way of handling defaults.
265+
*/
266+
static PyObject *parse_mutable_defaults_with_helper_class(PyObject *Py_UNUSED(module),
267+
PyObject *args) {
268+
PyObject *ret = NULL;
269+
/* Pointers to the non-default argument, initialised by PyArg_ParseTuple below. */
270+
PyObject *arg_0 = NULL;
271+
static DefaultArg list_argument_c(PyList_New(0));
272+
273+
if (!PyArg_ParseTuple(args, "O|O", &arg_0, &list_argument_c)) {
274+
goto except;
275+
}
276+
PY_DEFAULT_CHECK(list_argument_c, PyList_Check, "list");
277+
278+
/* Your code here...*/
279+
280+
/* Append the first argument to the second.
281+
* PyList_Append() increments the refcount of arg_0. */
282+
if (PyList_Append(list_argument_c, arg_0)) {
283+
PyErr_SetString(PyExc_RuntimeError, "Can not append to list!");
284+
goto except;
285+
}
286+
287+
/* Success. */
288+
assert(!PyErr_Occurred());
289+
/* This increments the default or the given argument. */
290+
Py_INCREF(list_argument_c);
291+
ret = list_argument_c;
292+
goto finally;
293+
except:
294+
assert(PyErr_Occurred());
295+
Py_XDECREF(ret);
296+
ret = NULL;
297+
finally:
298+
return ret;
299+
}
300+
301+
The code is in ``src/cpy/ParseArgs/cParseArgsHelper.cpp``.
302+
303+
Here are some tests from ``tests/unit/test_c_parse_args_helper.py``.
304+
Firstly establish the known Python behaviour:
305+
306+
.. code-block:: python
307+
308+
def test_parse_mutable_defaults_with_helper_class_python():
309+
"""A local Python equivalent of cParseArgsHelper.parse_mutable_defaults_with_helper_class()."""
310+
311+
def parse_mutable_defaults_with_helper_class(obj, default_list=[]):
312+
default_list.append(obj)
313+
return default_list
314+
315+
result = parse_mutable_defaults_with_helper_class(1)
316+
assert sys.getrefcount(result) == 3
317+
assert result == [1, ]
318+
result = parse_mutable_defaults_with_helper_class(2)
319+
assert sys.getrefcount(result) == 3
320+
assert result == [1, 2]
321+
result = parse_mutable_defaults_with_helper_class(3)
322+
assert sys.getrefcount(result) == 3
323+
assert result == [1, 2, 3]
324+
325+
local_list = []
326+
assert sys.getrefcount(local_list) == 2
327+
assert parse_mutable_defaults_with_helper_class(10, local_list) == [10]
328+
assert sys.getrefcount(local_list) == 2
329+
assert parse_mutable_defaults_with_helper_class(11, local_list) == [10, 11]
330+
assert sys.getrefcount(local_list) == 2
331+
332+
result = parse_mutable_defaults_with_helper_class(4)
333+
assert result == [1, 2, 3, 4]
334+
assert sys.getrefcount(result) == 3
335+
336+
And now the equivalent in C:
337+
338+
.. code-block:: python
339+
340+
from cPyExtPatt import cParseArgsHelper
341+
342+
def test_parse_mutable_defaults_with_helper_class_c():
343+
result = cParseArgsHelper.parse_mutable_defaults_with_helper_class(1)
344+
assert sys.getrefcount(result) == 3
345+
assert result == [1, ]
346+
result = cParseArgsHelper.parse_mutable_defaults_with_helper_class(2)
347+
assert sys.getrefcount(result) == 3
348+
assert result == [1, 2]
349+
result = cParseArgsHelper.parse_mutable_defaults_with_helper_class(3)
350+
assert sys.getrefcount(result) == 3
351+
assert result == [1, 2, 3]
352+
353+
local_list = []
354+
assert sys.getrefcount(local_list) == 2
355+
assert cParseArgsHelper.parse_mutable_defaults_with_helper_class(10, local_list) == [10]
356+
assert sys.getrefcount(local_list) == 2
357+
assert cParseArgsHelper.parse_mutable_defaults_with_helper_class(11, local_list) == [10, 11]
358+
assert sys.getrefcount(local_list) == 2
359+
360+
result = cParseArgsHelper.parse_mutable_defaults_with_helper_class(4)
361+
assert result == [1, 2, 3, 4]
362+
assert sys.getrefcount(result) == 3
363+
364+
365+
366+
367+
368+
369+
370+
371+
372+
373+
374+
375+
376+
377+
378+
379+
380+
381+
169382
.. index::
170383
single: C++; Homogeneous Containers
171384
single: C++; Project PyCppContainers

0 commit comments

Comments
 (0)