You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by ke...@apache.org on 2024/11/03 23:50:42 UTC

(tinkerpop) 15/17: update bigdecimal in statics to be consistent with server definition (#2875)

This is an automated email from the ASF dual-hosted git repository.

kenhuuu pushed a commit to branch master-http-final
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit c3e29aad63dc98989b00bec993a0aeb5e24e561e
Author: Yang Xia <55...@users.noreply.github.com>
AuthorDate: Thu Oct 31 17:49:06 2024 -0700

    update bigdecimal in statics to be consistent with server definition (#2875)
---
 .../python/gremlin_python/process/traversal.py     |  6 +---
 .../src/main/python/gremlin_python/statics.py      | 27 +++++++++++++++--
 .../gremlin_python/structure/io/graphsonV4.py      | 27 ++++-------------
 .../tests/structure/io/test_functionalityio.py     |  1 -
 .../tests/structure/io/test_graphbinaryv4model.py  |  8 ++---
 .../python/tests/structure/io/test_graphsonV4.py   | 34 ++++------------------
 .../tests/structure/io/test_graphsonv4model.py     | 14 ++++-----
 .../src/main/python/tests/test_statics.py          | 18 ++++++++++++
 8 files changed, 63 insertions(+), 72 deletions(-)

diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index 8eac3e8349..bc37dc0506 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
@@ -841,11 +841,7 @@ class GremlinLang(object):
             else:
                 return f'{arg}D'
         if isinstance(arg, BigDecimal):
-            # TODO double check implementation when revisiting types definition.
-            if arg.scale > 0:
-                return f'{arg.unscaled_value:.{arg.scale}f}M'
-            else:
-                return f'{arg.unscaled_value}M'
+            return f'{arg.value}M'
 
         if isinstance(arg, datetime):
             return f'datetime("{arg.isoformat()}")'
diff --git a/gremlin-python/src/main/python/gremlin_python/statics.py b/gremlin-python/src/main/python/gremlin_python/statics.py
index ac05fe2448..b5a83fbc32 100644
--- a/gremlin-python/src/main/python/gremlin_python/statics.py
+++ b/gremlin-python/src/main/python/gremlin_python/statics.py
@@ -16,7 +16,7 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-
+import decimal
 from types import FunctionType
 from aenum import Enum
 
@@ -74,12 +74,35 @@ class GremlinType(object):
         self.gremlin_type = gremlin_type
 
 
-# TODO scale and unscaled value implementations are not following definition, need to revisit
 class BigDecimal(object):
+    """
+    Provides a way to represent a BigDecimal for Gremlin.
+    """
     def __init__(self, scale, unscaled_value):
         self.scale = scale
         self.unscaled_value = unscaled_value
 
+    @property
+    def value(self):
+        self._as_decimal = decimal.Decimal(self.unscaled_value)
+        precision = len(self._as_decimal.as_tuple().digits)
+        with decimal.localcontext(decimal.Context(prec=precision)):
+            return self._as_decimal.scaleb(-self.scale)
+
+"""
+Create a BigDecimal from a number that can be converted to a Decimal. Note precision may be lost during the conversion.
+"""
+def to_bigdecimal(value):
+    try:
+        decimal_value = value if isinstance(value, decimal.Decimal) else decimal.Decimal(str(value))
+        scale = -decimal_value.as_tuple().exponent
+        unscaled_value = int("".join(map(str, decimal_value.as_tuple().digits)))
+    except TypeError:
+        raise ValueError("BigDecimal does not support NaN, Infinity or -Infinity")
+    except Exception as err:
+        raise ValueError(f'Encountered error: {err}. Value must be able to convert to a Decimal.')
+    return BigDecimal(scale, unscaled_value if decimal_value >= 0 else -unscaled_value)
+
 
 staticMethods = {}
 staticEnums = {}
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV4.py b/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV4.py
index 7a63c8bee7..d20d3b4e31 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV4.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/io/graphsonV4.py
@@ -29,7 +29,7 @@ from aenum import Enum
 from isodate import parse_duration, duration_isoformat
 
 from gremlin_python import statics
-from gremlin_python.statics import FloatType, BigDecimal, ShortType, IntType, LongType, TypeType, DictType, ListType, SetType, SingleByte, SingleChar
+from gremlin_python.statics import FloatType, BigDecimal, ShortType, IntType, LongType, TypeType, DictType, ListType, SetType, SingleByte, SingleChar, to_bigdecimal
 from gremlin_python.process.traversal import Direction, P, TextP, Traversal, Traverser, TraversalStrategy, T
 from gremlin_python.structure.graph import Edge, Property, Vertex, VertexProperty, Path
 from gremlin_python.structure.io.util import HashableDict, SymbolUtil
@@ -455,35 +455,18 @@ class FloatIO(_NumberIO):
         return cls.python_type(v)
 
 
-class BigDecimalIO(_NumberIO):
-    python_type = Decimal
+class BigDecimalIO(_GraphSONTypeIO):
+    python_type = BigDecimal
     graphson_type = "g:BigDecimal"
     graphson_base_type = "BigDecimal"
 
     @classmethod
     def dictify(cls, n, writer):
-        if isinstance(n, bool):  # because isinstance(False, int) and isinstance(True, int)
-            return n
-        elif math.isnan(n):
-            return GraphSONUtil.typed_value(cls.graphson_base_type, "NaN", "g")
-        elif math.isinf(n) and n > 0:
-            return GraphSONUtil.typed_value(cls.graphson_base_type, "Infinity", "g")
-        elif math.isinf(n) and n < 0:
-            return GraphSONUtil.typed_value(cls.graphson_base_type, "-Infinity", "g")
-        else:
-            return GraphSONUtil.typed_value(cls.graphson_base_type, str(n), "g")
+        return GraphSONUtil.typed_value(cls.graphson_base_type, str(n.value), "g")
 
     @classmethod
     def objectify(cls, v, _):
-        if isinstance(v, str):
-            if v == 'NaN':
-                return Decimal('nan')
-            elif v == "Infinity":
-                return Decimal('inf')
-            elif v == "-Infinity":
-                return Decimal('-inf')
-
-        return Decimal(v)
+        return to_bigdecimal(v)
 
 
 class DoubleIO(FloatIO):
diff --git a/gremlin-python/src/main/python/tests/structure/io/test_functionalityio.py b/gremlin-python/src/main/python/tests/structure/io/test_functionalityio.py
index be74a28710..8dfe37adab 100644
--- a/gremlin-python/src/main/python/tests/structure/io/test_functionalityio.py
+++ b/gremlin-python/src/main/python/tests/structure/io/test_functionalityio.py
@@ -157,7 +157,6 @@ def test_bigint_negative(remote_connection):
         g.V(vid).drop().iterate()
 
 
-@pytest.mark.skip(reason="BigDecimal implementation needs revisiting")
 def test_bigdecimal(remote_connection):
     if not isinstance(remote_connection._client.response_serializer(), GraphBinarySerializersV4):
         return
diff --git a/gremlin-python/src/main/python/tests/structure/io/test_graphbinaryv4model.py b/gremlin-python/src/main/python/tests/structure/io/test_graphbinaryv4model.py
index ab18c91443..63298d4bcd 100644
--- a/gremlin-python/src/main/python/tests/structure/io/test_graphbinaryv4model.py
+++ b/gremlin-python/src/main/python/tests/structure/io/test_graphbinaryv4model.py
@@ -48,22 +48,20 @@ def read_file_by_name(resource_name):
 
 def test_pos_bigdecimal():
     def decimal_cmp(x, y):
-        if x.scale == y.scale and x.unscaled_value == y.unscaled_value:
+        if x.scale == y.scale and x.unscaled_value == y.unscaled_value and x.value == y.value:
             return True
         else:
             return False
 
-    # gremlin-python adds an extra 0 byte to the value.
     run_writeread("pos-bigdecimal", decimal_cmp)
 
 def test_neg_bigdecimal():
     def decimal_cmp(x, y):
-        if x.scale == y.scale and x.unscaled_value == y.unscaled_value:
+        if x.scale == y.scale and x.unscaled_value == y.unscaled_value and x.value == y.value:
             return True
         else:
             return False
-    
-    # gremlin-python adds an extra 0 byte to the value.
+
     run_writeread("neg-bigdecimal", decimal_cmp)
 
 def test_pos_biginteger():
diff --git a/gremlin-python/src/main/python/tests/structure/io/test_graphsonV4.py b/gremlin-python/src/main/python/tests/structure/io/test_graphsonV4.py
index 942fb0f0ea..44b2fa7187 100644
--- a/gremlin-python/src/main/python/tests/structure/io/test_graphsonV4.py
+++ b/gremlin-python/src/main/python/tests/structure/io/test_graphsonV4.py
@@ -152,36 +152,15 @@ class TestGraphSONReader:
             "@type": "g:BigDecimal",
             "@value": 31.2
         }))
