From 068f06568be288b8628a4e24118503e4d9b7af1b Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sat, 25 Apr 2009 21:40:15 +0000 Subject: Issue #5835, deprecate PyOS_ascii_formatd. If anyone wants to clean up the documentation, feel free. It's my first documentation foray, and it's not that great. Will port to py3k with a different strategy. --- Doc/c-api/conversion.rst | 35 ++++++++++ Include/pystrtod.h | 12 +++- Lib/test/test_ascii_formatd.py | 62 +++++++++++++++++ Misc/NEWS | 3 + Modules/cPickle.c | 3 +- Objects/floatobject.c | 5 +- Objects/stringobject.c | 9 +-- Objects/unicodeobject.c | 15 ++-- Python/pystrtod.c | 154 +++++++++++++++++++++++++++++------------ 9 files changed, 232 insertions(+), 66 deletions(-) create mode 100644 Lib/test/test_ascii_formatd.py diff --git a/Doc/c-api/conversion.rst b/Doc/c-api/conversion.rst index 0c81bc0..e391b76 100644 --- a/Doc/c-api/conversion.rst +++ b/Doc/c-api/conversion.rst @@ -76,7 +76,42 @@ The following functions provide locale-independent string to number conversions. the conversion failed. .. versionadded:: 2.4 + .. deprecated:: 2.7 + This function is removed in Python 2.7 and 3.1. Use :func:`PyOS_double_to_string` + instead. +.. cfunction:: char * PyOS_double_to_string(double val, char format_code, int precision, int flags, int *ptype) + + Convert a :ctype:`double` *val* to a string using supplied + *format_code*, *precision*, and *flags*. + + *format_code* must be one of ``'e'``, ``'E'``, ``'f'``, ``'F'``, + ``'g'``, ``'G'``, ``'s'``, or ``'r'``. For ``'s'`` and ``'r'``, the + supplied *precision* must be 0 and is ignored. These specify the + standards :func:`str` and :func:`repr` formats, respectively. + + *flags* can be zero or more of the values *Py_DTSF_SIGN*, + *Py_DTSF_ADD_DOT_0*, or *Py_DTSF_ALT*, and-ed together. + + *Py_DTSF_SIGN* means always precede the returned string with a + sign character, even if *val* is non-negative. + + *Py_DTSF_ADD_DOT_0* means ensure that the returned string will + not look like an integer. + + *Py_DTSF_ALT* means apply "alternate" formatting rules. See the + documentation for the :func:`PyOS_snprintf` ``'#'`` specifier + for details. + + If *ptype* is non-NULL, then the value it points to will be set to + one of *Py_DTST_FINITE*, *Py_DTST_INFINITE*, or *Py_DTST_NAN*, + signifying that *val* is a finite number, an infinite number, or + not a number, respectively. + + The return value is a pointer to *buffer* with the converted string or NULL if + the conversion failed. + + .. versionadded:: 2.7 .. cfunction:: double PyOS_ascii_atof(const char *nptr) diff --git a/Include/pystrtod.h b/Include/pystrtod.h index 1caa7ae..106448b 100644 --- a/Include/pystrtod.h +++ b/Include/pystrtod.h @@ -8,7 +8,17 @@ extern "C" { PyAPI_FUNC(double) PyOS_ascii_strtod(const char *str, char **ptr); PyAPI_FUNC(double) PyOS_ascii_atof(const char *str); -PyAPI_FUNC(char *) PyOS_ascii_formatd(char *buffer, size_t buf_len, const char *format, double d); + +/* Deprecated in 2.7 and 3.1. Will disappear in 2.8 (if it exists) and 3.2 */ +PyAPI_FUNC(char *) PyOS_ascii_formatd(char *buffer, size_t buf_len, + const char *format, double d); + +/* Use PyOS_double_to_string instead. It's the same, except it allocates + the appropriately sized buffer and returns it. This function will go + away in Python 2.8 and 3.2. */ +PyAPI_FUNC(void) _PyOS_double_to_string(char *buf, size_t buf_len, double val, + char format_code, int precision, + int flags, int* type); /* The caller is responsible for calling PyMem_Free to free the buffer that's is returned. */ diff --git a/Lib/test/test_ascii_formatd.py b/Lib/test/test_ascii_formatd.py new file mode 100644 index 0000000..3501955 --- /dev/null +++ b/Lib/test/test_ascii_formatd.py @@ -0,0 +1,62 @@ +# PyOS_ascii_formatd is deprecated and not called from anywhere in +# Python itself. So this module is the only place it gets tested. +# Test that it works, and test that it's deprecated. + +import unittest +from test_support import check_warnings, run_unittest, cpython_only + + +class FormatDeprecationTests(unittest.TestCase): + + @cpython_only + def testFormatDeprecation(self): + # delay importing ctypes until we know we're in CPython + from ctypes import (pythonapi, create_string_buffer, sizeof, byref, + c_double) + PyOS_ascii_formatd = pythonapi.PyOS_ascii_formatd + buf = create_string_buffer(' ' * 100) + + with check_warnings() as w: + PyOS_ascii_formatd(byref(buf), sizeof(buf), '%+.10f', + c_double(10.0)) + self.assertEqual(buf.value, '+10.0000000000') + + self.assertEqual(str(w.message), 'PyOS_ascii_formatd is deprecated, ' + 'use PyOS_double_to_string instead') + +class FormatTests(unittest.TestCase): + # ensure that, for the restricted set of format codes, + # %-formatting returns the same values os PyOS_ascii_formatd + @cpython_only + def testFormat(self): + # delay importing ctypes until we know we're in CPython + from ctypes import (pythonapi, create_string_buffer, sizeof, byref, + c_double) + PyOS_ascii_formatd = pythonapi.PyOS_ascii_formatd + buf = create_string_buffer(' ' * 100) + + tests = [ + ('%f', 100.0), + ('%g', 100.0), + ('%#g', 100.0), + ('%#.2g', 100.0), + ('%#.2g', 123.4567), + ('%#.2g', 1.234567e200), + ('%e', 1.234567e200), + ('%e', 1.234), + ('%+e', 1.234), + ('%-e', 1.234), + ] + + with check_warnings(): + for format, val in tests: + PyOS_ascii_formatd(byref(buf), sizeof(buf), format, + c_double(val)) + self.assertEqual(buf.value, format % val) + + +def test_main(): + run_unittest(FormatDeprecationTests, FormatTests) + +if __name__ == '__main__': + test_main() diff --git a/Misc/NEWS b/Misc/NEWS index 7c36bda..b211512 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -12,6 +12,9 @@ What's New in Python 2.7 alpha 1 Core and Builtins ----------------- +- Issue #5835: Deprecate PyOS_ascii_formatd and replace it with + _PyOS_double_to_string or PyOS_double_to_string. + - Issue #5283: Setting __class__ in __del__ caused a segfault. - Issue #5816: complex(repr(z)) now recovers z exactly, even when diff --git a/Modules/cPickle.c b/Modules/cPickle.c index fb13ba1..6c7ed99 100644 --- a/Modules/cPickle.c +++ b/Modules/cPickle.c @@ -1166,7 +1166,8 @@ save_float(Picklerobject *self, PyObject *args) else { char c_str[250]; c_str[0] = FLOAT; - PyOS_ascii_formatd(c_str + 1, sizeof(c_str) - 2, "%.17g", x); + _PyOS_double_to_string(c_str + 1, sizeof(c_str) - 2, x, 'g', + 17, 0, NULL); /* Extend the formatted string with a newline character */ strcat(c_str, "\n"); diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 4f041f4..382b991 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -342,7 +342,6 @@ static void format_float(char *buf, size_t buflen, PyFloatObject *v, int precision) { register char *cp; - char format[32]; int i; /* Subroutine for float_repr and float_print. @@ -352,8 +351,8 @@ format_float(char *buf, size_t buflen, PyFloatObject *v, int precision) in such cases, we append ".0" to the string. */ assert(PyFloat_Check(v)); - PyOS_snprintf(format, 32, "%%.%ig", precision); - PyOS_ascii_formatd(buf, buflen, format, v->ob_fval); + _PyOS_double_to_string(buf, buflen, v->ob_fval, 'g', precision, + 0, NULL); cp = buf; if (*cp == '-') cp++; diff --git a/Objects/stringobject.c b/Objects/stringobject.c index 89614e6..316a271a 100644 --- a/Objects/stringobject.c +++ b/Objects/stringobject.c @@ -4332,9 +4332,6 @@ Py_LOCAL_INLINE(int) formatfloat(char *buf, size_t buflen, int flags, int prec, int type, PyObject *v) { - /* fmt = '%#.' + `prec` + `type` - worst case length = 3 + 10 (len of INT_MAX) + 1 = 14 (use 20)*/ - char fmt[20]; double x; x = PyFloat_AsDouble(v); if (x == -1.0 && PyErr_Occurred()) { @@ -4378,10 +4375,8 @@ formatfloat(char *buf, size_t buflen, int flags, "formatted float is too long (precision too large?)"); return -1; } - PyOS_snprintf(fmt, sizeof(fmt), "%%%s.%d%c", - (flags&F_ALT) ? "#" : "", - prec, type); - PyOS_ascii_formatd(buf, buflen, fmt, x); + _PyOS_double_to_string(buf, buflen, x, type, prec, + (flags&F_ALT)?Py_DTSF_ALT:0, NULL); return (int)strlen(buf); } diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 6edc2f8..62191ad 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -8245,11 +8245,13 @@ strtounicode(Py_UNICODE *buffer, const char *charbuffer) } static int -doubletounicode(Py_UNICODE *buffer, size_t len, const char *format, double x) +doubletounicode(Py_UNICODE *buffer, size_t len, int format_code, + int precision, int flags, double x) { Py_ssize_t result; - PyOS_ascii_formatd((char *)buffer, len, format, x); + _PyOS_double_to_string((char *)buffer, len, x, format_code, precision, + flags, NULL); result = strtounicode(buffer, (char *)buffer); return Py_SAFE_DOWNCAST(result, Py_ssize_t, int); } @@ -8276,9 +8278,6 @@ formatfloat(Py_UNICODE *buf, int type, PyObject *v) { - /* fmt = '%#.' + `prec` + `type` - worst case length = 3 + 10 (len of INT_MAX) + 1 = 14 (use 20)*/ - char fmt[20]; double x; x = PyFloat_AsDouble(v); @@ -8320,10 +8319,8 @@ formatfloat(Py_UNICODE *buf, "formatted float is too long (precision too large?)"); return -1; } - PyOS_snprintf(fmt, sizeof(fmt), "%%%s.%d%c", - (flags&F_ALT) ? "#" : "", - prec, type); - return doubletounicode(buf, buflen, fmt, x); + return doubletounicode(buf, buflen, type, prec, + (flags&F_ALT)?Py_DTSF_ALT:0, x); } static PyObject* diff --git a/Python/pystrtod.c b/Python/pystrtod.c index 703ae64..f9a8831 100644 --- a/Python/pystrtod.c +++ b/Python/pystrtod.c @@ -236,6 +236,25 @@ change_decimal_from_locale_to_dot(char* buffer) } +Py_LOCAL_INLINE(void) +ensure_sign(char* buffer, size_t buf_size) +{ + Py_ssize_t len; + + if (buffer[0] == '-') + /* Already have a sign. */ + return; + + /* Include the trailing 0 byte. */ + len = strlen(buffer)+1; + if (len >= buf_size+1) + /* No room for the sign, don't do anything. */ + return; + + memmove(buffer+1, buffer, len); + buffer[0] = '+'; +} + /* From the C99 standard, section 7.19.6: The exponent always contains at least two digits, and only as many more digits as necessary to represent the exponent. @@ -363,7 +382,7 @@ ensure_decimal_point(char* buffer, size_t buf_size) #define FLOAT_FORMATBUFLEN 120 /** - * PyOS_ascii_formatd: + * _PyOS_ascii_formatd: * @buffer: A buffer to place the resulting string in * @buf_size: The length of the buffer. * @format: The printf()-style format to use for the @@ -380,7 +399,8 @@ ensure_decimal_point(char* buffer, size_t buf_size) * * Return value: The pointer to the buffer with the converted string. **/ -char * +/* DEPRECATED, will be deleted in 2.8 and 3.2 */ +PyAPI_FUNC(char *) PyOS_ascii_formatd(char *buffer, size_t buf_size, const char *format, @@ -393,6 +413,11 @@ PyOS_ascii_formatd(char *buffer, also with at least one character past the decimal. */ char tmp_format[FLOAT_FORMATBUFLEN]; + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "PyOS_ascii_formatd is deprecated, " + "use PyOS_double_to_string instead", 1) < 0) + return NULL; + /* The last character in the format string must be the format char */ format_char = format[format_len - 1]; @@ -456,20 +481,22 @@ PyOS_ascii_formatd(char *buffer, return buffer; } -PyAPI_FUNC(char *) PyOS_double_to_string(double val, - char format_code, - int precision, - int flags, - int *type) +PyAPI_FUNC(void) +_PyOS_double_to_string(char *buf, size_t buf_len, double val, + char format_code, int precision, + int flags, int *ptype) { - char buf[128]; char format[32]; - Py_ssize_t len; - char *result; - char *p; int t; int upper = 0; + if (buf_len < 1) { + assert(0); + /* There's no way to signal this error. Just return. */ + return; + } + buf[0] = 0; + /* Validate format_code, and map upper and lower case */ switch (format_code) { case 'e': /* exponent */ @@ -490,25 +517,29 @@ PyAPI_FUNC(char *) PyOS_double_to_string(double val, break; case 'r': /* repr format */ /* Supplied precision is unused, must be 0. */ - if (precision != 0) { - PyErr_BadInternalCall(); - return NULL; - } + if (precision != 0) + return; precision = 17; format_code = 'g'; break; case 's': /* str format */ /* Supplied precision is unused, must be 0. */ - if (precision != 0) { - PyErr_BadInternalCall(); - return NULL; - } + if (precision != 0) + return; precision = 12; format_code = 'g'; break; default: - PyErr_BadInternalCall(); - return NULL; + assert(0); + return; + } + + /* Check for buf too small to fit "-inf". Other buffer too small + conditions are dealt with when converting or formatting finite + numbers. */ + if (buf_len < 5) { + assert(0); + return; } /* Handle nan and inf. */ @@ -524,41 +555,74 @@ PyAPI_FUNC(char *) PyOS_double_to_string(double val, } else { t = Py_DTST_FINITE; + /* Build the format string. */ + PyOS_snprintf(format, sizeof(format), "%%%s.%i%c", + (flags & Py_DTSF_ALT ? "#" : ""), precision, + format_code); - if (flags & Py_DTSF_ADD_DOT_0) - format_code = 'Z'; + /* Have PyOS_snprintf do the hard work. */ + PyOS_snprintf(buf, buf_len, format, val); - PyOS_snprintf(format, 32, "%%%s.%i%c", (flags & Py_DTSF_ALT ? "#" : ""), precision, format_code); - PyOS_ascii_formatd(buf, sizeof(buf), format, val); - } + /* Do various fixups on the return string */ - len = strlen(buf); + /* Get the current locale, and find the decimal point string. + Convert that string back to a dot. */ + change_decimal_from_locale_to_dot(buf); - /* Add 1 for the trailing 0 byte. - Add 1 because we might need to make room for the sign. - */ - result = PyMem_Malloc(len + 2); - if (result == NULL) { - PyErr_NoMemory(); - return NULL; + /* If an exponent exists, ensure that the exponent is at least + MIN_EXPONENT_DIGITS digits, providing the buffer is large + enough for the extra zeros. Also, if there are more than + MIN_EXPONENT_DIGITS, remove as many zeros as possible until + we get back to MIN_EXPONENT_DIGITS */ + ensure_minumim_exponent_length(buf, buf_len); + + /* Possibly make sure we have at least one character after the + decimal point (and make sure we have a decimal point). */ + if (flags & Py_DTSF_ADD_DOT_0) + ensure_decimal_point(buf, buf_len); } - p = result; - /* Add sign when requested. It's convenient (esp. when formatting - complex numbers) to include a sign even for inf and nan. */ + /* Add the sign if asked and the result isn't negative. */ if (flags & Py_DTSF_SIGN && buf[0] != '-') - *p++ = '+'; - - strcpy(p, buf); + ensure_sign(buf, buf_len); if (upper) { /* Convert to upper case. */ - char *p1; - for (p1 = p; *p1; p1++) - *p1 = toupper(*p1); + char *p; + for (p = buf; *p; p++) + *p = toupper(*p); + } + + if (ptype) + *ptype = t; +} + + +PyAPI_FUNC(char *) PyOS_double_to_string(double val, + char format_code, + int precision, + int flags, + int *ptype) +{ + char buf[128]; + Py_ssize_t len; + char *result; + + _PyOS_double_to_string(buf, sizeof(buf), val, format_code, precision, + flags, ptype); + len = strlen(buf); + if (len == 0) { + PyErr_BadInternalCall(); + return NULL; + } + + /* Add 1 for the trailing 0 byte. */ + result = PyMem_Malloc(len + 1); + if (result == NULL) { + PyErr_NoMemory(); + return NULL; } + strcpy(result, buf); - if (type) - *type = t; return result; } -- cgit v0.12