Source code for xoutil.formatter

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# xoutil.formatter
# ---------------------------------------------------------------------
# Copyright (c) 2015 Merchise and Contributors
# Copyright (c) 2013, 2014 Merchise Autrement and Contributors
# Copyright (c) 2009-2012 Medardo Rodríguez
# All rights reserved.
#
# Author: Medardo Rodriguez
# Contributors: see CONTRIBUTORS and HISTORY file
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the LICENCE attached (see LICENCE file) in the distribution
# package.


'''Smart formatting.'''


from __future__ import (division as _py3_division,
                        print_function as _py3_print,
                        unicode_literals as _py3_unicode,
                        absolute_import as _py3_abs_imports)

from xoutil.eight import string_types as _str_base
from xoutil.eight.meta import metaclass


class DelimiterFactory(object):
    def __new__(cls, owner, key, start, end):
        return key


class BaseFactory(object):
    _unsafe = False

    def __init__(self, owner, key, start, end):
        self.key = key
        self.match = owner.template[start:end]
        self.start = start


class MapFactory(BaseFactory):
    def __call__(self, mapping):
        from xoutil.eight import text_type
        return text_type(mapping[self.key])


class PyFactory(BaseFactory):
    def __init__(self, owner, key, start, end):
        super(PyFactory, self).__init__(owner, compile(key, '', 'eval'),
                                        start, end)

    def __call__(self, mapping):
        from xoutil.eight import text_type
        return text_type(eval(self.key, mapping))


class InvalidFactory(object):
    _unsafe = True

    def __init__(self, owner, key, start, end):
        self.owner = owner
        self.match = owner.template[start:end]
        self.start = start

    def __call__(self, mapping):
        start = self.start
        lines = self.owner.template[:start].splitlines(True)
        if lines:
            col = start - len(''.join(lines[:-1]))
            line = len(lines)
        else:
            col = line = 1
        msg = ('Invalid place-holder in string: '
               'line "%d", col "%d"') % (line, col)
        raise ValueError(msg)


class _TemplateClass(type):
    'Metaclass for Template.'

    # TODO: Not needed, convert to a static method

    _alters = (('escaped', r'(?P=delimiter)', '%s', DelimiterFactory),
               ('key', r'(?:[a-z_]\d*)+', '%s', MapFactory),
               ('xkey', r'[^?{}][^{}]*', '{%s}', MapFactory),
               ('python', r'[^{}]+', '{\?%s}', PyFactory),
               ('invalid', r'.', '%s', InvalidFactory))

    def __init__(cls, name, bases, attrs):
        import re
        super(_TemplateClass, cls).__init__(name, bases, attrs)
        alters, factories = [], {}
        for kind, pattern, wrapper, factory in cls._alters:
            factories[kind] = factory
            alters.append(wrapper % ('(?P<%s>%s)' % (kind, pattern)))
        rexp = r'(?P<delimiter>%s)(?:%s)' % (re.escape(cls.delimiter),
                                             '|'.join(alters))
        cls.pattern = re.compile(rexp, re.IGNORECASE | re.VERBOSE)
        cls.factories = factories


[docs]class Template(metaclass(_TemplateClass)): ''' A string class for supporting $-substitutions. It has similar interface that `string.Template` but using "eval" instead simple dictionary looking. This means that you get all the functionality provided by `string.Template` (although, perhaps modified) and you get also the ability to write more complex expressions. If you need repetition or other flow-control sentences you should use other templating system. If you enclose and expression within ``${?...}`` it will be evaluated as a python expression. Simple variables are allowed just with ``$var`` or ``${var}``:: >>> tpl = Template(str('${?1 + 1} is 2, and ${?x + x} is $x + ${x}')) >>> (tpl % dict(x=4)) == '2 is 2, and 8 is 4 + 4' True The mapping may be given by calling the template:: >>> tpl(x=5) == '2 is 2, and 10 is 5 + 5' True ''' delimiter = str('$') def __init__(self, template): self.template = template self.items = [] pivot, valid = 0, True while valid: token = self.pattern.search(template, pivot) if token: start, end = token.span() if start > pivot: self._append(template[pivot:start]) factory, key = self._GetFactory(token) self._append(factory(self, key, start, end)) pivot = end else: aux = template[pivot:] if aux: self._append(aux) valid = False def __str__(self): return str('%s(%s)') % (self.__class__.__name__, self.template) def __call__(self, mapping={}, **kwargs): # TODO: Don't update if object kwargs.update(mapping) # Don't modify mapping if given res = self.template.__class__() for item in self.items: if isinstance(item, _str_base): res += item else: res += item(kwargs) return res def __mod__(self, mapping): '''template % {'x':1}''' return self(mapping) def substitute(self, mapping={}, **kwargs): return self(mapping, **kwargs) def safe_substitute(self, mapping={}, **kwargs): # TODO: Don't update if object kwargs.update(mapping) # Don't modify mapping if given res = self.template.__class__() for item in self.items: if isinstance(item, _str_base): res += item else: if item._unsafe: res += item.match else: try: res += item(kwargs) except: res += str('') # item.match return res def _append(self, item): if (isinstance(item, _str_base) and self.items and isinstance(self.items[-1], _str_base)): self.items[-1] += item else: self.items.append(item) def _GetFactory(self, token): keys = list(self.factories) # Get keys i, count = 0, len(keys) res = None while not res and (i < count): key = keys[i] aux = token.group(key) if aux is not None: res = self.factories[key] else: i += 1 return res, aux
[docs]def count(source, chars): ''' Counts how chars from `chars` are found in `source`:: >>> count('Todos los nenes del mundo vamos una rueda a hacer', 'a') 1 # The vowel "i" is missing >>> count('Todos los nenes del mundo vamos una rueda a hacer', 'aeiuo') 4 ''' res = 0 for ch in chars: if ch in source: res += 1 return res