Skip to content

Commit 979f45b

Browse files
Implement Phase 1: Core type system with store parameter support
Phase 1.1 - Core type mappings already complete in declare.py Phase 1.2 - Enhanced AttributeType with store parameter support: - Added parse_type_spec() to parse "<type@store>" into (type_name, store_name) - Updated get_type() to handle parameterized types - Updated is_type_registered() to ignore store parameters - Updated resolve_dtype() to propagate store through type chains - Returns (final_dtype, type_chain, store_name) tuple - Store from outer type overrides inner type's store Phase 1.3 - Updated heading and declaration parsing: - Updated get_adapter() to return (adapter, store_name) tuple - Updated substitute_special_type() to capture store from ADAPTED types - Store parameter is now properly passed through type resolution Co-authored-by: dimitri-yatsenko <dimitri@datajoint.com>
1 parent 5c1e854 commit 979f45b

File tree

3 files changed

+91
-34
lines changed

3 files changed

+91
-34
lines changed

src/datajoint/attribute_adapter.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import warnings
1313
from typing import Any
1414

15-
from .attribute_type import AttributeType, get_type, is_type_registered
15+
from .attribute_type import AttributeType, get_type, is_type_registered, parse_type_spec
1616
from .errors import DataJointError
1717

1818
# Pattern to detect blob types for internal pack/unpack
@@ -154,7 +154,7 @@ def get(self, value: Any) -> Any:
154154
raise NotImplementedError(f"{self.__class__.__name__} must implement get() or migrate to decode()")
155155

156156

157-
def get_adapter(context: dict | None, adapter_name: str) -> AttributeType:
157+
def get_adapter(context: dict | None, adapter_name: str) -> tuple[AttributeType, str | None]:
158158
"""
159159
Get an attribute type/adapter by name.
160160
@@ -165,47 +165,49 @@ def get_adapter(context: dict | None, adapter_name: str) -> AttributeType:
165165
Args:
166166
context: Schema context dictionary (for legacy adapters).
167167
adapter_name: The adapter/type name, with or without angle brackets.
168+
May include store parameter (e.g., "<xblob@cold>").
168169
169170
Returns:
170-
The AttributeType instance.
171+
Tuple of (AttributeType instance, store_name or None).
171172
172173
Raises:
173174
DataJointError: If the adapter is not found or invalid.
174175
"""
175-
adapter_name = adapter_name.lstrip("<").rstrip(">")
176+
# Parse type name and optional store parameter
177+
type_name, store_name = parse_type_spec(adapter_name)
176178

177179
# First, check the global type registry (new system)
178-
if is_type_registered(adapter_name):
179-
return get_type(adapter_name)
180+
if is_type_registered(type_name):
181+
return get_type(type_name), store_name
180182

181183
# Fall back to context-based lookup (legacy system)
182184
if context is None:
183185
raise DataJointError(
184-
f"Attribute type <{adapter_name}> is not registered. " "Use @dj.register_type to register custom types."
186+
f"Attribute type <{type_name}> is not registered. " "Use @dj.register_type to register custom types."
185187
)
186188

187189
try:
188-
adapter = context[adapter_name]
190+
adapter = context[type_name]
189191
except KeyError:
190192
raise DataJointError(
191-
f"Attribute type <{adapter_name}> is not defined. "
193+
f"Attribute type <{type_name}> is not defined. "
192194
"Register it with @dj.register_type or include it in the schema context."
193195
)
194196

195197
# Validate it's an AttributeType (or legacy AttributeAdapter)
196198
if not isinstance(adapter, AttributeType):
197199
raise DataJointError(
198-
f"Attribute adapter '{adapter_name}' must be an instance of "
200+
f"Attribute adapter '{type_name}' must be an instance of "
199201
"datajoint.AttributeType (or legacy datajoint.AttributeAdapter)"
200202
)
201203

