use std::slice;
use crate::{dataset::Dataset, error::Result, python::pool::attach};
use pyo3::exceptions::{PyNotImplementedError, PyTypeError, PyValueError};
use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PyTuple};
#[pyclass(frozen, subclass)]
pub struct PyDatasetBase;
#[pymethods]
impl PyDatasetBase {
#[new]
#[pyo3(signature = (*_args, **_kwargs))]
fn new(_args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>) -> Self {
Self
}
fn __len__(&self) -> PyResult<usize> {
Err(PyNotImplementedError::new_err(
"PyDataset.__len__ must be implemented by subclasses",
))
}
fn __getitem__(&self, _index: usize) -> PyResult<Py<PyAny>> {
Err(PyNotImplementedError::new_err(
"PyDataset.__getitem__ must be implemented by subclasses",
))
}
}
pub(crate) type PyDataset = Py<PyDatasetBase>;
pub(crate) fn len_py(dataset: &PyDataset, py: Python<'_>) -> PyResult<usize> {
dataset
.call_method0(py, intern!(py, "__len__"))?
.extract(py)
}
pub(crate) fn get_item_py(
dataset: &PyDataset,
py: Python<'_>,
index: usize,
) -> PyResult<Py<PyAny>> {
let item = dataset.call_method1(py, intern!(py, "__getitem__"), (index,))?;
check_not_awaitable(py, slice::from_ref(&item))?;
Ok(item)
}
fn check_not_awaitable(py: Python<'_>, items: &[Py<PyAny>]) -> PyResult<()> {
let Some(first) = items.first() else {
return Ok(());
};
let awaitable = intern!(py, "__await__");
if first.bind(py).get_type().getattr_opt(awaitable)?.is_none() {
return Ok(());
}
for item in items {
if let Some(close) = item.bind(py).getattr_opt(intern!(py, "close"))? {
close.call0()?;
}
}
Err(PyTypeError::new_err(
"PyDataset.__getitem__ returned an awaitable; subclass PyAsyncDataset \
and define `async def __getitem__` instead",
))
}
pub(crate) enum Fetcher {
Batched(Py<PyAny>),
PerItem(Py<PyAny>),
}
impl Fetcher {
pub(crate) fn new(dataset: &PyDataset, py: Python<'_>) -> PyResult<Self> {
let dataset = dataset.bind(py);
Ok(match dataset.getattr_opt(intern!(py, "__getitems__"))? {
Some(getitems) if getitems.is_truthy()? => Self::Batched(getitems.unbind()),
_ => Self::PerItem(dataset.getattr(intern!(py, "__getitem__"))?.unbind()),
})
}
pub(crate) fn method(&self) -> &Py<PyAny> {
match self {
Self::Batched(method) | Self::PerItem(method) => method,
}
}
pub(crate) fn fetch(&self, py: Python<'_>, indices: &[usize]) -> PyResult<Vec<Py<PyAny>>> {
match self {
Self::PerItem(getitem) => {
let items = indices
.iter()
.map(|&i| getitem.call1(py, (i,)))
.collect::<PyResult<Vec<_>>>()?;
check_not_awaitable(py, &items)?;
Ok(items)
}
Self::Batched(getitems) => {
let samples = getitems.bind(py).call1((PyList::new(py, indices)?,))?;
let samples = samples
.try_iter()?
.map(|sample| sample.map(Bound::unbind))
.collect::<PyResult<Vec<_>>>()?;
if samples.len() != indices.len() {
return Err(PyValueError::new_err(format!(
"__getitems__ returned {} samples for {} indices",
samples.len(),
indices.len()
)));
}
check_not_awaitable(py, &samples)?;
Ok(samples)
}
}
}
}
impl Dataset for PyDataset {
type Item = Py<PyAny>;
fn get(&self, index: usize) -> Result<Self::Item> {
Ok(attach(|py| get_item_py(self, py, index))?)
}
fn get_batch(&self, indices: &[usize]) -> Result<Vec<Py<PyAny>>> {
Ok(attach(|py| Fetcher::new(self, py)?.fetch(py, indices))?)
}
fn len(&self) -> usize {
Python::attach(|py| {
len_py(self, py).unwrap_or_else(|e| panic!("PyDataset.__len__ failed: {e}"))
})
}
}