Skip to content

Commit

Permalink
fix(api): fail on trying to construct an iterable of a deferred object
Browse files Browse the repository at this point in the history
  • Loading branch information
cpcloud committed Aug 8, 2023
1 parent 083bdae commit 89bf919
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 5 deletions.
9 changes: 7 additions & 2 deletions ibis/expr/deferred.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import operator
from typing import Any, Callable
from typing import Any, Callable, NoReturn

_BINARY_OPS: dict[str, Callable[[Any, Any], Any]] = {
"+": operator.add,
Expand Down Expand Up @@ -59,9 +59,14 @@ def __hash__(self) -> int:
def _resolve(self, param: Any) -> Any:
return param

def __iter__(self) -> NoReturn:
raise TypeError(f"{self.__class__.__name__!r} object is not iterable")

def __getattr__(self, attr: str) -> Deferred:
if attr.startswith("__"):
raise AttributeError(f"'Deferred' object has no attribute {attr!r}")
raise AttributeError(
f"{self.__class__.__name__!r} object has no attribute {attr!r}"
)
return DeferredAttr(self, attr)

def __getitem__(self, key: Any) -> Deferred:
Expand Down
15 changes: 15 additions & 0 deletions ibis/expr/tests/test_deferred.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def test_magic_methods_not_deferred():
with pytest.raises(AttributeError, match="__fizzbuzz__"):
_.__fizzbuzz__()

with pytest.raises(AttributeError, match="DeferredAttr.+__fizzbuzz__"):
_.a.__fizzbuzz__()


def test_getattr(table):
expr = _.a
Expand Down Expand Up @@ -166,3 +169,15 @@ def test_unary_ops(symbol, op, table):
res = expr.resolve(table)
assert res.equals(sol)
assert repr(expr) == f"{symbol}_.a"


@pytest.mark.parametrize("obj", [_, _.a, _.a.b[0]])
def test_deferred_is_not_iterable(obj):
with pytest.raises(TypeError, match="object is not iterable"):
sorted(obj)

with pytest.raises(TypeError, match="object is not iterable"):
iter(obj)

with pytest.raises(TypeError, match="is not an iterator"):
next(obj)
7 changes: 5 additions & 2 deletions ibis/expr/types/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import contextlib
import os
import webbrowser
from typing import TYPE_CHECKING, Any, Mapping, Tuple
from typing import TYPE_CHECKING, Any, Mapping, NoReturn, Tuple

from public import public

import ibis.expr.operations as ops
from ibis.common.exceptions import IbisError, IbisTypeError, TranslationError
from ibis.common.exceptions import IbisError, TranslationError
from ibis.common.grounds import Immutable
from ibis.config import _default_backend, options
from ibis.util import experimental
Expand Down Expand Up @@ -45,6 +45,9 @@ class Expr(Immutable, Coercible):
def __init__(self, arg: ops.Node) -> None:
object.__setattr__(self, "_arg", arg)

def __iter__(self) -> NoReturn:
raise TypeError(f"{self.__class__.__name__!r} object is not iterable")

@classmethod
def __coerce__(cls, value):
if isinstance(value, cls):
Expand Down
10 changes: 9 additions & 1 deletion ibis/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,15 @@ def is_iterable(o: Any) -> bool:
>>> is_iterable([])
True
"""
return not isinstance(o, (str, bytes)) and isinstance(o, collections.abc.Iterable)
if isinstance(o, (str, bytes)):
return False

try:
iter(o)
except TypeError:
return False
else:
return True


def convert_unit(value, unit, to, floor: bool = True):
Expand Down

0 comments on commit 89bf919

Please sign in to comment.