From c76ebf25abf227c4a5765aa8e2d8466d86050894 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 22 Jun 2021 17:58:47 +0300 Subject: [PATCH 1/3] gh-121798: Add class method Decimal.from_number() It is an alternate constructor which only accepts a single numeric argument. Unlike to Decimal.from_float() it accepts also Decimal. Unlike to the standard constructor, it does not accept strings and tuples. --- Doc/library/decimal.rst | 17 +++++++ Doc/whatsnew/3.14.rst | 7 +++ Lib/_pydecimal.py | 15 ++++++ Lib/test/test_decimal.py | 24 ++++++++++ ...-07-15-19-25-25.gh-issue-121798.GmuBDu.rst | 2 + Modules/_decimal/_decimal.c | 46 +++++++++++++++++++ Modules/_decimal/docstrings.h | 13 ++++++ 7 files changed, 124 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-07-15-19-25-25.gh-issue-121798.GmuBDu.rst diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index db323802a6f68c..aff293aba14094 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -598,6 +598,23 @@ Decimal objects .. versionadded:: 3.1 + .. classmethod:: from_number(number) + + Alternative constructor that only accepts numbers (instances of + :class:`float`, :class:`int` or :class:`Decimal`), but not strings + or tuples. + + .. doctest:: + + >>> Decimal.from_number(314) + Decimal('314') + >>> Decimal.from_number(0.1) + Decimal('0.1000000000000000055511151231257827021181583404541015625') + >>> Decimal.from_number(Decimal('3.14')) + Decimal('3.14') + + .. versionadded:: 3.14 + .. method:: fma(other, third, context=None) Fused multiply-add. Return self*other+third with no rounding of the diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index da9b45cd8e58b3..85a2b4cab750fb 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -96,6 +96,13 @@ ast (Contributed by Bénédikt Tran in :gh:`121141`.) +decimal +------- + +* Add alternative :class:`~decimal.Decimal` constructor + :meth:`Decimal.from_number() `. + (Contributed by Serhiy Storchaka in :gh:`121798`.) + os -- diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 613123ec7b4329..75c4b92a9ab38e 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -582,6 +582,21 @@ def __new__(cls, value="0", context=None): raise TypeError("Cannot convert %r to Decimal" % value) + @classmethod + def from_number(cls, number): + """Converts a real number to a decimal number, exactly. + + >>> Decimal.from_number(314) # int + Decimal('314') + >>> Decimal.from_number(0.1) # float + Decimal('0.1000000000000000055511151231257827021181583404541015625') + >>> Decimal.from_number(Decimal('3.14')) # another decimal instance + Decimal('3.14') + """ + if isinstance(number, (int, Decimal, float)): + return cls(number) + raise TypeError("Cannot convert %r to Decimal" % number) + @classmethod def from_float(cls, f): """Converts a float to a decimal number, exactly. diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 46755107de0102..383b5a58988cd4 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -812,6 +812,29 @@ def test_explicit_context_create_from_float(self): x = random.expovariate(0.01) * (random.random() * 2.0 - 1.0) self.assertEqual(x, float(nc.create_decimal(x))) # roundtrip + def test_from_number(self, cls=None): + Decimal = self.decimal.Decimal + if cls is None: + cls = Decimal + + def check(arg, expected): + d = cls.from_number(arg) + self.assertIs(type(d), cls) + self.assertEqual(d, expected) + + check(314, Decimal(314)) + check(3.14, Decimal.from_float(3.14)) + check(Decimal('3.14'), Decimal('3.14')) + self.assertRaises(TypeError, cls.from_number, 3+4j) + self.assertRaises(TypeError, cls.from_number, '314') + self.assertRaises(TypeError, cls.from_number, (0, (3, 1, 4), 0)) + self.assertRaises(TypeError, cls.from_number, object()) + + def test_from_number_subclass(self, cls=None): + class DecimalSubclass(self.decimal.Decimal): + pass + self.test_from_number(DecimalSubclass) + def test_unicode_digits(self): Decimal = self.decimal.Decimal @@ -1280,6 +1303,7 @@ def __init__(self, a): a = A.from_float(42) self.assertEqual(self.decimal.Decimal, a.a_type) + @requires_cdecimal class CFormatTest(FormatTest, unittest.TestCase): decimal = C diff --git a/Misc/NEWS.d/next/Library/2024-07-15-19-25-25.gh-issue-121798.GmuBDu.rst b/Misc/NEWS.d/next/Library/2024-07-15-19-25-25.gh-issue-121798.GmuBDu.rst new file mode 100644 index 00000000000000..5706e4bffeb4a1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-15-19-25-25.gh-issue-121798.GmuBDu.rst @@ -0,0 +1,2 @@ +Add alternative :class:`~decimal.Decimal` constructor +:meth:`Decimal.from_number() `. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 94a2cc2c8e5f8a..a89261e5986803 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -2828,6 +2828,51 @@ dec_from_float(PyObject *type, PyObject *pyfloat) return result; } +/* 'v' can have any numeric type accepted by the Decimal constructor. Attempt + an exact conversion. If the result does not meet the restrictions + for an mpd_t, fail with InvalidOperation. */ +static PyObject * +PyDecType_FromNumberExact(PyTypeObject *type, PyObject *v, PyObject *context) +{ + decimal_state *state = get_module_state_by_def(type); + assert(v != NULL); + if (PyDec_Check(state, v)) { + return PyDecType_FromDecimalExact(type, v, context); + } + else if (PyLong_Check(v)) { + return PyDecType_FromLongExact(type, v, context); + } + else if (PyFloat_Check(v)) { + if (dec_addstatus(context, MPD_Float_operation)) { + return NULL; + } + return PyDecType_FromFloatExact(type, v, context); + } + else { + PyErr_Format(PyExc_TypeError, + "conversion from %s to Decimal is not supported", + Py_TYPE(v)->tp_name); + return NULL; + } +} + +/* class method */ +static PyObject * +dec_from_number(PyObject *type, PyObject *number) +{ + PyObject *context; + PyObject *result; + + decimal_state *state = get_module_state_by_def((PyTypeObject *)type); + CURRENT_CONTEXT(state, context); + result = PyDecType_FromNumberExact(state->PyDec_Type, number, context); + if (type != (PyObject *)state->PyDec_Type && result != NULL) { + Py_SETREF(result, PyObject_CallFunctionObjArgs(type, result, NULL)); + } + + return result; +} + /* create_decimal_from_float */ static PyObject * ctx_from_float(PyObject *context, PyObject *v) @@ -5017,6 +5062,7 @@ static PyMethodDef dec_methods [] = /* Miscellaneous */ { "from_float", dec_from_float, METH_O|METH_CLASS, doc_from_float }, + { "from_number", dec_from_number, METH_O|METH_CLASS, doc_from_number }, { "as_tuple", PyDec_AsTuple, METH_NOARGS, doc_as_tuple }, { "as_integer_ratio", dec_as_integer_ratio, METH_NOARGS, doc_as_integer_ratio }, diff --git a/Modules/_decimal/docstrings.h b/Modules/_decimal/docstrings.h index a1823cdd32b74c..b34bff83d3f4e9 100644 --- a/Modules/_decimal/docstrings.h +++ b/Modules/_decimal/docstrings.h @@ -189,6 +189,19 @@ Decimal.from_float(0.1) is not the same as Decimal('0.1').\n\ \n\ \n"); +PyDoc_STRVAR(doc_from_number, +"from_number($type, number, /)\n--\n\n\ +Class method that converts a real number to a decimal number, exactly.\n\ +\n\ + >>> Decimal.from_number(314) # int\n\ + Decimal('314')\n\ + >>> Decimal.from_number(0.1) # float\n\ + Decimal('0.1000000000000000055511151231257827021181583404541015625')\n\ + >>> Decimal.from_number(Decimal('3.14')) # another decimal instance\n\ + Decimal('3.14')\n\ +\n\ +\n"); + PyDoc_STRVAR(doc_fma, "fma($self, /, other, third, context=None)\n--\n\n\ Fused multiply-add. Return self*other+third with no rounding of the\n\ From 9bb70f2932f0e21874d091d2c30cd5df6bebcdba Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 16 Jul 2024 00:12:36 +0300 Subject: [PATCH 2/3] Update Lib/test/test_decimal.py Co-authored-by: Nice Zombies --- Lib/test/test_decimal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 383b5a58988cd4..eecdf503dd775d 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1303,7 +1303,6 @@ def __init__(self, a): a = A.from_float(42) self.assertEqual(self.decimal.Decimal, a.a_type) - @requires_cdecimal class CFormatTest(FormatTest, unittest.TestCase): decimal = C From e38d8f9b8179600cf76993f4c3aaebe2166fe186 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 17 Jul 2024 21:23:46 +0300 Subject: [PATCH 3/3] Update documentation. --- Doc/library/decimal.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index aff293aba14094..d37f0d8291926c 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -600,8 +600,8 @@ Decimal objects .. classmethod:: from_number(number) - Alternative constructor that only accepts numbers (instances of - :class:`float`, :class:`int` or :class:`Decimal`), but not strings + Alternative constructor that only accepts instances of + :class:`float`, :class:`int` or :class:`Decimal`, but not strings or tuples. .. doctest::