202204
# For legacy adapters from context, store the name they were looked up by
203205
if isinstance(adapter, AttributeAdapter):
204-
adapter._type_name = adapter_name
206+
adapter._type_name = type_name
205207

206208
# Validate the dtype/attribute_type
207209
dtype = adapter.dtype
208210
if not isinstance(dtype, str) or not re.match(r"^\w", dtype):
209-
raise DataJointError(f"Invalid dtype '{dtype}' in attribute type <{adapter_name}>")
211+
raise DataJointError(f"Invalid dtype '{dtype}' in attribute type <{type_name}>")
210212

211-
return adapter
213+
return adapter, store_name

src/datajoint/attribute_type.py

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,32 @@ class GraphType(dj.AttributeType):
242242
return cls
243243

244244

245+
def parse_type_spec(spec: str) -> tuple[str, str | None]:
246+
"""
247+
Parse a type specification into type name and optional store parameter.
248+
249+
Handles formats like:
250+
- "<xblob>" -> ("xblob", None)
251+
- "<xblob@cold>" -> ("xblob", "cold")
252+
- "xblob@cold" -> ("xblob", "cold")
253+
- "xblob" -> ("xblob", None)
254+
255+
Args:
256+
spec: Type specification string, with or without angle brackets.
257+
258+
Returns:
259+
Tuple of (type_name, store_name). store_name is None if not specified.
260+
"""
261+
# Strip angle brackets
262+
spec = spec.strip("<>").strip()
263+
264+
if "@" in spec:
265+
type_name, store_name = spec.split("@", 1)
266+
return type_name.strip(), store_name.strip()
267+
268+
return spec, None
269+
270+
245271
def unregister_type(name: str) -> None:
246272
"""
247273
Remove a type from the registry.
@@ -269,27 +295,30 @@ def get_type(name: str) -> AttributeType:
269295
270296
Args:
271297
name: The type name, with or without angle brackets.
298+
Store parameters (e.g., "<xblob@cold>") are stripped.
272299
273300
Returns:
274301
The registered AttributeType instance.
275302
276303
Raises:
277304
DataJointError: If the type is not found.
278305
"""
279-
name = name.strip("<>")
306+
# Strip angle brackets and store parameter
307+
type_name, _ = parse_type_spec(name)
280308

281309
# Check explicit registry first
282-
if name in _type_registry:
283-
return _type_registry[name]
310+
if type_name in _type_registry:
311+
return _type_registry[type_name]
284312

285313
# Lazy-load entry points
286314
_load_entry_points()
287315

288-
if name in _type_registry:
289-
return _type_registry[name]
316+
if type_name in _type_registry:
317+
return _type_registry[type_name]
290318

291319
raise DataJointError(
292-
f"Unknown attribute type: <{name}>. " f"Ensure the type is registered via @dj.register_type or installed as a package."
320+
f"Unknown attribute type: <{type_name}>. "
321+
f"Ensure the type is registered via @dj.register_type or installed as a package."
293322
)
294323

295324

