Skip to content

Commit 2be5f11

Browse files
committed
Introduce AttributeType system to replace AttributeAdapter
This commit introduces a modern, extensible custom type system for DataJoint: **New Features:** - AttributeType base class with encode()/decode() methods - Global type registry with @register_type decorator - Entry point discovery for third-party type packages (datajoint.types) - Type chaining: dtype can reference another custom type - Automatic validation via validate() method before encoding - resolve_dtype() for resolving chained types **API Changes:** - New: dj.AttributeType, dj.register_type, dj.list_types - AttributeAdapter is now deprecated (backward-compatible wrapper) - Feature flag DJ_SUPPORT_ADAPTED_TYPES is no longer required **Entry Point Specification:** Third-party packages can declare types in pyproject.toml: [project.entry-points."datajoint.types"] zarr_array = "dj_zarr:ZarrArrayType" **Migration Path:** Old AttributeAdapter subclasses continue to work but emit DeprecationWarning. Migrate to AttributeType with encode/decode.
1 parent 93ce01e commit 2be5f11

File tree

10 files changed

+993
-50
lines changed

10 files changed

+993
-50
lines changed

src/datajoint/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
"kill",
4646
"MatCell",
4747
"MatStruct",
48-
"AttributeAdapter",
48+
"AttributeType",
49+
"register_type",
50+
"list_types",
51+
"AttributeAdapter", # Deprecated, use AttributeType
4952
"errors",
5053
"DataJointError",
5154
"key",
@@ -57,6 +60,7 @@
5760
from . import errors
5861
from .admin import kill
5962
from .attribute_adapter import AttributeAdapter
63+
from .attribute_type import AttributeType, list_types, register_type
6064
from .blob import MatCell, MatStruct
6165
from .cli import cli
6266
from .connection import Connection, conn

src/datajoint/attribute_adapter.py

Lines changed: 159 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,191 @@
1+
"""
2+
Legacy attribute adapter module.
3+
4+
This module provides backward compatibility for the deprecated AttributeAdapter class.
5+
New code should use :class:`datajoint.AttributeType` instead.
6+
7+
.. deprecated:: 0.15
8+
Use :class:`datajoint.AttributeType` with ``encode``/``decode`` methods.
9+
"""
10+
111
import re
12+
import warnings
13+
from typing import Any
214

3-
from .errors import DataJointError, _support_adapted_types
15+
from .attribute_type import AttributeType, get_type, is_type_registered
16+
from .errors import DataJointError
417

518

6-
class AttributeAdapter:
19+
class AttributeAdapter(AttributeType):
720
"""
8-
Base class for adapter objects for user-defined attribute types.
21+
Legacy base class for attribute adapters.
22+
23+
.. deprecated:: 0.15
24+
Use :class:`datajoint.AttributeType` with ``encode``/``decode`` methods instead.
25+
26+
This class provides backward compatibility for existing adapters that use
27+
the ``attribute_type``, ``put()``, and ``get()`` API.
28+
29+
Migration guide::
30+
31+
# Old style (deprecated):
32+
class GraphAdapter(dj.AttributeAdapter):
33+
attribute_type = "longblob"
34+
35+
def put(self, graph):
36+
return list(graph.edges)
37+
38+
def get(self, edges):
39+
return nx.Graph(edges)
40+
41+
# New style (recommended):
42+
@dj.register_type
43+
class GraphType(dj.AttributeType):
44+
type_name = "graph"
45+
dtype = "longblob"
46+
47+
def encode(self, graph, *, key=None):
48+
return list(graph.edges)
49+
50+
def decode(self, edges, *, key=None):
51+
return nx.Graph(edges)
952
"""
1053

54+
# Subclasses can set this as a class attribute instead of property
55+
attribute_type: str = None # type: ignore
56+
57+
def __init__(self):
58+
# Emit deprecation warning on instantiation
59+
warnings.warn(
60+
f"{self.__class__.__name__} uses the deprecated AttributeAdapter API. "
61+
"Migrate to AttributeType with encode/decode methods.",
62+
DeprecationWarning,
63+
stacklevel=2,
64+
)
65+
1166
@property
12-
def attribute_type(self):
67+
def type_name(self) -> str:
1368
"""
14-
:return: a supported DataJoint attribute type to use; e.g. "longblob", "blob@store"
69+
Infer type name from class name for legacy adapters.
70+
71+
Legacy adapters were identified by their variable name in the context dict,
72+
not by a property. For backward compatibility, we use the lowercase class name.
1573
"""
16-
raise NotImplementedError("Undefined attribute adapter")
74+
# Check if a _type_name was explicitly set (for context-based lookup)
75+
if hasattr(self, "_type_name"):
76+
return self._type_name
77+
# Fall back to class name
78+
return self.__class__.__name__.lower()
1779

