Skip to content

gh-117557: Improve error messages when a string, bytes or bytearray of length 1 are expected #117631

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
261 changes: 230 additions & 31 deletions Lib/test/clinic.test.c

Large diffs are not rendered by default.

26 changes: 20 additions & 6 deletions Lib/test/test_ctypes/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
import unittest
from ctypes import (CDLL, Structure, Array, CFUNCTYPE,
byref, POINTER, pointer, ArgumentError,
byref, POINTER, pointer, ArgumentError, sizeof,
c_char, c_wchar, c_byte, c_char_p, c_wchar_p,
c_short, c_int, c_long, c_longlong, c_void_p,
c_float, c_double, c_longdouble)
Expand Down Expand Up @@ -72,7 +72,8 @@ def callback(*args):

self.assertEqual(str(cm.exception),
"argument 1: TypeError: one character bytes, "
"bytearray or integer expected")
"bytearray, or an integer in range(256) expected, "
"not bytes of length 3")

def test_wchar_parm(self):
f = dll._testfunc_i_bhilfd
Expand All @@ -84,14 +85,27 @@ def test_wchar_parm(self):
with self.assertRaises(ArgumentError) as cm:
f(1, 2, 3, 4, 5.0, 6.0)
self.assertEqual(str(cm.exception),
"argument 2: TypeError: unicode string expected "
"instead of int instance")
"argument 2: TypeError: a unicode character expected, "
"not instance of int")

with self.assertRaises(ArgumentError) as cm:
f(1, "abc", 3, 4, 5.0, 6.0)
self.assertEqual(str(cm.exception),
"argument 2: TypeError: one character unicode string "
"expected")
"argument 2: TypeError: a unicode character expected, "
"not a string of length 3")

with self.assertRaises(ArgumentError) as cm:
f(1, "", 3, 4, 5.0, 6.0)
self.assertEqual(str(cm.exception),
"argument 2: TypeError: a unicode character expected, "
"not a string of length 0")

if sizeof(c_wchar) < 4:
with self.assertRaises(ArgumentError) as cm:
f(1, "\U0001f40d", 3, 4, 5.0, 6.0)
self.assertEqual(str(cm.exception),
"argument 2: TypeError: the string '\\U0001f40d' "
"cannot be converted to a single wchar_t character")

def test_c_char_p_parm(self):
"""Test the error message when converting an incompatible type to c_char_p."""
Expand Down
22 changes: 18 additions & 4 deletions Lib/test/test_ctypes/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from ctypes import (CDLL, PyDLL, ArgumentError,
Structure, Array, Union,
_Pointer, _SimpleCData, _CFuncPtr,
POINTER, pointer, byref,
POINTER, pointer, byref, sizeof,
c_void_p, c_char_p, c_wchar_p, py_object,
c_bool,
c_char, c_wchar,
Expand Down Expand Up @@ -87,19 +87,33 @@ def test_c_char(self):
with self.assertRaises(TypeError) as cm:
c_char.from_param(b"abc")
self.assertEqual(str(cm.exception),
"one character bytes, bytearray or integer expected")
"one character bytes, bytearray, or an integer "
"in range(256) expected, not bytes of length 3")

def test_c_wchar(self):
with self.assertRaises(TypeError) as cm:
c_wchar.from_param("abc")
self.assertEqual(str(cm.exception),
"one character unicode string expected")
"a unicode character expected, not a string of length 3")

with self.assertRaises(TypeError) as cm:
c_wchar.from_param("")
self.assertEqual(str(cm.exception),
"a unicode character expected, not a string of length 0")

with self.assertRaises(TypeError) as cm:
c_wchar.from_param(123)
self.assertEqual(str(cm.exception),
"unicode string expected instead of int instance")
"a unicode character expected, not instance of int")

if sizeof(c_wchar) < 4:
with self.assertRaises(TypeError) as cm:
c_wchar.from_param('\U0001f40d')
self.assertEqual(str(cm.exception),
"the string '\\U0001f40d' cannot be converted to "
"a single wchar_t character")



def test_int_pointers(self):
LPINT = POINTER(c_int)
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -1578,7 +1578,7 @@ def __int__(self):
self.assertRaisesRegex(TypeError, '%u format: a real number is required, not complex', operator.mod, '%u', 3j)
self.assertRaisesRegex(TypeError, '%i format: a real number is required, not complex', operator.mod, '%i', 2j)
self.assertRaisesRegex(TypeError, '%d format: a real number is required, not complex', operator.mod, '%d', 1j)
self.assertRaisesRegex(TypeError, '%c requires int or char', operator.mod, '%c', pi)
self.assertRaisesRegex(TypeError, r'%c requires an int or a unicode character, not .*\.PseudoFloat', operator.mod, '%c', pi)

