diff --git a/ibis/common/exceptions.py b/ibis/common/exceptions.py index 745614b324846..1c60107fed337 100644 --- a/ibis/common/exceptions.py +++ b/ibis/common/exceptions.py @@ -15,10 +15,11 @@ from __future__ import annotations +import difflib from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Iterable class TableNotFound(Exception): @@ -45,6 +46,24 @@ class RelationError(ExpressionError): """RelationError.""" +class FieldNotFoundError(AttributeError, IbisError): + """When you try to access `table_or_struct.does_not_exist`.""" + + def __init__(self, obj: object, name: str, options: Iterable[str]) -> None: + # Be a little more restrictive with the cutoff + self.obj = obj + self.name = name + self.options = set(options) + self.typos = set(difflib.get_close_matches(name, self.options, cutoff=0.6)) + if len(self.typos) == 1: + msg = f"'{name}' not found in {obj.__class__.__name__} object. Did you mean '{next(iter(self.typos))}'?" + elif len(self.typos) > 1: + msg = f"'{name}' not found in {obj.__class__.__name__} object. Did you mean one of {self.typos}?" + else: + msg = f"'{name}' not found in {obj.__class__.__name__} object. Possible options: {self.options}" + super().__init__(msg) + + class TranslationError(IbisError): """TranslationError.""" diff --git a/ibis/expr/operations/relations.py b/ibis/expr/operations/relations.py index 8bd06eac215cc..0825ca76e831d 100644 --- a/ibis/expr/operations/relations.py +++ b/ibis/expr/operations/relations.py @@ -17,7 +17,7 @@ FrozenDict, FrozenOrderedDict, ) -from ibis.common.exceptions import IbisTypeError, IntegrityError, RelationError +from ibis.common.exceptions import FieldNotFoundError, IntegrityError, RelationError from ibis.common.grounds import Concrete from ibis.common.patterns import Between, InstanceOf from ibis.common.typing import Coercible, VarTuple @@ -90,13 +90,9 @@ class Field(Value): shape = ds.columnar - def __init__(self, rel, name): + def __init__(self, rel: Relation, name: str): if name not in rel.schema: - columns_formatted = ", ".join(map(repr, rel.schema.names)) - raise IbisTypeError( - f"Column {name!r} is not found in table. " - f"Existing columns: {columns_formatted}." - ) + raise FieldNotFoundError(rel.to_expr(), name, rel.schema.names) super().__init__(rel=rel, name=name) @attribute diff --git a/ibis/expr/types/relations.py b/ibis/expr/types/relations.py index 50f56ddeeda3a..3173359325d3b 100644 --- a/ibis/expr/types/relations.py +++ b/ibis/expr/types/relations.py @@ -240,21 +240,39 @@ def _fast_bind(self, *args, **kwargs): args = () else: args = util.promote_list(args[0]) - # bind positional arguments + values = [] + errs = [] + # bind positional arguments for arg in args: - values.extend(bind(self, arg)) + try: + # need tuple to cause generator to evaluate + bindings = tuple(bind(self, arg)) + except com.FieldNotFoundError as e: + errs.append(e) + continue + values.extend(bindings) # bind keyword arguments where each entry can produce only one value # which is then named with the given key for key, arg in kwargs.items(): - bindings = tuple(bind(self, arg)) + try: + # need tuple to cause generator to evaluate + bindings = tuple(bind(self, arg)) + except com.FieldNotFoundError as e: + errs.append(e) + continue if len(bindings) != 1: raise com.IbisInputError( "Keyword arguments cannot produce more than one value" ) (value,) = bindings values.append(value.name(key)) + if errs: + raise com.IbisError( + "Error binding arguments to table expression: " + + "; ".join(str(e) for e in errs) + ) return values def bind(self, *args: Any, **kwargs: Any) -> tuple[Value, ...]: @@ -739,8 +757,9 @@ def __getattr__(self, key: str) -> ir.Column: """ try: return ops.Field(self, key).to_expr() - except com.IbisTypeError: - pass + except com.FieldNotFoundError as e: + if e.typos: + raise e # A mapping of common attribute typos, mapping them to the proper name common_typos = { diff --git a/ibis/expr/types/structs.py b/ibis/expr/types/structs.py index 4c7cdc197094d..cbd1c2358e5fb 100644 --- a/ibis/expr/types/structs.py +++ b/ibis/expr/types/structs.py @@ -9,7 +9,7 @@ import ibis.expr.operations as ops from ibis import util from ibis.common.deferred import deferrable -from ibis.common.exceptions import IbisError +from ibis.common.exceptions import FieldNotFoundError, IbisError from ibis.expr.types.generic import Column, Scalar, Value, literal if TYPE_CHECKING: @@ -205,7 +205,7 @@ def __getitem__(self, name: str) -> ir.Value: KeyError: 'foo_bar' """ if name not in self.names: - raise KeyError(name) + raise FieldNotFoundError(self, name, self.names) return ops.StructField(self, name).to_expr() def __setstate__(self, instance_dictionary): @@ -264,10 +264,7 @@ def __getattr__(self, name: str) -> ir.Value: ... AttributeError: foo_bar """ - try: - return self[name] - except KeyError: - raise AttributeError(name) from None + return self[name] @property def names(self) -> Sequence[str]: