diff --git a/ibis/backends/bigquery/tests/unit/test_compiler.py b/ibis/backends/bigquery/tests/unit/test_compiler.py index ef2e81654a59..a3e15d28f501 100644 --- a/ibis/backends/bigquery/tests/unit/test_compiler.py +++ b/ibis/backends/bigquery/tests/unit/test_compiler.py @@ -13,7 +13,7 @@ import ibis.expr.datatypes as dt import ibis.expr.operations as ops from ibis import _ -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError to_sql = ibis.bigquery.compile diff --git a/ibis/backends/clickhouse/tests/test_aggregations.py b/ibis/backends/clickhouse/tests/test_aggregations.py index a6e6a2b18f64..1200da7f9e5b 100644 --- a/ibis/backends/clickhouse/tests/test_aggregations.py +++ b/ibis/backends/clickhouse/tests/test_aggregations.py @@ -7,7 +7,7 @@ import pytest import ibis -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError pytest.importorskip("clickhouse_connect") diff --git a/ibis/backends/dask/tests/execution/test_functions.py b/ibis/backends/dask/tests/execution/test_functions.py index 4bdd65b0021e..e8ecd89700d8 100644 --- a/ibis/backends/dask/tests/execution/test_functions.py +++ b/ibis/backends/dask/tests/execution/test_functions.py @@ -13,8 +13,8 @@ import ibis import ibis.expr.datatypes as dt +from ibis.common.annotations import ValidationError from ibis.common.exceptions import OperationNotDefinedError -from ibis.common.patterns import ValidationError dd = pytest.importorskip("dask.dataframe") from dask.dataframe.utils import tm # noqa: E402 diff --git a/ibis/backends/impala/tests/test_udf.py b/ibis/backends/impala/tests/test_udf.py index 6d766077bf9c..9f99d13c923b 100644 --- a/ibis/backends/impala/tests/test_udf.py +++ b/ibis/backends/impala/tests/test_udf.py @@ -14,7 +14,7 @@ import ibis.expr.types as ir from ibis import util from ibis.backends.impala import ddl -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError from ibis.expr import rules pytest.importorskip("impala") diff --git a/ibis/backends/impala/tests/test_unary_builtins.py b/ibis/backends/impala/tests/test_unary_builtins.py index 3164c051178c..1e1a5fc7d5f7 100644 --- a/ibis/backends/impala/tests/test_unary_builtins.py +++ b/ibis/backends/impala/tests/test_unary_builtins.py @@ -5,7 +5,7 @@ import ibis import ibis.expr.types as ir from ibis.backends.impala.tests.conftest import translate -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError @pytest.fixture(scope="module") diff --git a/ibis/backends/pandas/tests/execution/test_functions.py b/ibis/backends/pandas/tests/execution/test_functions.py index 8f0d0066ded8..708b9574bf5d 100644 --- a/ibis/backends/pandas/tests/execution/test_functions.py +++ b/ibis/backends/pandas/tests/execution/test_functions.py @@ -16,7 +16,7 @@ from ibis.backends.pandas.execution import execute from ibis.backends.pandas.tests.conftest import TestConf as tm from ibis.backends.pandas.udf import udf -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError @pytest.mark.parametrize( diff --git a/ibis/backends/pandas/tests/execution/test_window.py b/ibis/backends/pandas/tests/execution/test_window.py index fcb6a7b1b060..8f292b3c090b 100644 --- a/ibis/backends/pandas/tests/execution/test_window.py +++ b/ibis/backends/pandas/tests/execution/test_window.py @@ -17,7 +17,7 @@ from ibis.backends.pandas.dispatch import pre_execute from ibis.backends.pandas.execution import execute from ibis.backends.pandas.tests.conftest import TestConf as tm -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError from ibis.legacy.udf.vectorized import reduction diff --git a/ibis/backends/tests/test_generic.py b/ibis/backends/tests/test_generic.py index 627f36a90d5c..ff1c178088c6 100644 --- a/ibis/backends/tests/test_generic.py +++ b/ibis/backends/tests/test_generic.py @@ -18,7 +18,7 @@ import ibis.expr.datatypes as dt import ibis.selectors as s from ibis import _ -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError try: import duckdb diff --git a/ibis/backends/tests/test_string.py b/ibis/backends/tests/test_string.py index 667a63a7e183..80fd4f5ee523 100644 --- a/ibis/backends/tests/test_string.py +++ b/ibis/backends/tests/test_string.py @@ -9,8 +9,8 @@ import ibis import ibis.common.exceptions as com import ibis.expr.datatypes as dt +from ibis.common.annotations import ValidationError from ibis.common.exceptions import OperationNotDefinedError -from ibis.common.patterns import ValidationError try: from google.api_core.exceptions import BadRequest diff --git a/ibis/backends/tests/test_temporal.py b/ibis/backends/tests/test_temporal.py index e3c739ba84fa..5c2d6413d829 100644 --- a/ibis/backends/tests/test_temporal.py +++ b/ibis/backends/tests/test_temporal.py @@ -17,7 +17,7 @@ import ibis.common.exceptions as com import ibis.expr.datatypes as dt from ibis.backends.pandas.execution.temporal import day_name -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError try: from duckdb import InvalidInputException as DuckDBInvalidInputException diff --git a/ibis/common/annotations.py b/ibis/common/annotations.py index 46c4ba8f34fa..9b31d3488578 100644 --- a/ibis/common/annotations.py +++ b/ibis/common/annotations.py @@ -8,9 +8,10 @@ Any, FrozenDictOf, Function, + NoMatch, Option, + Pattern, TupleOf, - Validator, ) from ibis.common.typing import get_type_hints @@ -22,6 +23,10 @@ VAR_POSITIONAL = inspect.Parameter.VAR_POSITIONAL +class ValidationError(Exception): + ... + + class Annotation: """Base class for all annotations. @@ -29,57 +34,62 @@ class Annotation: Parameters ---------- - validator : Validator, default noop - Validator to validate the field. + pattern : Pattern, default noop + Pattern to validate the field. default : Any, default EMPTY Default value of the field. typehint : type, default EMPTY Type of the field, not used for validation. """ - __slots__ = ("_validator", "_default", "_typehint") + __slots__ = ("_pattern", "_default", "_typehint") - def __init__(self, validator=None, default=EMPTY, typehint=EMPTY): - if validator is None or isinstance(validator, Validator): + def __init__(self, pattern=None, default=EMPTY, typehint=EMPTY): + if pattern is None or isinstance(pattern, Pattern): pass - elif callable(validator): - validator = Function(validator) + elif callable(pattern): + pattern = Function(pattern) else: - raise TypeError(f"Unsupported validator {validator!r}") + raise TypeError(f"Unsupported pattern {pattern!r}") + self._pattern = pattern self._default = default self._typehint = typehint - self._validator = validator def __eq__(self, other): return ( type(self) is type(other) + and self._pattern == other._pattern and self._default == other._default and self._typehint == other._typehint - and self._validator == other._validator ) def __repr__(self): return ( - f"{self.__class__.__name__}(validator={self._validator!r}, " + f"{self.__class__.__name__}(pattern={self._pattern!r}, " f"default={self._default!r}, typehint={self._typehint!r})" ) def validate(self, arg, context=None): - if self._validator is None: + if self._pattern is None: return arg - return self._validator.validate(arg, context) + + result = self._pattern.match(arg, context) + if result is NoMatch: + raise ValidationError(f"{arg!r} doesn't match {self._pattern!r}") + + return result class Attribute(Annotation): """Annotation to mark a field in a class. - An optional validator can be provider to validate the field every time it + An optional pattern can be provider to validate the field every time it is set. Parameters ---------- - validator : Validator, default noop - Validator to validate the field. + pattern : Pattern, default noop + Pattern to validate the field. default : Callable, default EMPTY Callable to compute the default value of the field. """ @@ -105,8 +115,8 @@ class Argument(Annotation): Parameters ---------- - validator - Optional validator to validate the argument. + pattern + Optional pattern to validate the argument. default Optional default value of the argument. typehint @@ -120,47 +130,47 @@ class Argument(Annotation): def __init__( self, - validator: Validator | None = None, + pattern: Pattern | None = None, default: AnyType = EMPTY, typehint: type | None = None, kind: int = POSITIONAL_OR_KEYWORD, ): - super().__init__(validator, default, typehint) + super().__init__(pattern, default, typehint) self._kind = kind @classmethod - def required(cls, validator=None, **kwargs): + def required(cls, pattern=None, **kwargs): """Annotation to mark a mandatory argument.""" - return cls(validator, **kwargs) + return cls(pattern, **kwargs) @classmethod - def default(cls, default, validator=None, **kwargs): + def default(cls, default, pattern=None, **kwargs): """Annotation to allow missing arguments with a default value.""" - return cls(validator, default, **kwargs) + return cls(pattern, default, **kwargs) @classmethod - def optional(cls, validator=None, default=None, **kwargs): + def optional(cls, pattern=None, default=None, **kwargs): """Annotation to allow and treat `None` values as missing arguments.""" - if validator is None: - validator = Option(Any(), default=default) + if pattern is None: + pattern = Option(Any(), default=default) else: - validator = Option(validator, default=default) - return cls(validator, default=None, **kwargs) + pattern = Option(pattern, default=default) + return cls(pattern, default=None, **kwargs) @classmethod - def varargs(cls, validator=None, **kwargs): + def varargs(cls, pattern=None, **kwargs): """Annotation to mark a variable length positional argument.""" - validator = None if validator is None else TupleOf(validator) - return cls(validator, kind=VAR_POSITIONAL, **kwargs) + pattern = None if pattern is None else TupleOf(pattern) + return cls(pattern, kind=VAR_POSITIONAL, **kwargs) @classmethod - def varkwargs(cls, validator=None, **kwargs): - validator = None if validator is None else FrozenDictOf(Any(), validator) - return cls(validator, kind=VAR_KEYWORD, **kwargs) + def varkwargs(cls, pattern=None, **kwargs): + pattern = None if pattern is None else FrozenDictOf(Any(), pattern) + return cls(pattern, kind=VAR_KEYWORD, **kwargs) class Parameter(inspect.Parameter): - """Augmented Parameter class to additionally hold a validator object.""" + """Augmented Parameter class to additionally hold a pattern object.""" __slots__ = () @@ -241,18 +251,18 @@ def merge(cls, *signatures, **annotations): ) @classmethod - def from_callable(cls, fn, validators=None, return_validator=None): + def from_callable(cls, fn, patterns=None, return_pattern=None): """Create a validateable signature from a callable. Parameters ---------- fn : Callable Callable to create a signature from. - validators : list or dict, default None - Pass validators to add missing or override existing argument type + patterns : list or dict, default None + Pass patterns to add missing or override existing argument type annotations. - return_validator : Validator, default None - Validator for the return value of the callable. + return_pattern : Pattern, default None + Pattern for the return value of the callable. Returns ------- @@ -261,15 +271,13 @@ def from_callable(cls, fn, validators=None, return_validator=None): sig = super().from_callable(fn) typehints = get_type_hints(fn) - if validators is None: - validators = {} - elif isinstance(validators, (list, tuple)): - # create a mapping of parameter name to validator - validators = dict(zip(sig.parameters.keys(), validators)) - elif not isinstance(validators, dict): - raise TypeError( - f"validators must be a list or dict, got {type(validators)}" - ) + if patterns is None: + patterns = {} + elif isinstance(patterns, (list, tuple)): + # create a mapping of parameter name to pattern + patterns = dict(zip(sig.parameters.keys(), patterns)) + elif not isinstance(patterns, dict): + raise TypeError(f"patterns must be a list or dict, got {type(patterns)}") parameters = [] for param in sig.parameters.values(): @@ -278,36 +286,36 @@ def from_callable(cls, fn, validators=None, return_validator=None): default = param.default typehint = typehints.get(name) - if name in validators: - validator = validators[name] + if name in patterns: + pattern = patterns[name] elif typehint is not None: - validator = Validator.from_typehint(typehint) + pattern = Pattern.from_typehint(typehint) else: - validator = None + pattern = None if kind is VAR_POSITIONAL: - annot = Argument.varargs(validator, typehint=typehint) + annot = Argument.varargs(pattern, typehint=typehint) elif kind is VAR_KEYWORD: - annot = Argument.varkwargs(validator, typehint=typehint) + annot = Argument.varkwargs(pattern, typehint=typehint) elif default is EMPTY: - annot = Argument.required(validator, kind=kind, typehint=typehint) + annot = Argument.required(pattern, kind=kind, typehint=typehint) else: annot = Argument.default( - default, validator, kind=param.kind, typehint=typehint + default, pattern, kind=param.kind, typehint=typehint ) parameters.append(Parameter(param.name, annot)) - if return_validator is not None: - return_annotation = return_validator + if return_pattern is not None: + return_annotation = return_pattern elif (typehint := typehints.get("return")) is not None: - return_annotation = Validator.from_typehint(typehint) + return_annotation = Pattern.from_typehint(typehint) else: return_annotation = EMPTY return cls(parameters, return_annotation=return_annotation) - def unbind(self, this: AnyType): + def unbind(self, this: dict[str, Any]) -> tuple[tuple[Any, ...], dict[str, Any]]: """Reverse bind of the parameters. Attempts to reconstructs the original arguments as keyword only arguments. @@ -355,7 +363,7 @@ def validate(self, *args, **kwargs): validated : dict Dictionary of validated arguments. """ - # bind the signature to the passed arguments and apply the validators + # bind the signature to the passed arguments and apply the patterns # before passing the arguments, so self.__init__() receives already # validated arguments as keywords bound = self.bind(*args, **kwargs) @@ -396,7 +404,12 @@ def validate_return(self, value, context): """ if self.return_annotation is EMPTY: return value - return self.return_annotation.validate(value, context) + + result = self.return_annotation.match(value, context) + if result is NoMatch: + raise ValidationError(f"{value!r} doesn't match {self}") + + return result # aliases for convenience @@ -409,7 +422,7 @@ def validate_return(self, value, context): varkwargs = Argument.varkwargs -# TODO(kszucs): try to cache validator objects +# TODO(kszucs): try to cache pattern objects # TODO(kszucs): try a quicker curry implementation @@ -424,20 +437,20 @@ def annotated(_1=None, _2=None, _3=None, **kwargs): ... def foo(x: int, y: str) -> float: ... return float(x) + float(y) - 2. With argument validators passed as keyword arguments + 2. With argument patterns passed as keyword arguments >>> from ibis.common.patterns import InstanceOf as instance_of >>> @annotated(x=instance_of(int), y=instance_of(str)) ... def foo(x, y): ... return float(x) + float(y) - 3. With mixing type annotations and validators where the latter takes precedence + 3. With mixing type annotations and patterns where the latter takes precedence >>> @annotated(x=instance_of(float)) ... def foo(x: int, y: str) -> float: ... return float(x) + float(y) - 4. With argument validators passed as a list and/or an optional return validator + 4. With argument patterns passed as a list and/or an optional return pattern >>> @annotated([instance_of(int), instance_of(str)], instance_of(float)) ... def foo(x, y): @@ -447,18 +460,18 @@ def annotated(_1=None, _2=None, _3=None, **kwargs): ---------- *args : Union[ tuple[Callable], - tuple[list[Validator], Callable], - tuple[list[Validator], Validator, Callable] + tuple[list[Pattern], Callable], + tuple[list[Pattern], Pattern, Callable] ] Positional arguments. - If a single callable is passed, it's wrapped with the signature - - If two arguments are passed, the first one is a list of validators for the + - If two arguments are passed, the first one is a list of patterns for the arguments and the second one is the callable to wrap - - If three arguments are passed, the first one is a list of validators for the - arguments, the second one is a validator for the return value and the third + - If three arguments are passed, the first one is a list of patterns for the + arguments, the second one is a pattern for the return value and the third one is the callable to wrap - **kwargs : dict[str, Validator] - Validators for the arguments. + **kwargs : dict[str, Pattern] + Patterns for the arguments. Returns ------- @@ -468,19 +481,19 @@ def annotated(_1=None, _2=None, _3=None, **kwargs): return functools.partial(annotated, **kwargs) elif _2 is None: if callable(_1): - func, validators, return_validator = _1, None, None + func, patterns, return_pattern = _1, None, None else: return functools.partial(annotated, _1, **kwargs) elif _3 is None: - if not isinstance(_2, Validator): - func, validators, return_validator = _2, _1, None + if not isinstance(_2, Pattern): + func, patterns, return_pattern = _2, _1, None else: return functools.partial(annotated, _1, _2, **kwargs) else: - func, validators, return_validator = _3, _1, _2 + func, patterns, return_pattern = _3, _1, _2 sig = Signature.from_callable( - func, validators=validators or kwargs, return_validator=return_validator + func, patterns=patterns or kwargs, return_pattern=return_pattern ) @functools.wraps(func) diff --git a/ibis/common/grounds.py b/ibis/common/grounds.py index 2109e76f4e61..e26912e11f32 100644 --- a/ibis/common/grounds.py +++ b/ibis/common/grounds.py @@ -25,7 +25,7 @@ ) from ibis.common.caching import WeakCache from ibis.common.collections import FrozenDict -from ibis.common.patterns import Validator +from ibis.common.patterns import Pattern from ibis.common.typing import evaluate_annotations @@ -60,7 +60,7 @@ def __new__(metacls, clsname, bases, dct, **kwargs): with contextlib.suppress(AttributeError): signatures.append(parent.__signature__) - # collection type annotations and convert them to validators + # collection type annotations and convert them to patterns module = dct.get("__module__") qualname = dct.get("__qualname__") or clsname annotations = dct.get("__annotations__", {}) @@ -70,17 +70,17 @@ def __new__(metacls, clsname, bases, dct, **kwargs): for name, typehint in typehints.items(): if get_origin(typehint) is ClassVar: continue - validator = Validator.from_typehint(typehint) + pattern = Pattern.from_typehint(typehint) if name in dct: - dct[name] = Argument.default(dct[name], validator, typehint=typehint) + dct[name] = Argument.default(dct[name], pattern, typehint=typehint) else: - dct[name] = Argument.required(validator, typehint=typehint) + dct[name] = Argument.required(pattern, typehint=typehint) # collect the newly defined annotations slots = list(dct.pop("__slots__", [])) namespace, arguments = {}, {} for name, attrib in dct.items(): - if isinstance(attrib, Validator): + if isinstance(attrib, Pattern): attrib = Argument.required(attrib) if isinstance(attrib, Argument): diff --git a/ibis/common/patterns.py b/ibis/common/patterns.py index 643a7e9bf9c5..4ea59635da26 100644 --- a/ibis/common/patterns.py +++ b/ibis/common/patterns.py @@ -39,10 +39,6 @@ class CoercionError(Exception): ... -class ValidationError(Exception): - ... - - class MatchError(Exception): ... @@ -63,7 +59,14 @@ def __coerce__(cls, value: Any, **kwargs: Any) -> Self: ... -class Validator(ABC): +class NoMatch(metaclass=Sentinel): + """Marker to indicate that a pattern didn't match.""" + + +# TODO(kszucs): have an As[int] or Coerced[int] type in ibis.common.typing which +# would be used to annotate an argument as coercible to int or to a certain type +# without needing for the type to inherit from Coercible +class Pattern(Hashable): __slots__ = () @classmethod @@ -194,15 +197,6 @@ def from_typehint(cls, annot: type, allow_coercion: bool = True) -> Pattern: f"Cannot create validator from annotation {annot!r} {origin!r}" ) - -class NoMatch(metaclass=Sentinel): - """Marker to indicate that a pattern didn't match.""" - - -# TODO(kszucs): have an As[int] or Coerced[int] type in ibis.common.typing which -# would be used to annotate an argument as coercible to int or to a certain type -# without needing for the type to inherit from Coercible -class Pattern(Validator, Hashable): @abstractmethod def match(self, value: AnyType, context: dict[str, AnyType]) -> AnyType: """Match a value against the pattern. @@ -241,30 +235,6 @@ def __rshift__(self, name: str) -> Pattern: def __rmatmul__(self, name: str) -> Pattern: return Capture(self, name) - def validate( - self, value: AnyType, context: Optional[dict[str, AnyType]] = None - ) -> Any: - """Validate a value against the pattern. - - If the pattern doesn't match the value, then it raises a `ValidationError`. - - Parameters - ---------- - value - The value to match the pattern against. - context - A dictionary providing arbitrary context for the pattern matching. - - Returns - ------- - match - The matched / validated value. - """ - result = self.match(value, context=context) - if result is NoMatch: - raise ValidationError(f"{value!r} doesn't match {self}") - return result - class Matcher(Pattern): """A lightweight alternative to `ibis.common.grounds.Concrete`. @@ -293,7 +263,7 @@ def __hash__(self) -> int: return self.__precomputed_hash__ def __setattr__(self, name, value) -> None: - raise AttributeError("Can't set attributes on immutable ENode instance") + raise AttributeError("Can't set attributes on immutable instance") def __repr__(self): fields = {k: getattr(self, k) for k in self.__slots__} diff --git a/ibis/common/tests/test_annotations.py b/ibis/common/tests/test_annotations.py index fcd306442b73..bc5db5067399 100644 --- a/ibis/common/tests/test_annotations.py +++ b/ibis/common/tests/test_annotations.py @@ -6,7 +6,14 @@ import pytest from typing_extensions import Annotated # noqa: TCH002 -from ibis.common.annotations import Argument, Attribute, Parameter, Signature, annotated +from ibis.common.annotations import ( + Argument, + Attribute, + Parameter, + Signature, + ValidationError, + annotated, +) from ibis.common.patterns import ( Any, CoercedTo, @@ -14,7 +21,6 @@ NoMatch, Option, TupleOf, - ValidationError, pattern, ) @@ -24,13 +30,13 @@ def test_argument_repr(): argument = Argument(is_int, typehint=int, default=None) assert repr(argument) == ( - "Argument(validator=InstanceOf(type=), default=None, " + "Argument(pattern=InstanceOf(type=), default=None, " "typehint=)" ) def test_default_argument(): - annotation = Argument.default(validator=lambda x, context: int(x), default=3) + annotation = Argument.default(pattern=lambda x, context: int(x), default=3) assert annotation.validate(1) == 1 with pytest.raises(TypeError): annotation.validate(None) @@ -82,7 +88,7 @@ class Foo: assert field.initialize(Foo) == 20 - field2 = Attribute(validator=lambda x, this: str(x), default=lambda self: self.a) + field2 = Attribute(pattern=lambda x, this: str(x), default=lambda self: self.a) assert field != field2 assert field2.initialize(Foo) == "10" @@ -103,7 +109,7 @@ def fn(x, this): ofn = Argument.optional(fn) op = Parameter("test", annotation=ofn) - assert op.annotation._validator == Option(fn, default=None) + assert op.annotation._pattern == Option(fn, default=None) assert op.default is None assert op.annotation.validate(None, {"other": 1}) is None @@ -218,12 +224,12 @@ def add_other(x, this): a = Parameter("a", annotation=Argument.required(CoercedTo(float))) b = Parameter("b", annotation=Argument.required(CoercedTo(float))) -c = Parameter("c", annotation=Argument.default(default=0, validator=CoercedTo(float))) +c = Parameter("c", annotation=Argument.default(default=0, pattern=CoercedTo(float))) d = Parameter( "d", - annotation=Argument.default(default=tuple(), validator=TupleOf(CoercedTo(float))), + annotation=Argument.default(default=tuple(), pattern=TupleOf(CoercedTo(float))), ) -e = Parameter("e", annotation=Argument.optional(validator=CoercedTo(float))) +e = Parameter("e", annotation=Argument.optional(pattern=CoercedTo(float))) sig = Signature(parameters=[a, b, c, d, e]) diff --git a/ibis/common/tests/test_grounds.py b/ibis/common/tests/test_grounds.py index c05f0ccba285..613e02616bdf 100644 --- a/ibis/common/tests/test_grounds.py +++ b/ibis/common/tests/test_grounds.py @@ -10,6 +10,7 @@ from ibis.common.annotations import ( Parameter, Signature, + ValidationError, argument, attribute, optional, @@ -36,7 +37,6 @@ Option, Pattern, TupleOf, - ValidationError, ) from ibis.tests.util import assert_pickle_roundtrip @@ -333,7 +333,7 @@ def test_annotable_with_recursive_generic_type_annotations(): # testing cons list pattern = Pattern.from_typehint(List[Integer]) values = ["1", 2.0, 3] - result = pattern.validate(values, {}) + result = pattern.match(values, {}) expected = ConsList(1, ConsList(2, ConsList(3, EmptyList()))) assert result == expected assert result[0] == 1 @@ -346,7 +346,7 @@ def test_annotable_with_recursive_generic_type_annotations(): # testing cons map pattern = Pattern.from_typehint(Map[Integer, Float]) values = {"1": 2, 3: "4.0", 5: 6.0} - result = pattern.validate(values, {}) + result = pattern.match(values, {}) expected = ConsMap((1, 2.0), ConsMap((3, 4.0), ConsMap((5, 6.0), EmptyMap()))) assert result == expected assert result[1] == 2.0 diff --git a/ibis/common/tests/test_patterns.py b/ibis/common/tests/test_patterns.py index d6e9259879c6..bdffbf2dbb30 100644 --- a/ibis/common/tests/test_patterns.py +++ b/ibis/common/tests/test_patterns.py @@ -23,6 +23,7 @@ import pytest from typing_extensions import Annotated +from ibis.common.annotations import ValidationError from ibis.common.collections import FrozenDict from ibis.common.graph import Node from ibis.common.patterns import ( @@ -63,7 +64,6 @@ Topmost, TupleOf, TypeOf, - ValidationError, match, pattern, ) @@ -638,7 +638,6 @@ def test_matching_mapping(): ) def test_various_patterns(pattern, value, expected): assert pattern.match(value, context={}) == expected - assert pattern.validate(value, context={}) == expected @pytest.mark.parametrize( @@ -663,8 +662,6 @@ def test_various_patterns(pattern, value, expected): ) def test_various_not_matching_patterns(pattern, value): assert pattern.match(value, context={}) is NoMatch - with pytest.raises(ValidationError): - pattern.validate(value, context={}) @pattern diff --git a/ibis/common/tests/test_temporal.py b/ibis/common/tests/test_temporal.py index 890b312a5106..dce932383aa3 100644 --- a/ibis/common/tests/test_temporal.py +++ b/ibis/common/tests/test_temporal.py @@ -51,9 +51,9 @@ def test_interval_units(singular, plural, short): def test_interval_unit_coercions(singular, plural, short): u = IntervalUnit[singular.upper()] v = CoercedTo(IntervalUnit) - assert v.validate(singular, {}) == u - assert v.validate(plural, {}) == u - assert v.validate(short, {}) == u + assert v.match(singular, {}) == u + assert v.match(plural, {}) == u + assert v.match(short, {}) == u @pytest.mark.parametrize( @@ -70,7 +70,7 @@ def test_interval_unit_coercions(singular, plural, short): ) def test_interval_unit_aliases(alias, expected): v = CoercedTo(IntervalUnit) - assert v.validate(alias, {}) == IntervalUnit(expected) + assert v.match(alias, {}) == IntervalUnit(expected) @pytest.mark.parametrize( @@ -119,7 +119,7 @@ def test_normalize_timedelta_invalid(value, unit): def test_interval_unit_compatibility(): v = CoercedTo(IntervalUnit) for unit in itertools.chain(DateUnit, TimeUnit): - interval = v.validate(unit, {}) + interval = v.match(unit, {}) assert isinstance(interval, IntervalUnit) assert unit.value == interval.value diff --git a/ibis/expr/analysis.py b/ibis/expr/analysis.py index 32c2f0fa90eb..694a042919fc 100644 --- a/ibis/expr/analysis.py +++ b/ibis/expr/analysis.py @@ -12,8 +12,8 @@ import ibis.expr.operations.relations as rels import ibis.expr.types as ir from ibis import util +from ibis.common.annotations import ValidationError from ibis.common.exceptions import IbisTypeError, IntegrityError -from ibis.common.patterns import ValidationError # --------------------------------------------------------------------- # Some expression metaprogramming / graph transformations to support diff --git a/ibis/expr/datatypes/tests/test_core.py b/ibis/expr/datatypes/tests/test_core.py index d74dc667792f..3a8c85e032ac 100644 --- a/ibis/expr/datatypes/tests/test_core.py +++ b/ibis/expr/datatypes/tests/test_core.py @@ -11,7 +11,8 @@ from typing_extensions import Annotated import ibis.expr.datatypes as dt -from ibis.common.patterns import As, Attrs, NoMatch, Pattern, ValidationError +from ibis.common.annotations import ValidationError +from ibis.common.patterns import As, Attrs, NoMatch, Pattern from ibis.common.temporal import TimestampUnit, TimeUnit diff --git a/ibis/expr/datatypes/tests/test_parse.py b/ibis/expr/datatypes/tests/test_parse.py index 61192b09ba56..51f4d9eeb4a6 100644 --- a/ibis/expr/datatypes/tests/test_parse.py +++ b/ibis/expr/datatypes/tests/test_parse.py @@ -4,7 +4,7 @@ import pytest import ibis.expr.datatypes as dt -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError @pytest.mark.parametrize( diff --git a/ibis/expr/operations/histograms.py b/ibis/expr/operations/histograms.py index 5bdf06fcb19d..43e71077ae9c 100644 --- a/ibis/expr/operations/histograms.py +++ b/ibis/expr/operations/histograms.py @@ -7,8 +7,7 @@ import ibis.expr.datashape as ds import ibis.expr.datatypes as dt -from ibis.common.annotations import attribute -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError, attribute from ibis.common.typing import VarTuple # noqa: TCH001 from ibis.expr.operations.core import Column, Value diff --git a/ibis/expr/operations/logical.py b/ibis/expr/operations/logical.py index b22690b6cf22..bf350c8c2e1c 100644 --- a/ibis/expr/operations/logical.py +++ b/ibis/expr/operations/logical.py @@ -7,9 +7,8 @@ import ibis.expr.datashape as ds import ibis.expr.datatypes as dt import ibis.expr.rules as rlz -from ibis.common.annotations import attribute +from ibis.common.annotations import ValidationError, attribute from ibis.common.exceptions import IbisTypeError -from ibis.common.patterns import ValidationError from ibis.common.typing import VarTuple # noqa: TCH001 from ibis.expr.operations.core import Binary, Column, Unary, Value from ibis.expr.operations.generic import _Negatable diff --git a/ibis/expr/operations/tests/test_generic.py b/ibis/expr/operations/tests/test_generic.py index 5fffc8d9417a..1e60b164a8bc 100644 --- a/ibis/expr/operations/tests/test_generic.py +++ b/ibis/expr/operations/tests/test_generic.py @@ -5,12 +5,7 @@ import ibis.expr.datashape as ds import ibis.expr.datatypes as dt import ibis.expr.operations as ops -from ibis.common.patterns import ( - CoercedTo, - GenericCoercedTo, - Pattern, - ValidationError, -) +from ibis.common.patterns import CoercedTo, GenericCoercedTo, NoMatch, Pattern @pytest.mark.parametrize( @@ -32,58 +27,56 @@ def test_literal_coercion_type_inference(value, dtype): def test_coerced_to_literal(): p = CoercedTo(ops.Literal) one = ops.Literal(1, dt.int8) - assert p.validate(ops.Literal(1, dt.int8), {}) == one - assert p.validate(1, {}) == one - assert p.validate(False, {}) == ops.Literal(False, dt.boolean) + assert p.match(ops.Literal(1, dt.int8), {}) == one + assert p.match(1, {}) == one + assert p.match(False, {}) == ops.Literal(False, dt.boolean) p = GenericCoercedTo(ops.Literal[dt.Int8]) - assert p.validate(ops.Literal(1, dt.int8), {}) == one + assert p.match(ops.Literal(1, dt.int8), {}) == one p = Pattern.from_typehint(ops.Literal[dt.Int8]) assert p == GenericCoercedTo(ops.Literal[dt.Int8]) one = ops.Literal(1, dt.int16) - with pytest.raises(ValidationError): - p.validate(one, {}) + assert p.match(one, {}) is NoMatch def test_coerced_to_value(): one = ops.Literal(1, dt.int8) p = Pattern.from_typehint(ops.Value) - assert p.validate(1, {}) == one + assert p.match(1, {}) == one p = Pattern.from_typehint(ops.Value[dt.Int8, ds.Any]) - assert p.validate(1, {}) == one + assert p.match(1, {}) == one p = Pattern.from_typehint(ops.Value[dt.Int8, ds.Scalar]) - assert p.validate(1, {}) == one + assert p.match(1, {}) == one p = Pattern.from_typehint(ops.Value[dt.Int8, ds.Columnar]) - with pytest.raises(ValidationError): - p.validate(1, {}) + assert p.match(1, {}) is NoMatch # dt.Integer is not instantiable so it will be only used for checking # that the produced literal has any integer datatype p = Pattern.from_typehint(ops.Value[dt.Integer, ds.Any]) - assert p.validate(1, {}) == one + assert p.match(1, {}) == one # same applies here, the coercion itself will use only the inferred datatype # but then the result is checked against the given typehint p = Pattern.from_typehint(ops.Value[dt.Int8 | dt.Int16, ds.Any]) - assert p.validate(1, {}) == one - assert p.validate(128, {}) == ops.Literal(128, dt.int16) + assert p.match(1, {}) == one + assert p.match(128, {}) == ops.Literal(128, dt.int16) p1 = Pattern.from_typehint(ops.Value[dt.Int8, ds.Any]) p2 = Pattern.from_typehint(ops.Value[dt.Int16, ds.Scalar]) - assert p1.validate(1, {}) == one + assert p1.match(1, {}) == one # this is actually supported by creating an explicit dtype # in Value.__coerce__ based on the `T` keyword argument - assert p2.validate(1, {}) == ops.Literal(1, dt.int16) - assert p2.validate(128, {}) == ops.Literal(128, dt.int16) + assert p2.match(1, {}) == ops.Literal(1, dt.int16) + assert p2.match(128, {}) == ops.Literal(128, dt.int16) p = p1 | p2 - assert p.validate(1, {}) == one + assert p.match(1, {}) == one @pytest.mark.pandas diff --git a/ibis/expr/operations/udf.py b/ibis/expr/operations/udf.py index cb0f6af9bff6..304663dbe691 100644 --- a/ibis/expr/operations/udf.py +++ b/ibis/expr/operations/udf.py @@ -128,9 +128,9 @@ def make_node( arg = rlz.ValueOf(dt.dtype(raw_dtype)) if (default := param.default) is EMPTY: - fields[name] = Argument.required(validator=arg) + fields[name] = Argument.required(pattern=arg) else: - fields[name] = Argument.default(validator=arg, default=default) + fields[name] = Argument.default(pattern=arg, default=default) fields["dtype"] = dt.dtype(return_annotation) diff --git a/ibis/expr/tests/test_schema.py b/ibis/expr/tests/test_schema.py index 69078194c980..759c3aa69693 100644 --- a/ibis/expr/tests/test_schema.py +++ b/ibis/expr/tests/test_schema.py @@ -338,7 +338,7 @@ class ObjectWithSchema(Annotable): def test_schema_is_coercible(): s = sch.Schema({"a": dt.int64, "b": dt.Array(dt.int64)}) - assert CoercedTo(sch.Schema).validate(PreferenceA, {}) == s + assert CoercedTo(sch.Schema).match(PreferenceA, {}) == s o = ObjectWithSchema(schema=PreferenceA) assert o.schema == s diff --git a/ibis/expr/types/core.py b/ibis/expr/types/core.py index f1de94bb63c2..04f4a1e1082d 100644 --- a/ibis/expr/types/core.py +++ b/ibis/expr/types/core.py @@ -12,7 +12,7 @@ from ibis.common.grounds import Immutable from ibis.config import _default_backend, options from ibis.util import experimental -from ibis.common.patterns import ValidationError, Coercible, CoercionError +from ibis.common.annotations import ValidationError from rich.jupyter import JupyterMixin from ibis.common.patterns import Coercible, CoercionError diff --git a/ibis/tests/expr/test_analytics.py b/ibis/tests/expr/test_analytics.py index 725680f6169f..c6c57c3b2e5a 100644 --- a/ibis/tests/expr/test_analytics.py +++ b/ibis/tests/expr/test_analytics.py @@ -17,7 +17,7 @@ import ibis import ibis.expr.types as ir -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError from ibis.tests.expr.mocks import MockBackend from ibis.tests.util import assert_equal diff --git a/ibis/tests/expr/test_decimal.py b/ibis/tests/expr/test_decimal.py index b2d545438b22..6bba31443373 100644 --- a/ibis/tests/expr/test_decimal.py +++ b/ibis/tests/expr/test_decimal.py @@ -7,7 +7,7 @@ import ibis import ibis.expr.datatypes as dt import ibis.expr.types as ir -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError from ibis.expr import api diff --git a/ibis/tests/expr/test_operations.py b/ibis/tests/expr/test_operations.py index c1eb3d7415f1..8f44b5906a97 100644 --- a/ibis/tests/expr/test_operations.py +++ b/ibis/tests/expr/test_operations.py @@ -11,7 +11,7 @@ import ibis.expr.operations as ops import ibis.expr.rules as rlz import ibis.expr.types as ir -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError t = ibis.table([("a", "int64")], name="t") diff --git a/ibis/tests/expr/test_table.py b/ibis/tests/expr/test_table.py index ce8555b5d233..a489f03c66e8 100644 --- a/ibis/tests/expr/test_table.py +++ b/ibis/tests/expr/test_table.py @@ -20,8 +20,8 @@ import ibis.selectors as s from ibis import _ from ibis import literal as L +from ibis.common.annotations import ValidationError from ibis.common.exceptions import RelationError -from ibis.common.patterns import ValidationError from ibis.expr import api from ibis.expr.types import Column, Table from ibis.tests.expr.mocks import MockAlchemyBackend, MockBackend diff --git a/ibis/tests/expr/test_udf.py b/ibis/tests/expr/test_udf.py index a13d4c6a89e7..e59be3f9337c 100644 --- a/ibis/tests/expr/test_udf.py +++ b/ibis/tests/expr/test_udf.py @@ -6,7 +6,7 @@ import ibis.expr.datatypes as dt import ibis.expr.operations as ops import ibis.expr.types as ir -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError @pytest.fixture diff --git a/ibis/tests/expr/test_value_exprs.py b/ibis/tests/expr/test_value_exprs.py index ca34226da4cf..611a99e08540 100644 --- a/ibis/tests/expr/test_value_exprs.py +++ b/ibis/tests/expr/test_value_exprs.py @@ -22,9 +22,9 @@ import ibis.expr.operations as ops import ibis.expr.types as ir from ibis import _, literal +from ibis.common.annotations import ValidationError from ibis.common.collections import frozendict from ibis.common.exceptions import IbisTypeError -from ibis.common.patterns import ValidationError from ibis.expr import api from ibis.tests.util import assert_equal diff --git a/ibis/tests/expr/test_window_frames.py b/ibis/tests/expr/test_window_frames.py index 2fcfc8a4973e..6a657c78e749 100644 --- a/ibis/tests/expr/test_window_frames.py +++ b/ibis/tests/expr/test_window_frames.py @@ -10,8 +10,9 @@ import ibis.expr.datashape as ds import ibis.expr.datatypes as dt import ibis.expr.operations as ops +from ibis.common.annotations import ValidationError from ibis.common.exceptions import IbisInputError, IbisTypeError -from ibis.common.patterns import Pattern, ValidationError +from ibis.common.patterns import NoMatch, Pattern def test_window_boundary(): @@ -35,21 +36,19 @@ def test_window_boundary_typevars(): p = Pattern.from_typehint(ops.WindowBoundary[dt.Integer, ds.Any]) b = ops.WindowBoundary(5, preceding=False) - assert p.validate(b, {}) == b - with pytest.raises(ValidationError): - p.validate(ops.WindowBoundary(5.0, preceding=False), {}) - with pytest.raises(ValidationError): - p.validate(ops.WindowBoundary(lit, preceding=True), {}) + assert p.match(b, {}) == b + assert p.match(ops.WindowBoundary(5.0, preceding=False), {}) is NoMatch + assert p.match(ops.WindowBoundary(lit, preceding=True), {}) is NoMatch p = Pattern.from_typehint(ops.WindowBoundary[dt.Interval, ds.Any]) b = ops.WindowBoundary(lit, preceding=True) - assert p.validate(b, {}) == b + assert p.match(b, {}) == b def test_window_boundary_coercions(): RowsWindowBoundary = ops.WindowBoundary[dt.Integer, ds.Any] p = Pattern.from_typehint(RowsWindowBoundary) - assert p.validate(1, {}) == RowsWindowBoundary(ops.Literal(1, dtype=dt.int8), False) + assert p.match(1, {}) == RowsWindowBoundary(ops.Literal(1, dtype=dt.int8), False) def test_window_builder_rows(): diff --git a/ibis/tests/test_config.py b/ibis/tests/test_config.py index 9ead874c4666..5640764dd994 100644 --- a/ibis/tests/test_config.py +++ b/ibis/tests/test_config.py @@ -2,7 +2,7 @@ import pytest -from ibis.common.patterns import ValidationError +from ibis.common.annotations import ValidationError from ibis.config import options