https://github.com/python/cpython/commit/f5f1ac84b3b9d688b9e7d5943c975904b6b74513
commit: f5f1ac84b3b9d688b9e7d5943c975904b6b74513
branch: main
author: Serhiy Storchaka <storch...@gmail.com>
committer: serhiy-storchaka <storch...@gmail.com>
date: 2025-04-08T22:08:00+03:00
summary:

gh-112068: C API: Add support of nullable arguments in PyArg_Parse (GH-121303)

files:
A Misc/NEWS.d/next/C_API/2025-01-08-18-55-57.gh-issue-112068.ofI5Fl.rst
M Doc/c-api/arg.rst
M Doc/whatsnew/3.14.rst
M Lib/test/test_capi/test_getargs.py
M Lib/test/test_mmap.py
M Modules/_ctypes/_ctypes.c
M Modules/_interpretersmodule.c
M Modules/_json.c
M Modules/_threadmodule.c
M Modules/mmapmodule.c
M Python/getargs.c

diff --git a/Doc/c-api/arg.rst b/Doc/c-api/arg.rst
index d7b277e9eae03e..81b093a3510914 100644
--- a/Doc/c-api/arg.rst
+++ b/Doc/c-api/arg.rst
@@ -113,14 +113,18 @@ There are three ways strings and buffers can be converted 
to C:
 ``z`` (:class:`str` or ``None``) [const char \*]
    Like ``s``, but the Python object may also be ``None``, in which case the C
    pointer is set to ``NULL``.
+   It is the same as ``s?`` with the C pointer was initialized to ``NULL``.
 
 ``z*`` (:class:`str`, :term:`bytes-like object` or ``None``) [Py_buffer]
    Like ``s*``, but the Python object may also be ``None``, in which case the
    ``buf`` member of the :c:type:`Py_buffer` structure is set to ``NULL``.
+   It is the same as ``s*?`` with the ``buf`` member of the :c:type:`Py_buffer`
+   structure was initialized to ``NULL``.
 
 ``z#`` (:class:`str`, read-only :term:`bytes-like object` or ``None``) [const 
char \*, :c:type:`Py_ssize_t`]
    Like ``s#``, but the Python object may also be ``None``, in which case the C
    pointer is set to ``NULL``.
+   It is the same as ``s#?`` with the C pointer was initialized to ``NULL``.
 
 ``y`` (read-only :term:`bytes-like object`) [const char \*]
    This format converts a bytes-like object to a C pointer to a
@@ -377,6 +381,17 @@ Other objects
       Non-tuple sequences are deprecated if *items* contains format units
       which store a borrowed buffer or a borrowed reference.
 
+``unit?`` (anything or ``None``) [*matching-variable(s)*]
+   ``?`` modifies the behavior of the preceding format unit.
+   The C variable(s) corresponding to that parameter should be initialized
+   to their default value --- when the argument is ``None``,
+   :c:func:`PyArg_ParseTuple` does not touch the contents of the corresponding
+   C variable(s).
+   If the argument is not ``None``, it is parsed according to the specified
+   format unit.
+
+   .. versionadded:: next
+
 A few other characters have a meaning in a format string.  These may not occur
 inside nested parentheses.  They are:
 
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 9765bd31333c3c..5f84d8ba8b02c2 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -1846,6 +1846,11 @@ New features
   file.
   (Contributed by Victor Stinner in :gh:`127350`.)
 
+* Add support of nullable arguments in :c:func:`PyArg_ParseTuple` and
+  similar functions.
+  Adding ``?`` after any format unit makes ``None`` be accepted as a value.
+  (Contributed by Serhiy Storchaka in :gh:`112068`.)
+
 * Add macros :c:func:`Py_PACK_VERSION` and :c:func:`Py_PACK_FULL_VERSION` for
   bit-packing Python version numbers.
   (Contributed by Petr Viktorin in :gh:`128629`.)
diff --git a/Lib/test/test_capi/test_getargs.py 
b/Lib/test/test_capi/test_getargs.py
index 60822d5d794a18..b9cad8d2600e56 100644
--- a/Lib/test/test_capi/test_getargs.py
+++ b/Lib/test/test_capi/test_getargs.py
@@ -1387,6 +1387,123 @@ def test_nested_sequence(self):
                     "argument 1 must be sequence of length 1, not 0"):
                 parse(([],), {}, '(' + f + ')', ['a'])
 
