Skip to content

Commit ae8cdf1

Browse files
feat(#1458): rename Renderable → SparkAdapter
Renderable conflicts with the broader notion of graphically renderable field types and is too generic for an interface targeted specifically at Spark / Lakehouse Sync. Rename for clarity: - Class: Renderable → SparkAdapter (parallels StorageAdapter) - Method: render_spark → to_spark (matches pandas/Arrow conventions like to_pandas, to_arrow, __dataframe__) - Module: datajoint.rendering → datajoint.spark - Tests: tests/unit/test_rendering.py → tests/unit/test_spark.py - Top-level export: dj.Renderable → dj.SparkAdapter
1 parent 97801f5 commit ae8cdf1

4 files changed

Lines changed: 121 additions & 121 deletions

File tree

src/datajoint/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
"get_codec",
5252
"ObjectRef",
5353
"NpyRef",
54-
# Renderable Codec Protocol
55-
"Renderable",
54+
# SparkAdapter Codec Protocol
55+
"SparkAdapter",
5656
# Storage Adapter API
5757
"StorageAdapter",
5858
"get_storage_adapter",
@@ -87,7 +87,7 @@
8787
from .instance import Instance, _ConfigProxy, _get_singleton_connection, _global_config, _check_thread_safe
8888
from .logging import logger
8989
from .objectref import ObjectRef
90-
from .rendering import Renderable
90+
from .spark import SparkAdapter
9191
from .storage_adapter import StorageAdapter, get_storage_adapter
9292
from .schemas import _Schema, VirtualModule, list_schemas, virtual_schema
9393
from .autopopulate import AutoPopulate
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
2-
Renderable Codec Protocol.
2+
SparkAdapter Codec Protocol.
33
4-
Opt-in contract for codecs that can render their decoded values to
5-
Spark-native types — primitives, lists, dicts, and nested combinations.
4+
Opt-in contract for codecs that adapt their decoded values to Spark-native
5+
types — primitives, lists, dicts, and nested combinations.
66
77
Codecs implement this method when they want their column eligible for
88
downstream typed-query systems (Spark SQL, Delta Sharing, BI tools).
@@ -16,9 +16,9 @@
1616
- Generic codecs need no acknowledgement (no ``NotImplementedError`` stubs).
1717
- Existing plugin codecs continue to work unchanged.
1818
- Codec authors opt in by adding the method on their own release cadence.
19-
- Consumers detect support structurally via ``isinstance(codec, Renderable)``.
19+
- Consumers detect support structurally via ``isinstance(codec, SparkAdapter)``.
2020
21-
See ``datajoint-docs/src/reference/specs/renderable.md`` for the
21+
See ``datajoint-docs/src/reference/specs/spark-adapter.md`` for the
2222
normative specification (signature, return-value shape constraints,
2323
worked codec examples).
2424
"""
@@ -29,17 +29,17 @@
2929

3030

3131
@runtime_checkable
32-
class Renderable(Protocol):
32+
class SparkAdapter(Protocol):
3333
"""
34-
A codec that can render its decoded values to Spark-native types.
34+
A codec that adapts its decoded values to Spark-native types.
3535
3636
Opt-in. Codecs implementing this method declare that their decoded
3737
values can be expressed as primitives, lists, or dicts of the same —
3838
i.e., shapes that map cleanly to Spark's ``StructType`` /
3939
``ArrayType`` / ``MapType``.
4040
4141
Consumers (e.g., a Databricks silver-layer publish pipeline) check
42-
``isinstance(codec, Renderable)`` per column to determine eligibility.
42+
``isinstance(codec, SparkAdapter)`` per column to determine eligibility.
4343
4444
Allowed return-value shapes:
4545
@@ -62,18 +62,18 @@ class FloatArrayCodec(dj.Codec):
6262
def encode(self, value, *, key=None, store_name=None): ...
6363
def decode(self, stored, *, key=None) -> np.ndarray: ...
6464
65-
def render_spark(self, decoded: np.ndarray, *, key=None) -> list[float]:
65+
def to_spark(self, decoded: np.ndarray, *, key=None) -> list[float]:
6666
return decoded.tolist() # → Spark ARRAY<DOUBLE>
6767
6868
Eligibility check::
6969
70-
from datajoint import Renderable
71-
isinstance(FloatArrayCodec(), Renderable) # True
70+
from datajoint import SparkAdapter
71+
isinstance(FloatArrayCodec(), SparkAdapter) # True
7272
"""
7373

74-
def render_spark(self, decoded: Any, *, key: dict | None = None) -> Any:
74+
def to_spark(self, decoded: Any, *, key: dict | None = None) -> Any:
7575
"""
76-
Render a decoded codec value to a Spark-native shape.
76+
Adapt a decoded codec value to a Spark-native shape.
7777
7878
Parameters
7979
----------

tests/unit/test_rendering.py

Lines changed: 0 additions & 105 deletions
This file was deleted.

tests/unit/test_spark.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""
2+
Unit tests for the SparkAdapter Codec Protocol (#1458).
3+
4+
The Protocol is a structural-typing contract — codecs opt in by
5+
implementing ``to_spark`` and consumers detect support via
6+
``isinstance(codec, SparkAdapter)``. These tests cover the detection
7+
behavior, not specific rendering implementations (which live downstream).
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import datajoint as dj
13+
from datajoint.spark import SparkAdapter
14+
15+
16+
class _SparkAdapterCodec:
17+
"""A minimal codec-like object that opts into the protocol."""
18+
19+
name = "fake_spark_adapter"
20+
21+
def to_spark(self, decoded, *, key=None):
22+
return list(decoded) if hasattr(decoded, "__iter__") else decoded
23+
24+
25+
class _OpaqueCodec:
26+
"""A minimal codec-like object that does NOT opt into the protocol."""
27+
28+
name = "fake_opaque"
29+
30+
def encode(self, value, *, key=None, store_name=None):
31+
return bytes(value)
32+
33+
def decode(self, stored, *, key=None):
34+
return stored
35+
36+
37+
def test_protocol_detects_opt_in():
38+
"""A class implementing ``to_spark`` is detected as a SparkAdapter."""
39+
assert isinstance(_SparkAdapterCodec(), SparkAdapter)
40+
41+
42+
def test_protocol_rejects_non_opt_in():
43+
"""A class without ``to_spark`` is not detected as a SparkAdapter."""
44+
assert not isinstance(_OpaqueCodec(), SparkAdapter)
45+
46+
47+
def test_protocol_exported_at_top_level():
48+
"""``dj.SparkAdapter`` is accessible at the top level."""
49+
assert dj.SparkAdapter is SparkAdapter
50+
51+
52+
def test_protocol_is_runtime_checkable():
53+
"""The Protocol is decorated with @runtime_checkable (the test fixtures
54+
above rely on this)."""
55+
# Direct assertion: classes lacking runtime_checkable would raise TypeError
56+
# on isinstance(). The previous tests would error rather than fail.
57+
try:
58+
isinstance(object(), SparkAdapter)
59+
except TypeError:
60+
raise AssertionError("SparkAdapter must be @runtime_checkable")
61+
62+
63+
def test_blob_codec_is_not_spark_adapter():
64+
"""The built-in <blob@> codec is intentionally non-adapting per the spec."""
65+
from datajoint.builtin_codecs.blob import BlobCodec
66+
67+
assert not isinstance(BlobCodec(), SparkAdapter)
68+
69+
70+
def test_hash_codec_is_not_spark_adapter():
71+
"""The built-in <hash@> codec is intentionally non-adapting per the spec."""
72+
from datajoint.builtin_codecs.hash import HashCodec
73+
74+
assert not isinstance(HashCodec(), SparkAdapter)
75+
76+
77+
def test_to_spark_invocation_passes_through():
78+
"""A codec implementing the method can be invoked and returns its result."""
79+
codec = _SparkAdapterCodec()
80+
assert codec.to_spark([1, 2, 3]) == [1, 2, 3]
81+
assert codec.to_spark(42) == 42
82+
83+
84+
def test_to_spark_method_accepts_key_kwarg():
85+
"""The method signature accepts the optional ``key`` keyword argument."""
86+
codec = _SparkAdapterCodec()
87+
# Should not raise
88+
codec.to_spark([1, 2, 3], key={"some_pk": 1})
89+
90+
91+
def test_subclass_adding_to_spark_becomes_adapter():
92+
"""A subclass of an opaque codec that adds the method becomes a SparkAdapter."""
93+
94+
class _OpaqueBase:
95+
name = "base"
96+
97+
def encode(self, value, *, key=None, store_name=None):
98+
return b""
99+
100+
class _TypedSubclass(_OpaqueBase):
101+
def to_spark(self, decoded, *, key=None):
102+
return decoded
103+
104+
assert not isinstance(_OpaqueBase(), SparkAdapter)
105+
assert isinstance(_TypedSubclass(), SparkAdapter)

0 commit comments

Comments
 (0)