Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 68 additions & 19 deletions src/datajoint/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,29 +580,78 @@ def aggr(self, group, *attributes, exclude_nonmatching=False, **named_attributes
aggregate = aggr # alias for aggr

# ---------- Fetch operators --------------------
@property
def fetch(self):
def fetch(
self,
*attrs,
offset=None,
limit=None,
order_by=None,
format=None,
as_dict=None,
squeeze=False,
):
"""
Fetch data from the table (backward-compatible with DataJoint 0.14).

.. deprecated:: 2.0
Use the new explicit output methods instead:
- ``to_dicts()`` for list of dictionaries
- ``to_pandas()`` for pandas DataFrame
- ``to_arrays()`` for numpy structured array
- ``to_arrays('a', 'b')`` for tuple of arrays
- ``keys()`` for primary keys

Parameters
----------
*attrs : str
Attributes to fetch. If empty, fetches all.
offset : int, optional
Number of tuples to skip.
limit : int, optional
Maximum number of tuples to return.
order_by : str or list, optional
Attribute(s) for ordering results.
format : str, optional
Output format: 'array' or 'frame' (pandas DataFrame).
as_dict : bool, optional
Return as list of dicts instead of structured array.
squeeze : bool, optional
Remove extra dimensions from arrays. Default False.

Returns
-------
np.recarray, list[dict], or pd.DataFrame
Query results in requested format.
"""
The fetch() method has been removed in DataJoint 2.0.
import warnings

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

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

See migration guide: https://docs.datajoint.com/how-to/migrate-from-0x/
"""
raise AttributeError(
"fetch() has been removed in DataJoint 2.0. "
"Use to_dicts(), to_pandas(), to_arrays(), or keys() instead. "
"See table.fetch.__doc__ for details."
)
# Handle specific attributes requested
if attrs:
if as_dict or as_dict is None:
# fetch('col1', 'col2', as_dict=True) or fetch('col1', 'col2')
return self.proj(*attrs).to_dicts(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)
else:
# fetch('col1', 'col2', as_dict=False) -> tuple of arrays
return self.to_arrays(*attrs, order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)

# Handle as_dict=True -> to_dicts()
if as_dict:
return self.to_dicts(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)

# Default: return structured array (legacy behavior)
return self.to_arrays(order_by=order_by, limit=limit, offset=offset, squeeze=squeeze)

def fetch1(self, *attrs, squeeze=False):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/datajoint/heading.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ def _init_from_database(self) -> None:
if original_type.startswith("external"):
raise DataJointError(
f"Legacy datatype `{original_type}`. See migration guide: "
"https://docs.datajoint.com/how-to/migrate-from-0x/"
"https://docs.datajoint.com/how-to/migrate-to-v20/"
)
# Not a special type - that's fine, could be native passthrough
category = None
Expand Down
2 changes: 1 addition & 1 deletion src/datajoint/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# version bump auto managed by Github Actions:
# label_prs.yaml(prep), release.yaml(bump), post_release.yaml(edit)
# manually set this version will be eventually overwritten by the above actions
__version__ = "2.0.0a26"
__version__ = "2.0.0a27"
106 changes: 106 additions & 0 deletions tests/unit/test_fetch_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Tests for backward-compatible fetch() method."""

import warnings
from unittest.mock import MagicMock

import numpy as np
import pytest


class TestFetchBackwardCompat:
"""Test backward-compatible fetch() emits deprecation warning and delegates correctly."""

@pytest.fixture
def mock_expression(self):
"""Create a mock QueryExpression with mocked output methods."""
from datajoint.expression import QueryExpression

expr = MagicMock(spec=QueryExpression)
# Make fetch() callable by using the real implementation
expr.fetch = QueryExpression.fetch.__get__(expr, QueryExpression)

# Mock the output methods
expr.to_arrays = MagicMock(return_value=np.array([(1, "a"), (2, "b")]))
expr.to_dicts = MagicMock(return_value=[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}])
expr.to_pandas = MagicMock()
expr.proj = MagicMock(return_value=expr)

return expr

def test_fetch_emits_deprecation_warning(self, mock_expression):
"""fetch() should emit a DeprecationWarning."""
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
mock_expression.fetch()

assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "fetch() is deprecated" in str(w[0].message)

def test_fetch_default_returns_arrays(self, mock_expression):
"""fetch() with no args should call to_arrays()."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
mock_expression.fetch()

mock_expression.to_arrays.assert_called_once_with(order_by=None, limit=None, offset=None, squeeze=False)

def test_fetch_as_dict_true(self, mock_expression):
"""fetch(as_dict=True) should call to_dicts()."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
mock_expression.fetch(as_dict=True)

mock_expression.to_dicts.assert_called_once_with(order_by=None, limit=None, offset=None, squeeze=False)

def test_fetch_with_attrs_returns_dicts(self, mock_expression):
"""fetch('col1', 'col2') should call proj().to_dicts()."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
mock_expression.fetch("col1", "col2")

mock_expression.proj.assert_called_once_with("col1", "col2")
mock_expression.to_dicts.assert_called_once()

def test_fetch_with_attrs_as_dict_false(self, mock_expression):
"""fetch('col1', 'col2', as_dict=False) should call to_arrays('col1', 'col2')."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
mock_expression.fetch("col1", "col2", as_dict=False)

mock_expression.to_arrays.assert_called_once_with(
"col1", "col2", order_by=None, limit=None, offset=None, squeeze=False
)

def test_fetch_format_frame(self, mock_expression):
"""fetch(format='frame') should call to_pandas()."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
mock_expression.fetch(format="frame")

mock_expression.to_pandas.assert_called_once_with(order_by=None, limit=None, offset=None, squeeze=False)

def test_fetch_format_frame_with_attrs_raises(self, mock_expression):
"""fetch(format='frame') with attrs should raise error."""
from datajoint.errors import DataJointError

with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
with pytest.raises(DataJointError, match="format='frame' cannot be combined"):
mock_expression.fetch("col1", format="frame")

def test_fetch_passes_order_by_limit_offset(self, mock_expression):
"""fetch() should pass order_by, limit, offset to output methods."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
mock_expression.fetch(order_by="id", limit=10, offset=5)

mock_expression.to_arrays.assert_called_once_with(order_by="id", limit=10, offset=5, squeeze=False)

def test_fetch_passes_squeeze(self, mock_expression):
"""fetch(squeeze=True) should pass squeeze to output methods."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
mock_expression.fetch(squeeze=True)

mock_expression.to_arrays.assert_called_once_with(order_by=None, limit=None, offset=None, squeeze=True)
Loading