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__.
diff --git a/Doc/library/struct.rst b/Doc/library/struct.rst
index a115c1d..3664e35 100644
--- a/Doc/library/struct.rst
+++ b/Doc/library/struct.rst
@@ -123,6 +123,18 @@
.. 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 c315d8a..aacadd6 100644
--- a/Lib/test/test_struct.py
+++ b/Lib/test/test_struct.py
@@ -2,10 +2,6 @@
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 @@
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 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 1ea44d1..b1431a3 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -15,6 +15,18 @@
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 @@
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 97f2f75..e8fe67d 100644
--- a/Modules/_struct.c
+++ b/Modules/_struct.c
@@ -17,7 +17,10 @@
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 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