]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-110815: Support non-ASCII keyword names in PyArg_ParseTupleAndKeywords() (GH-110816)
authorSerhiy Storchaka <storchaka@gmail.com>
Sat, 14 Oct 2023 05:50:03 +0000 (08:50 +0300)
committerGitHub <noreply@github.com>
Sat, 14 Oct 2023 05:50:03 +0000 (08:50 +0300)
It already mostly worked, except in the case when invalid keyword
argument with non-ASCII name was passed to function with non-ASCII
parameter names. Then it crashed in the debug mode.

Doc/c-api/arg.rst
Doc/whatsnew/3.13.rst
Lib/test/test_capi/test_getargs.py
Misc/NEWS.d/next/C API/2023-10-13-14-18-06.gh-issue-110815.tEFLVl.rst [new file with mode: 0644]
Python/getargs.c

index c43dd0f4303cd474886cc1fe4498144231154eba..62d87d898e682c5520a4c152ebe0d14f9b57a8b6 100644 (file)
@@ -416,8 +416,10 @@ API Functions
 .. c:function:: int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...)
 
    Parse the parameters of a function that takes both positional and keyword
-   parameters into local variables.  The *keywords* argument is a
-   ``NULL``-terminated array of keyword parameter names.  Empty names denote
+   parameters into local variables.
+   The *keywords* argument is a ``NULL``-terminated array of keyword parameter
+   names specified as null-terminated ASCII or UTF-8 encoded C strings.
+   Empty names denote
    :ref:`positional-only parameters <positional-only_parameter>`.
    Returns true on success; on failure, it returns false and raises the
    appropriate exception.
@@ -426,6 +428,9 @@ API Functions
       Added support for :ref:`positional-only parameters
       <positional-only_parameter>`.
 
+   .. versionchanged:: 3.13
+      Added support for non-ASCII keyword parameter names.
+
 
 .. c:function:: int PyArg_VaParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], va_list vargs)
 
index dfce976fbb50eebf04509310992d032b7585868b..eb49e015fd0dcc1f3ad29d6e87992ed95d1d78ec 100644 (file)
@@ -1045,6 +1045,10 @@ New Features
   but pass event arguments as a Python :class:`tuple` object.
   (Contributed by Victor Stinner in :gh:`85283`.)
 
+* :c:func:`PyArg_ParseTupleAndKeywords` now supports non-ASCII keyword
+  parameter names.
+  (Contributed by Serhiy Storchaka in :gh:`110815`.)
+
 Porting to Python 3.13
 ----------------------
 
index 7fc25f8259739950dcda7718bdc664bbde3b1c30..96d34ab94c5981e39c8b747de266df29f404ae8a 100644 (file)
@@ -1235,6 +1235,57 @@ class ParseTupleAndKeywords_Test(unittest.TestCase):
         with self.assertRaisesRegex(SystemError, 'Empty keyword'):
             parse((1,), {}, 'O|OO', ['', 'a', ''])
 
+    def test_nonascii_keywords(self):
+        parse = _testcapi.parse_tuple_and_keywords
+
+        for name in ('a', 'ä', 'ŷ', '㷷', '𐀀'):
+            with self.subTest(name=name):
+                self.assertEqual(parse((), {name: 1}, 'O', [name]), (1,))
+                self.assertEqual(parse((), {}, '|O', [name]), (NULL,))
+                with self.assertRaisesRegex(TypeError,
+                        f"function missing required argument '{name}'"):
+                    parse((), {}, 'O', [name])
+                with self.assertRaisesRegex(TypeError,
+                        fr"argument for function given by name \('{name}'\) "
+                        fr"and position \(1\)"):
+                    parse((1,), {name: 2}, 'O|O', [name, 'b'])
+                with self.assertRaisesRegex(TypeError,
+                        f"'{name}' is an invalid keyword argument"):
+                    parse((), {name: 1}, '|O', ['b'])
+                with self.assertRaisesRegex(TypeError,
+                        "'b' is an invalid keyword argument"):
+                    parse((), {'b': 1}, '|O', [name])
+
+                invalid = name.encode() + (name.encode()[:-1] or b'\x80')
+                self.assertEqual(parse((), {}, '|O', [invalid]), (NULL,))
+                self.assertEqual(parse((1,), {'b': 2}, 'O|O', [invalid, 'b']),
+                                    (1, 2))
+                with self.assertRaisesRegex(TypeError,
+                        f"function missing required argument '{name}\ufffd'"):
+                    parse((), {}, 'O', [invalid])
+                with self.assertRaisesRegex(UnicodeDecodeError,
+                        f"'utf-8' codec can't decode bytes? "):
+                    parse((), {'b': 1}, '|OO', [invalid, 'b'])
+                with self.assertRaisesRegex(UnicodeDecodeError,
+                        f"'utf-8' codec can't decode bytes? "):
+                    parse((), {'b': 1}, '|O', [invalid])
+
+                for name2 in ('b', 'ë', 'ĉ', 'Ɐ', '𐀁'):
+                    with self.subTest(name2=name2):
+                        with self.assertRaisesRegex(TypeError,
+                                f"'{name2}' is an invalid keyword argument"):
+                            parse((), {name2: 1}, '|O', [name])
+
+                name2 = name.encode().decode('latin1')
+                if name2 != name:
+                    with self.assertRaisesRegex(TypeError,
+                            f"'{name2}' is an invalid keyword argument"):
+                        parse((), {name2: 1}, '|O', [name])
+                    name3 = name + '3'
+                    with self.assertRaisesRegex(TypeError,
+                            f"'{name2}' is an invalid keyword argument"):
+                        parse((), {name2: 1, name3: 2}, '|OO', [name, name3])
+
 
 class Test_testcapi(unittest.TestCase):
     locals().update((name, getattr(_testcapi, name))
diff --git a/Misc/NEWS.d/next/C API/2023-10-13-14-18-06.gh-issue-110815.tEFLVl.rst b/Misc/NEWS.d/next/C API/2023-10-13-14-18-06.gh-issue-110815.tEFLVl.rst
new file mode 100644 (file)
index 0000000..216d2d2
--- /dev/null
@@ -0,0 +1 @@
+Support non-ASCII keyword names in :c:func:`PyArg_ParseTupleAndKeywords`.
index d590e2e153389e9076ebda6921c860464536e6ef..a0eef2ccee5c3786916515fbde1227171255bb5c 100644 (file)
@@ -1729,7 +1729,7 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const char *format,
                 return cleanreturn(0, &freelist);
             }
             for (i = pos; i < len; i++) {
-                if (_PyUnicode_EqualToASCIIString(key, kwlist[i])) {
+                if (PyUnicode_EqualToUTF8(key, kwlist[i])) {
                     match = 1;
                     break;
                 }