From 154b7ad07e775f15a0a556c7ec3ae5ab5762ae1e Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Sun, 7 Mar 2010 16:24:45 +0000 Subject: [PATCH] Issue #1530559: When packing a non-integer with any integer conversion code using struct.pack, attempt to convert to an integer first using the argument's __int__ method (if present). Also raise a DeprecationWarning for any such usage of __int__. This fixes a regression from 2.6, where some (but not all) integer conversion codes already used __int__. --- Doc/library/struct.rst | 12 +++++++ Lib/test/test_struct.py | 69 ++++++++++++++++++++++++++++------------- Misc/NEWS | 16 ++++++++++ Modules/_struct.c | 66 +++++++++++++++++++++++++++++++-------- 4 files changed, 128 insertions(+), 35 deletions(-) diff --git a/Doc/library/struct.rst b/Doc/library/struct.rst index a115c1d026b..3664e351730 100644 --- a/Doc/library/struct.rst +++ b/Doc/library/struct.rst @@ -123,6 +123,18 @@ Notes: .. versionadded:: 2.2 +(3) + When attempting to pack a non-integer using any of the integer conversion + codes, the non-integer's :meth:`__int__` method (if present) will be called + to convert to an integer before packing. However, this behaviour is + deprecated, and will raise :exc:`DeprecationWarning`. + + .. versionchanged:: 2.7 + Prior to version 2.7, not all integer conversion codes would use the + :meth:`__int__` method to convert, and :exc:`DeprecationWarning` was + raised only for float arguments. + + A format character may be preceded by an integral repeat count. For example, the format string ``'4h'`` means exactly the same as ``'hhhh'``. diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index c315d8a3152..aacadd6c84f 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -2,10 +2,6 @@ import array import unittest import struct import warnings -warnings.filterwarnings("ignore", "struct integer overflow masking is deprecated", - DeprecationWarning) - -from functools import wraps from test.test_support import run_unittest import sys @@ -35,22 +31,27 @@ def bigendian_to_native(value): class StructTest(unittest.TestCase): def check_float_coerce(self, format, number): - # SF bug 1530559. struct.pack raises TypeError where it used to convert. - with warnings.catch_warnings(): + # SF bug 1530559. struct.pack raises TypeError where it used + # to convert. + with warnings.catch_warnings(record=True) as w: + # ignore everything except the + # DeprecationWarning we're looking for + warnings.simplefilter("ignore") warnings.filterwarnings( - "ignore", - category=DeprecationWarning, + "always", message=".*integer argument expected, got float", - module=__name__) - self.assertEqual(struct.pack(format, number), struct.pack(format, int(number))) - - with warnings.catch_warnings(): - warnings.filterwarnings( - "error", category=DeprecationWarning, - message=".*integer argument expected, got float", - module="unittest") - self.assertRaises(DeprecationWarning, struct.pack, format, number) + module=__name__ + ) + got = struct.pack(format, number) + nwarn = len(w) + self.assertEqual(nwarn, 1, + "expected exactly one warning from " + "struct.pack({!r}, {!r}); " + "got {} warnings".format( + format, number, nwarn)) + expected = struct.pack(format, int(number)) + self.assertEqual(got, expected) def test_isbigendian(self): self.assertEqual((struct.pack('=i', 1)[0] == chr(0)), ISBIGENDIAN) @@ -291,17 +292,41 @@ class StructTest(unittest.TestCase): class NotAnIntOS: def __int__(self): - return 10585 + return 85 def __long__(self): return -163L - for badobject in ("a string", 3+42j, randrange, - NotAnIntNS(), NotAnIntOS()): - self.assertRaises(struct.error, - struct.pack, format, + for badobject in ("a string", 3+42j, randrange): + self.assertRaises((TypeError, struct.error), + struct.pack, self.format, badobject) + # an attempt to convert a non-integer (with an + # implicit conversion via __int__) should succeed, + # with a DeprecationWarning + for nonint in NotAnIntNS(), NotAnIntOS(): + with warnings.catch_warnings(record=True) as w: + # ignore everything except the + # DeprecationWarning we're looking for + warnings.simplefilter("ignore") + warnings.filterwarnings( + "always", + message=(".*integer argument expected, " + "got non-integer.*"), + category=DeprecationWarning, + module=__name__ + ) + got = struct.pack(self.format, nonint) + nwarn = len(w) + self.assertEqual(nwarn, 1, + "expected exactly one warning from " + "struct.pack({!r}, {!r}); " + "got {} warnings".format( + self.format, nonint, nwarn)) + expected = struct.pack(self.format, int(nonint)) + self.assertEqual(got, expected) + byteorders = '', '@', '=', '<', '>', '!' for code in integer_codes: for byteorder in byteorders: diff --git a/Misc/NEWS b/Misc/NEWS index 1ea44d124bd..b1431a3e005 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -15,6 +15,18 @@ Core and Builtins Library ------- +Extension Modules +----------------- + +- Issue #1530559: When passing a non-integer argument to struct.pack + with *any* integer format code (one of 'bBhHiIlLqQ'), struct.pack + attempts to use the argument's __int__ method to convert to an + integer before packing. It also produces a DeprecationWarning in + this case. (In Python 2.6, the behaviour was inconsistent: __int__ + was used for some integer codes but not for others, and the set of + integer codes for which it was used differed between native packing + and standard packing.) + What's New in Python 2.7 alpha 4? ================================= @@ -2192,6 +2204,10 @@ Extension Modules TypeError used to be raised (with a confusing error message) for 'I', 'L', '*B', '*H', '*I', '*L', and struct.error in other cases. + Note: as of Python 2.7 beta 1, the above is out of date. In 2.7 + beta 1, any argument with an __int__ method can be packed, but use + of this feature triggers a DeprecationWarning. + - Issue #4873: Fix resource leaks in error cases of pwd and grp. - Issue #4751: For hashlib algorithms provided by OpenSSL, the Python diff --git a/Modules/_struct.c b/Modules/_struct.c index 97f2f75cf36..e8fe67d5fa3 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -17,7 +17,10 @@ static PyTypeObject PyStructType; typedef int Py_ssize_t; #endif -#define FLOAT_COERCE "integer argument expected, got float" +/* warning messages */ +#define FLOAT_COERCE_WARN "integer argument expected, got float" +#define NON_INTEGER_WARN "integer argument expected, got non-integer " \ + "(implicit conversion using __int__ is deprecated)" /* The translation function for each format character is table driven */ @@ -104,21 +107,58 @@ static char *integer_codes = "bBhHiIlLqQ"; static PyObject * get_pylong(PyObject *v) { + PyObject *r; assert(v != NULL); - if (PyInt_Check(v)) - return PyLong_FromLong(PyInt_AS_LONG(v)); - if (PyLong_Check(v)) { - Py_INCREF(v); - return v; - } - if (PyFloat_Check(v)) { - if (PyErr_WarnEx(PyExc_DeprecationWarning, FLOAT_COERCE, 1)<0) + if (!PyInt_Check(v) && !PyLong_Check(v)) { + PyNumberMethods *m; + /* Not an integer; try to use __int__ to convert to an + integer. This behaviour is deprecated, and is removed in + Python 3.x. */ + m = Py_TYPE(v)->tp_as_number; + if (m != NULL && m->nb_int != NULL) { + /* Special case warning message for floats, for + backwards compatibility. */ + if (PyFloat_Check(v)) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + FLOAT_COERCE_WARN, 1)) + return NULL; + } + else { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + NON_INTEGER_WARN, 1)) + return NULL; + } + v = m->nb_int(v); + if (v == NULL) + return NULL; + if (!PyInt_Check(v) && !PyLong_Check(v)) { + PyErr_SetString(PyExc_TypeError, + "__int__ method returned non-integer"); + return NULL; + } + } + else { + PyErr_SetString(StructError, + "cannot convert argument to integer"); return NULL; - return PyNumber_Long(v); + } } - PyErr_SetString(StructError, - "cannot convert argument to long"); - return NULL; + else + /* Ensure we own a reference to v. */ + Py_INCREF(v); + + if (PyInt_Check(v)) { + r = PyLong_FromLong(PyInt_AS_LONG(v)); + Py_DECREF(v); + } + else if (PyLong_Check(v)) { + assert(PyLong_Check(v)); + r = v; + } + else + assert(0); /* shouldn't ever get here */ + + return r; } /* Helper to convert a Python object to a C long. Sets an exception