-        assert isinstance(x, Decimal)
-        assert Decimal(31.2) == x
+        assert isinstance(x, BigDecimal)
+        assert Decimal('31.2') == x.value
         ##
         x = self.graphson_reader.read_object(json.dumps({
             "@type": "g:BigDecimal",
             "@value": 123456789987654321123456789987654321
         }))
-        assert isinstance(x, Decimal)
-        assert Decimal('123456789987654321123456789987654321') == x
-        ##
-        x = self.graphson_reader.read_object(json.dumps({
-            "@type": "g:BigDecimal",
-            "@value": "NaN"
-        }))
-        assert isinstance(x, Decimal)
-        assert math.isnan(x)
-        ##
-        x = self.graphson_reader.read_object(json.dumps({
-            "@type": "g:BigDecimal",
-            "@value": "Infinity"
-        }))
-        assert isinstance(x, Decimal)
-        assert math.isinf(x) and x > 0
-        ##
-        x = self.graphson_reader.read_object(json.dumps({
-            "@type": "g:BigDecimal",
-            "@value": "-Infinity"
-        }))
-        assert isinstance(x, Decimal)
-        assert math.isinf(x) and x < 0
+        assert isinstance(x, BigDecimal)
+        assert to_bigdecimal(123456789987654321123456789987654321).value == x.value
         ##
         x = self.graphson_reader.read_object(json.dumps({
             "@type": "g:BigInteger",
@@ -371,10 +350,7 @@ class TestGraphSONWriter:
         assert {"@type": "g:Double", "@value": "NaN"} == json.loads(self.graphson_writer.write_object(float('nan')))
         assert {"@type": "g:Double", "@value": "Infinity"} == json.loads(self.graphson_writer.write_object(float('inf')))
         assert {"@type": "g:Double", "@value": "-Infinity"} == json.loads(self.graphson_writer.write_object(float('-inf')))
-        assert {"@type": "g:BigDecimal", "@value": "123456789987654321123456789987654321"} == json.loads(self.graphson_writer.write_object(Decimal('123456789987654321123456789987654321')))
-        assert {"@type": "g:BigDecimal", "@value": "NaN"} == json.loads(self.graphson_writer.write_object(Decimal('nan')))
-        assert {"@type": "g:BigDecimal", "@value": "Infinity"} == json.loads(self.graphson_writer.write_object(Decimal('inf')))
-        assert {"@type": "g:BigDecimal", "@value": "-Infinity"} == json.loads(self.graphson_writer.write_object(Decimal('-inf')))
+        assert {"@type": "g:BigDecimal", "@value": "123.456789987654321123456789987654321"} == json.loads(self.graphson_writer.write_object(to_bigdecimal("123.456789987654321123456789987654321")))
         assert {"@type": "g:BigInteger", "@value": "123456789987654321123456789987654321"} == json.loads(self.graphson_writer.write_object(long(123456789987654321123456789987654321)))
         assert {"@type": "g:BigInteger", "@value": "123456789987654321123456789987654321"} == json.loads(self.graphson_writer.write_object(123456789987654321123456789987654321))
         assert """true""" == self.graphson_writer.write_object(True)
diff --git a/gremlin-python/src/main/python/tests/structure/io/test_graphsonv4model.py b/gremlin-python/src/main/python/tests/structure/io/test_graphsonv4model.py
index 683ff030a5..6f6886e860 100644
--- a/gremlin-python/src/main/python/tests/structure/io/test_graphsonv4model.py
+++ b/gremlin-python/src/main/python/tests/structure/io/test_graphsonv4model.py
@@ -47,27 +47,25 @@ def read_file_by_name(resource_name):
     with open(full_name, 'r') as resource_file:
         return json.dumps(json.load(resource_file), separators=(',', ':'))
 
-@pytest.mark.skip(reason="enable after big decimal implementation/serialization is unified in python (")
 def test_pos_bigdecimal():
     def decimal_cmp(x, y):
-        if x.scale == y.scale and x.unscaled_value == y.unscaled_value:
+        # python json library can only read a BigDecimal into float during deser, precision will be lost with GraphSON
+        # so compare float only
+        if float(x.value) == float(y.value):
             return True
         else:
             return False
 
-    # gremlin-python adds an extra 0 byte to the value.
     run_writeread("pos-bigdecimal", decimal_cmp)
 
-@pytest.mark.skip(reason="enable after big decimal implementation/serialization is unified in python")
 def test_neg_bigdecimal():
     def decimal_cmp(x, y):
-        if x.scale == y.scale and x.unscaled_value == y.unscaled_value:
+        if float(x.value) == float(y.value):
             return True
         else:
             return False
-    
-    # gremlin-python adds an extra 0 byte to the value.
-    run_writeread("neg-bigdecimal", decimal_cmp())
+
+    run_writeread("neg-bigdecimal", decimal_cmp)
 
 def test_pos_biginteger():
     # gremlin-python adds an extra 0 byte to the value.
diff --git a/gremlin-python/src/main/python/tests/test_statics.py b/gremlin-python/src/main/python/tests/test_statics.py
index 2c3f864754..e22718e8e1 100644
--- a/gremlin-python/src/main/python/tests/test_statics.py
+++ b/gremlin-python/src/main/python/tests/test_statics.py
@@ -19,6 +19,8 @@
 
 __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)'
 
+from decimal import Decimal
+
 from gremlin_python import statics
 from gremlin_python.process.traversal import Cardinality
 from gremlin_python.process.traversal import P
@@ -63,3 +65,19 @@ class TestStatics(object):
             raise Exception("SingleChar should throw a value error if input is not a single character string")
         except ValueError:
             pass
+
+    def test_bigdecimal(self):
+        assert statics.to_bigdecimal(1.23456).value == statics.BigDecimal(5,123456).value
+        assert statics.to_bigdecimal(-1.23456).value == statics.BigDecimal(5,-123456).value
+        # make sure the precision isn't changed globally
+        assert Decimal("123456789").scaleb(-5) == Decimal('1234.56789')
+        try:
+            statics.to_bigdecimal('NaN')
+            raise Exception("to_bigdecimal should throw a value error with NaN, Infinity or -Infinity")
+        except ValueError:
+            pass
+        try:
+            statics.to_bigdecimal('abc')
+            raise Exception("to_bigdecimal should throw a value error if input is not a convertable to Decimal")
+        except ValueError:
+            pass