An attempt to emulate C++ templates in python, complete with non-type template parameters.
Created
November 15, 2019 16:04
-
-
Save eric-wieser/4b6d536f1b6e38ae5d866c529674cfee to your computer and use it in GitHub Desktop.
C++-style templated types for Python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class TemplateParameter: | |
""" | |
A parameter used in a template specification. | |
Name is used only for display purposes | |
""" | |
def __init__(self, name): | |
self.name = name | |
def __repr__(self): | |
return self.name | |
def _make_template_name(name, args) -> str: | |
return "{}[{}]".format( | |
name, ', '.join( | |
a.name if isinstance(a, TemplateParameter) else repr(a) | |
for a in args | |
) | |
) | |
class _TemplateSpec: | |
""" | |
Return type of ``Template[K]``. | |
This should be used as a metaclass, see :class:`Template` for documentation. | |
""" | |
def __init__(self, params): | |
assert all(isinstance(p, TemplateParameter) for p in params) | |
self.params = params | |
def __repr__(self): | |
return make_template_name('Template', self.params) | |
def __call__(self, name, bases, dict): | |
return Template(name, bases, dict, params=self.params) | |
class _TemplateMeta(type): | |
""" Meta-meta-class helper to enable ``Template[K]`` """ | |
def __getitem__(cls, items) -> _TemplateSpec: | |
if not isinstance(items, tuple): | |
items = (items,) | |
return _TemplateSpec(items) | |
class Template(type, metaclass=_TemplateMeta): | |
""" | |
The main mechanism for declaring template types. | |
The result of `type(SomeTemplateClass)`. | |
Use as: | |
T = TemplateParameter('T') | |
N = TemplateParameter('N') | |
# Sequence is a template taking one argument | |
class Sequence(metaclass=Template[T]): | |
pass | |
# MyList is a template taking two arguments, where the first is passed | |
# down for use in the `Sequence` base class. | |
class MyList(Sequence[T], metaclass=Template[T, N]): | |
def __init__(self, value=None): | |
# self.__args contains the arguments values | |
T_val = self.__args[T] | |
N_val = self.__args[N] | |
if value is None: | |
self.value = [T_val()] * N_val | |
else: | |
assert len(value) == N_val | |
self.value = value | |
def __newinferred__(cls, value): | |
''' This is used to infer type arguments ''' | |
T_val = type(value[0]) | |
N_val = len(value) | |
return cls[T_val, N_val](value) | |
assert isinstance(MyList, Template) | |
m = MyList[int, 3]() | |
assert isinstance(m, MyList[int, 3]) | |
assert isinstance(m, MyList) | |
assert isinstance(m, Sequence[int]) | |
assert isinstance(m, Sequence) | |
m = MyList(["Hello", "World"]) | |
assert isinstance(m, MyList[str, 2]) | |
""" | |
def __new__(metacls, name, bases, dict_, params): | |
bases_no_templates = [] | |
base_template_exprs = [] | |
for b in bases: | |
if isinstance(b, TemplateExpression): | |
bases_no_templates.append(b.template._base) | |
base_template_exprs.append(b) | |
else: | |
bases_no_templates.append(b) | |
base = type(name, tuple(bases_no_templates), dict_) | |
return type.__new__(metacls, name, (), dict( | |
_base=base, | |
_base_exprs=tuple(base_template_exprs), | |
_instantiations={}, | |
__params__=params | |
)) | |
def __init__(cls, *args, **kwargs): pass | |
def __subclasscheck__(cls, subclass): | |
for c in subclass.mro(): | |
if getattr(c, '__template__') == cls: | |
return True | |
return False | |
def __instancecheck__(cls, instance): | |
return cls.__subclasscheck__(type(instance)) | |
def __getitem__(cls, items): | |
if not isinstance(items, tuple): | |
items = (items,) | |
if len(items) != len(cls.__params__): | |
raise TypeError( | |
"{} expected {} template arguments ({}), got {}".format( | |
cls, | |
len(cls.__params__), cls.__params__, len(items) | |
) | |
) | |
if any(isinstance(i, TemplateParameter) for i in items): | |
return TemplateExpression(cls, items) | |
else: | |
return TemplateExpression(cls, items)._substitute({}) | |
def __call__(cls, *args, **kwargs): | |
try: | |
f = cls._base.__newinferred__ | |
except AttributeError: | |
pass | |
else: | |
i = f(cls, *args, **kwargs) | |
if not isinstance(i, cls): | |
raise TypeError( | |
"__newinferred__ did not return a {}, instead a {}".format(cls, type(i)) | |
) | |
return i | |
raise TypeError("No type arguments passed to {}, and __newinferred__ is not defined".format(cls)) | |
class TemplateExpression: | |
""" | |
The result of an expression like ``SomeTemplate[T]`` or ``SomeTemplate[T, 1]``. | |
Note that this is not used if the full set of arguments are specified. | |
""" | |
def __init__(self, template, args): | |
self.template = template | |
self.args = args | |
def __repr__(self): | |
return _make_template_name(self.template.__qualname__, self.args) | |
def _substitute(self, arg_values: dict): | |
""" | |
Replace remaining TemplateParameters with values. | |
Used during template instantiation, don't call directly. | |
""" | |
args = tuple([ | |
arg_values[a] if isinstance(a, TemplateParameter) else a | |
for a in self.args | |
]) | |
try: | |
return self.template._instantiations[args] | |
except KeyError: | |
arg_dict = { | |
p: a | |
for p, a, in zip(self.template.__params__, args) | |
} | |
bases = tuple( | |
expr._substitute(arg_dict) | |
for expr in self.template._base_exprs | |
) + (self.template._base,) | |
inst = type( | |
_make_template_name(self.template.__qualname__, args), | |
bases, { | |
'_{}__args'.format(self.template.__name__): arg_dict, | |
'__template__': self.template, | |
} | |
) | |
self.template._instantiations[args] = inst | |
return inst |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
K = TemplateParameter('K') | |
V = TemplateParameter('V') | |
class Foo(metaclass=Template[K,V]): | |
@classmethod | |
def get_k(cls): | |
return cls.__args[K] | |
@classmethod | |
def get_v(cls): | |
print(__class__, "class") | |
return cls.__args[V] | |
print("FOO") | |
print(Foo.mro()) | |
print(Foo[K,K]) | |
# print(type(Foo[K, K]), Template[K]) | |
# assert Foo[1, 2].bar() == 2 | |
assert isinstance(Foo, Template) | |
assert isinstance(Foo[K, 2], TemplateExpression) | |
assert issubclass(Foo[1, 2], Foo) | |
assert isinstance(Foo[1, 2](), Foo) | |
assert Foo[1, 2].get_k() == 1 | |
assert Foo[1, 2].get_v() == 2 | |
class Bar(Foo[1, 2], metaclass=Template[K]): | |
pass | |
assert Bar[3].get_k() == 1 | |
assert Bar[3].get_v() == 2 | |
class Baz(Foo[K,K], metaclass=Template[K]): | |
pass | |
assert Baz[3].get_k() == 3 | |
assert Baz[3].get_v() == 3 | |
print("Baz.mro", Baz[3].mro()) | |
class Outer(Baz[K], Foo[1, 2], metaclass=Template[K]): | |
def __init__(self, s): | |
self.s = s | |
def __newinferred__(cls, s): | |
return Outer[len(s)](s) | |
assert Outer[4].get_k() == 4 | |
assert Outer[4].get_v() == 4 | |
assert Outer[4] is Outer[4] | |
print("Outer.mro", Outer[4].mro()) | |
print(Outer("Hello world")) | |
T = TemplateParameter('T') | |
N = TemplateParameter('N') | |
# Sequence is a template taking one argument | |
class Sequence(metaclass=Template[T]): | |
pass | |
# MyList is a template taking two arguments, where the first is passed | |
# down for use in the `Sequence` base class. | |
class MyList(Sequence[T], metaclass=Template[T, N]): | |
def __init__(self, value=None): | |
# self.__args contains the arguments values | |
T_val = self.__args[T] | |
N_val = self.__args[N] | |
if value is None: | |
self.value = [T_val()] * N_val | |
else: | |
assert len(value) == N_val | |
self.value = value | |
def __newinferred__(cls, value): | |
''' This is used to infer type arguments ''' | |
T_val = type(value[0]) | |
N_val = len(value) | |
return cls[T_val, N_val](value) | |
m = MyList[int, 3]() | |
assert isinstance(m, MyList[int, 3]) | |
assert isinstance(m, MyList) | |
assert isinstance(m, Sequence[int]) | |
assert isinstance(m, Sequence) | |
m = MyList(["Hello", "World"]) | |
assert isinstance(m, MyList[str, 2]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment