From 71108697049a2e5ca14170769a4c6a9be325d444 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 23 Apr 2024 18:00:55 +0300 Subject: [PATCH 1/9] Imaginary type and IEC 60559-compatible complex arithmetic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Generally, mixed-mode arithmetic combining real and complex variables should be performed directly, not by first coercing the real to complex, lest the sign of zero be rendered uninformative; the same goes for combinations of pure imaginary quantities with complex variables." (c) Kahan, W: Branch cuts for complex elementary functions. That's why C standards since C99 introduce imaginary types. This patch implements similar extension to the Python language. Lets consider (actually interrelated) problems, which will be solved on this way. 1) Now complex arithmetic could be used for implementation of mathematical functions without special "corner cases", with textbooks formulae. Take the inverse tangent as an example: >>> z = complex(-0.0, 2) >>> cmath.atan(z) (-1.5707963267948966+0.5493061443340549j) >>> atan = lambda z: 1j*(cmath.log(1 - 1j*z) - cmath.log(1 + 1j*z))/2 >>> atan(z) # real part had wrong sign before (-1.5707963267948966+0.5493061443340549j) 2) Previously, we have unsigned imaginary literals with the following semantics: a±bj = complex(a ± 0.0, ±b) complex(a, ±b)*cj = complex(a*0.0 ∓ b*c, a*c ± b*0.0) While this behaviour was well documented, most users would expect instead here: a±bj = complex(a, ±b) complex(a, ±b)*cj = complex(∓b*c, a*c) i.e. that it follows to the rectangular notation for complex numbers. For example: >>> -0.0+1j # was 1j (-0.0+1j) >>> float('inf')*1j # was (nan+infj) infj >>> -0.0+1j # was 1j (-0.0+1j) >>> complex(-0.0, 1) # was (-0+1j), not funny signed integer zero (-0.0+1j) 3) The ``eval(repr(x)) == x`` invariant now holds for the complex type. What's changed: * Added a new subtype (imaginary) of the complex type with few overloaded methods (conjugate() and __getnewargs__()). * Complex and imaginary types implement IEC 60559-compatible complex arithmetic (as specified by C11 Annex G). * Imaginary literals now produce instances of imaginary type. * cmath.infj/nanj were changed to be of imaginary type. * Modules ast, code, copy, marshal got support for imaginary type. * Few tests adapted to use complex, instead of imaginary literals * Print dot for signed zeros in the real part of repr(complex) * repr(complex) now prints real part even if it's zero. --- Doc/data/stable_abi.dat | 2 + Doc/library/cmath.rst | 4 +- Doc/library/functions.rst | 23 +- Doc/reference/lexical_analysis.rst | 4 +- Include/complexobject.h | 6 + Include/internal/pycore_complexobject.h | 6 + Lib/ast.py | 4 +- Lib/copy.py | 5 +- Lib/test/test_bytes.py | 12 +- Lib/test/test_capi/test_complex.py | 86 ++++- Lib/test/test_complex.py | 184 ++++++++-- Lib/test/test_descr.py | 2 +- Lib/test/test_format.py | 13 +- Lib/test/test_fractions.py | 2 +- Lib/test/test_marshal.py | 11 +- Lib/test/test_socket.py | 4 +- Lib/test/test_stable_abi_ctypes.py | 2 + Lib/test/test_str.py | 12 +- .../testmock/testmagicmethods.py | 4 +- Lib/unittest/mock.py | 2 +- Misc/stable_abi.toml | 5 + Modules/_testinternalcapi/complex.c | 82 +++-- Modules/_testlimitedcapi/complex.c | 23 ++ Modules/cmathmodule.c | 6 +- Objects/clinic/complexobject.c.h | 101 +++++- Objects/codeobject.c | 14 + Objects/complexobject.c | 338 +++++++++++++++++- Objects/object.c | 1 + Objects/unicode_formatter.c | 15 +- PC/python3dll.c | 2 + Parser/action_helpers.c | 5 +- Parser/pegen.c | 9 +- Python/ast.c | 5 +- Python/ast_unparse.c | 2 +- Python/bltinmodule.c | 6 + Python/marshal.c | 32 ++ Tools/c-analyzer/cpython/globals-to-fix.tsv | 1 + 37 files changed, 893 insertions(+), 142 deletions(-) diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 7ad5f3ecfab5b4..797ad582d654d8 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -307,6 +307,8 @@ func,PyGILState_Release,3.2,, type,PyGILState_STATE,3.2,, type,PyGetSetDef,3.2,,full-abi data,PyGetSetDescr_Type,3.2,, +func,PyImaginary_FromDouble,3.14,, +data,PyImaginary_Type,3.14,, func,PyImport_AddModule,3.2,, func,PyImport_AddModuleObject,3.7,, func,PyImport_AddModuleRef,3.13,, diff --git a/Doc/library/cmath.rst b/Doc/library/cmath.rst index b6d5dbee21dcd5..e5fdf5a4622738 100644 --- a/Doc/library/cmath.rst +++ b/Doc/library/cmath.rst @@ -330,7 +330,7 @@ Constants .. data:: infj Complex number with zero real part and positive infinity imaginary - part. Equivalent to ``complex(0.0, float('inf'))``. + part. Equivalent to ``float('inf')*1j``. .. versionadded:: 3.6 @@ -346,7 +346,7 @@ Constants .. data:: nanj Complex number with zero real part and NaN imaginary part. Equivalent to - ``complex(0.0, float('nan'))``. + ``float('nan')*1j``. .. versionadded:: 3.6 diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 61799e303a1639..e6c2f3e4596951 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -33,10 +33,11 @@ are always available. They are listed here in alphabetical order. | | :func:`complex` | | | | **P** | | **V** | | | | | **I** | | :func:`pow` | | :func:`vars` | | | **D** | | :func:`id` | | :func:`print` | | | -| | :func:`delattr` | | :func:`input` | | :func:`property` | | **Z** | -| | |func-dict|_ | | :func:`int` | | | | :func:`zip` | -| | :func:`dir` | | :func:`isinstance` | | | | | -| | :func:`divmod` | | :func:`issubclass` | | | | **_** | +| | :func:`delattr` | | :func:`imaginary` | | :func:`property` | | | +| | |func-dict|_ | | :func:`input` | | | | **Z** | +| | :func:`dir` | | :func:`int` | | | | :func:`zip` | +| | :func:`divmod` | | :func:`isinstance` | | | | | +| | | | :func:`issubclass` | | | | **_** | | | | | :func:`iter` | | | | :func:`__import__` | +-------------------------+-----------------------+-----------------------+-------------------------+ @@ -386,7 +387,7 @@ are always available. They are listed here in alphabetical order. >>> complex('+1.23') (1.23+0j) >>> complex('-4.5j') - -4.5j + (0.0-4.5j) >>> complex('-1.23+4.5j') (-1.23+4.5j) >>> complex('\t( -1.23+4.5J )\n') @@ -396,7 +397,7 @@ are always available. They are listed here in alphabetical order. >>> complex(1.23) (1.23+0j) >>> complex(imag=-4.5) - -4.5j + (0.0-4.5j) >>> complex(-1.23, 4.5) (-1.23+4.5j) @@ -440,7 +441,7 @@ are always available. They are listed here in alphabetical order. See also :meth:`complex.from_number` which only accepts a single numeric argument. - If all arguments are omitted, returns ``0j``. + If all arguments are omitted, returns ``0.0+0j``. The complex type is described in :ref:`typesnumeric`. @@ -968,6 +969,14 @@ are always available. They are listed here in alphabetical order. .. audit-event:: builtins.id id id +.. class:: imaginary(x=0.0) + + Return an imaginary number with the value ``float(x)*1j``. If argument is + omitted, returns ``0j``. + + .. versionadded:: next + + .. function:: input() input(prompt, /) diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index ea386706e8b511..a929f51b2af235 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -1363,8 +1363,8 @@ Imaginary literals Python has :ref:`complex number ` objects, but no complex literals. -Instead, *imaginary literals* denote complex numbers with a zero -real part. +Instead, *imaginary literals* denote complex numbers without a real part, an instance +of :class:`imaginary`. For example, in math, the complex number 3+4.2\ *i* is written as the real number 3 added to the imaginary number 4.2\ *i*. diff --git a/Include/complexobject.h b/Include/complexobject.h index ebe49a832f7414..2ff40ecfe52128 100644 --- a/Include/complexobject.h +++ b/Include/complexobject.h @@ -9,15 +9,21 @@ extern "C" { /* Complex object interface */ PyAPI_DATA(PyTypeObject) PyComplex_Type; +PyAPI_DATA(PyTypeObject) PyImaginary_Type; #define PyComplex_Check(op) PyObject_TypeCheck((op), &PyComplex_Type) #define PyComplex_CheckExact(op) Py_IS_TYPE((op), &PyComplex_Type) +#define PyImaginary_Check(op) PyObject_TypeCheck((op), &PyImaginary_Type) +#define PyImaginary_CheckExact(op) Py_IS_TYPE((op), &PyImaginary_Type) + PyAPI_FUNC(PyObject *) PyComplex_FromDoubles(double real, double imag); PyAPI_FUNC(double) PyComplex_RealAsDouble(PyObject *op); PyAPI_FUNC(double) PyComplex_ImagAsDouble(PyObject *op); +PyAPI_FUNC(PyObject *) PyImaginary_FromDouble(double imag); + #ifndef Py_LIMITED_API # define Py_CPYTHON_COMPLEXOBJECT_H # include "cpython/complexobject.h" diff --git a/Include/internal/pycore_complexobject.h b/Include/internal/pycore_complexobject.h index f595f6ab7a674d..0ce974c1e817f5 100644 --- a/Include/internal/pycore_complexobject.h +++ b/Include/internal/pycore_complexobject.h @@ -21,11 +21,17 @@ extern int _PyComplex_FormatAdvancedWriter( // Operations on complex numbers. PyAPI_FUNC(Py_complex) _Py_cr_sum(Py_complex, double); +PyAPI_FUNC(Py_complex) _Py_ci_sum(Py_complex, double); PyAPI_FUNC(Py_complex) _Py_cr_diff(Py_complex, double); PyAPI_FUNC(Py_complex) _Py_rc_diff(double, Py_complex); +PyAPI_FUNC(Py_complex) _Py_ci_diff(Py_complex, double); +PyAPI_FUNC(Py_complex) _Py_ic_diff(double, Py_complex); PyAPI_FUNC(Py_complex) _Py_cr_prod(Py_complex, double); +PyAPI_FUNC(Py_complex) _Py_ci_prod(Py_complex, double); PyAPI_FUNC(Py_complex) _Py_cr_quot(Py_complex, double); PyAPI_FUNC(Py_complex) _Py_rc_quot(double, Py_complex); +PyAPI_FUNC(Py_complex) _Py_ci_quot(Py_complex, double); +PyAPI_FUNC(Py_complex) _Py_ic_quot(double, Py_complex); #ifdef __cplusplus diff --git a/Lib/ast.py b/Lib/ast.py index 983ac1710d0205..b0853d4d615df6 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -89,7 +89,7 @@ def _convert_literal(node): isinstance(node, UnaryOp) and isinstance(node.op, (UAdd, USub)) and isinstance(node.operand, Constant) - and type(operand := node.operand.value) in (int, float, complex) + and type(operand := node.operand.value) in (int, float, imaginary) ): if isinstance(node.op, UAdd): return + operand @@ -101,7 +101,7 @@ def _convert_literal(node): and isinstance(node.left, (Constant, UnaryOp)) and isinstance(node.right, Constant) and type(left := _convert_literal(node.left)) in (int, float) - and type(right := _convert_literal(node.right)) is complex + and type(right := _convert_literal(node.right)) is imaginary ): if isinstance(node.op, Add): return left + right diff --git a/Lib/copy.py b/Lib/copy.py index fff7e93c2a1b89..ea6133336d457d 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -101,7 +101,7 @@ def copy(x): _copy_atomic_types = frozenset({types.NoneType, int, float, bool, complex, str, tuple, - bytes, frozenset, type, range, slice, property, + imaginary, bytes, frozenset, type, range, slice, property, types.BuiltinFunctionType, types.EllipsisType, types.NotImplementedType, types.FunctionType, types.CodeType, weakref.ref, super}) @@ -164,7 +164,8 @@ def deepcopy(x, memo=None): _atomic_types = frozenset({types.NoneType, types.EllipsisType, types.NotImplementedType, int, float, bool, complex, bytes, str, types.CodeType, type, range, - types.BuiltinFunctionType, types.FunctionType, weakref.ref, property}) + types.BuiltinFunctionType, types.FunctionType, weakref.ref, + property, imaginary}) _deepcopy_dispatch = d = {} diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index f10e4041937f4f..fa4b2486ce22ed 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -785,12 +785,12 @@ def __int__(self): ('%X format: an integer is required, not float', b'%X', 2.11), ('%o format: an integer is required, not float', b'%o', 1.79), ('%x format: an integer is required, not PseudoFloat', b'%x', pi), - ('%x format: an integer is required, not complex', b'%x', 3j), - ('%X format: an integer is required, not complex', b'%X', 2j), - ('%o format: an integer is required, not complex', b'%o', 1j), - ('%u format: a real number is required, not complex', b'%u', 3j), - ('%i format: a real number is required, not complex', b'%i', 2j), - ('%d format: a real number is required, not complex', b'%d', 2j), + ('%x format: an integer is required, not complex', b'%x', 1+3j), + ('%X format: an integer is required, not complex', b'%X', 1+2j), + ('%o format: an integer is required, not complex', b'%o', 1+1j), + ('%u format: a real number is required, not complex', b'%u', 1+3j), + ('%i format: a real number is required, not complex', b'%i', 1+2j), + ('%d format: a real number is required, not complex', b'%d', 1+2j), ( r'%c requires an integer in range\(256\)' r' or a single byte, not .*\.PseudoFloat', diff --git a/Lib/test/test_capi/test_complex.py b/Lib/test/test_capi/test_complex.py index c3189a67cc7e2d..caef8bebdf148e 100644 --- a/Lib/test/test_capi/test_complex.py +++ b/Lib/test/test_capi/test_complex.py @@ -24,6 +24,9 @@ class BadComplex3: def __complex__(self): raise RuntimeError +class ImaginarySubclass(imaginary): + pass + class CAPIComplexTest(ComplexesAreIdenticalMixin, unittest.TestCase): def test_check(self): @@ -177,7 +180,14 @@ def test_py_cr_sum(self): # Test _Py_cr_sum() _py_cr_sum = _testinternalcapi._py_cr_sum - self.assertComplexesAreIdentical(_py_cr_sum(-0j, -0.0)[0], + self.assertComplexesAreIdentical(_py_cr_sum(-0.0 - 0j, -0.0)[0], + complex(-0.0, -0.0)) + + def test_py_ci_sum(self): + # Test _Py_cr_sum() + _py_ci_sum = _testinternalcapi._py_ci_sum + + self.assertComplexesAreIdentical(_py_ci_sum(-0.0 - 0j, -0.0)[0], complex(-0.0, -0.0)) def test_py_c_diff(self): @@ -190,7 +200,14 @@ def test_py_cr_diff(self): # Test _Py_cr_diff() _py_cr_diff = _testinternalcapi._py_cr_diff - self.assertComplexesAreIdentical(_py_cr_diff(-0j, 0.0)[0], + self.assertComplexesAreIdentical(_py_cr_diff(-0.0 - 0j, 0.0)[0], + complex(-0.0, -0.0)) + + def test_py_ci_diff(self): + # Test _Py_ci_diff() + _py_ci_diff = _testinternalcapi._py_ci_diff + + self.assertComplexesAreIdentical(_py_ci_diff(-0.0 - 0j, 0.0)[0], complex(-0.0, -0.0)) def test_py_rc_diff(self): @@ -200,6 +217,13 @@ def test_py_rc_diff(self): self.assertComplexesAreIdentical(_py_rc_diff(-0.0, 0j)[0], complex(-0.0, -0.0)) + def test_py_ic_diff(self): + # Test _Py_ic_diff() + _py_ic_diff = _testinternalcapi._py_ic_diff + + self.assertComplexesAreIdentical(_py_ic_diff(-0.0, 0j)[0], + complex(-0.0, -0.0)) + def test_py_c_neg(self): # Test _Py_c_neg() _py_c_neg = _testcapi._py_c_neg @@ -219,6 +243,13 @@ def test_py_cr_prod(self): self.assertComplexesAreIdentical(_py_cr_prod(complex('inf+1j'), INF)[0], complex('inf+infj')) + def test_py_ci_prod(self): + # Test _Py_ci_prod() + _py_ci_prod = _testinternalcapi._py_ci_prod + + self.assertComplexesAreIdentical(_py_ci_prod(complex('inf+1j'), INF)[0], + complex('-inf+infj')) + def test_py_c_quot(self): # Test _Py_c_quot() _py_c_quot = _testcapi._py_c_quot @@ -248,6 +279,13 @@ def test_py_cr_quot(self): self.assertComplexesAreIdentical(_py_cr_quot(complex('inf+1j'), 2**1000)[0], INF + 2**-1000*1j) + def test_py_ci_quot(self): + # Test _Py_ci_quot() + _py_ci_quot = _testinternalcapi._py_ci_quot + + self.assertComplexesAreIdentical(_py_ci_quot(complex('1+infj'), 2**1000)[0], + INF - 2**-1000*1j) + def test_py_rc_quot(self): # Test _Py_rc_quot() _py_rc_quot = _testinternalcapi._py_rc_quot @@ -255,6 +293,13 @@ def test_py_rc_quot(self): self.assertComplexesAreIdentical(_py_rc_quot(1.0, complex('nan-infj'))[0], 0j) + def test_py_ic_quot(self): + # Test _Py_ic_quot() + _py_ic_quot = _testinternalcapi._py_ic_quot + + self.assertComplexesAreIdentical(_py_ic_quot(1.0, complex('inf-nanj'))[0], + -0.0) + def test_py_c_pow(self): # Test _Py_c_pow() _py_c_pow = _testcapi._py_c_pow @@ -276,7 +321,6 @@ def test_py_c_pow(self): self.assertEqual(_py_c_pow(max_num, 2), (complex(INF, INF), errno.ERANGE)) - def test_py_c_abs(self): # Test _Py_c_abs() _py_c_abs = _testcapi._py_c_abs @@ -295,5 +339,41 @@ def test_py_c_abs(self): self.assertEqual(_py_c_abs(complex(*[DBL_MAX]*2))[1], errno.ERANGE) +class CAPIImaginaryTest(unittest.TestCase): + def test_check(self): + # Test PyImaginary_Check() + check = _testlimitedcapi.imaginary_check + + self.assertTrue(check(2j)) + self.assertTrue(check(ImaginarySubclass(2))) + self.assertFalse(check(ComplexSubclass(1+2j))) + self.assertFalse(check(Complex())) + self.assertFalse(check(3)) + self.assertFalse(check(3.0)) + self.assertFalse(check(object())) + + # CRASHES check(NULL) + + def test_checkexact(self): + # PyImaginary_CheckExact() + checkexact = _testlimitedcapi.imaginary_checkexact + + self.assertTrue(checkexact(2j)) + self.assertFalse(checkexact(ImaginarySubclass(2))) + self.assertFalse(checkexact(ComplexSubclass(1+2j))) + self.assertFalse(checkexact(Complex())) + self.assertFalse(checkexact(3)) + self.assertFalse(checkexact(3.0)) + self.assertFalse(checkexact(object())) + + # CRASHES checkexact(NULL) + + def test_fromdouble(self): + # Test PyImaginary_FromDouble() + fromdouble = _testlimitedcapi.imaginary_fromdouble + + self.assertEqual(fromdouble(2.0), 2.0j) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 0c7e7341f13d4e..7532400714f44c 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -39,6 +39,9 @@ def __float__(self): class ComplexSubclass(complex): pass +class ImaginarySubclass(imaginary): + pass + class OtherComplexSubclass(complex): pass @@ -195,6 +198,26 @@ def test_truediv(self): self.assertComplexesAreIdentical(float(INF)/complex(NAN, INF), complex(NAN, NAN)) + self.assertEqual((1+1j)/float(2), 0.5+0.5j) + self.assertRaises(TypeError, operator.truediv, None, 1+1j) + self.assertRaises(TypeError, operator.truediv, 1+1j, None) + + self.assertEqual(1j/imaginary(2), 0.5) + self.assertEqual((1+1j)/imaginary(2), 0.5-0.5j) + self.assertEqual(1j/complex(1, 1), 0.5+0.5j) + self.assertEqual(1j/float(2), 0.5j) + self.assertEqual(float(1)/(1+2j), 0.2-0.4j) + self.assertEqual(float(1)/(-1+2j), -0.2-0.4j) + self.assertEqual(float(1)/(1-2j), 0.2+0.4j) + + z = float(1)/(NAN+2j) + self.assertTrue(isnan(z.real)) + self.assertTrue(isnan(z.imag)) + + self.assertRaises(ZeroDivisionError, operator.truediv, 1j, 0j) + self.assertRaises(ZeroDivisionError, operator.truediv, 1j, complex(0, 0)) + self.assertRaises(ZeroDivisionError, operator.truediv, 1j, 0.0) + def test_truediv_zero_division(self): for a, b in ZERO_DIVISION: with self.assertRaises(ZeroDivisionError): @@ -269,10 +292,28 @@ def test_add(self): complex(-0.0, -0.0)) self.assertComplexesAreIdentical((-0.0) + complex(-0.0, -0.0), complex(-0.0, -0.0)) + self.assertEqual(1j + imaginary(1), imaginary(2)) self.assertRaises(OverflowError, operator.add, 1j, 10**1000) self.assertRaises(TypeError, operator.add, 1j, None) + self.assertRaises(TypeError, operator.add, 1+1j, None) self.assertRaises(TypeError, operator.add, None, 1j) + self.assertComplexesAreIdentical(float(0.0) + 0j, complex(0, 0)) + self.assertComplexesAreIdentical(0j + float(0.0), complex(0, 0)) + self.assertComplexesAreIdentical(float(-0.0) + 0j, complex(-0.0, 0)) + self.assertComplexesAreIdentical(0j + float(-0.0), complex(-0.0, 0)) + self.assertComplexesAreIdentical((-0.0+0j) + float(0.0), complex(0, 0)) + self.assertComplexesAreIdentical(float(0.0) + (-0.0+0j), complex(0, 0)) + self.assertComplexesAreIdentical((1+0j) + complex(1-0j), complex(2, 0)) + self.assertComplexesAreIdentical(0j + complex(-0.0-0j), complex(-0.0, 0)) + self.assertComplexesAreIdentical(0j + complex(-0j), complex(0, 0)) + self.assertComplexesAreIdentical((1+0j) + complex(-0.0-0j), complex(1, 0)) + self.assertComplexesAreIdentical(complex(-0.0+0j) + (-0j), complex(-0.0, 0)) + self.assertComplexesAreIdentical((1+0j) + float(1.0), complex(2, 0)) + self.assertComplexesAreIdentical(float(1.0) + (1+0j), complex(2, 0)) + self.assertComplexesAreIdentical((1-0j) + float(1.0), complex(2, -0.0)) + self.assertComplexesAreIdentical(float(1.0) + (1-0j), complex(2, -0.0)) + def test_sub(self): self.assertEqual(1j - int(+1), complex(-1, 1)) self.assertEqual(1j - int(-1), complex(1, 1)) @@ -284,13 +325,34 @@ def test_sub(self): complex(-1, 1)) self.assertComplexesAreIdentical(complex(2, 1) - complex(1, 2), complex(1, -1)) + self.assertEqual(1j - imaginary(2), imaginary(-1)) self.assertRaises(OverflowError, operator.sub, 1j, 10**1000) self.assertRaises(TypeError, operator.sub, 1j, None) + self.assertRaises(TypeError, operator.sub, 1+1j, None) + self.assertRaises(TypeError, operator.sub, None, 1+1j) self.assertRaises(TypeError, operator.sub, None, 1j) + self.assertRaises(TypeError, operator.sub, 1j, None) + + self.assertComplexesAreIdentical(float(0.0) - 0j, complex(0, -0.0)) + self.assertComplexesAreIdentical(0j - float(0.0), complex(-0.0, 0)) + self.assertComplexesAreIdentical(float(-0.0) - 0j, complex(-0.0, -0.0)) + self.assertComplexesAreIdentical(0j - float(-0.0), complex(0, 0)) + self.assertComplexesAreIdentical((-0.0+0j) - float(0.0), complex(-0.0, 0)) + self.assertComplexesAreIdentical(float(0.0) - (-0.0+0j), complex(0, -0.0)) + self.assertComplexesAreIdentical((1+0j) - complex(1-0j), complex(0, 0)) + self.assertComplexesAreIdentical(0j - complex(-0.0-0j), complex(0, 0)) + self.assertComplexesAreIdentical(0j - complex(-0j), complex(-0.0, 0)) + self.assertComplexesAreIdentical((1+0j) - complex(-0.0-0j), complex(1, 0)) + self.assertComplexesAreIdentical(complex(-0.0+0j) - (-0j), complex(-0.0, 0)) + self.assertComplexesAreIdentical((1+0j) - float(1.0), complex(0, 0)) + self.assertComplexesAreIdentical(float(1.0) - (1+0j), complex(0, -0.0)) + self.assertComplexesAreIdentical((1-0j) - float(1.0), complex(0, -0.0)) + self.assertComplexesAreIdentical(float(1.0) - (1-0j), complex(0, 0)) def test_mul(self): self.assertEqual(1j * int(20), complex(0, 20)) self.assertEqual(1j * int(-1), complex(0, -1)) + self.assertEqual(2j * imaginary(3), -6.0) for c, r in [(2, complex(INF, 2)), (INF, complex(INF, INF)), (0, complex(NAN, 0)), (-0.0, complex(NAN, -0.0)), (NAN, complex(NAN, NAN))]: @@ -299,8 +361,27 @@ def test_mul(self): self.assertComplexesAreIdentical(c * complex(INF, 1), r) self.assertRaises(OverflowError, operator.mul, 1j, 10**1000) self.assertRaises(TypeError, operator.mul, 1j, None) + self.assertRaises(TypeError, operator.mul, 1+1j, None) self.assertRaises(TypeError, operator.mul, None, 1j) + self.assertComplexesAreIdentical(float(0.0) * 0j, complex(0, 0)) + self.assertComplexesAreIdentical(0j * float(0.0), complex(0.0, 0)) + self.assertComplexesAreIdentical(float(-0.0) * 0j, complex(0.0, -0.0)) + self.assertComplexesAreIdentical(0j * float(-0.0), complex(0, -0.0)) + self.assertComplexesAreIdentical((-0.0+0j) * float(0.0), complex(-0.0, 0)) + self.assertComplexesAreIdentical(float(0.0) * (-0.0+0j), complex(-0.0, 0)) + self.assertComplexesAreIdentical((-0.0+0j) * float(-0.0), complex(0, -0.0)) + self.assertComplexesAreIdentical(float(-0.0) * (-0.0+0j), complex(0, -0.0)) + self.assertComplexesAreIdentical((-0.0-0j) * float(-0.0), complex(0, 0)) + self.assertComplexesAreIdentical(float(-0.0) * (-0.0-0j), complex(0, 0)) + self.assertComplexesAreIdentical((1+0j) * complex(1-0j), complex(1, 0)) + self.assertComplexesAreIdentical(0j * complex(-0.0-0j), complex(0.0, -0.0)) + self.assertComplexesAreIdentical(0j * complex(-0j), complex(0, 0)) + self.assertComplexesAreIdentical((1+0j) * complex(-0.0-0j), complex(0, -0.0)) + self.assertComplexesAreIdentical((-0.0+0j) * complex(-0j), complex(0, 0)) + self.assertComplexesAreIdentical((1+0j) * float(1.0), complex(1, 0)) + self.assertComplexesAreIdentical(float(1.0) * (1+0j), complex(1, 0)) + for z, w, r in [(1e300+1j, complex(INF, INF), complex(NAN, INF)), (1e300+1j, complex(NAN, INF), complex(-INF, INF)), (1e300+1j, complex(INF, NAN), complex(INF, INF)), @@ -455,6 +536,7 @@ def test_boolcontext(self): def test_conjugate(self): self.assertClose(complex(5.3, 9.8).conjugate(), 5.3-9.8j) + self.assertEqual(1j.conjugate(), -1j) def test_constructor(self): def check(z, x, y): @@ -488,10 +570,10 @@ def check(z, x, y): "argument 'real' must be a real number, not .*WithComplex"): check(complex(WithComplex(4.25+0j), 0), 4.25, 0.0) with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + "argument 'real' must be a real number, not imaginary"): check(complex(4.25j, 0), 0.0, 4.25) with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + "argument 'real' must be a real number, not imaginary"): check(complex(0j, 4.25), 0.0, 4.25) with self.assertWarnsRegex(DeprecationWarning, "argument 'imag' must be a real number, not complex"): @@ -503,19 +585,19 @@ def check(z, x, y): "argument 'imag' must be a real number, not .*WithComplex"): complex(0, WithComplex(4.25+0j)) with self.assertWarnsRegex(DeprecationWarning, - "argument 'imag' must be a real number, not complex"): + "argument 'imag' must be a real number, not imaginary"): check(complex(0.0, 4.25j), -4.25, 0.0) with self.assertWarnsRegex(DeprecationWarning, "argument 'real' must be a real number, not complex"): check(complex(4.25+0j, 0j), 4.25, 0.0) with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + "argument 'real' must be a real number, not imaginary"): check(complex(4.25j, 0j), 0.0, 4.25) with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + "argument 'real' must be a real number, not imaginary"): check(complex(0j, 4.25+0j), 0.0, 4.25) with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + "argument 'real' must be a real number, not imaginary"): check(complex(0j, 4.25j), -4.25, 0.0) check(complex(real=4.25), 4.25, 0.0) @@ -599,18 +681,14 @@ def __complex__(self): self.assertRaises(TypeError, complex, WithIndex(None), 1.5) self.assertRaises(TypeError, complex, 1.5, WithIndex(None)) - class MyInt: - def __int__(self): - return 42 - - self.assertRaises(TypeError, complex, MyInt()) - self.assertRaises(TypeError, complex, MyInt(), 1.5) - self.assertRaises(TypeError, complex, 1.5, MyInt()) + self.assertRaises(TypeError, complex, MyInt(42)) + self.assertRaises(TypeError, complex, MyInt(42), 1.5) + self.assertRaises(TypeError, complex, 1.5, MyInt(42)) class complex0(complex): """Test usage of __complex__() when inheriting from 'complex'""" def __complex__(self): - return 42j + return 1+42j class complex1(complex): """Test usage of __complex__() with a __new__() method""" @@ -625,11 +703,30 @@ class complex2(complex): def __complex__(self): return None - check(complex(complex0(1j)), 0.0, 42.0) + check(complex(complex0(1j)), 1.0, 42.0) with self.assertWarns(DeprecationWarning): check(complex(complex1(1j)), 0.0, 2.0) self.assertRaises(TypeError, complex, complex2(1j)) + def test_imaginary_constructor(self): + self.assertEqual(imaginary(), 0j) + self.assertEqual(imaginary(-2), -2j) + self.assertEqual(imaginary(1.25), 1.25j) + self.assertEqual(imaginary("1.25"), 1.25j) + + self.assertEqual(imaginary(WithFloat(42.)), 42j) + self.assertRaises(TypeError, imaginary, WithFloat(None)) + + self.assertEqual(imaginary(WithIndex(42)), 42j) + self.assertRaises(OverflowError, imaginary, WithIndex(2**2000)) + + self.assertRaises(TypeError, imaginary, MyInt(42)) + self.assertRaises(TypeError, imaginary, 123, MyInt(42)) + self.assertRaises(ValueError, imaginary, "1.25j") + + self.assertRaises(TypeError, imaginary, complex()) + self.assertRaises(TypeError, imaginary, object()) + def test___complex__(self): z = 3 + 4j self.assertEqual(z.__complex__(), z) @@ -803,9 +900,13 @@ def test(v, expected, test_fn=self.assertEqual): test(complex(NAN, NAN), "(nan+nanj)") test(complex(-NAN, -NAN), "(nan+nanj)") - test(complex(0, INF), "infj") - test(complex(0, -INF), "-infj") - test(complex(0, NAN), "nanj") + test(complex(0, INF), "(0.0+infj)") + test(complex(0, -INF), "(0.0-infj)") + test(complex(0, NAN), "(0.0+nanj)") + + test(imaginary(INF), "infj") + test(imaginary(-INF), "-infj") + test(imaginary(NAN), "nanj") self.assertEqual(1-6j,complex(repr(1-6j))) self.assertEqual(1+6j,complex(repr(1+6j))) @@ -818,29 +919,43 @@ def test(v, expected, test_fn=self.assertEqual): test_fn(repr(v), expected) test_fn(str(v), expected) - test(complex(0., 1.), "1j") - test(complex(-0., 1.), "(-0+1j)") - test(complex(0., -1.), "-1j") - test(complex(-0., -1.), "(-0-1j)") + test(complex(0., 1.), "(0.0+1j)") + test(complex(-0., 1.), "(-0.0+1j)") + test(complex(0., -1.), "(0.0-1j)") + test(complex(-0., -1.), "(-0.0-1j)") - test(complex(0., 0.), "0j") - test(complex(0., -0.), "-0j") - test(complex(-0., 0.), "(-0+0j)") - test(complex(-0., -0.), "(-0-0j)") + test(imaginary(+1.), "1j") + test(imaginary(-1.), "-1j") + + test(complex(0., 0.), "(0.0+0j)") + test(complex(0., -0.), "(0.0-0j)") + test(complex(-0., 0.), "(-0.0+0j)") + test(complex(-0., -0.), "(-0.0-0j)") + + test(imaginary(+0.0), "0j") + test(imaginary(-0.0), "-0j") def test_pos(self): self.assertEqual(+(1+6j), 1+6j) self.assertEqual(+ComplexSubclass(1, 6), 1+6j) self.assertIs(type(+ComplexSubclass(1, 6)), complex) + self.assertEqual(+1j, 1j) + self.assertEqual(+ImaginarySubclass(1), 1j) + self.assertIs(type(+ImaginarySubclass(1)), imaginary) + def test_neg(self): self.assertEqual(-(1+6j), -1-6j) + self.assertComplexesAreIdentical(-0j, complex(0, -0.0)) + self.assertComplexesAreIdentical(-complex(-0.0+0j), complex(0, -0.0)) def test_getnewargs(self): self.assertEqual((1+2j).__getnewargs__(), (1.0, 2.0)) self.assertEqual((1-2j).__getnewargs__(), (1.0, -2.0)) - self.assertEqual((2j).__getnewargs__(), (0.0, 2.0)) - self.assertEqual((-0j).__getnewargs__(), (0.0, -0.0)) + self.assertEqual((0.0+2j).__getnewargs__(), (0.0, 2.0)) + self.assertEqual((2j).__getnewargs__(), (2.0,)) + self.assertEqual((0.0-0j).__getnewargs__(), (0.0, -0.0)) + self.assertEqual((-0j).__getnewargs__(), (-0.0,)) self.assertEqual(complex(0, INF).__getnewargs__(), (0.0, INF)) self.assertEqual(complex(INF, 0).__getnewargs__(), (INF, 0.0)) @@ -856,15 +971,11 @@ def test_negated_imaginary_literal(self): z0 = -0j z1 = -7j z2 = -1e1000j - # Note: In versions of Python < 3.2, a negated imaginary literal - # accidentally ended up with real part 0.0 instead of -0.0, thanks to a - # modification during CST -> AST translation (see issue #9011). That's - # fixed in Python 3.2. - self.assertFloatsAreIdentical(z0.real, -0.0) + self.assertFloatsAreIdentical(z0.real, +0.0) self.assertFloatsAreIdentical(z0.imag, -0.0) - self.assertFloatsAreIdentical(z1.real, -0.0) + self.assertFloatsAreIdentical(z1.real, +0.0) self.assertFloatsAreIdentical(z1.imag, -7.0) - self.assertFloatsAreIdentical(z2.real, -0.0) + self.assertFloatsAreIdentical(z2.real, +0.0) self.assertFloatsAreIdentical(z2.imag, -INF) @support.requires_IEEE_754 @@ -930,7 +1041,8 @@ def test_format(self): self.assertEqual(format(z, '3'), str(z)) self.assertEqual(format(1+3j, 'g'), '1+3j') - self.assertEqual(format(3j, 'g'), '0+3j') + self.assertEqual(format(0+3j, 'g'), '0.0+3j') + self.assertEqual(format(3j, 'g'), '3j') self.assertEqual(format(1.5+3.5j, 'g'), '1.5+3.5j') self.assertEqual(format(1.5+3.5j, '+g'), '+1.5+3.5j') diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 14f94285d3f3c2..f9a872190d1d35 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -2099,7 +2099,7 @@ def empty_seq(self): def zero(self): return 0 def complex_num(self): - return 1j + return 1+1j def stop(self): raise StopIteration def return_true(self, thing=None): diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index 1f626d87fa6c7a..32b46edea61a12 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -603,10 +603,15 @@ def test_negative_zero(self): self.assertEqual(f"{-1.:+z.0f}", "-1") self.assertEqual(f"{-1.:-z.0f}", "-1") - self.assertEqual(f"{0.j:z.1f}", "0.0+0.0j") - self.assertEqual(f"{-0.j:z.1f}", "0.0+0.0j") - self.assertEqual(f"{.01j:z.1f}", "0.0+0.0j") - self.assertEqual(f"{-.01j:z.1f}", "0.0+0.0j") + self.assertEqual(f"{0.0+0.j:z.1f}", "0.0+0.0j") + self.assertEqual(f"{0.0-0.j:z.1f}", "0.0+0.0j") + self.assertEqual(f"{0.0+.01j:z.1f}", "0.0+0.0j") + self.assertEqual(f"{0.0-.01j:z.1f}", "0.0+0.0j") + + self.assertEqual(f"{0.j:z.1f}", "0.0j") + self.assertEqual(f"{-0.j:z.1f}", "0.0j") + self.assertEqual(f"{.01j:z.1f}", "0.0j") + self.assertEqual(f"{-.01j:z.1f}", "0.0j") self.assertEqual(f"{-0.:z>6.1f}", "zz-0.0") # test fill, esp. 'z' fill self.assertEqual(f"{-0.:z>z6.1f}", "zzz0.0") diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index cf42b86358dbca..388bb250d29322 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -1673,7 +1673,7 @@ def test_complex_handling(self): # See issue gh-102840 for more details. a = F(1, 2) - b = 1j + b = 0.0+1j message = "unsupported operand type(s) for %s: '%s' and '%s'" # test forward self.assertRaisesMessage(TypeError, diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 8b1fb0eba1f8b6..d6a345447b73eb 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -317,7 +317,8 @@ def test_exact_type_match(self): # >>> class Int(int): pass # >>> type(loads(dumps(Int()))) # - for typ in (int, float, complex, tuple, list, dict, set, frozenset): + for typ in (int, float, complex, tuple, list, dict, set, frozenset, + imaginary): # Note: str subclasses are not tested because they get handled # by marshal's routines for objects supporting the buffer API. subtyp = type('subtyp', (typ,), {}) @@ -371,7 +372,7 @@ def readinto(self, buf): if n is not None and n > 4: n += 10**6 return n - for value in (1.0, 1j, b'0123456789', '0123456789'): + for value in (1.0, 1j, 1+1j, b'0123456789', '0123456789'): self.assertRaises(ValueError, marshal.load, BadReader(marshal.dumps(value))) @@ -627,7 +628,7 @@ def test_write_long_to_file(self): self.assertEqual(data, b'\x78\x56\x34\x12') def test_write_object_to_file(self): - obj = ('\u20ac', b'abc', 123, 45.6, 7+8j, 'long line '*1000) + obj = ('\u20ac', b'abc', 123, 45.6, 7+8j, 122j, 'long line '*1000) for v in range(marshal.version + 1): _testcapi.pymarshal_write_object_to_file(obj, os_helper.TESTFN, v) with open(os_helper.TESTFN, 'rb') as f: @@ -664,7 +665,7 @@ def test_read_long_from_file(self): os_helper.unlink(os_helper.TESTFN) def test_read_last_object_from_file(self): - obj = ('\u20ac', b'abc', 123, 45.6, 7+8j) + obj = ('\u20ac', b'abc', 123, 45.6, 7+8j, 666j) for v in range(marshal.version + 1): data = marshal.dumps(obj, v) with open(os_helper.TESTFN, 'wb') as f: @@ -680,7 +681,7 @@ def test_read_last_object_from_file(self): os_helper.unlink(os_helper.TESTFN) def test_read_object_from_file(self): - obj = ('\u20ac', b'abc', 123, 45.6, 7+8j) + obj = ('\u20ac', b'abc', 123, 45.6, 7+8j, 1j) for v in range(marshal.version + 1): data = marshal.dumps(obj, v) with open(os_helper.TESTFN, 'wb') as f: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 24ee0f2c280746..9ee3dd70a0ce9d 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -988,7 +988,7 @@ def testSendtoErrors(self): self.assertEqual(str(cm.exception), "a bytes-like object is required, not 'str'") with self.assertRaises(TypeError) as cm: - s.sendto(5j, sockname) + s.sendto(1+5j, sockname) self.assertEqual(str(cm.exception), "a bytes-like object is required, not 'complex'") with self.assertRaises(TypeError) as cm: @@ -1000,7 +1000,7 @@ def testSendtoErrors(self): self.assertEqual(str(cm.exception), "a bytes-like object is required, not 'str'") with self.assertRaises(TypeError) as cm: - s.sendto(5j, 0, sockname) + s.sendto(1+5j, 0, sockname) self.assertEqual(str(cm.exception), "a bytes-like object is required, not 'complex'") with self.assertRaises(TypeError) as cm: diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index cbec7e43a7c9fb..72e1846434f678 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -342,6 +342,8 @@ def test_windows_feature_macros(self): "PyGILState_GetThisThreadState", "PyGILState_Release", "PyGetSetDescr_Type", + "PyImaginary_FromDouble", + "PyImaginary_Type", "PyImport_AddModule", "PyImport_AddModuleObject", "PyImport_AddModuleRef", diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 2584fbf72d3fa6..c1d184ebee661f 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -1582,12 +1582,12 @@ def __int__(self): self.assertRaisesRegex(TypeError, '%X format: an integer is required, not float', operator.mod, '%X', 2.11) self.assertRaisesRegex(TypeError, '%o format: an integer is required, not float', operator.mod, '%o', 1.79) self.assertRaisesRegex(TypeError, '%x format: an integer is required, not PseudoFloat', operator.mod, '%x', pi) - self.assertRaisesRegex(TypeError, '%x format: an integer is required, not complex', operator.mod, '%x', 3j) - self.assertRaisesRegex(TypeError, '%X format: an integer is required, not complex', operator.mod, '%X', 2j) - self.assertRaisesRegex(TypeError, '%o format: an integer is required, not complex', operator.mod, '%o', 1j) - self.assertRaisesRegex(TypeError, '%u format: a real number is required, not complex', operator.mod, '%u', 3j) - self.assertRaisesRegex(TypeError, '%i format: a real number is required, not complex', operator.mod, '%i', 2j) - self.assertRaisesRegex(TypeError, '%d format: a real number is required, not complex', operator.mod, '%d', 1j) + self.assertRaisesRegex(TypeError, '%x format: an integer is required, not complex', operator.mod, '%x', 1+3j) + self.assertRaisesRegex(TypeError, '%X format: an integer is required, not complex', operator.mod, '%X', 1+2j) + self.assertRaisesRegex(TypeError, '%o format: an integer is required, not complex', operator.mod, '%o', 1+1j) + self.assertRaisesRegex(TypeError, '%u format: a real number is required, not complex', operator.mod, '%u', 1+3j) + self.assertRaisesRegex(TypeError, '%i format: a real number is required, not complex', operator.mod, '%i', 1+2j) + self.assertRaisesRegex(TypeError, '%d format: a real number is required, not complex', operator.mod, '%d', 1+1j) self.assertRaisesRegex(TypeError, r'%c requires an int or a unicode character, not .*\.PseudoFloat', operator.mod, '%c', pi) class RaisingNumber: diff --git a/Lib/test/test_unittest/testmock/testmagicmethods.py b/Lib/test/test_unittest/testmock/testmagicmethods.py index acdbd699d18134..684ded2a678163 100644 --- a/Lib/test/test_unittest/testmock/testmagicmethods.py +++ b/Lib/test/test_unittest/testmock/testmagicmethods.py @@ -273,7 +273,7 @@ def test_magic_mock_equality(self): def test_asyncmock_defaults(self): mock = AsyncMock() self.assertEqual(int(mock), 1) - self.assertEqual(complex(mock), 1j) + self.assertEqual(complex(mock), 1+1j) self.assertEqual(float(mock), 1.0) self.assertNotIn(object(), mock) self.assertEqual(len(mock), 0) @@ -299,7 +299,7 @@ def test_asyncmock_defaults(self): def test_magicmock_defaults(self): mock = MagicMock() self.assertEqual(int(mock), 1) - self.assertEqual(complex(mock), 1j) + self.assertEqual(complex(mock), 1+1j) self.assertEqual(float(mock), 1.0) self.assertNotIn(object(), mock) self.assertEqual(len(mock), 0) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 0bb6750655380d..99bb469c132446 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2098,7 +2098,7 @@ def method(self, /, *args, **kw): '__contains__': False, '__len__': 0, '__exit__': False, - '__complex__': 1j, + '__complex__': 1+1j, '__float__': 1.0, '__bool__': True, '__index__': 1, diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 4a03cc76f5e1e9..db778cc4b58715 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2519,6 +2519,11 @@ [function.PyEval_GetFrameLocals] added = '3.13' +[function.PyImaginary_FromDouble] + added = '3.14' +[data.PyImaginary_Type] + added = '3.14' + [function.Py_TYPE] added = '3.14' [function.Py_REFCNT] diff --git a/Modules/_testinternalcapi/complex.c b/Modules/_testinternalcapi/complex.c index 50080d5a389ceb..a331e0a0bffdc0 100644 --- a/Modules/_testinternalcapi/complex.c +++ b/Modules/_testinternalcapi/complex.c @@ -5,53 +5,67 @@ #include "pycore_complexobject.h" -#define _PY_CR_FUNC2(suffix) \ - static PyObject * \ - _py_cr_##suffix(PyObject *Py_UNUSED(module), PyObject *args) \ - { \ - Py_complex a, res; \ - double b; \ - \ - if (!PyArg_ParseTuple(args, "Dd", &a, &b)) { \ - return NULL; \ - } \ - \ - errno = 0; \ - res = _Py_cr_##suffix(a, b); \ - return Py_BuildValue("Di", &res, errno); \ +#define _PY_CX_FUNC2(suffix, prefix) \ + static PyObject * \ + _py_##prefix##_##suffix(PyObject *Py_UNUSED(module), PyObject *args) \ + { \ + Py_complex a, res; \ + double b; \ + \ + if (!PyArg_ParseTuple(args, "Dd", &a, &b)) { \ + return NULL; \ + } \ + \ + errno = 0; \ + res = _Py_##prefix##_##suffix(a, b); \ + return Py_BuildValue("Di", &res, errno); \ }; -#define _PY_RC_FUNC2(suffix) \ - static PyObject * \ - _py_rc_##suffix(PyObject *Py_UNUSED(module), PyObject *args) \ - { \ - Py_complex b, res; \ - double a; \ - \ - if (!PyArg_ParseTuple(args, "dD", &a, &b)) { \ - return NULL; \ - } \ - \ - errno = 0; \ - res = _Py_rc_##suffix(a, b); \ - return Py_BuildValue("Di", &res, errno); \ +#define _PY_XC_FUNC2(suffix, prefix) \ + static PyObject * \ + _py_##prefix##_##suffix(PyObject *Py_UNUSED(module), PyObject *args) \ + { \ + Py_complex b, res; \ + double a; \ + \ + if (!PyArg_ParseTuple(args, "dD", &a, &b)) { \ + return NULL; \ + } \ + \ + errno = 0; \ + res = _Py_##prefix##_##suffix(a, b); \ + return Py_BuildValue("Di", &res, errno); \ }; -_PY_CR_FUNC2(sum) -_PY_CR_FUNC2(diff) -_PY_RC_FUNC2(diff) -_PY_CR_FUNC2(prod) -_PY_CR_FUNC2(quot) -_PY_RC_FUNC2(quot) + +_PY_CX_FUNC2(sum, cr) +_PY_CX_FUNC2(sum, ci) +_PY_CX_FUNC2(diff, cr) +_PY_CX_FUNC2(diff, ci) +_PY_XC_FUNC2(diff, rc) +_PY_XC_FUNC2(diff, ic) +_PY_CX_FUNC2(prod, cr) +_PY_CX_FUNC2(prod, ci) +_PY_CX_FUNC2(quot, cr) +_PY_CX_FUNC2(quot, ci) +_PY_XC_FUNC2(quot, rc) +_PY_XC_FUNC2(quot, ic) + static PyMethodDef test_methods[] = { {"_py_cr_sum", _py_cr_sum, METH_VARARGS}, + {"_py_ci_sum", _py_ci_sum, METH_VARARGS}, {"_py_cr_diff", _py_cr_diff, METH_VARARGS}, + {"_py_ci_diff", _py_ci_diff, METH_VARARGS}, {"_py_rc_diff", _py_rc_diff, METH_VARARGS}, + {"_py_ic_diff", _py_ic_diff, METH_VARARGS}, {"_py_cr_prod", _py_cr_prod, METH_VARARGS}, + {"_py_ci_prod", _py_ci_prod, METH_VARARGS}, {"_py_cr_quot", _py_cr_quot, METH_VARARGS}, + {"_py_ci_quot", _py_ci_quot, METH_VARARGS}, {"_py_rc_quot", _py_rc_quot, METH_VARARGS}, + {"_py_ic_quot", _py_ic_quot, METH_VARARGS}, {NULL}, }; diff --git a/Modules/_testlimitedcapi/complex.c b/Modules/_testlimitedcapi/complex.c index e4c244e5c88d06..a71502ad9959b2 100644 --- a/Modules/_testlimitedcapi/complex.c +++ b/Modules/_testlimitedcapi/complex.c @@ -16,6 +16,20 @@ complex_checkexact(PyObject *Py_UNUSED(module), PyObject *obj) return PyLong_FromLong(PyComplex_CheckExact(obj)); } +static PyObject * +imaginary_check(PyObject *Py_UNUSED(module), PyObject *obj) +{ + NULLABLE(obj); + return PyLong_FromLong(PyImaginary_Check(obj)); +} + +static PyObject * +imaginary_checkexact(PyObject *Py_UNUSED(module), PyObject *obj) +{ + NULLABLE(obj); + return PyLong_FromLong(PyImaginary_CheckExact(obj)); +} + static PyObject * complex_fromdoubles(PyObject *Py_UNUSED(module), PyObject *args) { @@ -28,6 +42,12 @@ complex_fromdoubles(PyObject *Py_UNUSED(module), PyObject *args) return PyComplex_FromDoubles(real, imag); } +static PyObject * +imaginary_fromdouble(PyObject *Py_UNUSED(module), PyObject *obj) +{ + return PyImaginary_FromDouble(PyFloat_AsDouble(obj)); +} + static PyObject * complex_realasdouble(PyObject *Py_UNUSED(module), PyObject *obj) { @@ -65,6 +85,9 @@ static PyMethodDef test_methods[] = { {"complex_fromdoubles", complex_fromdoubles, METH_VARARGS}, {"complex_realasdouble", complex_realasdouble, METH_O}, {"complex_imagasdouble", complex_imagasdouble, METH_O}, + {"imaginary_check", imaginary_check, METH_O}, + {"imaginary_checkexact", imaginary_checkexact, METH_O}, + {"imaginary_fromdouble", imaginary_fromdouble, METH_O}, {NULL}, }; diff --git a/Modules/cmathmodule.c b/Modules/cmathmodule.c index a4ea5557a6a415..9f5c3329b79710 100644 --- a/Modules/cmathmodule.c +++ b/Modules/cmathmodule.c @@ -1190,15 +1190,13 @@ cmath_exec(PyObject *mod) return -1; } - Py_complex infj = {0.0, Py_INFINITY}; - if (PyModule_Add(mod, "infj", PyComplex_FromCComplex(infj)) < 0) { + if (PyModule_Add(mod, "infj", PyImaginary_FromDouble(Py_INFINITY)) < 0) { return -1; } if (PyModule_Add(mod, "nan", PyFloat_FromDouble(fabs(Py_NAN))) < 0) { return -1; } - Py_complex nanj = {0.0, fabs(Py_NAN)}; - if (PyModule_Add(mod, "nanj", PyComplex_FromCComplex(nanj)) < 0) { + if (PyModule_Add(mod, "nanj", PyImaginary_FromDouble(fabs(Py_NAN))) < 0) { return -1; } diff --git a/Objects/clinic/complexobject.c.h b/Objects/clinic/complexobject.c.h index c7303380de55f4..22b0e27cdc4396 100644 --- a/Objects/clinic/complexobject.c.h +++ b/Objects/clinic/complexobject.c.h @@ -185,4 +185,103 @@ complex_from_number(PyObject *type, PyObject *number) return return_value; } -/*[clinic end generated code: output=05d2ff43fc409733 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(imaginary_new__doc__, +"imaginary(x=0)\n" +"--\n" +"\n" +"Create an imaginary number from a real number or string.\n" +"\n" +"This is equivalent of float(x)*1j."); + +static PyObject * +imaginary_new_impl(PyTypeObject *type, PyObject *x); + +static PyObject * +imaginary_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { _Py_LATIN1_CHR('x'), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"x", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "imaginary", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + PyObject *x = NULL; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + x = fastargs[0]; +skip_optional_pos: + return_value = imaginary_new_impl(type, x); + +exit: + return return_value; +} + +PyDoc_STRVAR(imaginary_conjugate__doc__, +"conjugate($self, /)\n" +"--\n" +"\n" +"Return the complex conjugate of its argument. (-4j).conjugate() == 4j."); + +#define IMAGINARY_CONJUGATE_METHODDEF \ + {"conjugate", (PyCFunction)imaginary_conjugate, METH_NOARGS, imaginary_conjugate__doc__}, + +static PyObject * +imaginary_conjugate_impl(PyComplexObject *self); + +static PyObject * +imaginary_conjugate(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return imaginary_conjugate_impl((PyComplexObject *)self); +} + +PyDoc_STRVAR(imaginary___getnewargs____doc__, +"__getnewargs__($self, /)\n" +"--\n" +"\n"); + +#define IMAGINARY___GETNEWARGS___METHODDEF \ + {"__getnewargs__", (PyCFunction)imaginary___getnewargs__, METH_NOARGS, imaginary___getnewargs____doc__}, + +static PyObject * +imaginary___getnewargs___impl(PyComplexObject *self); + +static PyObject * +imaginary___getnewargs__(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return imaginary___getnewargs___impl((PyComplexObject *)self); +} +/*[clinic end generated code: output=4448bc53f794a795 input=a9049054013a1b77]*/ diff --git a/Objects/codeobject.c b/Objects/codeobject.c index 0d264a6e346f95..59ced0654c63d1 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -2960,6 +2960,15 @@ _PyCode_ConstantKey(PyObject *op) else key = PyTuple_Pack(2, Py_TYPE(op), op); } + else if (PyImaginary_CheckExact(op)) { + double d = ((PyComplexObject *)(op))->cval.imag; + if (d == 0.0 && copysign(1.0, d) < 0.0) { + key = PyTuple_Pack(3, Py_TYPE(op), op, Py_None); + } + else { + key = PyTuple_Pack(2, Py_TYPE(op), op); + } + } else if (PyComplex_CheckExact(op)) { Py_complex z; int real_negzero, imag_negzero; @@ -3187,6 +3196,11 @@ compare_constants(const void *key1, const void *key2) Py_complex c2 = ((PyComplexObject *)op2)->cval; return memcmp(&c1, &c2, sizeof(Py_complex)) == 0; } + else if (PyImaginary_CheckExact(op1)) { + double i1 = ((PyComplexObject *)op1)->cval.imag; + double i2 = ((PyComplexObject *)op2)->cval.imag; + return memcmp(&i1, &i2, sizeof(double)) == 0; + } // gh-130851: Treat instances of unexpected types as distinct if they are // not the same object. return 0; diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 03fc137c345734..dd381595d698a6 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -19,8 +19,9 @@ /*[clinic input] class complex "PyComplexObject *" "&PyComplex_Type" +class imaginary "PyComplexObject *" "&PyComplex_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=819e057d2d10f5ec]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=041bba3f29a299d0]*/ #include "clinic/complexobject.c.h" @@ -51,6 +52,20 @@ _Py_rc_sum(double a, Py_complex b) return _Py_cr_sum(b, a); } +Py_complex +_Py_ci_sum(Py_complex a, double b) +{ + Py_complex r = a; + r.imag += b; + return r; +} + +static inline Py_complex +_Py_ic_sum(double a, Py_complex b) +{ + return _Py_ci_sum(b, a); +} + Py_complex _Py_c_diff(Py_complex a, Py_complex b) { @@ -77,6 +92,23 @@ _Py_rc_diff(double a, Py_complex b) return r; } +Py_complex +_Py_ci_diff(Py_complex a, double b) +{ + Py_complex r = a; + r.imag -= b; + return r; +} + +Py_complex +_Py_ic_diff(double a, Py_complex b) +{ + Py_complex r; + r.real = -b.real; + r.imag = a - b.imag; + return r; +} + Py_complex _Py_c_neg(Py_complex a) { @@ -162,6 +194,21 @@ _Py_rc_prod(double a, Py_complex b) return _Py_cr_prod(b, a); } +Py_complex +_Py_ci_prod(Py_complex a, double b) +{ + Py_complex r; + r.real = -a.imag*b; + r.imag = a.real*b; + return r; +} + +static inline Py_complex +_Py_ic_prod(double a, Py_complex b) +{ + return _Py_ci_prod(b, a); +} + /* Avoid bad optimization on Windows ARM64 until the compiler is fixed */ #ifdef _M_ARM64 #pragma optimize("", off) @@ -302,6 +349,28 @@ _Py_rc_quot(double a, Py_complex b) return r; } + +Py_complex +_Py_ci_quot(Py_complex a, double b) +{ + Py_complex r = a; + if (b) { + r.real = a.imag / b; + r.imag = -a.real / b; + } + else { + errno = EDOM; + r.real = r.imag = 0.0; + } + return r; +} + +Py_complex +_Py_ic_quot(double a, Py_complex b) +{ + Py_complex r = _Py_rc_quot(a, b); + return (Py_complex){-r.imag, r.real}; +} #ifdef _M_ARM64 #pragma optimize("", on) #endif @@ -589,7 +658,7 @@ complex_repr(PyObject *op) const char *lead = ""; const char *tail = ""; - if (v->cval.real == 0. && copysign(1.0, v->cval.real)==1.0) { + if (PyImaginary_Check(v)) { /* Real part is +0: just output the imaginary part and do not include parens. */ re = ""; @@ -603,7 +672,8 @@ complex_repr(PyObject *op) /* Format imaginary part with sign, real part without. Include parens in the result. */ pre = PyOS_double_to_string(v->cval.real, format_code, - precision, 0, NULL); + precision, + v->cval.real? 0 : Py_DTSF_ADD_DOT_0, NULL); if (!pre) { PyErr_NoMemory(); goto done; @@ -679,19 +749,29 @@ real_to_complex(PyObject **pobj, Py_complex *pc) } /* Complex arithmetic rules implement special mixed-mode case where combining - a pure-real (float or int) value and a complex value is performed directly - without first coercing the real value to a complex value. + a pure-real (float or int) value and a complex value or a pure-imaginary + value (instances of the imaginary type, like 1j) and a complex value are + performed directly, without first coercing operands to a complex value. Let us consider the addition as an example, assuming that ints are implicitly converted to floats. We have the following rules (up to variants with changed order of operands): - complex(a, b) + complex(c, d) = complex(a + c, b + d) - float(a) + complex(b, c) = complex(a + b, c) + complex(a, b) + complex(c, d) = complex(a + c, b + d) (1) + float(a) + complex(b, c) = complex(a + b, c) (2) + imaginary(a) + complex(b, c) = complex(b, a + c) (3) + imaginary(a) + imaginary(b) = imaginary(a + b) (4) + float(a) + imaginary(b) = complex(a, b) (5) Similar rules are implemented for subtraction, multiplication and division. See C11's Annex G, sections G.5.1 and G.5.2. - */ + + Note, that the imaginary type implemented as a subtype of the complex + type. So, complex_op() functions below must implement only one type of + mixed-mode operations, i.e. when one argument is a float (2). The rest + (respectively: complex op imaginary (3), imaginary op imaginary (4) and + imaginary op float (5)) should be implemented by an appropriate + imaginary_op() function. */ #define COMPLEX_BINOP(NAME, FUNC) \ static PyObject * \ @@ -1437,3 +1517,245 @@ PyTypeObject PyComplex_Type = { PyObject_Free, /* tp_free */ .tp_version_tag = _Py_TYPE_VERSION_COMPLEX, }; + +/*[clinic input] +@classmethod +imaginary.__new__ as imaginary_new + x: object(c_default="NULL") = 0 + +Create an imaginary number from a real number or string. + +This is equivalent of float(x)*1j. +[clinic start generated code]*/ + +static PyObject * +imaginary_new_impl(PyTypeObject *type, PyObject *x) +/*[clinic end generated code: output=07242ea06eb219f6 input=0ec29c2535687795]*/ +{ + if (x == NULL) { + x = _PyLong_GetZero(); + } + + PyNumberMethods *nbx = Py_TYPE(x)->tp_as_number; + + if (nbx == NULL || (nbx->nb_float == NULL && nbx->nb_index == NULL + && !PyFloat_Check(x) && !PyUnicode_Check(x))) + { + PyErr_Format(PyExc_TypeError, + "imaginary() first argument must be a real number, " + "not '%.200s'", Py_TYPE(x)->tp_name); + return NULL; + } + + PyObject* tmp = PyNumber_Float(x); + + if (tmp == NULL) { + return NULL; + } + + PyObject *ret = type->tp_alloc(type, 0); + + if (ret != NULL) { + ((PyComplexObject *)ret)->cval = (Py_complex) {0.0, PyFloat_AS_DOUBLE(tmp)}; + } + Py_DECREF(tmp); + return ret; +} + +PyObject * +PyImaginary_FromDouble(double imag) +{ + /* Inline PyObject_New */ + PyComplexObject *op = PyObject_Malloc(sizeof(PyComplexObject)); + + if (op == NULL) { + return PyErr_NoMemory(); + } + _PyObject_Init((PyObject*)op, &PyImaginary_Type); + op->cval = (Py_complex){0.0, imag}; + return (PyObject *) op; +} + +#define CVAL(op) ((PyComplexObject *)(op))->cval +#define CREAL(op) ((PyComplexObject *)(op))->cval.real +#define CIMAG(op) ((PyComplexObject *)(op))->cval.imag + +static PyObject * +imaginary_neg(PyComplexObject *v) +{ + return PyImaginary_FromDouble(-v->cval.imag); +} + +static PyObject * +imaginary_pos(PyComplexObject *v) +{ + if (PyImaginary_CheckExact(v)) { + return Py_NewRef(v); + } + return PyImaginary_FromDouble(v->cval.imag); +} + +/* Imaginary type arithmetic. Binary operations below + must support also complex and float (or int) operands. */ + +#define IMAGINARY_ADDITIVE_BINOP(NAME, FUNC, OP) \ + static PyObject * \ + imaginary_##NAME(PyObject *v, PyObject *w) \ + { \ + Py_complex a; \ + if (PyImaginary_Check(w)) { \ + if (PyImaginary_Check(v)) { \ + return PyImaginary_FromDouble(CIMAG(v) OP CIMAG(w)); \ + } \ + if (PyComplex_Check(v)) { \ + a = _Py_ci_##FUNC(CVAL(v), CIMAG(w)); \ + } \ + else if (real_to_double(&v, &a.real) < 0) { \ + return v; \ + } \ + else { \ + a.imag = OP CIMAG(w); \ + } \ + } \ + else if (PyComplex_Check(w)) { \ + a = _Py_ic_##FUNC(CIMAG(v), CVAL(w)); \ + } \ + else if (real_to_double(&w, &a.real) < 0) { \ + return w; \ + } \ + else { \ + a.real = OP a.real; \ + a.imag = CIMAG(v); \ + } \ + return PyComplex_FromCComplex(a); \ + } + +IMAGINARY_ADDITIVE_BINOP(add, sum, +) +IMAGINARY_ADDITIVE_BINOP(sub, diff, -) + +static PyObject * +imaginary_mul(PyObject *v, PyObject *w) +{ + if (!PyImaginary_Check(v)) { + PyObject *tmp = v; + + v = w; + w = tmp; + } + + double b; + + if (PyComplex_Check(w)) { + if (PyImaginary_Check(w)) { + return PyFloat_FromDouble(-CIMAG(v)*CIMAG(w)); + } + + Py_complex a = _Py_ic_prod(CIMAG(v), CVAL(w)); + + return PyComplex_FromCComplex(a); + } + else if (real_to_double(&w, &b) < 0) { + return w; + } + return PyImaginary_FromDouble(CIMAG(v)*b); +} + +static PyObject * +imaginary_div(PyObject *v, PyObject *w) +{ + double b; + + if (PyImaginary_Check(w)) { + b = CIMAG(w); + if (b) { + if (PyImaginary_Check(v)) { + return PyFloat_FromDouble(CIMAG(v)/b); + } + if (PyComplex_Check(v)) { + Py_complex a = _Py_ci_quot(CVAL(v), b); + + return PyComplex_FromCComplex(a); + } + + double a; + + if (real_to_double(&v, &a) < 0) { + return v; + } + return PyImaginary_FromDouble(-a/b); + } + } + else { + if (PyComplex_Check(w)) { + Py_complex a = _Py_ic_quot(CIMAG(v), CVAL(w)); + + if (!errno) { + return PyComplex_FromCComplex(a); + } + } + else if (real_to_double(&w, &b) < 0) { + return w; + } + else if (b) { + return PyImaginary_FromDouble(CIMAG(v)/b); + } + } + PyErr_SetString(PyExc_ZeroDivisionError, "complex division by zero"); + return NULL; +} + +/*[clinic input] +imaginary.conjugate + +Return the complex conjugate of its argument. (-4j).conjugate() == 4j. +[clinic start generated code]*/ + +static PyObject * +imaginary_conjugate_impl(PyComplexObject *self) +/*[clinic end generated code: output=247bb3742efd769d input=8dcd1c93a873c492]*/ +{ + Py_complex c = self->cval; + + c.imag = -c.imag; + return PyImaginary_FromDouble(c.imag); +} + +/*[clinic input] +imaginary.__getnewargs__ + +[clinic start generated code]*/ + +static PyObject * +imaginary___getnewargs___impl(PyComplexObject *self) +/*[clinic end generated code: output=a587156eda821f7d input=da4b7e53915987d5]*/ +{ + Py_complex c = self->cval; + + return Py_BuildValue("(d)", c.imag); +} + +static PyMethodDef imaginary_methods[] = { + IMAGINARY_CONJUGATE_METHODDEF + IMAGINARY___GETNEWARGS___METHODDEF + {NULL} /* sentinel */ +}; + +static PyNumberMethods imaginary_as_number = { + .nb_add = (binaryfunc)imaginary_add, + .nb_subtract = (binaryfunc)imaginary_sub, + .nb_multiply = (binaryfunc)imaginary_mul, + .nb_true_divide = (binaryfunc)imaginary_div, + .nb_negative = (unaryfunc)imaginary_neg, + .nb_positive = (unaryfunc)imaginary_pos, +}; + +PyTypeObject PyImaginary_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "imaginary", + .tp_as_number = &imaginary_as_number, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = imaginary_new__doc__, + .tp_base = &PyComplex_Type, + .tp_new = imaginary_new, + .tp_methods = imaginary_methods, +}; diff --git a/Objects/object.c b/Objects/object.c index 0540112d7d2acf..ed1e5f4cb29e10 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2559,6 +2559,7 @@ static PyTypeObject* static_types[] = { // subclasses: _PyTypes_FiniTypes() deallocates them before their base // class &PyBool_Type, // base=&PyLong_Type + &PyImaginary_Type, // base=&PyComplex_Type &PyCMethod_Type, // base=&PyCFunction_Type &PyODictItems_Type, // base=&PyDictItems_Type &PyODictKeys_Type, // base=&PyDictKeys_Type diff --git a/Objects/unicode_formatter.c b/Objects/unicode_formatter.c index b8604d1355940a..0165f2403182b4 100644 --- a/Objects/unicode_formatter.c +++ b/Objects/unicode_formatter.c @@ -1608,7 +1608,7 @@ format_complex_internal(PyObject *value, /* Omitted type specifier. Should be like str(self). */ type = 'r'; default_precision = 0; - if (re == 0.0 && copysign(1.0, re) == 1.0) + if (PyImaginary_Check(value)) skip_re = 1; else add_parens = 1; @@ -1627,8 +1627,17 @@ format_complex_internal(PyObject *value, /* Cast "type", because if we're in unicode we need to pass an 8-bit char. This is safe, because we've restricted what "type" can be. */ - re_buf = PyOS_double_to_string(re, (char)type, precision, flags, - &re_float_type); + if (re == 0.0) { + if (PyImaginary_Check(value)) { + skip_re = 1; + } + re_buf = PyOS_double_to_string(re, (char)type, precision, + flags | Py_DTSF_ADD_DOT_0, + &re_float_type); + } + else + re_buf = PyOS_double_to_string(re, (char)type, precision, flags, + &re_float_type); if (re_buf == NULL) goto done; im_buf = PyOS_double_to_string(im, (char)type, precision, flags, diff --git a/PC/python3dll.c b/PC/python3dll.c index 05c86e6d5924d4..0eaf99694b9638 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -300,6 +300,7 @@ EXPORT_FUNC(PyGC_IsEnabled) EXPORT_FUNC(PyGILState_Ensure) EXPORT_FUNC(PyGILState_GetThisThreadState) EXPORT_FUNC(PyGILState_Release) +EXPORT_FUNC(PyImaginary_FromDouble) EXPORT_FUNC(PyImport_AddModule) EXPORT_FUNC(PyImport_AddModuleObject) EXPORT_FUNC(PyImport_AddModuleRef) @@ -910,6 +911,7 @@ EXPORT_DATA(PyFilter_Type) EXPORT_DATA(PyFloat_Type) EXPORT_DATA(PyFrozenSet_Type) EXPORT_DATA(PyGetSetDescr_Type) +EXPORT_DATA(PyImaginary_Type) EXPORT_DATA(PyList_Type) EXPORT_DATA(PyListIter_Type) EXPORT_DATA(PyListRevIter_Type) diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index b7a5b9d5e307b1..e17f593451dcba 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -839,7 +839,7 @@ _PyPegen_seq_delete_starred_exprs(Parser *p, asdl_seq *kwargs) expr_ty _PyPegen_ensure_imaginary(Parser *p, expr_ty exp) { - if (exp->kind != Constant_kind || !PyComplex_CheckExact(exp->v.Constant.value)) { + if (exp->kind != Constant_kind || !PyImaginary_CheckExact(exp->v.Constant.value)) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(exp, "imaginary number required in complex literal"); return NULL; } @@ -849,7 +849,8 @@ _PyPegen_ensure_imaginary(Parser *p, expr_ty exp) expr_ty _PyPegen_ensure_real(Parser *p, expr_ty exp) { - if (exp->kind != Constant_kind || PyComplex_CheckExact(exp->v.Constant.value)) { + if (exp->kind != Constant_kind || (PyComplex_CheckExact(exp->v.Constant.value) + || PyImaginary_CheckExact(exp->v.Constant.value))) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(exp, "real number required in complex literal"); return NULL; } diff --git a/Parser/pegen.c b/Parser/pegen.c index 70493031656028..685ccaec5d3714 100644 --- a/Parser/pegen.c +++ b/Parser/pegen.c @@ -617,7 +617,7 @@ parsenumber_raw(const char *s) const char *end; long x; double dx; - Py_complex compl; + double imag; int imflag; assert(s != NULL); @@ -641,12 +641,11 @@ parsenumber_raw(const char *s) } /* XXX Huge floats may silently fail */ if (imflag) { - compl.real = 0.; - compl.imag = PyOS_string_to_double(s, (char **)&end, NULL); - if (compl.imag == -1.0 && PyErr_Occurred()) { + imag = PyOS_string_to_double(s, (char **)&end, NULL); + if (imag == -1.0 && PyErr_Occurred()) { return NULL; } - return PyComplex_FromCComplex(compl); + return PyImaginary_FromDouble(imag); } dx = PyOS_string_to_double(s, NULL, NULL); if (dx == -1.0 && PyErr_Occurred()) { diff --git a/Python/ast.c b/Python/ast.c index e01dd0de51e596..a4b5eb801c9033 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -163,6 +163,7 @@ validate_constant(PyObject *value) if (PyLong_CheckExact(value) || PyFloat_CheckExact(value) || PyComplex_CheckExact(value) + || PyImaginary_CheckExact(value) || PyBool_Check(value) || PyUnicode_CheckExact(value) || PyBytes_CheckExact(value)) @@ -420,7 +421,7 @@ ensure_literal_number(expr_ty exp, bool allow_real, bool allow_imaginary) PyObject *value = exp->v.Constant.value; return (allow_real && PyFloat_CheckExact(value)) || (allow_real && PyLong_CheckExact(value)) || - (allow_imaginary && PyComplex_CheckExact(value)); + (allow_imaginary && PyImaginary_CheckExact(value)); } static int @@ -500,7 +501,7 @@ validate_pattern_match_value(expr_ty exp) PyObject *literal = exp->v.Constant.value; if (PyLong_CheckExact(literal) || PyFloat_CheckExact(literal) || PyBytes_CheckExact(literal) || PyComplex_CheckExact(literal) || - PyUnicode_CheckExact(literal)) { + PyImaginary_CheckExact(literal) || PyUnicode_CheckExact(literal)) { return 1; } PyErr_SetString(PyExc_ValueError, diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index c25699978cf651..04abdb2c512f45 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -92,7 +92,7 @@ append_repr(PyUnicodeWriter *writer, PyObject *obj) } if ((PyFloat_CheckExact(obj) && isinf(PyFloat_AS_DOUBLE(obj))) || - PyComplex_CheckExact(obj)) + PyComplex_CheckExact(obj) || PyImaginary_CheckExact(obj)) { _Py_DECLARE_STR(str_replace_inf, "1e309"); // evaluates to inf PyObject *new_repr = PyUnicode_Replace( diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 64249177eec5f2..8a7a42f3be24e5 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2929,6 +2929,11 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start) return PyComplex_FromDoubles(cs_to_double(re_sum), cs_to_double(im_sum)); } + if (PyImaginary_CheckExact(item)) { + double value = PyComplex_ImagAsDouble(item); + im_sum = cs_add(im_sum, value); + continue; + } if (PyComplex_CheckExact(item)) { z = PyComplex_AsCComplex(item); re_sum = cs_add(re_sum, z.real); @@ -3462,6 +3467,7 @@ _PyBuiltin_Init(PyInterpreterState *interp) SETBUILTIN("float", &PyFloat_Type); SETBUILTIN("frozenset", &PyFrozenSet_Type); SETBUILTIN("property", &PyProperty_Type); + SETBUILTIN("imaginary", &PyImaginary_Type); SETBUILTIN("int", &PyLong_Type); SETBUILTIN("list", &PyList_Type); SETBUILTIN("map", &PyMap_Type); diff --git a/Python/marshal.c b/Python/marshal.c index 8b56de6575559c..5fb78293c397a4 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -62,6 +62,8 @@ module marshal #define TYPE_ELLIPSIS '.' #define TYPE_BINARY_FLOAT 'g' // Version 0 uses TYPE_FLOAT instead. #define TYPE_BINARY_COMPLEX 'y' // Version 0 uses TYPE_COMPLEX instead. +#define TYPE_IMAGINARY 'j' +#define TYPE_BINARY_IMAGINARY 'J' #define TYPE_LONG 'l' // See also TYPE_INT. #define TYPE_STRING 's' // Bytes. (Name comes from Python 2.) #define TYPE_TUPLE '(' // See also TYPE_SMALL_TUPLE. @@ -509,6 +511,16 @@ w_complex_object(PyObject *v, char flag, WFILE *p) w_float_str(PyComplex_ImagAsDouble(v), p); } } + else if (PyImaginary_CheckExact(v)) { + if (p->version > 1) { + W_TYPE(TYPE_BINARY_IMAGINARY, p); + w_float_bin(PyComplex_ImagAsDouble(v), p); + } + else { + W_TYPE(TYPE_IMAGINARY, p); + w_float_str(PyComplex_ImagAsDouble(v), p); + } + } else if (PyBytes_CheckExact(v)) { W_TYPE(TYPE_STRING, p); w_pstring(PyBytes_AS_STRING(v), PyBytes_GET_SIZE(v), p); @@ -1249,6 +1261,26 @@ r_object(RFILE *p) break; } + case TYPE_IMAGINARY: + { + double i = r_float_str(p); + if (i == -1.0 && PyErr_Occurred()) + break; + retval = PyImaginary_FromDouble(i); + R_REF(retval); + break; + } + + case TYPE_BINARY_IMAGINARY: + { + double i = r_float_bin(p); + if (i == -1.0 && PyErr_Occurred()) + break; + retval = PyImaginary_FromDouble(i); + R_REF(retval); + break; + } + case TYPE_STRING: { const char *ptr; diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 3c3cb2f9c86f16..574a7285a458d2 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -20,6 +20,7 @@ Objects/classobject.c - PyInstanceMethod_Type - Objects/classobject.c - PyMethod_Type - Objects/codeobject.c - PyCode_Type - Objects/complexobject.c - PyComplex_Type - +Objects/complexobject.c - PyImaginary_Type - Objects/descrobject.c - PyClassMethodDescr_Type - Objects/descrobject.c - PyDictProxy_Type - Objects/descrobject.c - PyGetSetDescr_Type - From ca6ab2292ac8c8f03ce0bacc908e5f2c2ee20e8d Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 20 Oct 2025 13:33:43 +0300 Subject: [PATCH 2/9] Cleanup: don't expose imaginary as built-in --- Doc/library/functions.rst | 17 ++++------------- Doc/reference/lexical_analysis.rst | 3 +-- Lib/ast.py | 4 ++-- Lib/copy.py | 4 ++-- Lib/test/test_capi/test_complex.py | 2 ++ Lib/test/test_complex.py | 2 ++ Lib/test/test_marshal.py | 2 +- Python/bltinmodule.c | 1 - 8 files changed, 14 insertions(+), 21 deletions(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index e6c2f3e4596951..3d61938f52f876 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -33,11 +33,10 @@ are always available. They are listed here in alphabetical order. | | :func:`complex` | | | | **P** | | **V** | | | | | **I** | | :func:`pow` | | :func:`vars` | | | **D** | | :func:`id` | | :func:`print` | | | -| | :func:`delattr` | | :func:`imaginary` | | :func:`property` | | | -| | |func-dict|_ | | :func:`input` | | | | **Z** | -| | :func:`dir` | | :func:`int` | | | | :func:`zip` | -| | :func:`divmod` | | :func:`isinstance` | | | | | -| | | | :func:`issubclass` | | | | **_** | +| | :func:`delattr` | | :func:`input` | | :func:`property` | | **Z** | +| | |func-dict|_ | | :func:`int` | | | | :func:`zip` | +| | :func:`dir` | | :func:`isinstance` | | | | | +| | :func:`divmod` | | :func:`issubclass` | | | | **_** | | | | | :func:`iter` | | | | :func:`__import__` | +-------------------------+-----------------------+-----------------------+-------------------------+ @@ -969,14 +968,6 @@ are always available. They are listed here in alphabetical order. .. audit-event:: builtins.id id id -.. class:: imaginary(x=0.0) - - Return an imaginary number with the value ``float(x)*1j``. If argument is - omitted, returns ``0j``. - - .. versionadded:: next - - .. function:: input() input(prompt, /) diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index a929f51b2af235..3575e9f330e800 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -1363,8 +1363,7 @@ Imaginary literals Python has :ref:`complex number ` objects, but no complex literals. -Instead, *imaginary literals* denote complex numbers without a real part, an instance -of :class:`imaginary`. +Instead, *imaginary literals* denote complex numbers *without* a real part. For example, in math, the complex number 3+4.2\ *i* is written as the real number 3 added to the imaginary number 4.2\ *i*. diff --git a/Lib/ast.py b/Lib/ast.py index b0853d4d615df6..bd8e8080d47dd6 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -89,7 +89,7 @@ def _convert_literal(node): isinstance(node, UnaryOp) and isinstance(node.op, (UAdd, USub)) and isinstance(node.operand, Constant) - and type(operand := node.operand.value) in (int, float, imaginary) + and type(operand := node.operand.value) in (int, float, type(1j)) ): if isinstance(node.op, UAdd): return + operand @@ -101,7 +101,7 @@ def _convert_literal(node): and isinstance(node.left, (Constant, UnaryOp)) and isinstance(node.right, Constant) and type(left := _convert_literal(node.left)) in (int, float) - and type(right := _convert_literal(node.right)) is imaginary + and type(right := _convert_literal(node.right)) is type(1j) ): if isinstance(node.op, Add): return left + right diff --git a/Lib/copy.py b/Lib/copy.py index ea6133336d457d..e8ca9ac98a6863 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -101,7 +101,7 @@ def copy(x): _copy_atomic_types = frozenset({types.NoneType, int, float, bool, complex, str, tuple, - imaginary, bytes, frozenset, type, range, slice, property, + type(1j), bytes, frozenset, type, range, slice, property, types.BuiltinFunctionType, types.EllipsisType, types.NotImplementedType, types.FunctionType, types.CodeType, weakref.ref, super}) @@ -165,7 +165,7 @@ def deepcopy(x, memo=None): _atomic_types = frozenset({types.NoneType, types.EllipsisType, types.NotImplementedType, int, float, bool, complex, bytes, str, types.CodeType, type, range, types.BuiltinFunctionType, types.FunctionType, weakref.ref, - property, imaginary}) + property, type(1j)}) _deepcopy_dispatch = d = {} diff --git a/Lib/test/test_capi/test_complex.py b/Lib/test/test_capi/test_complex.py index caef8bebdf148e..f8584b84fbc6cc 100644 --- a/Lib/test/test_capi/test_complex.py +++ b/Lib/test/test_capi/test_complex.py @@ -24,6 +24,8 @@ class BadComplex3: def __complex__(self): raise RuntimeError +imaginary = type(1j) + class ImaginarySubclass(imaginary): pass diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 7532400714f44c..adbc9e5c709b85 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -39,6 +39,8 @@ def __float__(self): class ComplexSubclass(complex): pass +imaginary = type(1j) + class ImaginarySubclass(imaginary): pass diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index d6a345447b73eb..5d9959b0ad58d1 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -318,7 +318,7 @@ def test_exact_type_match(self): # >>> type(loads(dumps(Int()))) # for typ in (int, float, complex, tuple, list, dict, set, frozenset, - imaginary): + type(1j)): # Note: str subclasses are not tested because they get handled # by marshal's routines for objects supporting the buffer API. subtyp = type('subtyp', (typ,), {}) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 8a7a42f3be24e5..8f5aa19c45ce1d 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -3467,7 +3467,6 @@ _PyBuiltin_Init(PyInterpreterState *interp) SETBUILTIN("float", &PyFloat_Type); SETBUILTIN("frozenset", &PyFrozenSet_Type); SETBUILTIN("property", &PyProperty_Type); - SETBUILTIN("imaginary", &PyImaginary_Type); SETBUILTIN("int", &PyLong_Type); SETBUILTIN("list", &PyList_Type); SETBUILTIN("map", &PyMap_Type); From 66a84dd4ec50cb2d436118578094f2d99d26e4c2 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 21 Oct 2025 09:06:59 +0300 Subject: [PATCH 3/9] XXX C-API --- Doc/c-api/complex.rst | 36 +++++++++++++++++++++++++++++++++ Include/cpython/complexobject.h | 2 ++ 2 files changed, 38 insertions(+) diff --git a/Doc/c-api/complex.rst b/Doc/c-api/complex.rst index d135637a7417ab..573db673041738 100644 --- a/Doc/c-api/complex.rst +++ b/Doc/c-api/complex.rst @@ -120,6 +120,42 @@ Complex Number Objects Use :meth:`~object.__index__` if available. +Imaginary Number Objects +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. index:: pair: object; imaginary number + + +.. c:type:: PyImaginaryObject + + This subtype of :c:type:`PyComplexObject` represents a Python imaginary + number object. + + +.. c:var:: PyTypeObject PyImaginary_Type + + This instance of :c:type:`PyTypeObject` represents purely imaginary numbers, + the Python complex number type *without* real component. + + +.. c:function:: int PyImaginary_Check(PyObject *p) + + Return true if its argument is a :c:type:`PyImaginaryObject` or a subtype of + :c:type:`PyImaginaryObject`. This function always succeeds. + + +.. c:function:: int PyImaginary_CheckExact(PyObject *p) + + Return true if its argument is a :c:type:`PyImaginaryObject`, but not a + subtype of :c:type:`PyImaginaryObject`. This function always succeeds. + + +.. c:function:: PyObject* PyImaginary_FromDouble(double imag) + + Return a new :c:type:`PyImaginaryObject` object with *imag* imaginary + component. Return ``NULL`` with an exception set on error. + + Complex Numbers as C Structures ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Include/cpython/complexobject.h b/Include/cpython/complexobject.h index 58da80140dc4c9..72b611639620c3 100644 --- a/Include/cpython/complexobject.h +++ b/Include/cpython/complexobject.h @@ -29,6 +29,8 @@ typedef struct { Py_complex cval; } PyComplexObject; +#define PyImaginaryObject PyComplexObject + PyAPI_FUNC(PyObject *) PyComplex_FromCComplex(Py_complex); PyAPI_FUNC(Py_complex) PyComplex_AsCComplex(PyObject *op); From ab1ecc3167334ebb61497cf265354cd58ff79d3b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 31 Oct 2025 04:46:08 +0300 Subject: [PATCH 4/9] imaginary: accept imaginary input --- Lib/test/test_complex.py | 2 ++ Objects/complexobject.c | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index adbc9e5c709b85..45614da727cfbe 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -715,6 +715,8 @@ def test_imaginary_constructor(self): self.assertEqual(imaginary(-2), -2j) self.assertEqual(imaginary(1.25), 1.25j) self.assertEqual(imaginary("1.25"), 1.25j) + self.assertEqual(imaginary(1j), 1j) + self.assertEqual(imaginary(-123j), -123j) self.assertEqual(imaginary(WithFloat(42.)), 42j) self.assertRaises(TypeError, imaginary, WithFloat(None)) diff --git a/Objects/complexobject.c b/Objects/complexobject.c index dd381595d698a6..16720f04c66fc0 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -1535,6 +1535,9 @@ imaginary_new_impl(PyTypeObject *type, PyObject *x) if (x == NULL) { x = _PyLong_GetZero(); } + if (PyImaginary_CheckExact(x) && type == &PyImaginary_Type) { + return Py_NewRef(x); + } PyNumberMethods *nbx = Py_TYPE(x)->tp_as_number; From ddc52c1b3ba220cbf13bd95f252554621f757c60 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 1 Nov 2025 01:45:12 +0300 Subject: [PATCH 5/9] Amend ca6ab2292ac: fix pickling --- Lib/copyreg.py | 9 +++++++++ Lib/test/test_pickle.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/copyreg.py b/Lib/copyreg.py index a5e8add4a554d7..c4166ac90a35d0 100644 --- a/Lib/copyreg.py +++ b/Lib/copyreg.py @@ -28,7 +28,16 @@ def constructor(object): def pickle_complex(c): return complex, (c.real, c.imag) +_imaginary = type(1j) +class _imaginary_constructor: + def __new__(cls, imag): + return _imaginary(imag) + +def pickle_imaginary(c): + return _imaginary_constructor, (c.imag,) + pickle(complex, pickle_complex, complex) +pickle(_imaginary, pickle_imaginary) def pickle_union(obj): import typing, operator diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py index e2384b33345a45..7a92432417bbe9 100644 --- a/Lib/test/test_pickle.py +++ b/Lib/test/test_pickle.py @@ -736,12 +736,12 @@ def invoke_pickle(self, *flags): def test_invocation(self): # test 'python -m pickle pickle_file' data = { - 'a': [1, 2.0, 3+4j], + 'a': [1, 2.0, 3+4j, -123j], 'b': ('character string', b'byte string'), 'c': 'string' } expect = ''' - {'a': [1, 2.0, (3+4j)], + {'a': [1, 2.0, (3+4j), -123j], 'b': ('character string', b'byte string'), 'c': 'string'} ''' From e554fa121b190a8bcecc0bc932bf531a6d52c780 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 5 Nov 2025 10:37:50 +0300 Subject: [PATCH 6/9] Increment marshal version --- Include/cpython/marshal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Include/cpython/marshal.h b/Include/cpython/marshal.h index 6c1f7f96b6a2e8..159459fcaec3d9 100644 --- a/Include/cpython/marshal.h +++ b/Include/cpython/marshal.h @@ -6,7 +6,7 @@ PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromString(const char *, Py_ssize_t); PyAPI_FUNC(PyObject *) PyMarshal_WriteObjectToString(PyObject *, int); -#define Py_MARSHAL_VERSION 5 +#define Py_MARSHAL_VERSION 6 PyAPI_FUNC(long) PyMarshal_ReadLongFromFile(FILE *); PyAPI_FUNC(int) PyMarshal_ReadShortFromFile(FILE *); From 4d138ee70452199443c8ef6b5c08b62b45b5955f Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 20 Nov 2025 06:54:52 +0300 Subject: [PATCH 7/9] Revert "Cleanup: don't expose imaginary as built-in" This reverts commit ca6ab2292ac8c8f03ce0bacc908e5f2c2ee20e8d. --- Doc/library/functions.rst | 17 +++++++++++++---- Doc/reference/lexical_analysis.rst | 3 ++- Lib/ast.py | 4 ++-- Lib/copy.py | 4 ++-- Lib/test/test_capi/test_complex.py | 2 -- Lib/test/test_complex.py | 2 -- Lib/test/test_marshal.py | 2 +- Python/bltinmodule.c | 1 + 8 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 17f28ccf801cfb..711af50fcc6d16 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -33,10 +33,11 @@ are always available. They are listed here in alphabetical order. | | :func:`complex` | | | | **P** | | **V** | | | | | **I** | | :func:`pow` | | :func:`vars` | | | **D** | | :func:`id` | | :func:`print` | | | -| | :func:`delattr` | | :func:`input` | | :func:`property` | | **Z** | -| | |func-dict|_ | | :func:`int` | | | | :func:`zip` | -| | :func:`dir` | | :func:`isinstance` | | | | | -| | :func:`divmod` | | :func:`issubclass` | | | | **_** | +| | :func:`delattr` | | :func:`imaginary` | | :func:`property` | | | +| | |func-dict|_ | | :func:`input` | | | | **Z** | +| | :func:`dir` | | :func:`int` | | | | :func:`zip` | +| | :func:`divmod` | | :func:`isinstance` | | | | | +| | | | :func:`issubclass` | | | | **_** | | | | | :func:`iter` | | | | :func:`__import__` | +-------------------------+-----------------------+-----------------------+-------------------------+ @@ -977,6 +978,14 @@ are always available. They are listed here in alphabetical order. .. audit-event:: builtins.id id id +.. class:: imaginary(x=0.0) + + Return an imaginary number with the value ``float(x)*1j``. If argument is + omitted, returns ``0j``. + + .. versionadded:: next + + .. function:: input() input(prompt, /) diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index 3575e9f330e800..a929f51b2af235 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -1363,7 +1363,8 @@ Imaginary literals Python has :ref:`complex number ` objects, but no complex literals. -Instead, *imaginary literals* denote complex numbers *without* a real part. +Instead, *imaginary literals* denote complex numbers without a real part, an instance +of :class:`imaginary`. For example, in math, the complex number 3+4.2\ *i* is written as the real number 3 added to the imaginary number 4.2\ *i*. diff --git a/Lib/ast.py b/Lib/ast.py index 41ee363e6bda54..45181779c576a8 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -90,7 +90,7 @@ def _convert_literal(node): isinstance(node, UnaryOp) and isinstance(node.op, (UAdd, USub)) and isinstance(node.operand, Constant) - and type(operand := node.operand.value) in (int, float, type(1j)) + and type(operand := node.operand.value) in (int, float, imaginary) ): if isinstance(node.op, UAdd): return + operand @@ -102,7 +102,7 @@ def _convert_literal(node): and isinstance(node.left, (Constant, UnaryOp)) and isinstance(node.right, Constant) and type(left := _convert_literal(node.left)) in (int, float) - and type(right := _convert_literal(node.right)) is type(1j) + and type(right := _convert_literal(node.right)) is imaginary ): if isinstance(node.op, Add): return left + right diff --git a/Lib/copy.py b/Lib/copy.py index e8ca9ac98a6863..ea6133336d457d 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -101,7 +101,7 @@ def copy(x): _copy_atomic_types = frozenset({types.NoneType, int, float, bool, complex, str, tuple, - type(1j), bytes, frozenset, type, range, slice, property, + imaginary, bytes, frozenset, type, range, slice, property, types.BuiltinFunctionType, types.EllipsisType, types.NotImplementedType, types.FunctionType, types.CodeType, weakref.ref, super}) @@ -165,7 +165,7 @@ def deepcopy(x, memo=None): _atomic_types = frozenset({types.NoneType, types.EllipsisType, types.NotImplementedType, int, float, bool, complex, bytes, str, types.CodeType, type, range, types.BuiltinFunctionType, types.FunctionType, weakref.ref, - property, type(1j)}) + property, imaginary}) _deepcopy_dispatch = d = {} diff --git a/Lib/test/test_capi/test_complex.py b/Lib/test/test_capi/test_complex.py index f8584b84fbc6cc..caef8bebdf148e 100644 --- a/Lib/test/test_capi/test_complex.py +++ b/Lib/test/test_capi/test_complex.py @@ -24,8 +24,6 @@ class BadComplex3: def __complex__(self): raise RuntimeError -imaginary = type(1j) - class ImaginarySubclass(imaginary): pass diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 45614da727cfbe..a7ac5637a9d328 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -39,8 +39,6 @@ def __float__(self): class ComplexSubclass(complex): pass -imaginary = type(1j) - class ImaginarySubclass(imaginary): pass diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 5d9959b0ad58d1..d6a345447b73eb 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -318,7 +318,7 @@ def test_exact_type_match(self): # >>> type(loads(dumps(Int()))) # for typ in (int, float, complex, tuple, list, dict, set, frozenset, - type(1j)): + imaginary): # Note: str subclasses are not tested because they get handled # by marshal's routines for objects supporting the buffer API. subtyp = type('subtyp', (typ,), {}) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index a063db5311ba5e..73ff2d517eb2b1 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -3486,6 +3486,7 @@ _PyBuiltin_Init(PyInterpreterState *interp) SETBUILTIN("float", &PyFloat_Type); SETBUILTIN("frozenset", &PyFrozenSet_Type); SETBUILTIN("property", &PyProperty_Type); + SETBUILTIN("imaginary", &PyImaginary_Type); SETBUILTIN("int", &PyLong_Type); SETBUILTIN("list", &PyList_Type); SETBUILTIN("map", &PyMap_Type); From ce869947da0c82fca601fffdf9b02b6d52afba28 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 20 Nov 2025 08:06:05 +0300 Subject: [PATCH 8/9] cleanup docs, revert ab1ecc31673 --- Doc/library/functions.rst | 5 +++-- Lib/copyreg.py | 9 --------- Lib/test/test_complex.py | 2 -- Objects/clinic/complexobject.c.h | 4 ++-- Objects/complexobject.c | 7 ++----- 5 files changed, 7 insertions(+), 20 deletions(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 711af50fcc6d16..64d8bedab45fb0 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -980,8 +980,9 @@ are always available. They are listed here in alphabetical order. .. class:: imaginary(x=0.0) - Return an imaginary number with the value ``float(x)*1j``. If argument is - omitted, returns ``0j``. + Create an imaginary number from a number or a string. + + This is equivalent of ``float(x)*1j``. .. versionadded:: next diff --git a/Lib/copyreg.py b/Lib/copyreg.py index c4166ac90a35d0..a5e8add4a554d7 100644 --- a/Lib/copyreg.py +++ b/Lib/copyreg.py @@ -28,16 +28,7 @@ def constructor(object): def pickle_complex(c): return complex, (c.real, c.imag) -_imaginary = type(1j) -class _imaginary_constructor: - def __new__(cls, imag): - return _imaginary(imag) - -def pickle_imaginary(c): - return _imaginary_constructor, (c.imag,) - pickle(complex, pickle_complex, complex) -pickle(_imaginary, pickle_imaginary) def pickle_union(obj): import typing, operator diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index a7ac5637a9d328..7532400714f44c 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -713,8 +713,6 @@ def test_imaginary_constructor(self): self.assertEqual(imaginary(-2), -2j) self.assertEqual(imaginary(1.25), 1.25j) self.assertEqual(imaginary("1.25"), 1.25j) - self.assertEqual(imaginary(1j), 1j) - self.assertEqual(imaginary(-123j), -123j) self.assertEqual(imaginary(WithFloat(42.)), 42j) self.assertRaises(TypeError, imaginary, WithFloat(None)) diff --git a/Objects/clinic/complexobject.c.h b/Objects/clinic/complexobject.c.h index 22b0e27cdc4396..333468a94adb90 100644 --- a/Objects/clinic/complexobject.c.h +++ b/Objects/clinic/complexobject.c.h @@ -190,7 +190,7 @@ PyDoc_STRVAR(imaginary_new__doc__, "imaginary(x=0)\n" "--\n" "\n" -"Create an imaginary number from a real number or string.\n" +"Create an imaginary number from a number or a string.\n" "\n" "This is equivalent of float(x)*1j."); @@ -284,4 +284,4 @@ imaginary___getnewargs__(PyObject *self, PyObject *Py_UNUSED(ignored)) { return imaginary___getnewargs___impl((PyComplexObject *)self); } -/*[clinic end generated code: output=4448bc53f794a795 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c2de307dcaf92b58 input=a9049054013a1b77]*/ diff --git a/Objects/complexobject.c b/Objects/complexobject.c index e1bfdfd8c40044..93941091ecb921 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -1523,21 +1523,18 @@ PyTypeObject PyComplex_Type = { imaginary.__new__ as imaginary_new x: object(c_default="NULL") = 0 -Create an imaginary number from a real number or string. +Create an imaginary number from a number or a string. This is equivalent of float(x)*1j. [clinic start generated code]*/ static PyObject * imaginary_new_impl(PyTypeObject *type, PyObject *x) -/*[clinic end generated code: output=07242ea06eb219f6 input=0ec29c2535687795]*/ +/*[clinic end generated code: output=07242ea06eb219f6 input=b20209245b6b7da8]*/ { if (x == NULL) { x = _PyLong_GetZero(); } - if (PyImaginary_CheckExact(x) && type == &PyImaginary_Type) { - return Py_NewRef(x); - } PyNumberMethods *nbx = Py_TYPE(x)->tp_as_number; From 39f81bf779066fd17423b73f1a489e67e26cb2f6 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 20 Nov 2025 10:18:26 +0300 Subject: [PATCH 9/9] reject complex "literals" as values of Constant nodes --- Doc/library/ast.rst | 2 +- Python/ast.c | 5 ++--- Python/ast_unparse.c | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 2e7d0dbc26e5bc..8cae88fc2d732d 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -268,7 +268,7 @@ Literals A constant value. The ``value`` attribute of the ``Constant`` literal contains the Python object it represents. The values represented can be instances of :class:`str`, - :class:`bytes`, :class:`int`, :class:`float`, :class:`complex`, and :class:`bool`, + :class:`bytes`, :class:`int`, :class:`float`, :class:`imaginary`, and :class:`bool`, and the constants :data:`None` and :data:`Ellipsis`. .. doctest:: diff --git a/Python/ast.c b/Python/ast.c index a4b5eb801c9033..4abd9c11b5681f 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -162,7 +162,6 @@ validate_constant(PyObject *value) if (PyLong_CheckExact(value) || PyFloat_CheckExact(value) - || PyComplex_CheckExact(value) || PyImaginary_CheckExact(value) || PyBool_Check(value) || PyUnicode_CheckExact(value) @@ -500,8 +499,8 @@ validate_pattern_match_value(expr_ty exp) } PyObject *literal = exp->v.Constant.value; if (PyLong_CheckExact(literal) || PyFloat_CheckExact(literal) || - PyBytes_CheckExact(literal) || PyComplex_CheckExact(literal) || - PyImaginary_CheckExact(literal) || PyUnicode_CheckExact(literal)) { + PyBytes_CheckExact(literal) || PyImaginary_CheckExact(literal) || + PyUnicode_CheckExact(literal)) { return 1; } PyErr_SetString(PyExc_ValueError, diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index 04abdb2c512f45..08d94e93703f5f 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -92,7 +92,7 @@ append_repr(PyUnicodeWriter *writer, PyObject *obj) } if ((PyFloat_CheckExact(obj) && isinf(PyFloat_AS_DOUBLE(obj))) || - PyComplex_CheckExact(obj) || PyImaginary_CheckExact(obj)) + PyImaginary_CheckExact(obj)) { _Py_DECLARE_STR(str_replace_inf, "1e309"); // evaluates to inf PyObject *new_repr = PyUnicode_Replace(