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

Cole-Greer pushed a commit to branch simplePDT
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit bab3d4a26aaf762c6fe1b66a0ef84c92be29f1fe
Author: Cole Greer <[email protected]>
AuthorDate: Wed Jun 24 14:26:32 2026 -0700

    Add PrimitivePDT support to gremlin-python (first GLV)
    
    Implements PrimitivePDT in the Python GLV, mirroring the composite support.
    
    - structure/graph.py: PrimitiveProviderDefinedType(name, value) and registry
      support for primitive adapters (register + hydrate_primitive with graceful
      degradation); reuses the existing pdt_registry threading.
    - structure/io/graphbinaryV4.py: DataType.primitive_pdt=0xf1 and
      PrimitiveProviderDefinedTypeIO (writes/reads two fully-qualified Strings);
      reader hydration dispatch for PrimitiveProviderDefinedType, including
      primitive-nested-in-composite.
    - driver/serializer.py: primitive registry threaded through the same
      pdt_registry path as composite.
    
    GraphSON read support is intentionally omitted: the gremlin-python driver is
    GraphBinary-only for V4 (no GraphSON V4 deserializer exists), so there is no
    g:PrimitivePdt read path to add. Clients send PrimitivePDT as the 
gremlin-lang
    PDT("name","value") literal and receive it via GraphBinary.
    
    Tests: 40 passing (GraphBinary round-trip incl. opaque-value fidelity,
    registry hydration, primitive-nested-in-composite), 3 pre-existing
    entry_points skips.
    
    tinkerpop-2gy.8
    
    Assisted-by: Kiro:claude-opus-4.8
---
 .../python/gremlin_python/driver/serializer.py     |   2 +
 .../main/python/gremlin_python/structure/graph.py  |  74 +++++++++
 .../gremlin_python/structure/io/graphbinaryV4.py   |  32 +++-
 .../structure/io/test_provider_defined_type.py     | 179 +++++++++++++++++++++
 4 files changed, 285 insertions(+), 2 deletions(-)

diff --git a/gremlin-python/src/main/python/gremlin_python/driver/serializer.py 
b/gremlin-python/src/main/python/gremlin_python/driver/serializer.py
index c333a4cc54..47182b04da 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/serializer.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/serializer.py
@@ -51,6 +51,8 @@ class GraphBinarySerializersV4(object):
         else:
             
self._graphbinary_reader.pdt_registry._adapters_by_name.update(pdt_registry._adapters_by_name)
             
self._graphbinary_reader.pdt_registry._adapters_by_class.update(pdt_registry._adapters_by_class)
+            
self._graphbinary_reader.pdt_registry._primitive_adapters_by_name.update(pdt_registry._primitive_adapters_by_name)
+            
self._graphbinary_reader.pdt_registry._primitive_adapters_by_class.update(pdt_registry._primitive_adapters_by_class)
 
     @property
     def version(self):
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/graph.py 
b/gremlin-python/src/main/python/gremlin_python/structure/graph.py
index 619eb64899..5d66e12bac 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/graph.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/graph.py
@@ -173,10 +173,41 @@ class ProviderDefinedType(object):
         return f"pdt[{self._name}]{self._fields}"
 
 
+class PrimitiveProviderDefinedType(object):
+    """An immutable primitive provider-defined type consisting of a name and 
an opaque string value."""
+
+    def __init__(self, name, value):
+        if not name:
+            raise ValueError("name cannot be null or empty")
+        if value is None:
+            raise ValueError("value cannot be null")
+        self._name = name
+        self._value = value
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def value(self):
+        return self._value
+
+    def __eq__(self, other):
+        return isinstance(other, PrimitiveProviderDefinedType) and self._name 
== other._name and self._value == other._value
+
+    def __hash__(self):
+        return hash((self._name, self._value))
+
+    def __repr__(self):
+        return f"pdt[{self._name}]({self._value})"
+
+
 class ProviderDefinedTypeRegistry(object):
     def __init__(self):
         self._adapters_by_name = {}
         self._adapters_by_class = {}
+        self._primitive_adapters_by_name = {}
+        self._primitive_adapters_by_class = {}
 
     def register(self, type_name, deserialize_fn, serialize_fn=None, 
target_class=None):
         self._adapters_by_name[type_name] = {
@@ -190,6 +221,26 @@ class ProviderDefinedTypeRegistry(object):
                 'serialize': serialize_fn,
             }
 
