From eee75b931e4310cd43efa56b19cca7b76a2f5aa2 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 29 Sep 2026 06:26:32 +0300 Subject: [PATCH 01/11] Cleanup insert_from_end_inplace() --- fmt.c | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/fmt.c b/fmt.c index 0bb24f7d..1481bbc0 100644 --- a/fmt.c +++ b/fmt.c @@ -305,20 +305,10 @@ fill_number(PyUnicodeWriter *writer, const NumberFieldWidths *spec, Py_LOCAL(void) insert_from_end_inplace(char *str, Py_ssize_t n, char c) { - assert(str && n > 0); - - size_t len = strlen(str); - - if (len <= n) { - return; - } - - Py_ssize_t num_separators = ((Py_ssize_t)len - 1) / n; - Py_ssize_t new_len = (Py_ssize_t)len + num_separators; - Py_ssize_t src = (Py_ssize_t)len; - Py_ssize_t dest = new_len; - Py_ssize_t count = -1; + Py_ssize_t len = (Py_ssize_t)strlen(str); + Py_ssize_t src = len, dest = len + (len - 1)/n, count = -1; + assert(str && 0 < n); while (src >= 0) { if (count > 0 && count % n == 0 && src < len) { str[dest--] = c; From 721037c2b6ef990918710d563d5380e550feeb9e Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 2 Oct 2026 09:06:28 +0300 Subject: [PATCH 02/11] Remove fast path in MPZ_format() --- fmt.c | 10 ---------- gmp.c | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/fmt.c b/fmt.c index 1481bbc0..cfbb1eba 100644 --- a/fmt.c +++ b/fmt.c @@ -318,8 +318,6 @@ insert_from_end_inplace(char *str, Py_ssize_t n, char c) } } -extern PyObject * MPZ_to_str(MPZ_Object *u, int base, bool tag); - Py_LOCAL(PyObject *) MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) { @@ -350,14 +348,6 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) base = 10; break; } - /* Fast path */ - if (!format->alternate && !sign - && format->width == -1 - && !format->thousands_separators - && MPZ_CheckExact(u)) - { - return MPZ_to_str(u, base, false); - } sign |= negative; if (format->thousands_separators) { if (format->type == 'd') { diff --git a/gmp.c b/gmp.c index ad0402a3..c01128ee 100644 --- a/gmp.c +++ b/gmp.c @@ -51,7 +51,7 @@ MPZ_new(void) static const char *MPZ_TAG = "mpz("; -PyObject * +static PyObject * MPZ_to_str(MPZ_Object *u, int base, bool tag) { size_t len = 0; From 2d1278d847366cfa168acd92233da52b09461d70 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 7 Oct 2026 07:06:00 +0300 Subject: [PATCH 03/11] thousands_separators -> group --- fmt.c | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/fmt.c b/fmt.c index cfbb1eba..df9a6086 100644 --- a/fmt.c +++ b/fmt.c @@ -67,7 +67,7 @@ typedef struct { int alternate; char sign; Py_ssize_t width; - char thousands_separators; + char group; char type; } InternalFormatSpec; @@ -87,13 +87,14 @@ parse_internal_render_format_spec(PyObject *obj, const char *data = PyUnicode_AsUTF8AndSize(format_spec, NULL); int align_specified = 0; int fill_char_specified = 0; + int thousands_separators = 0; format->fill_char = ' '; format->align = '>'; format->alternate = 0; format->sign = '\0'; format->width = -1; - format->thousands_separators = '\0'; + format->group = 0; format->type = 'd'; if (!data) { return 0; /* LCOV_EXCL_LINE */ @@ -139,7 +140,7 @@ parse_internal_render_format_spec(PyObject *obj, } /* Underscore signifies add thousands separators */ if (end-pos && data[pos] == '_') { - format->thousands_separators = '_'; + thousands_separators = 1; ++pos; } /* Finally, parse the type field. */ @@ -164,6 +165,14 @@ parse_internal_render_format_spec(PyObject *obj, format->type = data[pos]; ++pos; } + if (thousands_separators) { + if (format->type == 'd') { + format->group = 3; + } + else { + format->group = 4; + } + } return 1; } @@ -324,7 +333,7 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) size_t len = 0; bool negative = zz_isneg(&u->z); bool sign = format->sign == '+' || format->sign == ' '; - Py_ssize_t min_leading = 0, group = 0, width = -1; + Py_ssize_t min_leading = 0, width = -1; int base; if (format->fill_char == '0' && format->align == '=') { @@ -349,30 +358,22 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) break; } sign |= negative; - if (format->thousands_separators) { - if (format->type == 'd') { - group = 3; - } - else { - group = 4; - } - } (void)zz_sizeinbase(&u->z, base, &len); if (format->alternate) { len += 2; } min_leading = width - (Py_ssize_t)len - sign; if (min_leading > 0) { - if (group > 0) { - min_leading = ((group*(width - sign))/(group + 1) + if (format->group) { + min_leading = ((format->group*(width - sign))/(format->group + 1) + 1 - (Py_ssize_t)len); } if (min_leading > 0) { len += (size_t)min_leading; } } - if (group > 0) { - len += (len - 1) / (size_t)group; + if (format->group) { + len += (len - 1) / (size_t)format->group; } len += sign; len++; /* '\0' */ @@ -420,8 +421,8 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) } p -= min_leading; } - if (group > 0 && u->z.size) { - insert_from_end_inplace(p + negative, group, '_'); + if (format->group && u->z.size) { + insert_from_end_inplace(p + negative, format->group, '_'); } if (saved_char) { *p = saved_char; From c43921cec7796502ce448c8b4fe53b64ce43410e Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 7 Oct 2026 07:25:14 +0300 Subject: [PATCH 04/11] Add base field to InternalFormatSpec --- fmt.c | 55 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/fmt.c b/fmt.c index df9a6086..12aa8706 100644 --- a/fmt.c +++ b/fmt.c @@ -68,6 +68,7 @@ typedef struct { char sign; Py_ssize_t width; char group; + int base; char type; } InternalFormatSpec; @@ -95,6 +96,7 @@ parse_internal_render_format_spec(PyObject *obj, format->sign = '\0'; format->width = -1; format->group = 0; + format->base = 10; format->type = 'd'; if (!data) { return 0; /* LCOV_EXCL_LINE */ @@ -163,10 +165,28 @@ parse_internal_render_format_spec(PyObject *obj, } if (end-pos == 1) { format->type = data[pos]; + switch (format->type) { + case 'b': + format->base = 2; + break; + case 'o': + format->base = 8; + break; + case 'x': + format->base = 16; + break; + case 'X': + format->base = -16; + break; + default: + case 'd': + format->base = 10; + break; + } ++pos; } if (thousands_separators) { - if (format->type == 'd') { + if (format->base == 10) { format->group = 3; } else { @@ -334,31 +354,12 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) bool negative = zz_isneg(&u->z); bool sign = format->sign == '+' || format->sign == ' '; Py_ssize_t min_leading = 0, width = -1; - int base; if (format->fill_char == '0' && format->align == '=') { width = format->width; } - switch (format->type) { - case 'b': - base = 2; - break; - case 'o': - base = 8; - break; - case 'x': - base = 16; - break; - case 'X': - base = -16; - break; - default: - case 'd': - base = 10; - break; - } sign |= negative; - (void)zz_sizeinbase(&u->z, base, &len); + (void)zz_sizeinbase(&u->z, format->base, &len); if (format->alternate) { len += 2; } @@ -388,19 +389,19 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) *(p++) = saved_char; } if (format->alternate) { - if (base == 2) { + if (format->base == 2) { *(p++) = '0'; *(p++) = 'b'; } - else if (base == 8) { + else if (format->base == 8) { *(p++) = '0'; *(p++) = 'o'; } - else if (base == 16) { + else if (format->base == 16) { *(p++) = '0'; *(p++) = 'x'; } - else if (base == -16) { + else if (format->base == -16) { *(p++) = '0'; *(p++) = 'X'; } @@ -413,7 +414,7 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) *(p++) = '0'; } - zz_err ret = zz_get_str(&u->z, base, p); + zz_err ret = zz_get_str(&u->z, format->base, p); if (min_leading > 0) { if (negative) { @@ -455,7 +456,7 @@ format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) /* The number of prefix chars is the same as the leading chars to skip */ - if (!format->alternate || format->type == 'd') { + if (!format->alternate || format->base == 10) { n_prefix = 0; } tmp = MPZ_format(value, format); From 11797a1750b664fc9dff7dcbedf0672358db738c Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 7 Oct 2026 07:44:57 +0300 Subject: [PATCH 05/11] Add prefix field to InternalFormatSpec --- fmt.c | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/fmt.c b/fmt.c index 12aa8706..3d2bf210 100644 --- a/fmt.c +++ b/fmt.c @@ -69,6 +69,7 @@ typedef struct { Py_ssize_t width; char group; int base; + char prefix[2]; char type; } InternalFormatSpec; @@ -97,6 +98,8 @@ parse_internal_render_format_spec(PyObject *obj, format->width = -1; format->group = 0; format->base = 10; + format->prefix[0] = '0'; + format->prefix[1] = '\0'; format->type = 'd'; if (!data) { return 0; /* LCOV_EXCL_LINE */ @@ -181,9 +184,23 @@ parse_internal_render_format_spec(PyObject *obj, default: case 'd': format->base = 10; + format->alternate = 0; break; } - ++pos; + } + if (format->alternate) { + if (format->base == 2) { + format->prefix[1] = 'b'; + } + else if (format->base == 8) { + format->prefix[1] = 'o'; + } + else if (format->base == 16) { + format->prefix[1] = 'x'; + } + else { + format->prefix[1] = 'X'; + } } if (thousands_separators) { if (format->base == 10) { @@ -388,23 +405,9 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) saved_char = '-'; *(p++) = saved_char; } - if (format->alternate) { - if (format->base == 2) { - *(p++) = '0'; - *(p++) = 'b'; - } - else if (format->base == 8) { - *(p++) = '0'; - *(p++) = 'o'; - } - else if (format->base == 16) { - *(p++) = '0'; - *(p++) = 'x'; - } - else if (format->base == -16) { - *(p++) = '0'; - *(p++) = 'X'; - } + if (format->prefix[1]) { + memcpy(p, format->prefix, 2); + p += 2; } if (saved_char) { saved_char = *(--p); From 1a51009487229f143c58319a4bc3122b59a4290f Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 7 Oct 2026 08:01:14 +0300 Subject: [PATCH 06/11] Cleanup MPZ_format() --- fmt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fmt.c b/fmt.c index 3d2bf210..6ed355e2 100644 --- a/fmt.c +++ b/fmt.c @@ -369,7 +369,7 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) { size_t len = 0; bool negative = zz_isneg(&u->z); - bool sign = format->sign == '+' || format->sign == ' '; + bool sign = format->sign; Py_ssize_t min_leading = 0, width = -1; if (format->fill_char == '0' && format->align == '=') { From 4b0f147618634cb7e8a965f2fe4d276e3d0978ff Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 7 Oct 2026 09:14:50 +0300 Subject: [PATCH 07/11] MPZ_format() -> zz_format() --- fmt.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fmt.c b/fmt.c index 6ed355e2..b5f23610 100644 --- a/fmt.c +++ b/fmt.c @@ -365,10 +365,10 @@ insert_from_end_inplace(char *str, Py_ssize_t n, char c) } Py_LOCAL(PyObject *) -MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) +zz_format(const zz_t *u, const InternalFormatSpec *format) { size_t len = 0; - bool negative = zz_isneg(&u->z); + bool negative = zz_isneg(u); bool sign = format->sign; Py_ssize_t min_leading = 0, width = -1; @@ -376,7 +376,7 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) width = format->width; } sign |= negative; - (void)zz_sizeinbase(&u->z, format->base, &len); + (void)zz_sizeinbase(u, format->base, &len); if (format->alternate) { len += 2; } @@ -417,7 +417,7 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) *(p++) = '0'; } - zz_err ret = zz_get_str(&u->z, format->base, p); + zz_err ret = zz_get_str(u, format->base, p); if (min_leading > 0) { if (negative) { @@ -425,7 +425,7 @@ MPZ_format(MPZ_Object *u, const InternalFormatSpec *format) } p -= min_leading; } - if (format->group && u->z.size) { + if (format->group && !zz_iszero(u)) { insert_from_end_inplace(p + negative, format->group, '_'); } if (saved_char) { @@ -462,14 +462,14 @@ format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) if (!format->alternate || format->base == 10) { n_prefix = 0; } - tmp = MPZ_format(value, format); + tmp = zz_format(&value->z, format); if (tmp == NULL) { goto done; /* LCOV_EXCL_LINE */ } n_digits = PyUnicode_GetLength(tmp); /* Is a sign character present in the output? If so, remember it and skip it */ - if (PyUnicode_ReadChar(tmp, inumeric_chars) == '-') { + if (zz_isneg(&value->z)) { sign_char = '-'; prefix++; n_digits--; From d5aea4be3e6f2dfcc0ea61699f8957cb67a4cdda Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 8 Oct 2026 06:06:29 +0300 Subject: [PATCH 08/11] Introduce calc_len() --- fmt.c | 67 ++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/fmt.c b/fmt.c index b5f23610..f953b991 100644 --- a/fmt.c +++ b/fmt.c @@ -364,13 +364,14 @@ insert_from_end_inplace(char *str, Py_ssize_t n, char c) } } -Py_LOCAL(PyObject *) -zz_format(const zz_t *u, const InternalFormatSpec *format) +Py_LOCAL(size_t) +calc_len(const zz_t *u, const InternalFormatSpec *format, + Py_ssize_t *min_leading) { size_t len = 0; bool negative = zz_isneg(u); bool sign = format->sign; - Py_ssize_t min_leading = 0, width = -1; + Py_ssize_t width = -1; if (format->fill_char == '0' && format->align == '=') { width = format->width; @@ -380,27 +381,27 @@ zz_format(const zz_t *u, const InternalFormatSpec *format) if (format->alternate) { len += 2; } - min_leading = width - (Py_ssize_t)len - sign; - if (min_leading > 0) { - if (format->group) { - min_leading = ((format->group*(width - sign))/(format->group + 1) - + 1 - (Py_ssize_t)len); - } - if (min_leading > 0) { - len += (size_t)min_leading; + *min_leading = width - (Py_ssize_t)len - sign; + if (*min_leading > 0) { + *min_leading = ((format->group*(width - sign))/(format->group + 1) + + 1 - (Py_ssize_t)len); + if (*min_leading > 0) { + len += (size_t)*min_leading; } } if (format->group) { len += (len - 1) / (size_t)format->group; } - len += sign; - len++; /* '\0' */ + return len + negative; +} - char *buf = malloc(len), *p = buf, saved_char = 0; +Py_LOCAL(zz_err) +zz_format(const zz_t *u, Py_ssize_t min_leading, + const InternalFormatSpec *format, char *buf) +{ + bool negative = zz_isneg(u); + char *p = buf, saved_char = 0; - if (!buf) { - return PyErr_NoMemory(); /* LCOV_EXCL_LINE */ - } if (negative) { saved_char = '-'; *(p++) = saved_char; @@ -419,6 +420,9 @@ zz_format(const zz_t *u, const InternalFormatSpec *format) zz_err ret = zz_get_str(u, format->base, p); + if (ret) { + return ret; /* LCOV_EXCL_LINE */ + } if (min_leading > 0) { if (negative) { *p = '0'; @@ -431,30 +435,21 @@ zz_format(const zz_t *u, const InternalFormatSpec *format) if (saved_char) { *p = saved_char; } - if (ret) { - /* LCOV_EXCL_START */ - free(buf); - return PyErr_NoMemory(); - /* LCOV_EXCL_STOP */ - } - p += strlen(p); - - PyObject *res = PyUnicode_FromString(buf); - - free(buf); - return res; + return ZZ_OK; } Py_LOCAL(PyObject *) format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) { PyObject *tmp = NULL; + char *buf = NULL; Py_ssize_t inumeric_chars = 0; char sign_char = '\0'; Py_ssize_t n_digits; /* Count of digits need from the computed string */ Py_ssize_t n_prefix = 2; /* Count of prefix chars, (e.g., '0x') */ Py_ssize_t n_total; Py_ssize_t prefix = 0; + Py_ssize_t min_leading = 0; NumberFieldWidths spec; /* The number of prefix chars is the same as the leading @@ -462,11 +457,21 @@ format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) if (!format->alternate || format->base == 10) { n_prefix = 0; } - tmp = zz_format(&value->z, format); + n_digits = (Py_ssize_t)calc_len(&value->z, format, &min_leading); + buf = malloc((size_t)n_digits + 1); + if (buf == NULL || zz_format(&value->z, min_leading, format, buf)) { + /* LCOV_EXCL_START */ + free(buf); + PyErr_NoMemory(); + goto done; + /* LCOV_EXCL_STOP */ + } + n_digits = (Py_ssize_t)strlen(buf); + tmp = PyUnicode_FromString(buf); + free(buf); if (tmp == NULL) { goto done; /* LCOV_EXCL_LINE */ } - n_digits = PyUnicode_GetLength(tmp); /* Is a sign character present in the output? If so, remember it and skip it */ if (zz_isneg(&value->z)) { From 8b15b2ff10a87e798d3e90b11fae714a858250c3 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 8 Oct 2026 08:18:06 +0300 Subject: [PATCH 09/11] Get rid of using PyUnicodeWriter_* API --- fmt.c | 111 ++++++++++++++++++---------------------------------------- 1 file changed, 34 insertions(+), 77 deletions(-) diff --git a/fmt.c b/fmt.c index f953b991..9a6b6d1c 100644 --- a/fmt.c +++ b/fmt.c @@ -1,9 +1,5 @@ #include "mpz.h" -extern PyObject * to_int(PyObject *self); - -#if defined(ON_CPYTHON) && PY_VERSION_HEX >= 0x030D00A0 - /************************************************************************/ /*********** standard format specifier parsing **************************/ /************************************************************************/ @@ -300,52 +296,31 @@ calc_number_widths(NumberFieldWidths *spec, Py_ssize_t n_prefix, } /* Fill in the digit parts of a number's string representation, - as determined in calc_number_widths(). - Return -1 on error, or 0 on success. */ -Py_LOCAL(int) -fill_number(PyUnicodeWriter *writer, const NumberFieldWidths *spec, - PyObject *digits, Py_ssize_t d_start, - Py_ssize_t p_start, char fill_char) + as determined in calc_number_widths(). */ +Py_LOCAL(void) +fill_number(const NumberFieldWidths *spec, + char *digits, const char prefix[2], char fill_char) { - if (spec->n_lpadding) { - for (Py_ssize_t i = 0; i < spec->n_lpadding; i++) { - if (PyUnicodeWriter_WriteChar(writer, (Py_UCS4)fill_char)) { - return -1; /* LCOV_EXCL_LINE */ - } - } + Py_ssize_t cur = 0; + + for (Py_ssize_t i = 0; i < spec->n_lpadding; i++) { + digits[cur++] = fill_char; } if (spec->n_sign == 1) { - if(PyUnicodeWriter_WriteChar(writer, (Py_UCS4)spec->sign)) { - return -1; /* LCOV_EXCL_LINE */ - } + digits[cur++] = spec->sign; } if (spec->n_prefix) { - if (PyUnicodeWriter_WriteSubstring(writer, digits, p_start, - spec->n_prefix + p_start)) - { - return -1; /* LCOV_EXCL_LINE */ - } + digits[cur++] = prefix[0]; + digits[cur++] = prefix[1]; } - if (spec->n_spadding) { - for (Py_ssize_t i = 0; i < spec->n_spadding; i++) { - if (PyUnicodeWriter_WriteChar(writer, (Py_UCS4)fill_char)) { - return -1; /* LCOV_EXCL_LINE */ - } - } + for (Py_ssize_t i = 0; i < spec->n_spadding; i++) { + digits[cur++] = fill_char; } - if (PyUnicodeWriter_WriteSubstring(writer, digits, d_start, - spec->n_digits + d_start)) - { - return -1; /* LCOV_EXCL_LINE */ - } - if (spec->n_rpadding) { - for (Py_ssize_t i = 0; i < spec->n_rpadding; i++) { - if (PyUnicodeWriter_WriteChar(writer, (Py_UCS4)fill_char)) { - return -1; /* LCOV_EXCL_LINE */ - } - } + cur += spec->n_digits; + for (Py_ssize_t i = 0; i < spec->n_rpadding; i++) { + digits[cur++] = fill_char; } - return 0; + digits[cur] = '\0'; } Py_LOCAL(void) @@ -448,13 +423,12 @@ format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) Py_ssize_t n_digits; /* Count of digits need from the computed string */ Py_ssize_t n_prefix = 2; /* Count of prefix chars, (e.g., '0x') */ Py_ssize_t n_total; - Py_ssize_t prefix = 0; Py_ssize_t min_leading = 0; NumberFieldWidths spec; /* The number of prefix chars is the same as the leading chars to skip */ - if (!format->alternate || format->base == 10) { + if (!format->alternate) { n_prefix = 0; } n_digits = (Py_ssize_t)calc_len(&value->z, format, &min_leading); @@ -467,16 +441,10 @@ format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) /* LCOV_EXCL_STOP */ } n_digits = (Py_ssize_t)strlen(buf); - tmp = PyUnicode_FromString(buf); - free(buf); - if (tmp == NULL) { - goto done; /* LCOV_EXCL_LINE */ - } /* Is a sign character present in the output? If so, remember it and skip it */ if (zz_isneg(&value->z)) { sign_char = '-'; - prefix++; n_digits--; inumeric_chars++; } @@ -490,22 +458,26 @@ format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) goto done; /* LCOV_EXCL_LINE */ } - /* Allocate the memory. */ - PyUnicodeWriter *writer = PyUnicodeWriter_Create(n_total); + char *old_buf = buf; - if (!writer) { - goto done; /* LCOV_EXCL_LINE */ - } - /* Populate the memory. */ - if (fill_number(writer, &spec, tmp, inumeric_chars, prefix, - format->fill_char)) - { + buf = realloc(old_buf, (size_t)n_total + 1); + if (buf == NULL) { /* LCOV_EXCL_START */ - PyUnicodeWriter_Discard(writer); + free(old_buf); + PyErr_NoMemory(); goto done; /* LCOV_EXCL_STOP */ } - return PyUnicodeWriter_Finish(writer); + memmove(buf + spec.n_lpadding + spec.n_sign + spec.n_prefix + spec.n_spadding, + buf + inumeric_chars, (size_t)spec.n_digits); + /* Populate the memory. */ + fill_number(&spec, buf, format->prefix, format->fill_char); + tmp = PyUnicode_FromString(buf); + free(buf); + if (tmp == NULL) { + goto done; /* LCOV_EXCL_LINE */ + } + return tmp; /* LCOV_EXCL_START */ done: Py_XDECREF(tmp); @@ -514,6 +486,7 @@ format_mpz_internal(MPZ_Object *value, const InternalFormatSpec *format) } extern PyObject * to_float(PyObject *self); +extern PyObject * to_int(PyObject *self); PyObject * __format__(PyObject *self, PyObject *format_spec) @@ -593,19 +566,3 @@ __format__(PyObject *self, PyObject *format_spec) return res; } } -#else -PyObject * -__format__(PyObject *self, PyObject *format_spec) -{ - PyObject *num = to_int(self); - - if (!num) { - return NULL; /* LCOV_EXCL_LINE */ - } - - PyObject *res = PyObject_CallMethod(num, "__format__", "O", format_spec); - - Py_DECREF(num); - return res; -} -#endif /* defined(ON_CPYTHON) && PY_VERSION_HEX >= 0x030D00A0 */ From 4222c69e2690c1ae0bb68f77c176d488724f2800 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 8 Oct 2026 09:05:45 +0300 Subject: [PATCH 10/11] Remove CPython-specific code in parse_internal_render_format_spec() --- fmt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fmt.c b/fmt.c index 9a6b6d1c..abffc18f 100644 --- a/fmt.c +++ b/fmt.c @@ -156,7 +156,7 @@ parse_internal_render_format_spec(PyObject *obj, if (actual_format_spec != NULL) { PyErr_Format(PyExc_ValueError, ("Invalid format specifier '%U' for object " - "of type '%.200s'"), actual_format_spec, + "of type '%.200U'"), actual_format_spec, PyType_GetFullyQualifiedName(Py_TYPE(obj))); Py_DECREF(actual_format_spec); } From 5ba0a2375386616591841b59fbdf8a675b279102 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 9 Oct 2026 10:30:49 +0300 Subject: [PATCH 11/11] Support only ASCII in parse_internal_render_format_spec() Closes #358 --- fmt.c | 108 ++++++++++++++++++++++------------------------ tests/test_mpz.py | 4 +- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/fmt.c b/fmt.c index abffc18f..93421c5a 100644 --- a/fmt.c +++ b/fmt.c @@ -8,12 +8,13 @@ returns nonnegative integer or -1 on error. */ Py_LOCAL(Py_ssize_t) -get_integer(const char *str, Py_ssize_t *ppos, Py_ssize_t end) +get_integer(char **start, const char *end) { - Py_ssize_t accumulator = 0, digitval, pos = *ppos; + char *pos = *start; + Py_ssize_t accumulator = 0, digitval; for (; pos < end; pos++) { - digitval = str[pos] - '0'; + digitval = *pos - '0'; if (digitval < 0 || digitval > 9) { break; } @@ -21,12 +22,11 @@ get_integer(const char *str, Py_ssize_t *ppos, Py_ssize_t end) if (accumulator > (PY_SSIZE_T_MAX - digitval) / 10) { PyErr_Format(PyExc_ValueError, "Too many decimal digits in format string"); - *ppos = pos; return -1; } accumulator = accumulator * 10 + digitval; } - *ppos = pos; + *start = pos; return accumulator; } @@ -77,12 +77,11 @@ typedef struct { */ Py_LOCAL(int) parse_internal_render_format_spec(PyObject *obj, - PyObject *format_spec, - Py_ssize_t start, Py_ssize_t end, + const char *start, + const char *end, InternalFormatSpec *format) { - Py_ssize_t pos = start; - const char *data = PyUnicode_AsUTF8AndSize(format_spec, NULL); + char *pos = (char *)start; int align_specified = 0; int fill_char_specified = 0; int thousands_separators = 0; @@ -97,41 +96,36 @@ parse_internal_render_format_spec(PyObject *obj, format->prefix[0] = '0'; format->prefix[1] = '\0'; format->type = 'd'; - if (!data) { - return 0; /* LCOV_EXCL_LINE */ - } + assert(pos); /* If the second char is an alignment token, then parse the fill char */ - if (end-pos >= 2 && is_alignment_token(data[pos+1])) { - format->align = data[pos+1]; - format->fill_char = data[pos]; + if (end-pos >= 2 && is_alignment_token(*(pos + 1))) { + format->fill_char = *pos++; + format->align = *pos++; fill_char_specified = 1; align_specified = 1; - pos += 2; } - else if (end-pos >= 1 && is_alignment_token(data[pos])) { - format->align = data[pos]; + else if (end-pos >= 1 && is_alignment_token(*pos)) { + format->align = *pos++; align_specified = 1; - ++pos; } /* Parse the various sign options */ - if (end-pos >= 1 && is_sign_element(data[pos])) { - format->sign = data[pos]; - ++pos; + if (end-pos >= 1 && is_sign_element(*pos)) { + format->sign = *pos++; } /* If the next character is #, we're in alternate mode */ - if (end-pos >= 1 && data[pos] == '#') { + if (end-pos >= 1 && *pos == '#') { format->alternate = 1; - ++pos; + pos++; } /* The special case for 0-padding (backwards compat) */ - if (!fill_char_specified && end-pos >= 1 && data[pos] == '0') { + if (!fill_char_specified && end-pos >= 1 && *pos == '0') { format->fill_char = '0'; if (!align_specified) { format->align = '='; } - ++pos; + pos++; } - format->width = get_integer(data, &pos, end); + format->width = get_integer(&pos, end); if (format->width == -1) { return 0; /* overflow */ } @@ -140,9 +134,9 @@ parse_internal_render_format_spec(PyObject *obj, format->width = -1; } /* Underscore signifies add thousands separators */ - if (end-pos && data[pos] == '_') { + if (end-pos && *pos == '_') { thousands_separators = 1; - ++pos; + pos++; } /* Finally, parse the type field. */ if (end-pos > 1) { @@ -150,20 +144,18 @@ parse_internal_render_format_spec(PyObject *obj, specifier. */ /* Create a temporary object that contains the format spec we're operating on. It's format_spec[start:end] (in Python syntax). */ - PyObject* actual_format_spec = PyUnicode_FromStringAndSize(data - + start, - end-start); - if (actual_format_spec != NULL) { + PyObject* actual_spec = PyUnicode_FromStringAndSize(start, end-start); + if (actual_spec != NULL) { PyErr_Format(PyExc_ValueError, ("Invalid format specifier '%U' for object " - "of type '%.200U'"), actual_format_spec, + "of type '%.200U'"), actual_spec, PyType_GetFullyQualifiedName(Py_TYPE(obj))); - Py_DECREF(actual_format_spec); + Py_DECREF(actual_spec); } return 0; } if (end-pos == 1) { - format->type = data[pos]; + format->type = *pos++; switch (format->type) { case 'b': format->base = 2; @@ -506,14 +498,28 @@ __format__(PyObject *self, PyObject *format_spec) InternalFormatSpec format; unaryfunc cast = to_int; + PyObject *bytes = PyUnicode_AsASCIIString(format_spec); - if (!parse_internal_render_format_spec(self, format_spec, 0, end, &format)) - { - PyErr_Clear(); - cast = to_int; - goto fallback; - } + if (bytes) { + char *format_str = PyBytes_AsString(bytes); + if (!parse_internal_render_format_spec(self, format_str, + format_str + end, + &format)) + { + Py_DECREF(bytes); + PyErr_Clear(); + goto fallback; + } + Py_DECREF(bytes); + } + else { + if (PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) { + PyErr_Clear(); + goto fallback; + } + return NULL; /* LCOV_EXCL_LINE */ + } switch (format.type) { case 'b': case 'd': @@ -534,21 +540,9 @@ __format__(PyObject *self, PyObject *format_spec) cast = to_float; break; default: - { - PyObject *type_name = PyType_GetFullyQualifiedName(Py_TYPE(self)); - - /* %c might be out-of-range, hence the two cases. */ - if (format.type > 32) { - PyErr_Format(PyExc_ValueError, - "Unknown format code '%c' for object of type '%U'", - (char)format.type, type_name); - } - else { - PyErr_Format(PyExc_ValueError, - "Unknown format code '\\x%x' for object of type '%U'", - (unsigned int)format.type, type_name); - } - } + PyErr_Format(PyExc_ValueError, + "Unknown format code '%c' for object of type '%U'", + format.type, PyType_GetFullyQualifiedName(Py_TYPE(self))); return NULL; } diff --git a/tests/test_mpz.py b/tests/test_mpz.py index 1f40f347..dfc44d52 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -125,9 +125,6 @@ def test_format_interface(): mx.__format__(321) with pytest.raises(ValueError, match="Unknown format code"): format(mx, "q") - with pytest.raises(ValueError, - match=r"(Unknown format code|Invalid format)"): - format(mx, "\x81") with pytest.raises(ValueError, match=(r"Invalid format specifier|" "Invalid conversion specification")): @@ -137,6 +134,7 @@ def test_format_interface(): assert format(mx, "f") == "123.000000" assert format(mx, "c") == "{" + assert format(mx, "ы=10b") == "ыыы1111011" @given(bigints())