18-
def get(self, value):
80+
@property
81+
def dtype(self) -> str:
82+
"""Map legacy attribute_type to new dtype property."""
83+
attr_type = self.attribute_type
84+
if attr_type is None:
85+
raise NotImplementedError(
86+
f"{self.__class__.__name__} must define 'attribute_type' "
87+
"(or migrate to AttributeType with 'dtype')"
88+
)
89+
return attr_type
90+
91+
def encode(self, value: Any, *, key: dict | None = None) -> Any:
92+
"""Delegate to legacy put() method."""
93+
return self.put(value)
94+
95+
def decode(self, stored: Any, *, key: dict | None = None) -> Any:
96+
"""Delegate to legacy get() method."""
97+
return self.get(stored)
98+
99+
def put(self, obj: Any) -> Any:
19100
"""
20-
convert value retrieved from the the attribute in a table into the adapted type
101+
Convert an object of the adapted type into a storable value.
102+
103+
.. deprecated:: 0.15
104+
Override ``encode()`` instead.
21105
22-
:param value: value from the database
106+
Args:
107+
obj: An object of the adapted type.
23108
24-
:return: object of the adapted type
109+
Returns:
110+
Value to store in the database.
25111
"""
26-
raise NotImplementedError("Undefined attribute adapter")
112+
raise NotImplementedError(
113+
f"{self.__class__.__name__} must implement put() or migrate to encode()"
114+
)
27115

28-
def put(self, obj):
116+
def get(self, value: Any) -> Any:
29117
"""
30-
convert an object of the adapted type into a value that DataJoint can store in a table attribute
118+
Convert a value from the database into the adapted type.
119+
120+
.. deprecated:: 0.15
121+
Override ``decode()`` instead.
122+
123+
Args:
124+
value: Value from the database.
31125
32-
:param obj: an object of the adapted type
33-
:return: value to store in the database
126+
Returns:
127+
Object of the adapted type.
34128
"""
35-
raise NotImplementedError("Undefined attribute adapter")
129+
raise NotImplementedError(
130+
f"{self.__class__.__name__} must implement get() or migrate to decode()"
131+
)
36132

37133

38-
def get_adapter(context, adapter_name):
134+
def get_adapter(context: dict | None, adapter_name: str) -> AttributeType:
39135
"""
40-
Extract the AttributeAdapter object by its name from the context and validate.
136+
Get an attribute type/adapter by name.
137+
138+
This function provides backward compatibility by checking both:
139+
1. The global type registry (new system)
140+
2. The schema context dict (legacy system)
141+
142+
Args:
143+
context: Schema context dictionary (for legacy adapters).
144+
adapter_name: The adapter/type name, with or without angle brackets.
145+
146+
Returns:
147+
The AttributeType instance.
148+
149+
Raises:
150+
DataJointError: If the adapter is not found or invalid.
41151
"""
42-
if not _support_adapted_types():
43-
raise DataJointError("Support for Adapted Attribute types is disabled.")
44152
adapter_name = adapter_name.lstrip("<").rstrip(">")
153+
154+
# First, check the global type registry (new system)
155+
if is_type_registered(adapter_name):
156+
return get_type(adapter_name)
157+
158+
# Fall back to context-based lookup (legacy system)
159+
if context is None:
160+
raise DataJointError(
161+
f"Attribute type <{adapter_name}> is not registered. "
162+
"Use @dj.register_type to register custom types."
163+
)
164+
45165
try:
46166
adapter = context[adapter_name]
47167
except KeyError:
48-
raise DataJointError("Attribute adapter '{adapter_name}' is not defined.".format(adapter_name=adapter_name))
49-
if not isinstance(adapter, AttributeAdapter):
50168
raise DataJointError(
51-
"Attribute adapter '{adapter_name}' must be an instance of datajoint.AttributeAdapter".format(
52-
adapter_name=adapter_name
53-
)
169+
f"Attribute type <{adapter_name}> is not defined. "
170+
"Register it with @dj.register_type or include it in the schema context."
54171
)
55-
if not isinstance(adapter.attribute_type, str) or not re.match(r"^\w", adapter.attribute_type):
172+
173+
# Validate it's an AttributeType (or legacy AttributeAdapter)
174+
if not isinstance(adapter, AttributeType):
56175
raise DataJointError(
57-
"Invalid attribute type {type} in attribute adapter '{adapter_name}'".format(
58-
type=adapter.attribute_type, adapter_name=adapter_name
59-
)
176+
f"Attribute adapter '{adapter_name}' must be an instance of "
177+
"datajoint.AttributeType (or legacy datajoint.AttributeAdapter)"
60178
)
179+
180+
# For legacy adapters from context, store the name they were looked up by
181+
if isinstance(adapter, AttributeAdapter):
182+
adapter._type_name = adapter_name
183+
184+
# Validate the dtype/attribute_type
185+
dtype = adapter.dtype
186+
if not isinstance(dtype, str) or not re.match(r"^\w", dtype):
187+
raise DataJointError(
188+
f"Invalid dtype '{dtype}' in attribute type <{adapter_name}>"
189+
)
190+
61191
return adapter

0 commit comments

Comments
 (0)