__all__ = (
"CovariantMeta",
"parametric",
"type_parameter",
"type_nonparametric",
"type_unparametrized",
"kind",
"Kind",
)
import contextlib
from typing import TypeVar
import beartype.door
from beartype.roar import BeartypeDoorNonpepException
from ._dispatcher import Dispatcher
from ._function import _owner_transfer
from ._type import resolve_type_hint
from ._util import TypeHint
from .repr import repr_short
T = TypeVar("T")
_dispatch = Dispatcher()
class ParametricTypeMeta(type):
"""Parametric types can be instantiated with indexing.
A concrete parametric type can be instantiated by calling `Type[Par1, Par2]`.
If `Type(Arg1, Arg2, **kw_args)` is called, this returns
`Type[type(Arg1), type(Arg2)](Arg1, Arg2, **kw_args)`.
"""
def __getitem__(cls, p):
if not cls.concrete:
# Initialise the type parameters. This can perform, e.g., validation.
p = p if isinstance(p, tuple) else (p,) # Ensure that it is a tuple.
p = cls.__init_type_parameter__(*p)
# Type parameter has been initialised! Proceed to construct the type.
p = p if isinstance(p, tuple) else (p,) # Again ensure that it is a tuple.
return cls.__new__(cls, *p)
else:
raise TypeError("Cannot specify type parameters. This type is concrete.")
def __concrete_class__(cls, *args, **kw_args):
"""If `cls` is not a concrete class, infer the type parameters and return a
concrete class. If `cls` is already a concrete class, simply return it.
Args:
*args: Positional arguments passed to the `__init__` method.
**kw_args: Keyword arguments passed to the `__init__` method.
Returns:
type: A concrete class.
"""
if getattr(cls, "parametric", False) and not cls.concrete:
type_parameter = cls.__infer_type_parameter__(*args, **kw_args)
cls = cls[type_parameter]
return cls
def __init_type_parameter__(cls, *ps):
"""Function called to initialise the type parameters.
The default behaviour is to just return `ps`.
Args:
*ps (object): Type parameters.
Returns:
object: Initialised type parameters.
"""
return ps
def __infer_type_parameter__(cls, *args, **kw_args):
"""Function called when the constructor of this parametric type is called
before the parameters have been specified.
The default behaviour is to take as parameters the type of every argument,
but this behaviour can be overridden by redefining this function on the
metaclass.
Args:
*args: Positional arguments passed to the `__init__` method.
**kw_args: Keyword arguments passed to the `__init__` method.
Returns:
type or tuple[type]: A type or tuple of types.
"""
type_parameter = tuple(type(arg) for arg in args)
if len(type_parameter) == 1:
type_parameter = type_parameter[0]
return type_parameter
@property
def parametric(cls):
"""bool: Check whether the type is a parametric type."""
return getattr(cls, "_parametric", False)
@property
def concrete(cls):
"""bool: Check whether the parametric type is instantiated or not."""
if cls.parametric:
return getattr(cls, "_concrete", False)
else:
raise RuntimeError(
"Cannot check whether a non-parametric type is instantiated or not."
)
@property
def type_parameter(cls):
"""object: Get the type parameter. Parametric type must be instantiated."""
if cls.concrete:
return cls._type_parameter
else:
raise RuntimeError(
"Cannot get the type parameter of non-instantiated parametric type."
)
def _default_le_type_par(p_left: TypeHint | object, p_right: TypeHint | object) -> bool:
if is_type(p_left) and is_type(p_right):
p_left = beartype.door.TypeHint(resolve_type_hint(p_left))
p_right = beartype.door.TypeHint(resolve_type_hint(p_right))
return p_left <= p_right
else:
return p_left == p_right
[docs]
def parametric(original_class=None):
"""A decorator for parametric classes.
When the constructor of this parametric type is called before the type parameter
has been specified, the type parameter is inferred from the arguments of the
constructor by calling `__inter_type_parameter__`. The default implementation is
shown here, but it is possible to override it::
@classmethod
def __infer_type_parameter__(cls, *args, **kw_args) -> tuple:
return tuple(type(arg) for arg in args)
After the type parameter is given or inferred, `__init_type_parameter__` is called.
Again, the default implementation is show here, but it is possible to override it::
@classmethod
def __init_type_parameter__(cls, *ps) -> tuple:
return ps
To determine which one instance of a parametric class is a subclass of another,
the type parameters are compared with `__le_type_parameter__`::
@classmethod
def __le_type_parameter__(cls, left, right) -> bool:
# Is `left <= right`?
...
"""
original_meta = type(original_class)
# Make a metaclass that derives from both the metaclass of `original_meta` and
# `CovariantMeta`, but make sure not to insert `CovariantMeta` twice, because that
# will error.
if CovariantMeta in original_meta.__mro__:
bases = (original_meta,)
name = original_meta.__name__
else:
bases = (CovariantMeta, original_meta)
name = f"CovariantMeta[{repr_short(original_meta)}]"
def __call__(cls, *args, **kw_args):
cls = cls.__concrete_class__(*args, **kw_args)
return original_meta.__call__(cls, *args, **kw_args)
def __instancecheck__(cls, instance):
# An implementation of `__instancecheck__` is necessary to ensure that
# `isinstance(A[SubType](), A[Type])`. `CovariantMeta` comes first in the MRO,
# but the implementation of `__instancecheck__` should be taken from
# `original_meta` if it exists. The implementation of `CovariantMeta` should be
# used as a fallback. Note that `original_meta.__instancecheck__` always exists.
# We check that it is not equal to the default `type.__instancecheck__`.
if original_meta.__instancecheck__ != type.__instancecheck__:
return original_meta.__instancecheck__(cls, instance)
else:
return CovariantMeta.__instancecheck__(cls, instance)
meta = type(
name,
bases,
{
"__call__": __call__,
"__instancecheck__": __instancecheck__,
},
)
subclasses = {}
def __new__(cls, *ps):
# Only create a new subclass if it doesn't exist already.
if ps not in subclasses:
def __new__(cls, *args, **kw_args):
return original_class.__new__(cls)
# Create subclass.
name = original_class.__name__
name += "[" + ", ".join(repr_short(p) for p in ps) + "]"
subclass = meta(
name,
(parametric_class,),
{"__new__": __new__},
)
subclass._parametric = True
subclass._concrete = True
subclass._type_parameter = ps[0] if len(ps) == 1 else ps
subclass.__module__ = original_class.__module__
# Attempt to correct docstring.
with contextlib.suppress(AttributeError):
subclass.__doc__ = original_class.__doc__
subclasses[ps] = subclass
return subclasses[ps]
def __init_subclass__(cls, **kw_args):
cls._parametric = False
# If the subclass has the same `__new__` as `ParametricClass`, then we should
# replace it with the `__new__` of `Class`. If the user already defined another
# `__new__`, then everything is fine.
if cls.__new__ is __new__:
def class_new(cls, *args, **kw_args):
return original_class.__new__(cls)
cls.__new__ = class_new
super(original_class, cls).__init_subclass__(**kw_args)
def __class_nonparametric__(cls):
"""Return the non-parametric type of an object.
:mod:`plum.parametric` produces parametric subtypes of classes. This
method can be used to get the original non-parametric type of an object.
See Also
--------
:func:`plum.type_nonparametric`
The more-user-friendly function equivalent of this method.
:func:`plum.type_unparametrized`
A function that returns the non-concrete, but still parametric, type
of an object.
Examples
--------
In this example we will demonstrate how to retrieve the original
non-parametric class from a :func:`plum.parametric` decorated class.
:func:`plum.parametric` defines a parametric class of the same name as
the original class, and then creates a subclass of the original class
with the type parameter inferred from the arguments of the constructor.
>>> from plum import parametric
>>> class Obj:
... @classmethod
... def __infer_type_parameter__(cls, *arg):
... return type(arg[0])
...
... def __init__(self, x):
... self.x = x
...
... def __repr__(self):
... return f"Obj({self.x})"
>>> PObj = parametric(Obj)
>>> PObj.mro()
[<class 'plum...Obj'>, <class 'plum...Obj'>, <class 'object'>]
Note that the class `Obj` appears twice in the MRO. The first one is the
parametric class, and the second one is the non-parametric class. The
non-parametric class is the original class that was passed to the
``parametric`` decorator.
Rather than navigating the MRO, we can get the non-parametric class of
an object by calling the ``__class_nonparametric__`` method.
>>> PObj(1).__class_nonparametric__() is Obj
True
"""
return original_class
def __class_unparametrized__(cls):
"""Return the unparametrized type of an object.
:mod:`plum.parametric` produces parametric subtypes of classes. This
method can be used to get the un-parametrized type of an object.
See Also
--------
:func:`plum.type_unparametrized`
The more-user-friendly function equivalent of this method.
:func:`plum.type_nonparametric`
A function to get the non-parametric type of an object.
Examples
--------
In this example we will demonstrate how to retrieve the original
non-parametric class from a :func:`plum.parametric` decorated class.
:func:`plum.parametric` defines a parametric class of the same name as
the original class, and then creates a subclass of the original class
with the type parameter inferred from the arguments of the constructor.
>>> from plum import parametric
>>> class Obj:
... @classmethod
... def __infer_type_parameter__(cls, *arg):
... return type(arg[0])
...
... def __init__(self, x):
... self.x = x
...
... def __repr__(self):
... return f"Obj({self.x})"
>>> PObj = parametric(Obj)
>>> PObj.mro()
[<class 'plum...Obj'>, <class 'plum...Obj'>, <class 'object'>]
Note that the class `Obj` appears twice in the MRO. The first one is the
non-concrete parametric class, and the second one is the non-parametric
class. Rather than navigating the MRO, we can get the non-concrete
parametric class of an object by calling the
``__class_unparametrized__`` method.
>>> PObj(1).__class_unparametrized__() is PObj
True
Note that this is still NOT the 'original'
non-:func:`plum.parametric`-wrapped type. This is the type that is
wrapped by :mod:`plum.parametric`, but without the inferred type
parameter(s).
"""
return parametric_class
# Create parametric class.
parametric_class = meta(
original_class.__name__,
(original_class,),
{
"__new__": __new__,
"__init_subclass__": __init_subclass__,
"__class_nonparametric__": __class_nonparametric__,
"__class_unparametrized__": __class_unparametrized__,
},
)
parametric_class._parametric = True
parametric_class._concrete = False
parametric_class.__module__ = original_class.__module__
# When dispatch is used in methods of `original_class`, because we return
# `parametric_class`, `parametric_class` will be inferred as the owner of those
# functions. This is erroneous, because the owner should be `original_class`. What
# will happen is that `original_class` will be the next in the MRO, which means
# that, whenever a `NotFoundLookupError` happens, the method will try itself again,
# resulting in an infinite loop. To prevent this from happening, we must adjust the
# owner.
_owner_transfer[parametric_class] = original_class
# Attempt to correct docstring.
with contextlib.suppress(AttributeError):
parametric_class.__doc__ = original_class.__doc__
return parametric_class
def is_concrete(t):
"""Check if a type `t` is a concrete instance of a parametric type.
Args:
t (type): Type to check.
Returns:
bool: `True` if `t` is a concrete instance of a parametric type and `False`
otherwise.
"""
return getattr(t, "parametric", False) and t.concrete
def is_type(x: object, /) -> bool:
"""Check whether `x` is a type or a type hint.
Under the hood, this attempts to construct a :class:`beartype.door.TypeHint` from
`x`. If successful, then `x` is deemed a type or type hint.
Args:
x (object): Object to check.
Returns:
bool: Whether `x` is a type or a type hint.
"""
try:
beartype.door.TypeHint(x)
except BeartypeDoorNonpepException:
return False
else:
return True
[docs]
def type_parameter(x: object, /) -> object:
"""Get the type parameter of concrete parametric type or an instance of a concrete
parametric type.
Args:
x (object): Concrete parametric type or instance thereof.
Returns:
object: Type parameter.
"""
t = x if is_type(x) else type(x)
if hasattr(t, "parametric"):
return t.type_parameter
raise ValueError(
f"`{x}` is not a concrete parametric type or an instance of a"
f" concrete parametric type."
)
[docs]
def type_nonparametric(q: T, /) -> type[T]:
"""Return the non-parametric type of an object.
:mod:`plum.parametric` produces parametric subtypes of classes. This method
can be used to get the original non-parametric type of an object.
See Also
--------
:func:`plum.type_unparametrized`
A function that returns the non-concrete, but still parametric, type of
an object.
Examples
--------
In this example we will demonstrate how to retrieve the original
non-parametric class from a :func:`plum.parametric` decorated class.
:func:`plum.parametric` defines a parametric class of the same name as the
original class, and then creates a subclass of the original class with the
type parameter inferred from the arguments of the constructor.
>>> from plum import parametric
>>> class Obj:
... @classmethod
... def __infer_type_parameter__(cls, *arg):
... return type(arg[0])
...
... def __init__(self, x):
... self.x = x
...
... def __repr__(self):
... return f"Obj({self.x})"
>>> PObj = parametric(Obj)
>>> pobj = PObj(1)
>>> type(pobj).mro()
[<class 'plum...Obj[int]'>, <class 'plum...Obj'>, <class 'plum...Obj'>,
<class 'object'>]
Note that the class `Obj` appears twice in the MRO. The first one is the
parametric class, and the second one is the non-parametric class. The
non-parametric class is the original class that was passed to the
``parametric`` decorator.
Rather than navigating the MRO, we can get the non-parametric class of an
object by calling ``type_nonparametric`` function.
>>> type(pobj) is PObj[int]
True
>>> type(pobj) is PObj
False
>>> type(pobj) is Obj
False
>>> type_nonparametric(pobj) is PObj[int]
False
>>> type_nonparametric(pobj) is PObj
False
>>> type_nonparametric(pobj) is Obj
True
"""
return (
q.__class_nonparametric__()
if isinstance(type(q), ParametricTypeMeta)
else type(q)
)
[docs]
def type_unparametrized(q: T, /) -> type[T]:
"""Return the unparametrized type of an object.
:mod:`plum.parametric` produces parametric subtypes of classes. This
function can be used to get the un-parametrized type of an object.
This function also works for normal, :mod:`plum.parametric`-wrapped classes.
See Also
--------
:func:`plum.type_nonparametric`
A function to get the non-parametric type of an object.
Examples
--------
In this example we will demonstrate how to retrieve the original
non-parametric class from a :func:`plum.parametric` decorated class.
:func:`plum.parametric` defines a parametric class of the same name as
the original class, and then creates a subclass of the original class
with the type parameter inferred from the arguments of the constructor.
>>> from plum import parametric
>>> class Obj:
... @classmethod
... def __infer_type_parameter__(cls, *arg):
... return type(arg[0])
...
... def __init__(self, x):
... self.x = x
...
... def __repr__(self):
... return f"Obj({self.x})"
>>> PObj = parametric(Obj)
>>> pobj = PObj(1)
>>> type(pobj).mro()
[<class 'plum...Obj[int]'>, <class 'plum...Obj'>,
<class 'plum...Obj'>, <class 'object'>]
Note that the class `Obj` appears twice in the MRO. The first one is the
non-concrete parametric class, and the second one is the non-parametric
class. Rather than navigating the MRO, we can get the non-concrete
parametric class of an object by calling the
``type_unparametrized`` function.
>>> type(pobj) is PObj[int]
True
>>> type(pobj) is PObj
False
>>> type(pobj) is Obj
False
>>> type_unparametrized(pobj) is PObj[int]
False
>>> type_unparametrized(pobj) is PObj
True
>>> type_unparametrized(pobj) is Obj
False
Note that this is still NOT the 'original'
non-:func:`plum.parametric`-wrapped type. This is the type that is
wrapped by :mod:`plum.parametric`, but without the inferred type
parameter(s).
"""
typ = type(q)
return q.__class_unparametrized__() if isinstance(typ, ParametricTypeMeta) else typ
[docs]
def kind(SuperClass=object):
"""Create a parametric wrapper type for dispatch purposes.
Args:
SuperClass (type): Super class.
Returns:
object: New parametric type wrapper.
"""
@parametric
class Kind(SuperClass):
def __init__(self, *xs):
self.xs = xs
def get(self):
return self.xs[0] if len(self.xs) == 1 else self.xs
return Kind
Kind = kind() #: A default kind provided for convenience.