@@ -309,16 +338,16 @@ def is_type_registered(name: str) -> bool:
309338
Check if a type name is registered.
310339
311340
Args:
312-
name: The type name to check.
341+
name: The type name to check (store parameters are ignored).
313342
314343
Returns:
315344
True if the type is registered.
316345
"""
317-
name = name.strip("<>")
318-
if name in _type_registry:
346+
type_name, _ = parse_type_spec(name)
347+
if type_name in _type_registry:
319348
return True
320349
_load_entry_points()
321-
return name in _type_registry
350+
return type_name in _type_registry
322351

323352

324353
def _load_entry_points() -> None:
@@ -368,23 +397,37 @@ def _load_entry_points() -> None:
368397
logger.warning(f"Failed to load attribute type '{ep.name}' from {ep.value}: {e}")
369398

370399

371-
def resolve_dtype(dtype: str, seen: set[str] | None = None) -> tuple[str, list[AttributeType]]:
400+
def resolve_dtype(
401+
dtype: str, seen: set[str] | None = None, store_name: str | None = None
402+
) -> tuple[str, list[AttributeType], str | None]:
372403
"""
373404
Resolve a dtype string, following type chains.
374405
375406
If dtype references another custom type (e.g., "<other_type>"), recursively
376-
resolves to find the ultimate storage type.
407+
resolves to find the ultimate storage type. Store parameters are propagated
408+
through the chain.
377409
378410
Args:
379-
dtype: The dtype string to resolve.
411+
dtype: The dtype string to resolve (e.g., "<xblob>", "<xblob@cold>", "longblob").
380412
seen: Set of already-seen type names (for cycle detection).
413+
store_name: Store name from outer type specification (propagated inward).
381414
382415
Returns:
383-
Tuple of (final_storage_type, list_of_types_in_chain).
416+
Tuple of (final_storage_type, list_of_types_in_chain, resolved_store_name).
384417
The chain is ordered from outermost to innermost type.
385418
386419
Raises:
387420
DataJointError: If a circular type reference is detected.
421+
422+
Examples:
423+
>>> resolve_dtype("<xblob>")
424+
("json", [XBlobType, ContentType], None)
425+
426+
>>> resolve_dtype("<xblob@cold>")
427+
("json", [XBlobType, ContentType], "cold")
428+
429+
>>> resolve_dtype("longblob")
430+
("longblob", [], None)
388431
"""
389432
if seen is None:
390433
seen = set()
@@ -393,7 +436,10 @@ def resolve_dtype(dtype: str, seen: set[str] | None = None) -> tuple[str, list[A
393436

394437
# Check if dtype is a custom type reference
395438
if dtype.startswith("<") and dtype.endswith(">"):
396-
type_name = dtype[1:-1]
439+
type_name, dtype_store = parse_type_spec(dtype)
440+
441+
# Store from this level overrides inherited store
442+
effective_store = dtype_store if dtype_store is not None else store_name
397443

398444
if type_name in seen:
399445
raise DataJointError(f"Circular type reference detected: <{type_name}>")
@@ -402,13 +448,19 @@ def resolve_dtype(dtype: str, seen: set[str] | None = None) -> tuple[str, list[A
402448
attr_type = get_type(type_name)
403449
chain.append(attr_type)
404450

405-
# Recursively resolve the inner dtype
406-
inner_dtype, inner_chain = resolve_dtype(attr_type.dtype, seen)
451+
# Recursively resolve the inner dtype, propagating store
452+
inner_dtype, inner_chain, resolved_store = resolve_dtype(attr_type.dtype, seen, effective_store)
407453
chain.extend(inner_chain)
408-
return inner_dtype, chain
454+
return inner_dtype, chain, resolved_store
455+
456+
# Not a custom type - check if it has a store suffix (e.g., "blob@store")
457+
if "@" in dtype:
458+
base_type, dtype_store = dtype.split("@", 1)
459+
effective_store = dtype_store if dtype_store else store_name
460+
return base_type, chain, effective_store
409461

410-
# Not a custom type - return as-is
411-
return dtype, chain
462+
# Plain type - return as-is with propagated store
463+
return dtype, chain, store_name
412464

413465

414466
# =============================================================================

src/datajoint/declare.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,10 @@ def substitute_special_type(match, category, foreign_key_sql, context):
489489
"ON UPDATE RESTRICT ON DELETE RESTRICT".format(external_table_root=EXTERNAL_TABLE_ROOT, **match)
490490
)
491491
elif category == "ADAPTED":
492-
attr_type = get_adapter(context, match["type"])
492+
attr_type, store_name = get_adapter(context, match["type"])
493+
# Store the store parameter if present
494+
if store_name is not None:
495+
match["store"] = store_name
493496
match["type"] = attr_type.dtype
494497
category = match_type(match["type"])
495498
if category in SPECIAL_TYPES:

0 commit comments

Comments
 (0)