Skip to content

Commit 82a9223

Browse files
Merge master into pre/v2.1
Includes: - Backward-compatible fetch() with deprecation warning - Fix migration guide link
2 parents 06e709f + 512af13 commit 82a9223

File tree

3 files changed

+175
-20
lines changed

3 files changed

+175
-20
lines changed

src/datajoint/expression.py

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

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

592-
Use the new explicit output methods instead:
593-
- table.to_dicts() # list of dictionaries
594-
- table.to_pandas() # pandas DataFrame
595-
- table.to_arrays() # numpy structured array
596-
- table.to_arrays('a', 'b') # tuple of numpy arrays
597-
- table.keys() # primary keys as list[dict]
598-
- table.to_polars() # polars DataFrame (requires pip install datajoint[polars])
599-
- table.to_arrow() # PyArrow Table (requires pip install datajoint[arrow])
632+
warnings.warn(
633+
"fetch() is deprecated in DataJoint 2.0. " "Use to_dicts(), to_pandas(), to_arrays(), or keys() instead.",
634+
DeprecationWarning,
635+
stacklevel=2,
636+
)
600637

601-
For single-row fetch, use fetch1() which is unchanged.
638+
# Handle format='frame' -> to_pandas()
639+
if format == "frame":
640+
if attrs or as_dict is not None:
641+
raise DataJointError("format='frame' cannot be combined with attrs or as_dict")
642+
return self.to_pandas(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)
602643

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

611660
def fetch1(self, *attrs, squeeze=False):
612661
"""

src/datajoint/heading.py

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

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)