Skip to content

Commit c9c6c5b

Browse files
feat: Add backward-compatible fetch() with deprecation warning
Restore fetch() method for easier migration from DataJoint 0.14. The method emits a DeprecationWarning and maps to 2.0 methods: - fetch() → to_arrays() - fetch(as_dict=True) → to_dicts() - fetch('col1', 'col2') → proj(...).to_dicts() - fetch('col1', 'col2', as_dict=False) → to_arrays('col1', 'col2') - fetch(format='frame') → to_pandas() Supports order_by, limit, offset, squeeze parameters. Updates migration guide link to new location. Bump version to 2.0.0a27. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8c85a9b commit c9c6c5b

File tree

4 files changed

+186
-21
lines changed

4 files changed

+186
-21
lines changed

src/datajoint/expression.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -580,29 +580,78 @@ def aggr(self, group, *attributes, exclude_nonmatching=False, **named_attributes
580580
aggregate = aggr # alias for aggr
581581

582582
# ---------- Fetch operators --------------------
583-
@property
584-
def fetch(self):
583+
def fetch(
584+
self,
585+
*attrs,
586+
offset=None,
587+
limit=None,
588+
order_by=None,
589+
format=None,
590+
as_dict=None,
591+
squeeze=False,
592+
):
593+
"""
594+
Fetch data from the table (backward-compatible with DataJoint 0.14).
595+
596+
.. deprecated:: 2.0
597+
Use the new explicit output methods instead:
598+
- ``to_dicts()`` for list of dictionaries
599+
- ``to_pandas()`` for pandas DataFrame
600+
- ``to_arrays()`` for numpy structured array
601+
- ``to_arrays('a', 'b')`` for tuple of arrays
602+
- ``keys()`` for primary keys
603+
604+
Parameters
605+
----------
606+
*attrs : str
607+
Attributes to fetch. If empty, fetches all.
608+
offset : int, optional
609+
Number of tuples to skip.
610+
limit : int, optional
611+
Maximum number of tuples to return.
612+
order_by : str or list, optional
613+
Attribute(s) for ordering results.
614+
format : str, optional
615+
Output format: 'array' or 'frame' (pandas DataFrame).
616+
as_dict : bool, optional
617+
Return as list of dicts instead of structured array.
618+
squeeze : bool, optional
619+
Remove extra dimensions from arrays. Default False.
620+
621+
Returns
622+
-------
623+
np.recarray, list[dict], or pd.DataFrame
624+
Query results in requested format.
585625
"""
586-
The fetch() method has been removed in DataJoint 2.0.
626+
import warnings
587627

588-
Use the new explicit output methods instead:
589-
- table.to_dicts() # list of dictionaries
590-
- table.to_pandas() # pandas DataFrame
591-
- table.to_arrays() # numpy structured array
592-
- table.to_arrays('a', 'b') # tuple of numpy arrays
593-
- table.keys() # primary keys as list[dict]
594-
- table.to_polars() # polars DataFrame (requires pip install datajoint[polars])
595-
- table.to_arrow() # PyArrow Table (requires pip install datajoint[arrow])
628+
warnings.warn(
629+
"fetch() is deprecated in DataJoint 2.0. " "Use to_dicts(), to_pandas(), to_arrays(), or keys() instead.",
630+
DeprecationWarning,
631+
stacklevel=2,
632+
)
596633

597-
For single-row fetch, use fetch1() which is unchanged.
634+
# Handle format='frame' -> to_pandas()
635+
if format == "frame":
636+
if attrs or as_dict is not None:
637+
raise DataJointError("format='frame' cannot be combined with attrs or as_dict")
638+
return self.to_pandas(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)
598639

599-
See migration guide: https://docs.datajoint.com/how-to/migrate-from-0x/
600-
"""
601-
raise AttributeError(
602-
"fetch() has been removed in DataJoint 2.0. "
603-
"Use to_dicts(), to_pandas(), to_arrays(), or keys() instead. "
604-
"See table.fetch.__doc__ for details."
605-
)
640+
# Handle specific attributes requested
641+
if attrs:
642+
if as_dict or as_dict is None:
643+
# fetch('col1', 'col2', as_dict=True) or fetch('col1', 'col2')
644+
return self.proj(*attrs).to_dicts(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)
645+
else:
646+
# fetch('col1', 'col2', as_dict=False) -> tuple of arrays
647+
return self.to_arrays(*attrs, order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)
648+
649+
# Handle as_dict=True -> to_dicts()
650+
if as_dict:
651+
return self.to_dicts(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)
652+
653+
# Default: return structured array (legacy behavior)
654+
return self.to_arrays(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)
606655

607656
def fetch1(self, *attrs, squeeze=False):
608657
"""

src/datajoint/heading.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ def _init_from_database(self) -> None:
467467
if original_type.startswith("external"):
468468
raise DataJointError(
469469
f"Legacy datatype `{original_type}`. See migration guide: "
470-
"https://docs.datajoint.com/how-to/migrate-from-0x/"
470+
"https://docs.datajoint.com/how-to/migrate-to-v20/"
471471
)
472472
# Not a special type - that's fine, could be native passthrough
473473
category = None

src/datajoint/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# version bump auto managed by Github Actions:
22
# label_prs.yaml(prep), release.yaml(bump), post_release.yaml(edit)
33
# manually set this version will be eventually overwritten by the above actions
4-
__version__ = "2.0.0a26"
4+
__version__ = "2.0.0a27"

tests/unit/test_fetch_compat.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Tests for backward-compatible fetch() method."""
2+
3+
import warnings
4+
from unittest.mock import MagicMock, patch
5+
6+
import numpy as np
7+
import pytest
8+
9+
10+
class TestFetchBackwardCompat:
11+
"""Test backward-compatible fetch() emits deprecation warning and delegates correctly."""
12+
13+
@pytest.fixture
14+
def mock_expression(self):
15+
"""Create a mock QueryExpression with mocked output methods."""
16+
from datajoint.expression import QueryExpression
17+
18+
expr = MagicMock(spec=QueryExpression)
19+
# Make fetch() callable by using the real implementation
20+
expr.fetch = QueryExpression.fetch.__get__(expr, QueryExpression)
21+
22+
# Mock the output methods
23+
expr.to_arrays = MagicMock(return_value=np.array([(1, "a"), (2, "b")]))
24+
expr.to_dicts = MagicMock(return_value=[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}])
25+
expr.to_pandas = MagicMock()
26+
expr.proj = MagicMock(return_value=expr)
27+
28+
return expr
29+
30+
def test_fetch_emits_deprecation_warning(self, mock_expression):
31+
"""fetch() should emit a DeprecationWarning."""
32+
with warnings.catch_warnings(record=True) as w:
33+
warnings.simplefilter("always")
34+
mock_expression.fetch()
35+
36+
assert len(w) == 1
37+
assert issubclass(w[0].category, DeprecationWarning)
38+
assert "fetch() is deprecated" in str(w[0].message)
39+
40+
def test_fetch_default_returns_arrays(self, mock_expression):
41+
"""fetch() with no args should call to_arrays()."""
42+
with warnings.catch_warnings():
43+
warnings.simplefilter("ignore", DeprecationWarning)
44+
mock_expression.fetch()
45+
46+
mock_expression.to_arrays.assert_called_once_with(
47+
order_by=None, limit=None, offset=None, squeeze=False
48+
)
49+
50+
def test_fetch_as_dict_true(self, mock_expression):
51+
"""fetch(as_dict=True) should call to_dicts()."""
52+
with warnings.catch_warnings():
53+
warnings.simplefilter("ignore", DeprecationWarning)
54+
mock_expression.fetch(as_dict=True)
55+
56+
mock_expression.to_dicts.assert_called_once_with(
57+
order_by=None, limit=None, offset=None, squeeze=False
58+
)
59+
60+
def test_fetch_with_attrs_returns_dicts(self, mock_expression):
61+
"""fetch('col1', 'col2') should call proj().to_dicts()."""
62+
with warnings.catch_warnings():
63+
warnings.simplefilter("ignore", DeprecationWarning)
64+
mock_expression.fetch("col1", "col2")
65+
66+
mock_expression.proj.assert_called_once_with("col1", "col2")
67+
mock_expression.to_dicts.assert_called_once()
68+
69+
def test_fetch_with_attrs_as_dict_false(self, mock_expression):
70+
"""fetch('col1', 'col2', as_dict=False) should call to_arrays('col1', 'col2')."""
71+
with warnings.catch_warnings():
72+
warnings.simplefilter("ignore", DeprecationWarning)
73+
mock_expression.fetch("col1", "col2", as_dict=False)
74+
75+
mock_expression.to_arrays.assert_called_once_with(
76+
"col1", "col2", order_by=None, limit=None, offset=None, squeeze=False
77+
)
78+
79+
def test_fetch_format_frame(self, mock_expression):
80+
"""fetch(format='frame') should call to_pandas()."""
81+
with warnings.catch_warnings():
82+
warnings.simplefilter("ignore", DeprecationWarning)
83+
mock_expression.fetch(format="frame")
84+
85+
mock_expression.to_pandas.assert_called_once_with(
86+
order_by=None, limit=None, offset=None, squeeze=False
87+
)
88+
89+
def test_fetch_format_frame_with_attrs_raises(self, mock_expression):
90+
"""fetch(format='frame') with attrs should raise error."""
91+
from datajoint.errors import DataJointError
92+
93+
with warnings.catch_warnings():
94+
warnings.simplefilter("ignore", DeprecationWarning)
95+
with pytest.raises(DataJointError, match="format='frame' cannot be combined"):
96+
mock_expression.fetch("col1", format="frame")
97+
98+
def test_fetch_passes_order_by_limit_offset(self, mock_expression):
99+
"""fetch() should pass order_by, limit, offset to output methods."""
100+
with warnings.catch_warnings():
101+
warnings.simplefilter("ignore", DeprecationWarning)
102+
mock_expression.fetch(order_by="id", limit=10, offset=5)
103+
104+
mock_expression.to_arrays.assert_called_once_with(
105+
order_by="id", limit=10, offset=5, squeeze=False
106+
)
107+
108+
def test_fetch_passes_squeeze(self, mock_expression):
109+
"""fetch(squeeze=True) should pass squeeze to output methods."""
110+
with warnings.catch_warnings():
111+
warnings.simplefilter("ignore", DeprecationWarning)
112+
mock_expression.fetch(squeeze=True)
113+
114+
mock_expression.to_arrays.assert_called_once_with(
115+
order_by=None, limit=None, offset=None, squeeze=True
116+
)

0 commit comments

Comments
 (0)