+    def test_specific_type_errors(self):
+        parse = _testcapi.parse_tuple_and_keywords
+
+        def check(format, arg, expected, got='list'):
+            errmsg = f'must be {expected}, not {got}'
+            with self.assertRaisesRegex(TypeError, errmsg):
+                parse((arg,), {}, format, ['a'])
+
+        check('k', [], 'int')
+        check('k?', [], 'int or None')
+        check('K', [], 'int')
+        check('K?', [], 'int or None')
+        check('c', [], 'a byte string of length 1')
+        check('c?', [], 'a byte string of length 1 or None')
+        check('c', b'abc', 'a byte string of length 1',
+              'a bytes object of length 3')
+        check('c?', b'abc', 'a byte string of length 1 or None',
+              'a bytes object of length 3')
+        check('c', bytearray(b'abc'), 'a byte string of length 1',
+              'a bytearray object of length 3')
+        check('c?', bytearray(b'abc'), 'a byte string of length 1 or None',
+              'a bytearray object of length 3')
+        check('C', [], 'a unicode character')
+        check('C?', [], 'a unicode character or None')
+        check('C', 'abc', 'a unicode character',
+              'a string of length 3')
+        check('C?', 'abc', 'a unicode character or None',
+              'a string of length 3')
+        check('s', [], 'str')
+        check('s?', [], 'str or None')
+        check('z', [], 'str or None')
+        check('z?', [], 'str or None')
+        check('es', [], 'str')
+        check('es?', [], 'str or None')
+        check('es#', [], 'str')
+        check('es#?', [], 'str or None')
+        check('et', [], 'str, bytes or bytearray')
+        check('et?', [], 'str, bytes, bytearray or None')
+        check('et#', [], 'str, bytes or bytearray')
+        check('et#?', [], 'str, bytes, bytearray or None')
+        check('w*', [], 'read-write bytes-like object')
+        check('w*?', [], 'read-write bytes-like object or None')
+        check('S', [], 'bytes')
+        check('S?', [], 'bytes or None')
+        check('U', [], 'str')
+        check('U?', [], 'str or None')
+        check('Y', [], 'bytearray')
+        check('Y?', [], 'bytearray or None')
+        check('(OO)', 42, '2-item tuple', 'int')
+        check('(OO)?', 42, '2-item tuple or None', 'int')
+        check('(OO)', (1, 2, 3), 'tuple of length 2', '3')
+
+    def test_nullable(self):
+        parse = _testcapi.parse_tuple_and_keywords
+
+        def check(format, arg, allows_none=False):
+            # Because some format units (such as y*) require cleanup,
+            # we force the parsing code to perform the cleanup by adding
+            # an argument that always fails.
+            # By checking for an exception, we ensure that the parsing
+            # of the first argument was successful.
+            self.assertRaises(OverflowError, parse,
+                              (arg, 256), {}, format + '?b', ['a', 'b'])
+            self.assertRaises(OverflowError, parse,
+                              (None, 256), {}, format + '?b', ['a', 'b'])
+            self.assertRaises(OverflowError, parse,
+                              (arg, 256), {}, format + 'b', ['a', 'b'])
+            self.assertRaises(OverflowError if allows_none else TypeError, 
parse,
+                              (None, 256), {}, format + 'b', ['a', 'b'])
+
+        check('b', 42)
+        check('B', 42)
+        check('h', 42)
+        check('H', 42)
+        check('i', 42)
+        check('I', 42)
+        check('n', 42)
+        check('l', 42)
+        check('k', 42)
+        check('L', 42)
+        check('K', 42)
+        check('f', 2.5)
+        check('d', 2.5)
+        check('D', 2.5j)
+        check('c', b'a')
+        check('C', 'a')
+        check('p', True, allows_none=True)
+        check('y', b'buffer')
+        check('y*', b'buffer')
+        check('y#', b'buffer')
+        check('s', 'string')
+        check('s*', 'string')
+        check('s#', 'string')
+        check('z', 'string', allows_none=True)
+        check('z*', 'string', allows_none=True)
+        check('z#', 'string', allows_none=True)
+        check('w*', bytearray(b'buffer'))
+        check('U', 'string')
+        check('S', b'bytes')
+        check('Y', bytearray(b'bytearray'))
+        check('O', object, allows_none=True)
+
+        check('(OO)', (1, 2))
+        self.assertEqual(parse((((1, 2), 3),), {}, '((OO)?O)', ['a']), (1, 2, 
3))
+        self.assertEqual(parse(((None, 3),), {}, '((OO)?O)', ['a']), (NULL, 
NULL, 3))
+        self.assertEqual(parse((((1, 2), 3),), {}, '((OO)O)', ['a']), (1, 2, 
3))
+        self.assertRaises(TypeError, parse, ((None, 3),), {}, '((OO)O)', ['a'])
+
+        parse((None,), {}, 'es?', ['a'])
+        parse((None,), {}, 'es#?', ['a'])
+        parse((None,), {}, 'et?', ['a'])
+        parse((None,), {}, 'et#?', ['a'])
+        parse((None,), {}, 'O!?', ['a'])
+        parse((None,), {}, 'O&?', ['a'])
+
+        # TODO: More tests for es?, es#?, et?, et#?, O!, O&
+
     @unittest.skipIf(_testinternalcapi is None, 'needs _testinternalcapi')
     def test_gh_119213(self):
         rc, out, err = script_helper.assert_python_ok("-c", """if True:
diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py
index b2a299ed172967..fd4197b7086976 100644
--- a/Lib/test/test_mmap.py
+++ b/Lib/test/test_mmap.py
@@ -732,7 +732,7 @@ def test_tagname(self):
         m2.close()
         m1.close()
 
-        with self.assertRaisesRegex(TypeError, 'tagname'):
+        with self.assertRaisesRegex(TypeError, 'must be str or None'):
             mmap.mmap(-1, 8, tagname=1)
 
     @cpython_only
diff --git 
a/Misc/NEWS.d/next/C_API/2025-01-08-18-55-57.gh-issue-112068.ofI5Fl.rst 
b/Misc/NEWS.d/next/C_API/2025-01-08-18-55-57.gh-issue-112068.ofI5Fl.rst
new file mode 100644
index 00000000000000..d49b1735825e8a
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-01-08-18-55-57.gh-issue-112068.ofI5Fl.rst
@@ -0,0 +1,3 @@
+Add support of nullable arguments in :c:func:`PyArg_Parse` and similar
+functions. Adding ``?`` after any format unit makes ``None`` be accepted as
+a value.
diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c
index bba006772efbfa..55e5eee0eb081a 100644
--- a/Modules/_ctypes/_ctypes.c
+++ b/Modules/_ctypes/_ctypes.c
@@ -3848,9 +3848,7 @@ _validate_paramflags(ctypes_state *st, PyTypeObject 
*type, PyObject *paramflags)
         PyObject *name = Py_None;
         PyObject *defval;
         PyObject *typ;
-        if (!PyArg_ParseTuple(item, "i|OO", &flag, &name, &defval) ||
-            !(name == Py_None || PyUnicode_Check(name)))
-        {
+        if (!PyArg_ParseTuple(item, "i|U?O", &flag, &name, &defval)) {
             PyErr_SetString(PyExc_TypeError,
                    "paramflags must be a sequence of (int [,string [,value]]) 
tuples");
             return 0;
@@ -3915,10 +3913,8 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, 
PyObject *kwds)
     void *handle;
     PyObject *paramflags = NULL;
 
-    if (!PyArg_ParseTuple(args, "O|O", &ftuple, &paramflags))
+    if (!PyArg_ParseTuple(args, "O|O?", &ftuple, &paramflags))
         return NULL;
-    if (paramflags == Py_None)
-        paramflags = NULL;
 
     ftuple = PySequence_Tuple(ftuple);
     if (!ftuple)
@@ -4050,10 +4046,8 @@ PyCFuncPtr_FromVtblIndex(PyTypeObject *type, PyObject 
*args, PyObject *kwds)
     GUID *iid = NULL;
     Py_ssize_t iid_len = 0;
 
-    if (!PyArg_ParseTuple(args, "is|Oz#", &index, &name, &paramflags, &iid, 
&iid_len))
+    if (!PyArg_ParseTuple(args, "is|O?z#", &index, &name, &paramflags, &iid, 
&iid_len))
         return NULL;
-    if (paramflags == Py_None)
-        paramflags = NULL;
 
     ctypes_state *st = get_module_state_by_def(Py_TYPE(type));
     if (!_validate_paramflags(st, type, paramflags)) {
diff --git a/Modules/_interpretersmodule.c b/Modules/_interpretersmodule.c
index 74f1c02cfab4c9..c444c4c32c71e3 100644
--- a/Modules/_interpretersmodule.c
+++ b/Modules/_interpretersmodule.c
@@ -1252,14 +1252,11 @@ interp_get_config(PyObject *self, PyObject *args, 
PyObject *kwds)
     PyObject *idobj = NULL;
     int restricted = 0;
     if (!PyArg_ParseTupleAndKeywords(args, kwds,
-                                     "O|$p:get_config", kwlist,
+                                     "O?|$p:get_config", kwlist,
                                      &idobj, &restricted))
     {
         return NULL;
     }
-    if (idobj == Py_None) {
-        idobj = NULL;
-    }
 
     int reqready = 0;
     PyInterpreterState *interp = \
@@ -1376,14 +1373,14 @@ capture_exception(PyObject *self, PyObject *args, 
PyObject *kwds)
     static char *kwlist[] = {"exc", NULL};
     PyObject *exc_arg = NULL;
     if (!PyArg_ParseTupleAndKeywords(args, kwds,
-                                     "|O:capture_exception", kwlist,
+                                     "|O?:capture_exception", kwlist,
                                      &exc_arg))
     {
         return NULL;
     }
 
     PyObject *exc = exc_arg;
-    if (exc == NULL || exc == Py_None) {
+    if (exc == NULL) {
         exc = PyErr_GetRaisedException();
         if (exc == NULL) {
             Py_RETURN_NONE;
diff --git a/Modules/_json.c b/Modules/_json.c
index cd8e697916226b..89b0a41dd10acb 100644
--- a/Modules/_json.c
+++ b/Modules/_json.c
@@ -1228,23 +1228,16 @@ encoder_new(PyTypeObject *type, PyObject *args, 
PyObject *kwds)
     static char *kwlist[] = {"markers", "default", "encoder", "indent", 
"key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL};
 
     PyEncoderObject *s;
-    PyObject *markers, *defaultfn, *encoder, *indent, *key_separator;
+    PyObject *markers = Py_None, *defaultfn, *encoder, *indent, *key_separator;
     PyObject *item_separator;
     int sort_keys, skipkeys, allow_nan;
 
-    if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUppp:make_encoder", 
kwlist,
-        &markers, &defaultfn, &encoder, &indent,
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!?OOOUUppp:make_encoder", 
kwlist,
+        &PyDict_Type, &markers, &defaultfn, &encoder, &indent,
         &key_separator, &item_separator,
         &sort_keys, &skipkeys, &allow_nan))
         return NULL;
 
-    if (markers != Py_None && !PyDict_Check(markers)) {
-        PyErr_Format(PyExc_TypeError,
-                     "make_encoder() argument 1 must be dict or None, "
-                     "not %.200s", Py_TYPE(markers)->tp_name);
-        return NULL;
-    }
-
     s = (PyEncoderObject *)type->tp_alloc(type, 0);
     if (s == NULL)
         return NULL;
diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c
index 9f6ac21c8a8ccf..6967f7ef42f173 100644
--- a/Modules/_threadmodule.c
+++ b/Modules/_threadmodule.c
@@ -651,12 +651,12 @@ PyThreadHandleObject_join(PyObject *op, PyObject *args)
     PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
 
     PyObject *timeout_obj = NULL;
-    if (!PyArg_ParseTuple(args, "|O:join", &timeout_obj)) {
+    if (!PyArg_ParseTuple(args, "|O?:join", &timeout_obj)) {
         return NULL;
     }
 
     PyTime_t timeout_ns = -1;
-    if (timeout_obj != NULL && timeout_obj != Py_None) {
+    if (timeout_obj != NULL) {
         if (_PyTime_FromSecondsObject(&timeout_ns, timeout_obj,
                                       _PyTime_ROUND_TIMEOUT) < 0) {
             return NULL;
@@ -1919,10 +1919,10 @@ thread_PyThread_start_joinable_thread(PyObject *module, 
PyObject *fargs,
     PyObject *func = NULL;
     int daemon = 1;
     thread_module_state *state = get_thread_state(module);
-    PyObject *hobj = NULL;
+    PyObject *hobj = Py_None;
     if (!PyArg_ParseTupleAndKeywords(fargs, fkwargs,
-                                     "O|Op:start_joinable_thread", keywords,
-                                     &func, &hobj, &daemon)) {
+                                     "O|O!?p:start_joinable_thread", keywords,
+                                     &func, state->thread_handle_type, &hobj, 
&daemon)) {
         return NULL;
     }
 
@@ -1932,14 +1932,6 @@ thread_PyThread_start_joinable_thread(PyObject *module, 
PyObject *fargs,
         return NULL;
     }
 
-    if (hobj == NULL) {
-        hobj = Py_None;
-    }
-    else if (hobj != Py_None && !Py_IS_TYPE(hobj, state->thread_handle_type)) {
-        PyErr_SetString(PyExc_TypeError, "'handle' must be a _ThreadHandle");
-        return NULL;
-    }
-
     if (PySys_Audit("_thread.start_joinable_thread", "OiO", func, daemon,
                     hobj) < 0) {
         return NULL;
diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c
index 67fd6db2f361d6..6a385562845849 100644
--- a/Modules/mmapmodule.c
+++ b/Modules/mmapmodule.c
@@ -23,7 +23,6 @@
 #endif
 
 #include <Python.h>
-#include "pycore_abstract.h"      // _Py_convert_optional_to_ssize_t()
 #include "pycore_bytesobject.h"   // _PyBytes_Find()
 #include "pycore_fileutils.h"     // _Py_stat_struct
 
@@ -516,7 +515,7 @@ mmap_read_method(PyObject *op, PyObject *args)
     mmap_object *self = mmap_object_CAST(op);
 
     CHECK_VALID(NULL);
-    if (!PyArg_ParseTuple(args, "|O&:read", _Py_convert_optional_to_ssize_t, 
&num_bytes))
+    if (!PyArg_ParseTuple(args, "|n?:read", &num_bytes))
         return NULL;
     CHECK_VALID(NULL);
 
@@ -1710,7 +1709,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, 
PyObject *kwdict)
     DWORD off_lo;       /* lower 32 bits of offset */
     DWORD size_hi;      /* upper 32 bits of size */
     DWORD size_lo;      /* lower 32 bits of size */
-    PyObject *tagname = Py_None;
+    PyObject *tagname = NULL;
     DWORD dwErr = 0;
     int fileno;
     HANDLE fh = 0;
@@ -1720,7 +1719,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, 
PyObject *kwdict)
                                 "tagname",
                                 "access", "offset", NULL };
 
-    if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|OiL", keywords,
+    if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|U?iL", keywords,
                                      &fileno, &map_size,
                                      &tagname, &access, &offset)) {
         return NULL;
@@ -1853,13 +1852,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, 
PyObject *kwdict)
     m_obj->weakreflist = NULL;
     m_obj->exports = 0;
     /* set the tag name */
-    if (!Py_IsNone(tagname)) {
-        if (!PyUnicode_Check(tagname)) {
-            Py_DECREF(m_obj);
-            return PyErr_Format(PyExc_TypeError, "expected str or None for "
-                                "'tagname', not %.200s",
-                                Py_TYPE(tagname)->tp_name);
-        }
+    if (tagname != NULL) {
         m_obj->tagname = PyUnicode_AsWideCharString(tagname, NULL);
         if (m_obj->tagname == NULL) {
             Py_DECREF(m_obj);
diff --git a/Python/getargs.c b/Python/getargs.c
index 08325ca5a87c49..16d5e52742d129 100644
--- a/Python/getargs.c
+++ b/Python/getargs.c
@@ -1,6 +1,8 @@
 
 /* New getargs implementation */
 
+#include <stdbool.h>
+
 #define PY_CXX_CONST const
 #include "Python.h"
 #include "pycore_abstract.h"      // _PyNumber_Index()
@@ -466,9 +468,12 @@ converttuple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     const char *format = *p_format;
     int i;
     Py_ssize_t len;
+    bool nullable = false;
     int istuple = PyTuple_Check(arg);
     int mustbetuple = istuple;
 
+    assert(*format == '(');
+    format++;
     for (;;) {
         int c = *format++;
         if (c == '(') {
@@ -477,8 +482,12 @@ converttuple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
             level++;
         }
         else if (c == ')') {
-            if (level == 0)
+            if (level == 0) {
+                if (*format == '?') {
+                    nullable = true;
+                }
                 break;
+            }
             level--;
         }
         else if (c == ':' || c == ';' || c == '\0')
@@ -515,6 +524,13 @@ converttuple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
         }
     }
 
+    if (arg == Py_None && nullable) {
+        const char *msg = skipitem(p_format, p_va, flags);
+        if (msg != NULL) {
+            levels[0] = 0;
+        }
+        return msg;
+    }
     if (istuple) {
         /* fallthrough */
     }
@@ -523,9 +539,10 @@ converttuple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     {
         levels[0] = 0;
         PyOS_snprintf(msgbuf, bufsize,
-                      "must be %d-item tuple, not %.50s",
-                  n,
-                  arg == Py_None ? "None" : Py_TYPE(arg)->tp_name);
+                      "must be %d-item tuple%s, not %.50s",
+                      n,
+                      nullable ? " or None" : "",
+                      arg == Py_None ? "None" : Py_TYPE(arg)->tp_name);
         return msgbuf;
     }
     else {
@@ -562,7 +579,7 @@ converttuple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
         return msgbuf;
     }
 
-    format = *p_format;
+    format = *p_format + 1;
     for (i = 0; i < n; i++) {
         const char *msg;
         PyObject *item = PyTuple_GET_ITEM(arg, i);
@@ -577,6 +594,10 @@ converttuple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
         }
     }
 
+    format++;
+    if (*format == '?') {
+        format++;
+    }
     *p_format = format;
     if (!istuple) {
         Py_DECREF(arg);
@@ -595,11 +616,8 @@ convertitem(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     const char *format = *p_format;
 
     if (*format == '(' /* ')' */) {
-        format++;
         msg = converttuple(arg, &format, p_va, flags, levels, msgbuf,
                            bufsize, freelist);
-        if (msg == NULL)
-            format++;
     }
     else {
         msg = convertsimple(arg, &format, p_va, flags,
@@ -629,7 +647,7 @@ _PyArg_BadArgument(const char *fname, const char 
*displayname,
 }
 
 static const char *
-converterr(const char *expected, PyObject *arg, char *msgbuf, size_t bufsize)
+converterr(bool nullable, const char *expected, PyObject *arg, char *msgbuf, 
size_t bufsize)
 {
     assert(expected != NULL);
     assert(arg != NULL);
@@ -639,20 +657,23 @@ converterr(const char *expected, PyObject *arg, char 
*msgbuf, size_t bufsize)
     }
     else {
         PyOS_snprintf(msgbuf, bufsize,
-                      "must be %.50s, not %.50s", expected,
+                      "must be %.50s%s, not %.50s", expected,
+                      nullable ? " or None" : "",
                       arg == Py_None ? "None" : Py_TYPE(arg)->tp_name);
     }
     return msgbuf;
 }
 
 static const char *
-convertcharerr(const char *expected, const char *what, Py_ssize_t size,
+convertcharerr(bool nullable, const char *expected, const char *what, 
Py_ssize_t size,
                char *msgbuf, size_t bufsize)
 {
     assert(expected != NULL);
     PyOS_snprintf(msgbuf, bufsize,
-                  "must be %.50s, not %.50s of length %zd",
-                  expected, what, size);
+                  "must be %.50s%s, not %.50s of length %zd",
+                  expected,
+                  nullable ? " or None" : "",
+                  what, size);
     return msgbuf;
 }
 
@@ -672,15 +693,26 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
               char *msgbuf, size_t bufsize, freelist_t *freelist)
 {
 #define RETURN_ERR_OCCURRED return msgbuf
+#define HANDLE_NULLABLE                 \
+        if (*format == '?') {           \
+            format++;                   \
+            if (arg == Py_None) {       \
+                break;                  \
+            }                           \
+            nullable = true;            \
+        }
+
 
     const char *format = *p_format;
     char c = *format++;
     const char *sarg;
+    bool nullable = false;
 
     switch (c) {
 
     case 'b': { /* unsigned byte -- very short int */
         unsigned char *p = va_arg(*p_va, unsigned char *);
+        HANDLE_NULLABLE;
         long ival = PyLong_AsLong(arg);
         if (ival == -1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -694,7 +726,6 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
                             "unsigned byte integer is greater than maximum");
             RETURN_ERR_OCCURRED;
         }
-        else
             *p = (unsigned char) ival;
         break;
     }
@@ -702,6 +733,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     case 'B': {/* byte sized bitfield - both signed and unsigned
                   values allowed */
         unsigned char *p = va_arg(*p_va, unsigned char *);
+        HANDLE_NULLABLE;
         unsigned long ival = PyLong_AsUnsignedLongMask(arg);
         if (ival == (unsigned long)-1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -712,6 +744,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
 
     case 'h': {/* signed short int */
         short *p = va_arg(*p_va, short *);
+        HANDLE_NULLABLE;
         long ival = PyLong_AsLong(arg);
         if (ival == -1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -733,6 +766,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     case 'H': { /* short int sized bitfield, both signed and
                    unsigned allowed */
         unsigned short *p = va_arg(*p_va, unsigned short *);
+        HANDLE_NULLABLE;
         unsigned long ival = PyLong_AsUnsignedLongMask(arg);
         if (ival == (unsigned long)-1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -743,6 +777,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
 
     case 'i': {/* signed int */
         int *p = va_arg(*p_va, int *);
+        HANDLE_NULLABLE;
         long ival = PyLong_AsLong(arg);
         if (ival == -1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -764,6 +799,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     case 'I': { /* int sized bitfield, both signed and
                    unsigned allowed */
         unsigned int *p = va_arg(*p_va, unsigned int *);
+        HANDLE_NULLABLE;
         unsigned long ival = PyLong_AsUnsignedLongMask(arg);
         if (ival == (unsigned long)-1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -776,6 +812,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     {
         PyObject *iobj;
         Py_ssize_t *p = va_arg(*p_va, Py_ssize_t *);
+        HANDLE_NULLABLE;
         Py_ssize_t ival = -1;
         iobj = _PyNumber_Index(arg);
         if (iobj != NULL) {
@@ -789,6 +826,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
     }
     case 'l': {/* long int */
         long *p = va_arg(*p_va, long *);
+        HANDLE_NULLABLE;
         long ival = PyLong_AsLong(arg);
         if (ival == -1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -799,17 +837,19 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
 
     case 'k': { /* long sized bitfield */
         unsigned long *p = va_arg(*p_va, unsigned long *);
+        HANDLE_NULLABLE;
         unsigned long ival;
         if (PyLong_Check(arg))
             ival = PyLong_AsUnsignedLongMask(arg);
         else
-            return converterr("int", arg, msgbuf, bufsize);
+            return converterr(nullable, "int", arg, msgbuf, bufsize);
         *p = ival;
         break;
     }
 
     case 'L': {/* long long */
         long long *p = va_arg( *p_va, long long * );
+        HANDLE_NULLABLE;
         long long ival = PyLong_AsLongLong(arg);
         if (ival == (long long)-1 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -820,17 +860,19 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
 
     case 'K': { /* long long sized bitfield */
         unsigned long long *p = va_arg(*p_va, unsigned long long *);
+        HANDLE_NULLABLE;
         unsigned long long ival;
         if (PyLong_Check(arg))
             ival = PyLong_AsUnsignedLongLongMask(arg);
         else
-            return converterr("int", arg, msgbuf, bufsize);
+            return converterr(nullable, "int", arg, msgbuf, bufsize);
         *p = ival;
         break;
     }
 
     case 'f': {/* float */
         float *p = va_arg(*p_va, float *);
+        HANDLE_NULLABLE;
         double dval = PyFloat_AsDouble(arg);
         if (dval == -1.0 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -841,6 +883,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
 
     case 'd': {/* double */
         double *p = va_arg(*p_va, double *);
+        HANDLE_NULLABLE;
         double dval = PyFloat_AsDouble(arg);
         if (dval == -1.0 && PyErr_Occurred())
             RETURN_ERR_OCCURRED;
@@ -851,6 +894,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
 
     case 'D': {/* complex double */
         Py_complex *p = va_arg(*p_va, Py_complex *);
+        HANDLE_NULLABLE;
         Py_complex cval;
         cval = PyComplex_AsCComplex(arg);
         if (PyErr_Occurred())
@@ -862,9 +906,10 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
 
     case 'c': {/* char */
         char *p = va_arg(*p_va, char *);
+        HANDLE_NULLABLE;
         if (PyBytes_Check(arg)) {
             if (PyBytes_GET_SIZE(arg) != 1) {
-                return convertcharerr("a byte string of length 1",
+                return convertcharerr(nullable, "a byte string of length 1",
                                       "a bytes object", PyBytes_GET_SIZE(arg),
                                       msgbuf, bufsize);
             }
@@ -872,27 +917,28 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
         }
         else if (PyByteArray_Check(arg)) {
             if (PyByteArray_GET_SIZE(arg) != 1) {
-                return convertcharerr("a byte string of length 1",
+                return convertcharerr(nullable, "a byte string of length 1",
                                       "a bytearray object", 
PyByteArray_GET_SIZE(arg),
                                       msgbuf, bufsize);
             }
             *p = PyByteArray_AS_STRING(arg)[0];
         }
         else
-            return converterr("a byte string of length 1", arg, msgbuf, 
bufsize);
+            return converterr(nullable, "a byte string of length 1", arg, 
msgbuf, bufsize);
         break;
     }
 
     case 'C': {/* unicode char */
         int *p = va_arg(*p_va, int *);
+        HANDLE_NULLABLE;
         int kind;
         const void *data;
 
         if (!PyUnicode_Check(arg))
-            return converterr("a unicode character", arg, msgbuf, bufsize);
+            return converterr(nullable, "a unicode character", arg, msgbuf, 
bufsize);
 
         if (PyUnicode_GET_LENGTH(arg) != 1) {
-            return convertcharerr("a unicode character",
+            return convertcharerr(nullable, "a unicode character",
                                   "a string", PyUnicode_GET_LENGTH(arg),
                                   msgbuf, bufsize);
         }
@@ -905,6 +951,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list 
*p_va, int flags,
 
     case 'p': {/* boolean *p*redicate */
         int *p = va_arg(*p_va, int *);
+        HANDLE_NULLABLE;
         int val = PyObject_IsTrue(arg);
         if (val > 0)
             *p = 1;
@@ -923,24 +970,31 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
         const char *buf;
         Py_ssize_t count;
         if (*format == '*') {
-            if (getbuffer(arg, (Py_buffer*)p, &buf) < 0)
-                return converterr(buf, arg, msgbuf, bufsize);
             format++;
+            HANDLE_NULLABLE;
+            if (getbuffer(arg, (Py_buffer*)p, &buf) < 0)
+                return converterr(nullable, buf, arg, msgbuf, bufsize);
             if (addcleanup(p, freelist, cleanup_buffer)) {
                 return converterr(
-                    "(cleanup problem)",
+                    nullable, "(cleanup problem)",
                     arg, msgbuf, bufsize);
             }
             break;
         }
-        count = convertbuffer(arg, (const void **)p, &buf);
-        if (count < 0)
-            return converterr(buf, arg, msgbuf, bufsize);
-        if (*format == '#') {
+        else if (*format == '#') {
             Py_ssize_t *psize = va_arg(*p_va, Py_ssize_t*);
-            *psize = count;
             format++;
-        } else {
+            HANDLE_NULLABLE;
+            count = convertbuffer(arg, (const void **)p, &buf);
+            if (count < 0)
+                return converterr(nullable, buf, arg, msgbuf, bufsize);
+            *psize = count;
+        }
+        else {
+            HANDLE_NULLABLE;
+            count = convertbuffer(arg, (const void **)p, &buf);
+            if (count < 0)
+                return converterr(nullable, buf, arg, msgbuf, bufsize);
             if (strlen(*p) != (size_t)count) {
                 PyErr_SetString(PyExc_ValueError, "embedded null byte");
                 RETURN_ERR_OCCURRED;
@@ -956,32 +1010,35 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
             /* "s*" or "z*" */
             Py_buffer *p = (Py_buffer *)va_arg(*p_va, Py_buffer *);
 
+            format++;
+            HANDLE_NULLABLE;
             if (c == 'z' && arg == Py_None)
                 PyBuffer_FillInfo(p, NULL, NULL, 0, 1, 0);
             else if (PyUnicode_Check(arg)) {
                 Py_ssize_t len;
                 sarg = PyUnicode_AsUTF8AndSize(arg, &len);
                 if (sarg == NULL)
-                    return converterr(CONV_UNICODE,
+                    return converterr(nullable, CONV_UNICODE,
                                       arg, msgbuf, bufsize);
                 PyBuffer_FillInfo(p, arg, (void *)sarg, len, 1, 0);
             }
             else { /* any bytes-like object */
                 const char *buf;
                 if (getbuffer(arg, p, &buf) < 0)
-                    return converterr(buf, arg, msgbuf, bufsize);
+                    return converterr(nullable, buf, arg, msgbuf, bufsize);
             }
             if (addcleanup(p, freelist, cleanup_buffer)) {
                 return converterr(
-                    "(cleanup problem)",
+                    nullable, "(cleanup problem)",
                     arg, msgbuf, bufsize);
             }
-            format++;
         } else if (*format == '#') { /* a string or read-only bytes-like 
object */
             /* "s#" or "z#" */
             const void **p = (const void **)va_arg(*p_va, const char **);
             Py_ssize_t *psize = va_arg(*p_va, Py_ssize_t*);
 
+            format++;
+            HANDLE_NULLABLE;
             if (c == 'z' && arg == Py_None) {
                 *p = NULL;
                 *psize = 0;
@@ -990,7 +1047,7 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
                 Py_ssize_t len;
                 sarg = PyUnicode_AsUTF8AndSize(arg, &len);
                 if (sarg == NULL)
-                    return converterr(CONV_UNICODE,
+                    return converterr(nullable, CONV_UNICODE,
                                       arg, msgbuf, bufsize);
                 *p = sarg;
                 *psize = len;
@@ -1000,22 +1057,22 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
                 const char *buf;
                 Py_ssize_t count = convertbuffer(arg, p, &buf);
                 if (count < 0)
-                    return converterr(buf, arg, msgbuf, bufsize);
+                    return converterr(nullable, buf, arg, msgbuf, bufsize);
                 *psize = count;
             }
-            format++;
         } else {
             /* "s" or "z" */
             const char **p = va_arg(*p_va, const char **);
             Py_ssize_t len;
             sarg = NULL;
 
+            HANDLE_NULLABLE;
             if (c == 'z' && arg == Py_None)
                 *p = NULL;
             else if (PyUnicode_Check(arg)) {
                 sarg = PyUnicode_AsUTF8AndSize(arg, &len);
                 if (sarg == NULL)
-                    return converterr(CONV_UNICODE,
+                    return converterr(nullable, CONV_UNICODE,
                                       arg, msgbuf, bufsize);
                 if (strlen(sarg) != (size_t)len) {
                     PyErr_SetString(PyExc_ValueError, "embedded null 
character");
@@ -1024,7 +1081,7 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
                 *p = sarg;
             }
             else
-                return converterr(c == 'z' ? "str or None" : "str",
+                return converterr(c == 'z' || nullable, "str",
                                   arg, msgbuf, bufsize);
         }
         break;
@@ -1053,13 +1110,46 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
             recode_strings = 0;
         else
             return converterr(
-                "(unknown parser marker combination)",
+                nullable, "(unknown parser marker combination)",
                 arg, msgbuf, bufsize);
         buffer = (char **)va_arg(*p_va, char **);
         format++;
         if (buffer == NULL)
-            return converterr("(buffer is NULL)",
+            return converterr(nullable, "(buffer is NULL)",
                               arg, msgbuf, bufsize);
+        Py_ssize_t *psize = NULL;
+        if (*format == '#') {
+            /* Using buffer length parameter '#':
+
+               - if *buffer is NULL, a new buffer of the
+               needed size is allocated and the data
+               copied into it; *buffer is updated to point
+               to the new buffer; the caller is
+               responsible for PyMem_Free()ing it after
+               usage
+
+               - if *buffer is not NULL, the data is
+               copied to *buffer; *buffer_len has to be
+               set to the size of the buffer on input;
+               buffer overflow is signalled with an error;
+               buffer has to provide enough room for the
+               encoded string plus the trailing 0-byte
+
+               - in both cases, *buffer_len is updated to
+               the size of the buffer /excluding/ the
+               trailing 0-byte
+
+            */
+            psize = va_arg(*p_va, Py_ssize_t*);
+
+            format++;
+            if (psize == NULL) {
+                return converterr(
+                    nullable, "(buffer_len is NULL)",
+                    arg, msgbuf, bufsize);
+            }
+        }
+        HANDLE_NULLABLE;
 
         /* Encode object */
         if (!recode_strings &&
@@ -1080,7 +1170,7 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
                                           encoding,
                                           NULL);
             if (s == NULL)
-                return converterr("(encoding failed)",
+                return converterr(nullable, "(encoding failed)",
                                   arg, msgbuf, bufsize);
             assert(PyBytes_Check(s));
             size = PyBytes_GET_SIZE(s);
@@ -1090,42 +1180,15 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
         }
         else {
             return converterr(
-                recode_strings ? "str" : "str, bytes or bytearray",
+                nullable,
+                recode_strings ? "str"
+                : nullable ? "str, bytes, bytearray"
+                : "str, bytes or bytearray",
                 arg, msgbuf, bufsize);
         }
 
         /* Write output; output is guaranteed to be 0-terminated */
-        if (*format == '#') {
-            /* Using buffer length parameter '#':
-
-               - if *buffer is NULL, a new buffer of the
-               needed size is allocated and the data
-               copied into it; *buffer is updated to point
-               to the new buffer; the caller is
-               responsible for PyMem_Free()ing it after
-               usage
-
-               - if *buffer is not NULL, the data is
-               copied to *buffer; *buffer_len has to be
-               set to the size of the buffer on input;
-               buffer overflow is signalled with an error;
-               buffer has to provide enough room for the
-               encoded string plus the trailing 0-byte
-
-               - in both cases, *buffer_len is updated to
-               the size of the buffer /excluding/ the
-               trailing 0-byte
-
-            */
-            Py_ssize_t *psize = va_arg(*p_va, Py_ssize_t*);
-
-            format++;
-            if (psize == NULL) {
-                Py_DECREF(s);
-                return converterr(
-                    "(buffer_len is NULL)",
-                    arg, msgbuf, bufsize);
-            }
+        if (psize != NULL) {
             if (*buffer == NULL) {
                 *buffer = PyMem_NEW(char, size + 1);
                 if (*buffer == NULL) {
@@ -1136,7 +1199,7 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
                 if (addcleanup(buffer, freelist, cleanup_ptr)) {
                     Py_DECREF(s);
                     return converterr(
-                        "(cleanup problem)",
+                        nullable, "(cleanup problem)",
                         arg, msgbuf, bufsize);
                 }
             } else {
@@ -1170,7 +1233,7 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
             if ((Py_ssize_t)strlen(ptr) != size) {
                 Py_DECREF(s);
                 return converterr(
-                    "encoded string without null bytes",
+                    nullable, "encoded string without null bytes",
                     arg, msgbuf, bufsize);
             }
             *buffer = PyMem_NEW(char, size + 1);
@@ -1181,7 +1244,7 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
             }
             if (addcleanup(buffer, freelist, cleanup_ptr)) {
                 Py_DECREF(s);
-                return converterr("(cleanup problem)",
+                return converterr(nullable, "(cleanup problem)",
                                 arg, msgbuf, bufsize);
             }
             memcpy(*buffer, ptr, size+1);
@@ -1192,29 +1255,32 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
 
     case 'S': { /* PyBytes object */
         PyObject **p = va_arg(*p_va, PyObject **);
+        HANDLE_NULLABLE;
         if (PyBytes_Check(arg))
             *p = arg;
         else
-            return converterr("bytes", arg, msgbuf, bufsize);
+            return converterr(nullable, "bytes", arg, msgbuf, bufsize);
         break;
     }
 
     case 'Y': { /* PyByteArray object */
         PyObject **p = va_arg(*p_va, PyObject **);
+        HANDLE_NULLABLE;
         if (PyByteArray_Check(arg))
             *p = arg;
         else
-            return converterr("bytearray", arg, msgbuf, bufsize);
+            return converterr(nullable, "bytearray", arg, msgbuf, bufsize);
         break;
     }
 
     case 'U': { /* PyUnicode object */
         PyObject **p = va_arg(*p_va, PyObject **);
+        HANDLE_NULLABLE;
         if (PyUnicode_Check(arg)) {
             *p = arg;
         }
         else
-            return converterr("str", arg, msgbuf, bufsize);
+            return converterr(nullable, "str", arg, msgbuf, bufsize);
         break;
     }
 
@@ -1225,10 +1291,11 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
             type = va_arg(*p_va, PyTypeObject*);
             p = va_arg(*p_va, PyObject **);
             format++;
+            HANDLE_NULLABLE;
             if (PyType_IsSubtype(Py_TYPE(arg), type))
                 *p = arg;
             else
-                return converterr(type->tp_name, arg, msgbuf, bufsize);
+                return converterr(nullable, type->tp_name, arg, msgbuf, 
bufsize);
 
         }
         else if (*format == '&') {
@@ -1237,16 +1304,18 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
             void *addr = va_arg(*p_va, void *);
             int res;
             format++;
+            HANDLE_NULLABLE;
             if (! (res = (*convert)(arg, addr)))
-                return converterr("(unspecified)",
+                return converterr(nullable, "(unspecified)",
                                   arg, msgbuf, bufsize);
             if (res == Py_CLEANUP_SUPPORTED &&
                 addcleanup(addr, freelist, convert) == -1)
-                return converterr("(cleanup problem)",
+                return converterr(nullable, "(cleanup problem)",
                                 arg, msgbuf, bufsize);
         }
         else {
             p = va_arg(*p_va, PyObject **);
+            HANDLE_NULLABLE;
             *p = arg;
         }
         break;
@@ -1258,29 +1327,30 @@ convertsimple(PyObject *arg, const char **p_format, 
va_list *p_va, int flags,
 
         if (*format != '*')
             return converterr(
-                "(invalid use of 'w' format character)",
+                nullable, "(invalid use of 'w' format character)",
                 arg, msgbuf, bufsize);
         format++;
+        HANDLE_NULLABLE;
 
         /* Caller is interested in Py_buffer, and the object supports it
            directly. The request implicitly asks for PyBUF_SIMPLE, so the
            result is C-contiguous with format 'B'. */
         if (PyObject_GetBuffer(arg, (Py_buffer*)p, PyBUF_WRITABLE) < 0) {
             PyErr_Clear();
-            return converterr("read-write bytes-like object",
+            return converterr(nullable, "read-write bytes-like object",
                               arg, msgbuf, bufsize);
         }
         assert(PyBuffer_IsContiguous((Py_buffer *)p, 'C'));
         if (addcleanup(p, freelist, cleanup_buffer)) {
             return converterr(
-                "(cleanup problem)",
+                nullable, "(cleanup problem)",
                 arg, msgbuf, bufsize);
         }
         break;
     }
 
     default:
-        return converterr("(impossible<bad format char>)", arg, msgbuf, 
bufsize);
+        return converterr(nullable, "(impossible<bad format char>)", arg, 
msgbuf, bufsize);
 
     }
 
@@ -2675,6 +2745,9 @@ skipitem(const char **p_format, va_list *p_va, int flags)
         return "impossible<bad format char>";
 
     }
+    if (*format == '?') {
+        format++;
+    }
 
     *p_format = format;
     return NULL;

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to