Source code for kemmering

import sys

PY2 = sys.version_info[0] == 2
strbase = basestring if PY2 else str  # nopep8
strclass = unicode if PY2 else str    # nopep8


[docs]class tag(object): """ An XML tag. `tag` is the name of the tag. Add a trailing forward slash (`/`) character to create a self-closing tag. `attrs` are the attributes for tag. The tag itself is callable. Call the tag to add children. .. doctest:: api-tag >>> from kemmering import tag >>> str(tag('a', b='c')('d')) '<a b="c">d</a>' >>> str(tag('e/')) '<e/>' """ self_closing = False def __init__(self, tag, **attrs): self._init(tag, attrs, ()) def _init(self, tag, attrs, children): if tag and tag.endswith('/'): self.self_closing = True tag = tag[:-1] self.tag = tag self.attrs = {k: v for k, v in attrs.items() if v is not None} self.children = () self._extend(*children) def _extend(self, *children): def mkchild(x): if isinstance(x, strbase): x = text(x) x.parent = self return x self.children += tuple(mkchild(x) for x in children) return self __call__ = _extend def _bind(self, context): attrs = {k: bind(v, context) for k, v in self.attrs.items()} children = tuple(bind(child, context) for child in self.children) return self._copy(attrs, children) def _copy(self, attrs, children): cls = type(self) obj = cls.__new__(cls) obj._init(self.tag, attrs, children) obj.self_closing = self.self_closing return obj def __str__(self): return ''.join(self._stream()) __unicode__ = __str__ def __repr__(self): if self.attrs: attrs = ', ' + ', '.join( ('%s=%s' % (k, repr(v)) for k, v in self.attrs.items()) ) else: attrs = '' if self.self_closing and not self.children: return 'tag({}{})'.format(repr(self.tag + '/'), attrs) children = ('(%s)' % ', '.join(map(repr, self.children)) if self.children else '') if self.tag: return 'tag(%s%s)%s' % (repr(self.tag), attrs, children) else: return 'notag{}'.format(children) def _stream(self): attrs = {k: v for k, v in self.attrs.items()} if attrs: attrs = ' ' + ' '.join( ('%s="%s"' % (k.rstrip('_'), v) for k, v in attrs.items()) ) else: attrs = '' if self.self_closing and not self.children: yield '<%s%s/>' % (self.tag, attrs) return if self.tag: yield '<%s%s>' % (self.tag, attrs) for child in self.children: for x in child._stream(): yield x if self.tag: yield '</%s>' % self.tag
[docs]class notag(tag): """ Used to represent a set of sibling elements with no enclosing parent tag. `children` is a sequence of elements. `notag` is particularly useful with `defer` and any of the template helpers which are based on `defer`. .. doctest:: api-notag >>> from kemmering import notag, tag >>> str(tag('a')( ... tag('b/'), ... notag( ... tag('c/'), ... tag('d/') ... ) ... )) '<a><b/><c/><d/></a>' """ def __init__(self, *children): super(notag, self).__init__(None) self(*children)
_nothing = notag() class text(strclass): def _stream(self): yield self
[docs]def bind(template, context): """ Realize a template by binding it to a context. `template` is a `tag` instance which should contain some instances of `defer` in its structure. `context` is the context object that is passed to deferred functions in the template. Most template helpers assume the context is a dictionary. Returns new `tag` instance that is a copy of the template with any deferred elements replaced by the return values of their deferred functions. """ if hasattr(template, '_bind'): template = template._bind(context) return template
[docs]class defer(object): """ Defer the realization of a part of a template until a later time. Use of a deferred function in a snippet comprised of `tag` objects, turns that snippet into a template. That template is said to be realized when `bind` is called on the snippet, which generates a concrete snippet based on the template and a context passed in via `bind`. `f` is a function with the signature: .. code-block:: python def deferred(context): "Return a `tag`, `notag`, or string." The function will be called at realization time when `bind` is called on the containing snippet and passed the context object. Most template helpers assume the context is a dictionary. Instances of `defer` may be used as children of `tag` objects or as values of tag atrributes. When used as a child of a tag, the return value should be either another `tag` instance or a string for a text element. When used as an attribute value, the return value should be a string or `None`. Setting an attribute to `None` causes it to be omitted from the realized template. Most use cases for `defer` are actually covered by more specific helpers based on `defer`, described below. """ def __init__(self, f): self.f = f def _bind(self, context): return bind(self.f(context), context) def _stream(self): raise ValueError("Unbound defer, unable to stream.") def __repr__(self): return '{}({})'.format( type(self).__name__, getattr(self.f, '__name__', repr(self.f)))
[docs]class from_context(defer): """ This specialization of `defer` simply returns a value from the bind context. `key` is the name to look up in the bind context dictionary. Whatever is the value in the bind context is returned. If `default` is specified and `key` is not found in the bind context, `default` is returned instead. .. doctest:: api-from_context >>> from kemmering import bind, from_context, tag >>> template = tag('a')(from_context('b', 'c')) >>> str(bind(template, {'b': 'd'})) '<a>d</a>' >>> str(bind(template, {})) '<a>c</a>' """ def __init__(self, key, default=_nothing): self.key = key self.default = default def _bind(self, context): value = context.get(self.key, self.default) if value is _nothing: raise KeyError(self.key) return bind(value, context) def __repr__(self): return '{}({})'.format(type(self).__name__, repr(self.key))
[docs]class in_context(defer): """ This specialization of `defer` looks up a value in the bind context by traversing the context using a sequence of keys where the bind context is presumed to be a structure of nested dictionaries. `keys` is a sequence of names to look up, in order, in the bind context. If `default` is specified and a value is not found in the bind context for the given keys, `default` is returned instead. .. doctest:: api-in_context >>> from kemmering import bind, in_context, tag >>> template = tag('a')(in_context(['b', 'c'], 'd')) >>> str(bind(template, {'b': {'c': 'e'}})) '<a>e</a>' >>> str(bind(template, {})) '<a>d</a>' """ def __init__(self, keys, default=_nothing): self.keys = keys self.default = default def _bind(self, context): value = context keys = self.keys while keys: key = keys[0] keys = keys[1:] value = value.get(key, _nothing) if value is _nothing: if self.default is _nothing: raise KeyError(self.keys) return self.default return bind(value, context) def __repr__(self): return '{}({})'.format(type(self).__name__, repr(self.keys))
[docs]class format_context(defer): """ This specialization of `defer` uses the bind context to perform Python string formatting on a format string. `s` is the format string. The return value is the equivalent of `s.format(**context)`. .. doctest:: api-format_context >>> from kemmering import bind, format_context, tag >>> template = tag('a')(format_context('{b} {c}')) >>> str(bind(template, {'b': 'd', 'c': 'e'})) '<a>d e</a>' """ def __init__(self, s): self.s = s def _bind(self, context): return self.s.format(**context) def __repr__(self): return '{}({})'.format(type(self).__name__, repr(self.s))
[docs]class cond(defer): """ This specialization of `defer` conditionally includes an element based on the bind context. `condition` is a function which accepts a single argument, `context` and returns a boolean. `condition` may optionally be a string, in which case it is used as a key for performing a dictionary lookup in the bind context, the value of which will be treated as a boolean. `affirmative` is the return value if the condition is `True`. `negative` is the return value of the condition is `False`. If the condition is `False` and no `negative` value is given, this element is elided from the realized snippet. .. doctest:: api-cond >>> from kemmering import bind, cond, tag >>> def has_b(context): ... return 'b' in context >>> template = tag('a')(cond(has_b, 'b')) >>> str(bind(template, {'b': ''})) '<a>b</a>' >>> str(bind(template, {})) '<a></a>' >>> template = tag('a')(cond('b', 'c', 'd')) >>> str(bind(template, {'b': True})) '<a>c</a>' >>> str(bind(template, {})) '<a>d</a>' """ def __init__(self, condition, affirmative, negative=_nothing): self.cond = condition self.yes = affirmative self.no = negative def _bind(self, context): cond = self.cond cond = cond(context) if callable(cond) else context.get(cond, False) return bind(self.yes, context) if cond else bind(self.no, context) def __repr__(self): return '{}({}, {}{})'.format( type(self).__name__, getattr(self.cond, '__name__', repr(self.cond)), repr(self.yes), '' if self.no is _nothing else ', {}'.format(repr(self.no)) )
[docs]class loop(defer): """ This specialization of `defer` repeats a snippet while iterating over a sequence. `key` is the name of a key that will be added to the context on each iteration whose value is the current item in the sequence. This makes the current item available to deferred functions in the repeated snippet. `key` may optionally be a `list` or `tuple` of string key names, in which case the sequence values, which should be sequences of equal length, will be unpacked into those keys. `seq` is a function which accepts a single argument, `context`, and returns an iterable sequence. Alternatively, `seq` can be the name of a key in the current context whose value is an iterable sequence. `template` is the snippet to be repeated. .. doctest:: api-loop >>> from kemmering import bind, from_context, loop, tag >>> def fruits(context): ... return ['apple', 'pear', 'banana'] >>> template = tag('ul')( ... loop('fruit', fruits, tag('li')(from_context('fruit')))) >>> str(bind(template, {'fruit': 'hamburger'})) '<ul><li>apple</li><li>pear</li><li>banana</li></ul>' >>> template = tag('ul')( ... loop('fruit', 'fruits', tag('li')(from_context('fruit')))) >>> str(bind(template, {'fruits': ['apple', 'pear', 'banana']})) '<ul><li>apple</li><li>pear</li><li>banana</li></ul>' And an example which uses unpacking: .. doctest:: api-loop >>> from kemmering import cond >>> from kemmering.html import pretty >>> def fruits(context): ... return enumerate(['apple', 'pear', 'banana']) >>> def is_even(context): ... return context['i'] % 2 == 0 >>> template = tag('ul')( ... loop(('i', 'fruit'), fruits, ... tag('li', class_=cond(is_even, 'even', 'odd'))( ... from_context('fruit')))) >>> print(pretty(bind(template, {}))) <ul> <li class="even">apple</li> <li class="odd">pear</li> <li class="even">banana</li> </ul> <BLANKLINE> """ def __init__(self, key, seq, template): self.key = key self.seq = seq self.template = template def _bind(self, context): def subcontext(value): sub = context.copy() if isinstance(self.key, (list, tuple)): _check_unpack(len(self.key), len(value)) sub.update({k: v for k, v in zip(self.key, value)}) else: sub[self.key] = value return sub seq = self.seq(context) if callable(self.seq) else context[self.seq] return notag(*( bind(self.template, subcontext(value)) for value in seq ))
def _check_unpack(expected, got): if got < expected: raise ValueError( 'not enough values to unpack (expected {}, got {}'.format( expected, got)) elif got > expected: raise ValueError( 'too many values to unpack (expected {})'.format( expected))