from __future__ import annotations
import dataclasses
import itertools
import typing
import typing_inspect
from .typing_tools import *
__all__ = [
"Expression",
"expression",
"PlaceholderExpression",
"IteratedPlaceholder",
"create_iterated_placeholder",
"clone_expression",
]
T = typing.TypeVar("T")
T_expression = typing.TypeVar("T_expression", bound="Expression")
CALLABLE = typing.TypeVar("CALLABLE", bound=typing.Callable)
[docs]@dataclasses.dataclass(eq=False, repr=False)
class Expression(GenericCheck):
"""
Top level object.
Subclass this type and provide relevent methods for your type. Do not add any fields.
Properties:
Calling the function, after replacing the typevars in it (if it is a bound method),
with the args and kwargs should resualt in an equivalent expression:
replace_fn_typevars(self.function, self.typevars)(*self.args, **self.kwargs) == self
The return type of the function, inferred by replacing the typevars in and with these args and kwargs,
should match the type of the expression. If the return type of the function is not subclass of expression,
then this should be a PlaceholderExpression of that type.
"""
function: typing.Callable
args: typing.List[object]
kwargs: typing.Dict[str, object]
def __str__(self):
arg_strings = (str(arg) for arg in self.args)
kwarg_strings = (f"{str(k)}={str(v)}" for k, v in self.kwargs.items())
return f"{self._function_str}({', '.join(itertools.chain(arg_strings, kwarg_strings))})"
@property
def _function_str(self):
return getattr(self.function, "__qualname__", str(self.function))
@property
def _type_str(self):
t = get_type(self)
if isinstance(t, typing._GenericAlias): # type: ignore
return repr(t)
return typing._type_repr(t) # type: ignore
def __repr__(self):
return (
f"{self._type_str}({self.function}, {repr(self.args)}, {repr(self.kwargs)})"
)
def _map(
self: T_expression,
fn: typing.Callable[[T], T],
function_fn: typing.Callable[[CALLABLE], CALLABLE] = None,
type_fn: typing.Callable[
[typing.Type[T_expression]], typing.Type[T_expression]
] = None,
) -> T_expression:
"""
Map a function on all args and recreate function.
"""
new_type: typing.Type[T_expression] = typing_inspect.get_generic_type(self)
new_expr = type(self)(
function=function_fn(self.function) if function_fn else self.function, # type: ignore
args=[fn(typing.cast(T, arg)) for arg in self.args],
kwargs={k: fn(typing.cast(T, v)) for k, v in self.kwargs.items()},
)
# copy generic class
if hasattr(self, "__orig_class__"):
new_expr.__orig_class__ = ( # type: ignore
type_fn(self.__orig_class__) if type_fn else self.__orig_class__ # type: ignore
)
return new_expr
def __eq__(self, value) -> bool:
if not isinstance(value, Expression):
return False
return (
self.function == value.function
and self.args == value.args
and self.kwargs == value.kwargs
)
[docs]def clone_expression(expr: T) -> T:
if isinstance(expr, Expression):
return expr._map(clone_expression) # type: ignore
return expr
[docs]class PlaceholderExpression(Expression, OfType[T], typing.Generic[T]):
"""
An expression that represents a type of `T`, for example T could be `int`.
This is needed when a function returns a non expression type, it still has to return
an expression under the covers until it has been replaced.
It is also needed when using Wildcards in expressions when doing matching.
"""
def __iter__(self):
return iter((create_iterated_placeholder(self),)) # type: ignore
def extract_expression_type(t: typing.Type) -> typing.Type[Expression]:
if issubclass(t, Expression):
return t
return PlaceholderExpression[t] # type: ignore
T_callable = typing.TypeVar("T_callable", bound=typing.Callable)
def wrapper(fn, args, kwargs, return_type):
expr_return_type = extract_expression_type(return_type)
# Clone expression when returning it, so if if we mutate child expression
# those one won't be mutated
return clone_expression(expr_return_type(fn, list(args), kwargs))
[docs]def expression(fn: T_callable) -> T_callable:
"""
Creates an expresion object by wrapping a Python function and providing a function
that will take in the args and return an expression of the right type.
"""
return typing.cast(T_callable, infer(fn, wrapper))
[docs]class IteratedPlaceholder(Expression, ExpandedType, typing.Generic[T]):
pass
[docs]@expression
def create_iterated_placeholder(
i: PlaceholderExpression[typing.Sequence[T]],
) -> IteratedPlaceholder[T]:
"""
If a placeholder is of an iterable T, calling iter on it should return an iterable placeholder of the inner type.
Used in matching with variable args.
"""
...