diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index cd819b8d06480a..fd8714c77da98d 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -417,10 +417,10 @@ are always available. They are listed here in alphabetical order. parts (the sign of the imaginary part is mandatory in this case). The string can optionally be surrounded by whitespaces and the round parentheses ``'('`` and ``')'``, which are ignored. - The string must not contain whitespace between ``'+'``, ``'-'``, the - ``'j'`` or ``'J'`` suffix, and the decimal number. - For example, ``complex('1+2j')`` is fine, but ``complex('1 + 2j')`` raises - :exc:`ValueError`. + The string must not contain whitespace between ``'+'`` (ASCII plus sign), + ``'-'`` (ASCII hyphen minus), ``'−'`` (Unicode minus sign, U+2212), the + ``'j'`` or ``'J'`` suffix, and the decimal number. For example, + ``complex('1+2j')`` is fine, but ``complex('1 + 2j')`` raises :exc:`ValueError`. More precisely, the input must conform to the :token:`~float:complexvalue` production rule in the following grammar, after parentheses and leading and trailing whitespace characters are removed: @@ -466,6 +466,11 @@ are always available. They are listed here in alphabetical order. Passing a complex number as the *real* or *imag* argument is now deprecated; it should only be passed as a single positional argument. + .. versionchanged:: next + ``'−'`` (Unicode minus sign, U+2212) can be now used as an + alternative to ``'-'`` (ASCII hyphen minus) for denoting + negative sign. + .. function:: delattr(object, name, /) @@ -781,7 +786,8 @@ are always available. They are listed here in alphabetical order. If the argument is a string, it should contain a decimal number, optionally preceded by a sign, and optionally embedded in whitespace. The optional - sign may be ``'+'`` or ``'-'``; a ``'+'`` sign has no effect on the value + sign may be ``'+'`` (ASCII plus sign), ``'-'`` (ASCII hyphen minus) or ``−`` + (Unicode minus sign, U+2212); a ``'+'`` sign has no effect on the value produced. The argument may also be a string representing a NaN (not-a-number), or positive or negative infinity. More precisely, the input must conform to the :token:`~float:floatvalue` @@ -789,7 +795,7 @@ are always available. They are listed here in alphabetical order. whitespace characters are removed: .. productionlist:: float - sign: "+" | "-" + sign: "+" | "-" | "−" infinity: "Infinity" | "inf" nan: "nan" digit: @@ -827,6 +833,11 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.8 Falls back to :meth:`~object.__index__` if :meth:`~object.__float__` is not defined. + .. versionchanged:: next + Added support for ``−`` (Unicode minus sign, U+2212) as an + alternative to ``-`` (ASCII hyphen minus) for denoting negative + floats. + .. index:: single: __format__ @@ -1036,8 +1047,9 @@ are always available. They are listed here in alphabetical order. If the argument is not a number or if *base* is given, then it must be a string, :class:`bytes`, or :class:`bytearray` instance representing an integer - in radix *base*. Optionally, the string can be preceded by ``+`` or ``-`` - (with no space in between), have leading zeros, be surrounded by whitespace, + in radix *base*. Optionally, the string can be directly preceded (with no whitespaces + in between) by ``+`` (ASCII plus sign), ``-`` (ASCII hyphen minus) or ``−`` (Unicode + minus sign, U+2212), have leading zeros, be surrounded by whitespace, and have single underscores interspersed between digits. A base-n integer string contains digits, each representing a value from 0 to @@ -1080,6 +1092,11 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.14 :func:`int` no longer delegates to the :meth:`~object.__trunc__` method. + .. versionchanged:: next + :func:`int` now supports ``−`` (Unicode minus sign, U+2212) + as an alternative to ``-`` (ASCII hyphen minus) for denoting + negative integers. + .. function:: isinstance(object, classinfo, /) Return ``True`` if the *object* argument is an instance of the *classinfo* diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index ce0d7cbb2e4276..bc825824eb011e 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -352,8 +352,9 @@ Notes: alternative conversions. (4) - float also accepts the strings "nan" and "inf" with an optional prefix "+" - or "-" for Not a Number (NaN) and positive or negative infinity. + float also accepts the strings "nan" and "inf" with an optional prefix ASCII plus + sign "+" or ASCII hyphen minus "-" or Unicode minus sign "−" (U+2212) for Not a Number + (NaN) and positive or negative infinity. (5) Python defines ``pow(0, 0)`` and ``0 ** 0`` to be ``1``, as is common for diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index d3156645eeec2d..a9389b1b533780 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -143,7 +143,9 @@ def test_long_fromunicodeobject(self): self.assertEqual(fromunicodeobject('0xcafe', 0), 0xcafe) self.assertRaises(ValueError, fromunicodeobject, 'cafe', 0) self.assertEqual(fromunicodeobject('-123', 10), -123) + self.assertEqual(fromunicodeobject('\N{MINUS SIGN}123', 10), -123) self.assertEqual(fromunicodeobject(' -123 ', 10), -123) + self.assertEqual(fromunicodeobject(' \N{MINUS SIGN}123 ', 10), -123) self.assertEqual(fromunicodeobject('1_23', 10), 123) self.assertRaises(ValueError, fromunicodeobject, '- 123', 10) self.assertRaises(ValueError, fromunicodeobject, '', 10) diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 0c7e7341f13d4e..9134731f49c2ff 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -669,18 +669,22 @@ def check(z, x, y): check(complex("1"), 1.0, 0.0) check(complex("1j"), 0.0, 1.0) check(complex("-1"), -1.0, 0.0) + check(complex("\N{MINUS SIGN}1"), -1.0, 0.0) check(complex("+1"), 1.0, 0.0) check(complex("1+2j"), 1.0, 2.0) check(complex("(1+2j)"), 1.0, 2.0) check(complex("(1.5+4.25j)"), 1.5, 4.25) check(complex("4.25+1J"), 4.25, 1.0) check(complex(" ( +4.25-6J )"), 4.25, -6.0) + check(complex(" ( +4.25\N{MINUS SIGN}6J )"), 4.25, -6.0) check(complex(" ( +4.25-J )"), 4.25, -1.0) + check(complex(" ( +4.25\N{MINUS SIGN}J )"), 4.25, -1.0) check(complex(" ( +4.25+j )"), 4.25, 1.0) check(complex("J"), 0.0, 1.0) check(complex("( j )"), 0.0, 1.0) check(complex("+J"), 0.0, 1.0) check(complex("( -j)"), 0.0, -1.0) + check(complex("( \N{MINUS SIGN}j)"), 0.0, -1.0) check(complex('1-1j'), 1.0, -1.0) check(complex('1J'), 0.0, 1.0) @@ -690,6 +694,7 @@ def check(z, x, y): check(complex('-1e-500+1e-500j'), -0.0, 0.0) check(complex('1e-500-1e-500j'), 0.0, -0.0) check(complex('-1e-500-1e-500j'), -0.0, -0.0) + check(complex('\N{MINUS SIGN}1e\N{MINUS SIGN}500\N{MINUS SIGN}1e\N{MINUS SIGN}500j'), -0.0, -0.0) # SF bug 543840: complex(string) accepts strings with \0 # Fixed in 2.3. @@ -700,6 +705,8 @@ def check(z, x, y): self.assertRaises(ValueError, complex, "1+") self.assertRaises(ValueError, complex, "1+1j+1j") self.assertRaises(ValueError, complex, "--") + self.assertRaises(ValueError, complex, "-\N{MINUS SIGN}") + self.assertRaises(ValueError, complex, "\N{MINUS SIGN}\N{MINUS SIGN}") self.assertRaises(ValueError, complex, "(1+2j") self.assertRaises(ValueError, complex, "1+2j)") self.assertRaises(ValueError, complex, "1+(2j)") @@ -726,7 +733,9 @@ def test_constructor_negative_nans_from_string(self): self.assertEqual(copysign(1., complex("-nan").real), -1.) self.assertEqual(copysign(1., complex("-nanj").imag), -1.) self.assertEqual(copysign(1., complex("-nan-nanj").real), -1.) + self.assertEqual(copysign(1., complex("-nan\N{MINUS SIGN}nanj").real), -1.) self.assertEqual(copysign(1., complex("-nan-nanj").imag), -1.) + self.assertEqual(copysign(1., complex("\N{MINUS SIGN}nan\N{MINUS SIGN}nanj").imag), -1.) def test_underscores(self): # check underscores diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index c03b0a09f71889..c9445bccd4b4c1 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -68,12 +68,16 @@ def test_float(self): self.assertRaises(ValueError, float, " +0x3.p-1 ") self.assertRaises(ValueError, float, "++3.14") self.assertRaises(ValueError, float, "+-3.14") + self.assertRaises(ValueError, float, "+\N{MINUS SIGN}3.14") self.assertRaises(ValueError, float, "-+3.14") self.assertRaises(ValueError, float, "--3.14") + self.assertRaises(ValueError, float, "-\N{MINUS SIGN}3.14") + self.assertRaises(ValueError, float, "\N{MINUS SIGN}\N{MINUS SIGN}3.14") self.assertRaises(ValueError, float, ".nan") self.assertRaises(ValueError, float, "+.inf") self.assertRaises(ValueError, float, ".") self.assertRaises(ValueError, float, "-.") + self.assertRaises(ValueError, float, "\N{MINUS SIGN}.") self.assertRaises(TypeError, float, {}) self.assertRaisesRegex(TypeError, "not 'dict'", float, {}) # Lone surrogate @@ -1110,30 +1114,40 @@ def test_nan_from_str(self): self.assertTrue(isnan(float("nan"))) self.assertTrue(isnan(float("+nan"))) self.assertTrue(isnan(float("-nan"))) + self.assertTrue(isnan(float("\N{MINUS SIGN}nan"))) self.assertEqual(repr(float("nan")), "nan") self.assertEqual(repr(float("+nan")), "nan") self.assertEqual(repr(float("-nan")), "nan") + self.assertEqual(repr(float("\N{MINUS SIGN}nan")), "nan") self.assertEqual(repr(float("NAN")), "nan") self.assertEqual(repr(float("+NAn")), "nan") self.assertEqual(repr(float("-NaN")), "nan") + self.assertEqual(repr(float("\N{MINUS SIGN}NaN")), "nan") self.assertEqual(str(float("nan")), "nan") self.assertEqual(str(float("+nan")), "nan") self.assertEqual(str(float("-nan")), "nan") + self.assertEqual(str(float("\N{MINUS SIGN}nan")), "nan") self.assertRaises(ValueError, float, "nana") self.assertRaises(ValueError, float, "+nana") self.assertRaises(ValueError, float, "-nana") + self.assertRaises(ValueError, float, "\N{MINUS SIGN}nana") self.assertRaises(ValueError, float, "na") self.assertRaises(ValueError, float, "+na") self.assertRaises(ValueError, float, "-na") + self.assertRaises(ValueError, float, "\N{MINUS SIGN}na") self.assertRaises(ValueError, float, "++nan") self.assertRaises(ValueError, float, "-+NAN") self.assertRaises(ValueError, float, "+-NaN") self.assertRaises(ValueError, float, "--nAn") + self.assertRaises(ValueError, float, "\N{MINUS SIGN}+NAN") + self.assertRaises(ValueError, float, "-\N{MINUS SIGN}nAn") + self.assertRaises(ValueError, float, "\N{MINUS SIGN}-nAn") + self.assertRaises(ValueError, float, "\N{MINUS SIGN}\N{MINUS SIGN}nAn") def test_nan_as_str(self): self.assertEqual(repr(1e300 * 1e300 * 0), "nan") @@ -1145,11 +1159,13 @@ def test_nan_as_str(self): def test_inf_signs(self): self.assertEqual(copysign(1.0, float('inf')), 1.0) self.assertEqual(copysign(1.0, float('-inf')), -1.0) + self.assertEqual(copysign(1.0, float('\N{MINUS SIGN}inf')), -1.0) def test_nan_signs(self): # The sign of float('nan') should be predictable. self.assertEqual(copysign(1.0, float('nan')), 1.0) self.assertEqual(copysign(1.0, float('-nan')), -1.0) + self.assertEqual(copysign(1.0, float('\N{MINUS SIGN}nan')), -1.0) fromHex = float.fromhex diff --git a/Lib/test/test_long.py b/Lib/test/test_long.py index b48a8812a1a2d1..188638fe1474fe 100644 --- a/Lib/test/test_long.py +++ b/Lib/test/test_long.py @@ -328,11 +328,11 @@ def test_long(self): ('1' + '0'*100, 10**100) ] for s, v in LL: - for sign in "", "+", "-": + for sign in "", "+", "-", "\N{MINUS SIGN}": for prefix in "", " ", "\t", " \t\t ": ss = prefix + sign + s vv = v - if sign == "-" and v is not ValueError: + if sign in ("-", "\N{MINUS SIGN}") and v is not ValueError: vv = -v try: self.assertEqual(int(ss), vv) @@ -360,9 +360,11 @@ def test_long(self): self.assertEqual(int('0', 0), 0) self.assertEqual(int('+0', 0), 0) self.assertEqual(int('-0', 0), 0) + self.assertEqual(int('\N{MINUS SIGN}0', 0), 0) self.assertEqual(int('00', 0), 0) self.assertRaises(ValueError, int, '08', 0) self.assertRaises(ValueError, int, '-012395', 0) + self.assertRaises(ValueError, int, '\N{MINUS SIGN}012395', 0) # invalid bases invalid_bases = [-909, diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-14-03-03-07.gh-issue-144087.KT9kaM.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-14-03-03-07.gh-issue-144087.KT9kaM.rst new file mode 100644 index 00000000000000..054f9d021a4808 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-14-03-03-07.gh-issue-144087.KT9kaM.rst @@ -0,0 +1,3 @@ +:func:`int`, :func:`float` and :func:`complex` now support strings +with ``−`` (Unicode minus sign, U+2212) as an alternative to ``-`` +(ASCII hyphen minus, U+002D). Contributed by Bartosz Sławecki. diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index fdcbcf51cb62c2..4e22bb41fb4feb 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -9475,6 +9475,9 @@ _PyUnicode_TransformDecimalAndSpaceToASCII(PyObject *unicode) else if (Py_UNICODE_ISSPACE(ch)) { out[i] = ' '; } + else if (ch == 0x2212) { /* MINUS SIGN */ + out[i] = '-'; + } else { int decimal = Py_UNICODE_TODECIMAL(ch); if (decimal < 0) {