+    def register_primitive(self, type_name, from_value, to_value=None, 
target_class=None):
+        """Register a primitive PDT adapter.
+
+        Args:
+            type_name: The PDT type name string.
+            from_value: Callable(str) -> object for deserialization.
+            to_value: Callable(object) -> str for serialization (optional).
+            target_class: The Python class this adapter produces (optional).
+        """
+        self._primitive_adapters_by_name[type_name] = {
+            'from_value': from_value,
+            'to_value': to_value,
+            'target_class': target_class
+        }
+        if target_class is not None:
+            self._primitive_adapters_by_class[target_class] = {
+                'type_name': type_name,
+                'to_value': to_value,
+            }
+
     @classmethod
     def create(cls):
         """Create a registry populated by entry_points discovery.
@@ -234,6 +285,11 @@ class ProviderDefinedTypeRegistry(object):
                 if h is not v:
                     changed = True
                 hydrated_fields[k] = h
+            elif isinstance(v, PrimitiveProviderDefinedType):
+                h = self.hydrate_primitive(v)
+                if h is not v:
+                    changed = True
+                hydrated_fields[k] = h
             else:
                 hydrated_fields[k] = v
 
@@ -247,10 +303,28 @@ class ProviderDefinedTypeRegistry(object):
             logging.getLogger(__name__).warning(f"PDT hydration failed for 
'{pdt.name}': {e}")
             return pdt
 
+    def hydrate_primitive(self, pdt):
+        """Attempt to hydrate a PrimitiveProviderDefinedType. Returns typed 
object or raw PDT."""
+        if not isinstance(pdt, PrimitiveProviderDefinedType):
+            return pdt
+        adapter = self._primitive_adapters_by_name.get(pdt.name)
+        if adapter is None:
+            return pdt
+        try:
+            return adapter['from_value'](pdt.value)
+        except Exception as e:
+            import logging
+            logging.getLogger(__name__).warning(f"Primitive PDT hydration 
failed for '{pdt.name}': {e}")
+            return pdt
+
     def get_adapter_by_class(self, cls):
         """Return (type_name, serialize_fn) tuple for the given class, or 
