Skip to content

Instantly share code, notes, and snippets.

@eric-wieser
Created November 15, 2019 16:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eric-wieser/4b6d536f1b6e38ae5d866c529674cfee to your computer and use it in GitHub Desktop.
Save eric-wieser/4b6d536f1b6e38ae5d866c529674cfee to your computer and use it in GitHub Desktop.
C++-style templated types for Python

An attempt to emulate C++ templates in python, complete with non-type template parameters.

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
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