class RaisingNumber:
def __int__(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve error messages when a string, bytes or bytearray object of length 1
is expected.
59 changes: 42 additions & 17 deletions Modules/_ctypes/cfield.c
Original file line number Diff line number Diff line change
Expand Up @@ -1100,25 +1100,45 @@ O_set(void *ptr, PyObject *value, Py_ssize_t size)
static PyObject *
c_set(void *ptr, PyObject *value, Py_ssize_t size)
{
if (PyBytes_Check(value) && PyBytes_GET_SIZE(value) == 1) {
if (PyBytes_Check(value)) {
if (PyBytes_GET_SIZE(value) != 1) {
PyErr_Format(PyExc_TypeError,
"one character bytes, bytearray, or an integer "
"in range(256) expected, not bytes of length %zd",
PyBytes_GET_SIZE(value));
return NULL;
}
*(char *)ptr = PyBytes_AS_STRING(value)[0];
_RET(value);
}
if (PyByteArray_Check(value) && PyByteArray_GET_SIZE(value) == 1) {
if (PyByteArray_Check(value)) {
if (PyByteArray_GET_SIZE(value) != 1) {
PyErr_Format(PyExc_TypeError,
"one character bytes, bytearray, or an integer "
"in range(256) expected, not bytearray of length %zd",
PyByteArray_GET_SIZE(value));
return NULL;
}
*(char *)ptr = PyByteArray_AS_STRING(value)[0];
_RET(value);
}
if (PyLong_Check(value))
{
long longval = PyLong_AsLong(value);
if (longval < 0 || longval >= 256)
goto error;
if (PyLong_Check(value)) {
int overflow;
long longval = PyLong_AsLongAndOverflow(value, &overflow);
if (longval == -1 && PyErr_Occurred()) {
return NULL;
}
if (overflow || longval < 0 || longval >= 256) {
PyErr_SetString(PyExc_TypeError, "integer not in range(256)");
return NULL;
}
*(char *)ptr = (char)longval;
_RET(value);
}
error:
PyErr_Format(PyExc_TypeError,
"one character bytes, bytearray or integer expected");
"one character bytes, bytearray, or an integer "
"in range(256) expected, not %T",
value);
return NULL;
}

Expand All @@ -1137,22 +1157,27 @@ u_set(void *ptr, PyObject *value, Py_ssize_t size)
wchar_t chars[2];
if (!PyUnicode_Check(value)) {
PyErr_Format(PyExc_TypeError,
"unicode string expected instead of %s instance",
Py_TYPE(value)->tp_name);
"a unicode character expected, not instance of %T",
value);
return NULL;
} else
Py_INCREF(value);
}

len = PyUnicode_AsWideChar(value, chars, 2);
if (len != 1) {
Py_DECREF(value);
PyErr_SetString(PyExc_TypeError,
"one character unicode string expected");
if (PyUnicode_GET_LENGTH(value) != 1) {
PyErr_Format(PyExc_TypeError,
"a unicode character expected, not a string of length %zd",
PyUnicode_GET_LENGTH(value));
}
else {
PyErr_Format(PyExc_TypeError,
"the string %A cannot be converted to a single wchar_t character",
value);
}
return NULL;
}

*(wchar_t *)ptr = chars[0];
Py_DECREF(value);

_RET(value);
}
Expand Down
32 changes: 23 additions & 9 deletions Modules/_cursesmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,20 @@ static int
PyCurses_ConvertToChtype(PyCursesWindowObject *win, PyObject *obj, chtype *ch)
{
long value;
if(PyBytes_Check(obj) && PyBytes_Size(obj) == 1) {
if (PyBytes_Check(obj)) {
if (PyBytes_GET_SIZE(obj) != 1) {
PyErr_Format(PyExc_TypeError,
"expect int or bytes or str of length 1, "
"got a bytes of length %zd",
PyBytes_GET_SIZE(obj));
return 0;
}
value = (unsigned char)PyBytes_AsString(obj)[0];
}
else if (PyUnicode_Check(obj)) {
if (PyUnicode_GetLength(obj) != 1) {
if (PyUnicode_GET_LENGTH(obj) != 1) {
PyErr_Format(PyExc_TypeError,
"expect bytes or str of length 1, or int, "
"expect int or bytes or str of length 1, "
"got a str of length %zi",
PyUnicode_GET_LENGTH(obj));
return 0;
Expand Down Expand Up @@ -272,7 +279,7 @@ PyCurses_ConvertToChtype(PyCursesWindowObject *win, PyObject *obj, chtype *ch)
}
else {
PyErr_Format(PyExc_TypeError,
"expect bytes or str of length 1, or int, got %s",
"expect int or bytes or str of length 1, got %s",
Py_TYPE(obj)->tp_name);
return 0;
}
Expand Down Expand Up @@ -315,7 +322,7 @@ PyCurses_ConvertToCchar_t(PyCursesWindowObject *win, PyObject *obj,
#ifdef HAVE_NCURSESW
if (PyUnicode_AsWideChar(obj, buffer, 2) != 1) {
PyErr_Format(PyExc_TypeError,
"expect bytes or str of length 1, or int, "
"expect int or bytes or str of length 1, "
"got a str of length %zi",
PyUnicode_GET_LENGTH(obj));
return 0;
Expand All @@ -326,7 +333,14 @@ PyCurses_ConvertToCchar_t(PyCursesWindowObject *win, PyObject *obj,
return PyCurses_ConvertToChtype(win, obj, ch);
#endif
}
else if(PyBytes_Check(obj) && PyBytes_Size(obj) == 1) {
else if (PyBytes_Check(obj)) {
if (PyBytes_GET_SIZE(obj) != 1) {
PyErr_Format(PyExc_TypeError,
"expect int or bytes or str of length 1, "
"got a bytes of length %zd",
PyBytes_GET_SIZE(obj));
return 0;
}
value = (unsigned char)PyBytes_AsString(obj)[0];
}
else if (PyLong_CheckExact(obj)) {
Expand All @@ -340,7 +354,7 @@ PyCurses_ConvertToCchar_t(PyCursesWindowObject *win, PyObject *obj,
}
else {
PyErr_Format(PyExc_TypeError,
"expect bytes or str of length 1, or int, got %s",
"expect int or bytes or str of length 1, got %s",
Py_TYPE(obj)->tp_name);
return 0;
}
Expand Down Expand Up @@ -4443,7 +4457,7 @@ PyCurses_ConvertToWchar_t(PyObject *obj,
wchar_t buffer[2];
if (PyUnicode_AsWideChar(obj, buffer, 2) != 1) {
PyErr_Format(PyExc_TypeError,
"expect str of length 1 or int, "
"expect int or str of length 1, "
"got a str of length %zi",
PyUnicode_GET_LENGTH(obj));
return 0;
Expand All @@ -4470,7 +4484,7 @@ PyCurses_ConvertToWchar_t(PyObject *obj,
}
else {
PyErr_Format(PyExc_TypeError,
"expect str of length 1 or int, got %s",
"expect int or str of length 1, got %s",
Py_TYPE(obj)->tp_name);
return 0;
}
Expand Down
40 changes: 28 additions & 12 deletions Modules/arraymodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -260,20 +260,32 @@ u_getitem(arrayobject *ap, Py_ssize_t i)
static int
u_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
{
PyObject *u;
if (!PyArg_Parse(v, "U;array item must be unicode character", &u)) {
if (!PyUnicode_Check(v)) {
PyErr_Format(PyExc_TypeError,
"array item must be a unicode character, not %T",
v);
return -1;
}

Py_ssize_t len = PyUnicode_AsWideChar(u, NULL, 0);
Py_ssize_t len = PyUnicode_AsWideChar(v, NULL, 0);
if (len != 2) {
PyErr_SetString(PyExc_TypeError,
"array item must be unicode character");
if (PyUnicode_GET_LENGTH(v) != 1) {
PyErr_Format(PyExc_TypeError,
"array item must be a unicode character, "
"not a string of length %zd",
PyUnicode_GET_LENGTH(v));
}
else {
PyErr_Format(PyExc_TypeError,
"string %A cannot be converted to "
"a single wchar_t character",
v);
}
return -1;
}

wchar_t w;
len = PyUnicode_AsWideChar(u, &w, 1);
len = PyUnicode_AsWideChar(v, &w, 1);
assert(len == 1);

if (i >= 0) {
Expand All @@ -291,19 +303,23 @@ w_getitem(arrayobject *ap, Py_ssize_t i)
static int
w_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
{
PyObject *u;
if (!PyArg_Parse(v, "U;array item must be unicode character", &u)) {
if (!PyUnicode_Check(v)) {
PyErr_Format(PyExc_TypeError,
"array item must be a unicode character, not %T",
v);
return -1;
}

if (PyUnicode_GetLength(u) != 1) {
PyErr_SetString(PyExc_TypeError,
"array item must be unicode character");
if (PyUnicode_GET_LENGTH(v) != 1) {
PyErr_Format(PyExc_TypeError,
"array item must be a unicode character, "
"not a string of length %zd",
PyUnicode_GET_LENGTH(v));
return -1;
}

if (i >= 0) {
((Py_UCS4 *)ap->ob_item)[i] = PyUnicode_READ_CHAR(u, 0);
((Py_UCS4 *)ap->ob_item)[i] = PyUnicode_READ_CHAR(v, 0);
}
return 0;
}
Expand Down
Loading
Loading