None."""
         return self._adapters_by_class.get(cls)
 
+    def get_primitive_adapter_by_class(self, cls):
+        """Return adapter dict for the given class, or None."""
+        return self._primitive_adapters_by_class.get(cls)
+
 
 # Module-level registry of @provider_defined decorated classes keyed by PDT 
name.
 _pdt_decorated_types = {}
diff --git 
a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py 
b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py
index c6098619ce..12c0a0f46e 100644
--- 
a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py
+++ 
b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py
@@ -31,7 +31,7 @@ from gremlin_python.process.traversal import Direction, T, 
Merge, GType
 from gremlin_python.statics import FloatType, BigDecimal, ShortType, IntType, 
LongType, BigIntType, \
     DictType, SetType, SingleByte, SingleChar
 from gremlin_python.structure.graph import Graph, Edge, Property, Vertex, 
VertexProperty, Path, ProviderDefinedType, \
-    _pdt_decorated_types
+    PrimitiveProviderDefinedType, _pdt_decorated_types
 from gremlin_python.structure.io.util import HashableDict, SymbolUtil, Marker
 
 log = logging.getLogger(__name__)
@@ -75,6 +75,7 @@ class DataType(Enum):
     char = 0x80
     duration = 0x81
     composite_pdt = 0xf0
+    primitive_pdt = 0xf1
     marker = 0xfd
 
 
@@ -168,6 +169,11 @@ class GraphBinaryReader(object):
             result = self.deserializers[DataType(bt)].objectify(buff, self, 
nullable)
         else:
             result = self.deserializers[data_type].objectify(buff, self, 
nullable)
+        if self.pdt_registry is not None and isinstance(result, 
PrimitiveProviderDefinedType):
+            hydrated = self.pdt_registry.hydrate_primitive(result)
+            if not isinstance(hydrated, PrimitiveProviderDefinedType):
+                return hydrated
+            result = hydrated
         if self.pdt_registry is not None and isinstance(result, 
ProviderDefinedType):
             hydrated = self.pdt_registry.hydrate(result)
             if not isinstance(hydrated, ProviderDefinedType):
@@ -969,4 +975,26 @@ class ProviderDefinedTypeIO(_GraphBinaryTypeIO):
     def _read_pdt(cls, b, r):
         name = r.read_object(b)
         fields = r.read_object(b)
-        return ProviderDefinedType(name, fields)
\ No newline at end of file
+        return ProviderDefinedType(name, fields)
+
+
+class PrimitiveProviderDefinedTypeIO(_GraphBinaryTypeIO):
+    python_type = PrimitiveProviderDefinedType
+    graphbinary_type = DataType.primitive_pdt
+
+    @classmethod
+    def dictify(cls, obj, writer, to_extend, as_value=False, nullable=True):
+        cls.prefix_bytes(cls.graphbinary_type, as_value, nullable, to_extend)
+        StringIO.dictify(obj.name, writer, to_extend)
+        StringIO.dictify(obj.value, writer, to_extend)
+        return to_extend
+
+    @classmethod
+    def objectify(cls, buff, reader, nullable=True):
+        return cls.is_null(buff, reader, cls._read_primitive_pdt, nullable)
+
+    @classmethod
+    def _read_primitive_pdt(cls, b, r):
+        name = r.read_object(b)
+        value = r.read_object(b)
+        return PrimitiveProviderDefinedType(name, value)
\ No newline at end of file
diff --git 
a/gremlin-python/src/main/python/tests/unit/structure/io/test_provider_defined_type.py
 
b/gremlin-python/src/main/python/tests/unit/structure/io/test_provider_defined_type.py
index aac685982c..147e4b670e 100644
--- 
a/gremlin-python/src/main/python/tests/unit/structure/io/test_provider_defined_type.py
+++ 
b/gremlin-python/src/main/python/tests/unit/structure/io/test_provider_defined_type.py
@@ -20,6 +20,7 @@ under the License.
 import pytest
 
 from gremlin_python.structure.graph import ProviderDefinedType, 
ProviderDefinedTypeRegistry, provider_defined
+from gremlin_python.structure.graph import PrimitiveProviderDefinedType
 from gremlin_python.structure.io.graphbinaryV4 import GraphBinaryWriter, 
GraphBinaryReader
 
 
@@ -242,3 +243,181 @@ class TestPdtRegistryWiring(object):
         with patch.object(Client, '_fill_pool'):
             drc = DriverRemoteConnection("ws://localhost:8182/gremlin", "g", 
pdt_registry=registry)
             assert 
drc._client._response_serializer._graphbinary_reader.pdt_registry is registry
+
+
+class TestPrimitiveProviderDefinedType(object):
+
+    def test_empty_name_rejected(self):
+        with pytest.raises(ValueError):
+            PrimitiveProviderDefinedType("", "123")
+
+    def test_none_name_rejected(self):
+        with pytest.raises(ValueError):
+            PrimitiveProviderDefinedType(None, "123")
+
+    def test_none_value_rejected(self):
+        with pytest.raises(ValueError):
+            PrimitiveProviderDefinedType("Uint32", None)
+
+    def test_equality(self):
+        a = PrimitiveProviderDefinedType("Uint32", "42")
+        b = PrimitiveProviderDefinedType("Uint32", "42")
+        assert a == b
+        assert hash(a) == hash(b)
+
+    def test_inequality(self):
+        a = PrimitiveProviderDefinedType("Uint32", "42")
+        b = PrimitiveProviderDefinedType("Uint32", "43")
+        assert a != b
+
+    def test_repr(self):
+        pdt = PrimitiveProviderDefinedType("Uint32", "42")
+        assert "Uint32" in repr(pdt)
+        assert "42" in repr(pdt)
+
+
+class TestPrimitiveProviderDefinedTypeGraphBinary(object):
+    graphbinary_writer = GraphBinaryWriter()
+    graphbinary_reader = GraphBinaryReader()
+
+    def test_round_trip_simple(self):
+        pdt = PrimitiveProviderDefinedType("Uint32", "42")
+        ba = self.graphbinary_writer.write_object(pdt)
+        result = self.graphbinary_reader.read_object(ba)
+        assert isinstance(result, PrimitiveProviderDefinedType)
+        assert result == pdt
+
+    def test_round_trip_leading_zeros(self):
+        """Opaque value: leading zeros must be preserved."""
+        pdt = PrimitiveProviderDefinedType("Uint32", "007")
+        ba = self.graphbinary_writer.write_object(pdt)
+        result = self.graphbinary_reader.read_object(ba)
+        assert result.value == "007"
+
+    def test_round_trip_large_number(self):
+        """Opaque value: large numbers preserved as string."""
+        pdt = PrimitiveProviderDefinedType("BigNum", 
"99999999999999999999999999999")
+        ba = self.graphbinary_writer.write_object(pdt)
+        result = self.graphbinary_reader.read_object(ba)
+        assert result.value == "99999999999999999999999999999"
+
+    def test_round_trip_non_numeric(self):
+        """Opaque value: non-numeric strings work."""
+        pdt = PrimitiveProviderDefinedType("TinkerId", "abc-def-123")
+        ba = self.graphbinary_writer.write_object(pdt)
+        result = self.graphbinary_reader.read_object(ba)
+        assert result.value == "abc-def-123"
+
+    def test_round_trip_empty_value(self):
+        """Edge case: empty string value."""
+        pdt = PrimitiveProviderDefinedType("Empty", "")
+        ba = self.graphbinary_writer.write_object(pdt)
+        result = self.graphbinary_reader.read_object(ba)
+        assert result.value == ""
+
+
+class TestPrimitiveRegistryHydration(object):
+
+    def test_hydrate_simple(self):
+        registry = ProviderDefinedTypeRegistry()
+        registry.register_primitive("Uint32", lambda v: int(v))
+        pdt = PrimitiveProviderDefinedType("Uint32", "42")
+        result = registry.hydrate_primitive(pdt)
+        assert result == 42
+
+    def test_hydrate_no_adapter_returns_raw(self):
+        registry = ProviderDefinedTypeRegistry()
+        pdt = PrimitiveProviderDefinedType("Unknown", "hello")
+        result = registry.hydrate_primitive(pdt)
+        assert result is pdt
+
+    def test_hydrate_adapter_throws_falls_back(self):
+        registry = ProviderDefinedTypeRegistry()
+        registry.register_primitive("Bad", lambda v: 1 / 0)
+        pdt = PrimitiveProviderDefinedType("Bad", "x")
+        result = registry.hydrate_primitive(pdt)
+        assert result is pdt
+
+    def test_reader_auto_hydrates_primitive(self):
+        registry = ProviderDefinedTypeRegistry()
+        registry.register_primitive("Uint32", lambda v: int(v))
+        writer = GraphBinaryWriter()
+        reader = GraphBinaryReader(pdt_registry=registry)
+
+        pdt = PrimitiveProviderDefinedType("Uint32", "42")
+        result = reader.read_object(writer.write_object(pdt))
+        assert result == 42
+
+    def test_reader_no_registry_returns_raw(self):
+        writer = GraphBinaryWriter()
+        reader = GraphBinaryReader()
+
+        pdt = PrimitiveProviderDefinedType("Uint32", "42")
+        result = reader.read_object(writer.write_object(pdt))
+        assert isinstance(result, PrimitiveProviderDefinedType)
+        assert result == pdt
+
+
+class TestPrimitiveNestedInComposite(object):
+
+    def test_primitive_nested_in_composite_hydrates(self):
+        """A PrimitiveProviderDefinedType nested as a field value in a 
composite PDT is hydrated."""
+        registry = ProviderDefinedTypeRegistry()
+        registry.register_primitive("Uint32", lambda v: int(v))
+        registry.register("com.example.Wrapper", lambda fields: {"id": 
fields["id"], "count": fields["count"]})
+
+        inner = PrimitiveProviderDefinedType("Uint32", "99")
+        outer = ProviderDefinedType("com.example.Wrapper", {"id": "abc", 
"count": inner})
+        result = registry.hydrate(outer)
+        assert result == {"id": "abc", "count": 99}
+
+    def test_primitive_nested_in_unregistered_composite_hydrates(self):
+        """Primitive nested inside an unregistered composite still hydrates."""
+        registry = ProviderDefinedTypeRegistry()
+        registry.register_primitive("Uint32", lambda v: int(v))
+
+        inner = PrimitiveProviderDefinedType("Uint32", "7")
+        outer = ProviderDefinedType("com.example.Unregistered", {"val": inner})
+        result = registry.hydrate(outer)
+        assert isinstance(result, ProviderDefinedType)
+        assert result.fields["val"] == 7
+
+    def test_graphbinary_primitive_nested_in_composite(self):
+        """Round-trip a composite PDT containing a primitive PDT field via 
GraphBinary."""
+        registry = ProviderDefinedTypeRegistry()
+        registry.register_primitive("Uint32", lambda v: int(v))
+        registry.register("com.example.Outer",
+                          lambda fields: {"name": fields["name"], "count": 
fields["count"]})
+        writer = GraphBinaryWriter()
+        reader = GraphBinaryReader(pdt_registry=registry)
+
+        inner = PrimitiveProviderDefinedType("Uint32", "5")
+        outer = ProviderDefinedType("com.example.Outer", {"name": "test", 
"count": inner})
+        ba = writer.write_object(outer)
+        result = reader.read_object(ba)
+        assert result == {"name": "test", "count": 5}
+
+
+class TestPrimitiveRegistryEntryPoints(object):
+
+    def test_entry_points_can_register_primitives(self):
+        """Verifies that the entry_points 'tinkerpop.pdt' mechanism works for 
primitives."""
+        from unittest.mock import patch, MagicMock
+
+        def register_primitives(registry):
+            registry.register_primitive("Uint32", lambda v: int(v))
+
+        mock_ep = MagicMock()
+        mock_ep.name = "mock_primitive"
+        mock_ep.load.return_value = register_primitives
+
+        with patch("importlib.metadata.entry_points") as mock_entry_points:
+            import sys
+            if sys.version_info >= (3, 10):
+                mock_entry_points.return_value = [mock_ep]
+            else:
+                mock_entry_points.return_value = {'tinkerpop.pdt': [mock_ep]}
+
+            registry = ProviderDefinedTypeRegistry.create()
+            pdt = PrimitiveProviderDefinedType("Uint32", "123")
+            assert registry.hydrate_primitive(pdt) == 123

Reply via email to