Source code for type_templating

"""
C++-style type templating for python

.. autoclass:: Template

.. autoclass:: TemplateParameter

.. autoclass:: TemplateExpression

.. data:: __version__

    The version of this package
"""
__all__ = [
    'TemplateParameter',
    'Template',
    'TemplateExpression',
]

from ._version import __version__


[docs]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 _substitute(self, arg_values): return arg_values[self]
def _make_template_name(name, args) -> str: return "{}[{}]".format( name, ', '.join( a.name if isinstance(a, TemplateParameter) else repr(a) for a in args ) ) def _mangle(name, member): return '_{}__{}'.format(name.lstrip('_'), member) class _TemplateSpec: """ Return type of ``Template[K]``. This should be used as a metaclass, see :class:`Template` for more info. """ 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)
[docs]class Template(type, metaclass=_TemplateMeta): r""" The main mechanism for declaring template types. The result of ``type(SomeTemplate)``. 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]) .. attribute:: __base__ The class definition use to define this template. Note that this is simply used as a base class, hence why it appears under this attribute. The base classes declared in the class definition will end up either in ``__expr_bases__`` or ``__base__.__bases__``. .. attribute:: __params__ The list of template parameters taken by this template .. attribute:: __expr_bases__ The list of :class:`TemplateExpression`\ s which should be expanded with the template parameters. """ def __new__(metacls, name, bases, dict_, *, params): bases_no_templates = [] base_template_exprs = [] for b in bases: if isinstance(b, TemplateExpression): bases_no_templates += b.template.__bases__ base_template_exprs.append(b) elif isinstance(b, TemplateParameter): base_template_exprs.append(b) else: bases_no_templates.append(b) d = dict_.copy() q = d['__qualname__'] d['__qualname__'] = q+".__base__" base = type(name+".__base__", tuple(bases_no_templates), d) return type.__new__(metacls, name, (base,), dict( __expr_bases__=tuple(base_template_exprs), __params__=params, __qualname__=q, # private dictionary of existing template instantiations _instantiations={}, )) def __init__(cls, *args, **kwargs): pass def __subclasscheck__(cls, subclass): for c in subclass.mro(): if getattr(c, '__template__', None) == 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): """ Construct a template without template arguments, inferring them """ try: f = cls.__base__.__newinferred__ except AttributeError: pass else: i = f(cls, *args, **kwargs) if not isinstance(i, cls): raise TypeError( "__newinferred__ should return a {}, but instead returned " "a {}.".format(cls, type(i)) ) return i raise TypeError( "No type arguments passed to {}, and __newinferred__ is not " "defined".format(cls) )
[docs]class TemplateExpression: """ The result of ``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.__expr_bases__ ) + self.template.__bases__ inst = type( _make_template_name(self.template.__qualname__, args), bases, { _mangle(self.template.__name__, 'args'): arg_dict, '__template__': self.template, } ) self.template._instantiations[args] = inst return inst