Skip to content

Commit 512af13

Browse files
Merge pull request #1355 from datajoint/feat/fetch-backward-compat
feat: Add backward-compatible fetch() with deprecation warning
2 parents 8c85a9b + 4faaa6f commit 512af13

File tree

4 files changed

+176
-21
lines changed

4 files changed

+176
-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: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Tests for backward-compatible fetch() method."""
2+
3+
import warnings
4+
from unittest.mock import MagicMock
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(order_by=None, limit=None, offset=None, squeeze=False)
47+
48+
def test_fetch_as_dict_true(self, mock_expression):
49+
"""fetch(as_dict=True) should call to_dicts()."""
50+
with warnings.catch_warnings():
51+
warnings.simplefilter("ignore", DeprecationWarning)
52+
mock_expression.fetch(as_dict=True)
53+
54+
mock_expression.to_dicts.assert_called_once_with(order_by=None, limit=None, offset=None, squeeze=False)
55+
56+
def test_fetch_with_attrs_returns_dicts(self, mock_expression):
57+
"""fetch('col1', 'col2') should call proj().to_dicts()."""
58+
with warnings.catch_warnings():
59+
warnings.simplefilter("ignore", DeprecationWarning)
60+
mock_expression.fetch("col1", "col2")
61+
62+
mock_expression.proj.assert_called_once_with("col1", "col2")
63+
mock_expression.to_dicts.assert_called_once()
64+
65+
def test_fetch_with_attrs_as_dict_false(self, mock_expression):
66+
"""fetch('col1', 'col2', as_dict=False) should call to_arrays('col1', 'col2')."""
67+
with warnings.catch_warnings():
68+
warnings.simplefilter("ignore", DeprecationWarning)
69+
mock_expression.fetch("col1", "col2", as_dict=False)
70+
71+
mock_expression.to_arrays.assert_called_once_with(
72+
"col1", "col2", order_by=None, limit=None, offset=None, squeeze=False
73+
)
74+
75+
def test_fetch_format_frame(self, mock_expression):
76+
"""fetch(format='frame') should call to_pandas()."""
77+
with warnings.catch_warnings():
78+
warnings.simplefilter("ignore", DeprecationWarning)
79+
mock_expression.fetch(format="frame")
80+
81+
mock_expression.to_pandas.assert_called_once_with(order_by=None, limit=None, offset=None, squeeze=False)
82+
83+
def test_fetch_format_frame_with_attrs_raises(self, mock_expression):
84+
"""fetch(format='frame') with attrs should raise error."""
85+
from datajoint.errors import DataJointError
86+
87+
with warnings.catch_warnings():
88+
warnings.simplefilter("ignore", DeprecationWarning)
89+
with pytest.raises(DataJointError, match="format='frame' cannot be combined"):
90+
mock_expression.fetch("col1", format="frame")
91+
92+
def test_fetch_passes_order_by_limit_offset(self, mock_expression):
93+
"""fetch() should pass order_by, limit, offset to output methods."""
94+
with warnings.catch_warnings():
95+
warnings.simplefilter("ignore", DeprecationWarning)
96+
mock_expression.fetch(order_by="id", limit=10, offset=5)
97+
98+
mock_expression.to_arrays.assert_called_once_with(order_by="id", limit=10, offset=5, squeeze=False)
99+
100+
def test_fetch_passes_squeeze(self, mock_expression):
101+
"""fetch(squeeze=True) should pass squeeze to output methods."""
102+
with warnings.catch_warnings():
103+
warnings.simplefilter("ignore", DeprecationWarning)
104+
mock_expression.fetch(squeeze=True)
105+
106+
mock_expression.to_arrays.assert_called_once_with(order_by=None, limit=None, offset=None, squeeze=True)

0 commit comments

Comments
 (0)