diff --git a/compressor/filters/base.py b/compressor/filters/base.py index 54f0498..bfcc016 100644 --- a/compressor/filters/base.py +++ b/compressor/filters/base.py @@ -5,7 +5,7 @@ import tempfile from compressor.conf import settings from compressor.exceptions import FilterError -from compressor.utils import cmd_split, FormattableString +from compressor.utils import cmd_split, stringformat logger = logging.getLogger("compressor.filters") @@ -56,7 +56,7 @@ class CompilerFilter(FilterBase): ext = ".%s" % self.type and self.type or "" outfile = tempfile.NamedTemporaryFile(mode='w', suffix=ext) self.options["outfile"] = outfile.name - cmd = FormattableString(self.command).format(**self.options) + cmd = stringformat.FormattableString(self.command).format(**self.options) proc = subprocess.Popen(cmd_split(cmd), stdout=self.stdout, stdin=self.stdin, stderr=self.stderr) if infile is not None: diff --git a/compressor/utils/__init__.py b/compressor/utils/__init__.py index bf419ae..2c13265 100644 --- a/compressor/utils/__init__.py +++ b/compressor/utils/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- import os -import re -import sys from shlex import split as cmd_split from compressor.exceptions import FilterError @@ -59,263 +57,3 @@ def walk(root, topdown=True, onerror=None, followlinks=False): if os.path.islink(p): for link_dirpath, link_dirnames, link_filenames in walk(p): yield (link_dirpath, link_dirnames, link_filenames) - -"""Advanced string formatting for Python >= 2.4. - -An implementation of the advanced string formatting (PEP 3101). - -Author: Florent Xicluna -""" - -if hasattr(str, 'partition'): - def partition(s, sep): - return s.partition(sep) -else: # Python 2.4 - def partition(s, sep): - try: - left, right = s.split(sep, 1) - except ValueError: - return s, '', '' - return left, sep, right - -_format_str_re = re.compile( - r'((?=^])?)' # alignment - r'([-+ ]?)' # sign - r'(#?)' r'(\d*)' r'(,?)' # base prefix, minimal width, thousands sep - r'((?:\.\d+)?)' # precision - r'(.?)$' # type -) -_field_part_re = re.compile( - r'(?:(\[)|\.|^)' # start or '.' or '[' - r'((?(1)[^]]*|[^.[]*))' # part - r'(?(1)(?:\]|$)([^.[]+)?)' # ']' and invalid tail -) - -if hasattr(re, '__version__'): - _format_str_sub = _format_str_re.sub -else: - # Python 2.4 fails to preserve the Unicode type - def _format_str_sub(repl, s): - if isinstance(s, unicode): - return unicode(_format_str_re.sub(repl, s)) - return _format_str_re.sub(repl, s) - -if hasattr(int, '__index__'): - def _is_integer(value): - return hasattr(value, '__index__') -else: # Python 2.4 - def _is_integer(value): - return isinstance(value, (int, long)) - - -def _strformat(value, format_spec=""): - """Internal string formatter. - - It implements the Format Specification Mini-Language. - """ - m = _format_spec_re.match(str(format_spec)) - if not m: - raise ValueError('Invalid conversion specification') - align, sign, prefix, width, comma, precision, conversion = m.groups() - is_numeric = hasattr(value, '__float__') - is_integer = is_numeric and _is_integer(value) - if prefix and not is_integer: - raise ValueError('Alternate form (#) not allowed in %s format ' - 'specifier' % (is_numeric and 'float' or 'string')) - if is_numeric and conversion == 'n': - # Default to 'd' for ints and 'g' for floats - conversion = is_integer and 'd' or 'g' - elif sign: - if not is_numeric: - raise ValueError("Sign not allowed in string format specifier") - if conversion == 'c': - raise ValueError("Sign not allowed with integer " - "format specifier 'c'") - if comma: - # TODO: thousand separator - pass - try: - if ((is_numeric and conversion == 's') or - (not is_integer and conversion in set('cdoxX'))): - raise ValueError - if conversion == 'c': - conversion = 's' - value = chr(value % 256) - rv = ('%' + prefix + precision + (conversion or 's')) % (value,) - except ValueError: - raise ValueError("Unknown format code %r for object of type %r" % - (conversion, value.__class__.__name__)) - if sign not in '-' and value >= 0: - # sign in (' ', '+') - rv = sign + rv - if width: - zero = (width[0] == '0') - width = int(width) - else: - zero = False - width = 0 - # Fastpath when alignment is not required - if width <= len(rv): - if not is_numeric and (align == '=' or (zero and not align)): - raise ValueError("'=' alignment not allowed in string format " - "specifier") - return rv - fill, align = align[:-1], align[-1:] - if not fill: - fill = zero and '0' or ' ' - if align == '^': - padding = width - len(rv) - # tweak the formatting if the padding is odd - if padding % 2: - rv += fill - rv = rv.center(width, fill) - elif align == '=' or (zero and not align): - if not is_numeric: - raise ValueError("'=' alignment not allowed in string format " - "specifier") - if value < 0 or sign not in '-': - rv = rv[0] + rv[1:].rjust(width - 1, fill) - else: - rv = rv.rjust(width, fill) - elif align in ('>', '=') or (is_numeric and not align): - # numeric value right aligned by default - rv = rv.rjust(width, fill) - else: - rv = rv.ljust(width, fill) - return rv - - -def _format_field(value, parts, conv, spec, want_bytes=False): - """Format a replacement field.""" - for k, part, _ in parts: - if k: - if part.isdigit(): - value = value[int(part)] - else: - value = value[part] - else: - value = getattr(value, part) - if conv: - value = ((conv == 'r') and '%r' or '%s') % (value,) - if hasattr(value, '__format__'): - value = value.__format__(spec) - elif hasattr(value, 'strftime') and spec: - value = value.strftime(str(spec)) - else: - value = _strformat(value, spec) - if want_bytes and isinstance(value, unicode): - return str(value) - return value - - -class FormattableString(object): - """Class which implements method format(). - - The method format() behaves like str.format() in python 2.6+. - - >>> FormattableString(u'{a:5}').format(a=42) - ... # Same as u'{a:5}'.format(a=42) - u' 42' - - """ - - __slots__ = '_index', '_kwords', '_nested', '_string', 'format_string' - - def __init__(self, format_string): - self._index = 0 - self._kwords = {} - self._nested = {} - - self.format_string = format_string - self._string = _format_str_sub(self._prepare, format_string) - - def __eq__(self, other): - if isinstance(other, FormattableString): - return self.format_string == other.format_string - # Compare equal with the original string. - return self.format_string == other - - def _prepare(self, match): - # Called for each replacement field. - part = match.group(0) - if part[0] == part[-1]: - # '{{' or '}}' - assert part == part[0] * len(part) - return part[:len(part) // 2] - repl = part[1:-1] - field, _, format_spec = partition(repl, ':') - literal, sep, conversion = partition(field, '!') - if sep and not conversion: - raise ValueError("end of format while looking for " - "conversion specifier") - if len(conversion) > 1: - raise ValueError("expected ':' after format specifier") - if conversion not in 'rsa': - raise ValueError("Unknown conversion specifier %s" % - str(conversion)) - name_parts = _field_part_re.findall(literal) - if literal[:1] in '.[': - # Auto-numbering - if self._index is None: - raise ValueError("cannot switch from manual field " - "specification to automatic field numbering") - name = str(self._index) - self._index += 1 - if not literal: - del name_parts[0] - else: - name = name_parts.pop(0)[1] - if name.isdigit() and self._index is not None: - # Manual specification - if self._index: - raise ValueError("cannot switch from automatic field " - "numbering to manual field specification") - self._index = None - empty_attribute = False - for k, v, tail in name_parts: - if not v: - empty_attribute = True - if tail: - raise ValueError("Only '.' or '[' may follow ']' " - "in format field specifier") - if name_parts and k == '[' and not literal[-1] == ']': - raise ValueError("Missing ']' in format string") - if empty_attribute: - raise ValueError("Empty attribute in format string") - if '{' in format_spec: - format_spec = _format_sub_re.sub(self._prepare, format_spec) - rv = (name_parts, conversion, format_spec) - self._nested.setdefault(name, []).append(rv) - else: - rv = (name_parts, conversion, format_spec) - self._kwords.setdefault(name, []).append(rv) - return r'%%(%s)s' % id(rv) - - def format(self, *args, **kwargs): - """Same as str.format() and unicode.format() in Python 2.6+.""" - if args: - kwargs.update(dict((str(i), value) - for (i, value) in enumerate(args))) - # Encode arguments to ASCII, if format string is bytes - want_bytes = isinstance(self._string, str) - params = {} - for name, items in self._kwords.items(): - value = kwargs[name] - for item in items: - parts, conv, spec = item - params[str(id(item))] = _format_field(value, parts, conv, spec, - want_bytes) - for name, items in self._nested.items(): - value = kwargs[name] - for item in items: - parts, conv, spec = item - spec = spec % params - params[str(id(item))] = _format_field(value, parts, conv, spec, - want_bytes) - return self._string % params diff --git a/compressor/utils/stringformat.py b/compressor/utils/stringformat.py new file mode 100644 index 0000000..40c4f82 --- /dev/null +++ b/compressor/utils/stringformat.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +"""Advanced string formatting for Python >= 2.4. + +An implementation of the advanced string formatting (PEP 3101). + +Author: Florent Xicluna +""" + +import re + +if hasattr(str, 'partition'): + def partition(s, sep): + return s.partition(sep) +else: # Python 2.4 + def partition(s, sep): + try: + left, right = s.split(sep, 1) + except ValueError: + return s, '', '' + return left, sep, right + +_format_str_re = re.compile( + r'((?=^])?)' # alignment + r'([-+ ]?)' # sign + r'(#?)' r'(\d*)' r'(,?)' # base prefix, minimal width, thousands sep + r'((?:\.\d+)?)' # precision + r'(.?)$' # type +) +_field_part_re = re.compile( + r'(?:(\[)|\.|^)' # start or '.' or '[' + r'((?(1)[^]]*|[^.[]*))' # part + r'(?(1)(?:\]|$)([^.[]+)?)' # ']' and invalid tail +) + +if hasattr(re, '__version__'): + _format_str_sub = _format_str_re.sub +else: + # Python 2.4 fails to preserve the Unicode type + def _format_str_sub(repl, s): + if isinstance(s, unicode): + return unicode(_format_str_re.sub(repl, s)) + return _format_str_re.sub(repl, s) + +if hasattr(int, '__index__'): + def _is_integer(value): + return hasattr(value, '__index__') +else: # Python 2.4 + def _is_integer(value): + return isinstance(value, (int, long)) + + +def _strformat(value, format_spec=""): + """Internal string formatter. + + It implements the Format Specification Mini-Language. + """ + m = _format_spec_re.match(str(format_spec)) + if not m: + raise ValueError('Invalid conversion specification') + align, sign, prefix, width, comma, precision, conversion = m.groups() + is_numeric = hasattr(value, '__float__') + is_integer = is_numeric and _is_integer(value) + if prefix and not is_integer: + raise ValueError('Alternate form (#) not allowed in %s format ' + 'specifier' % (is_numeric and 'float' or 'string')) + if is_numeric and conversion == 'n': + # Default to 'd' for ints and 'g' for floats + conversion = is_integer and 'd' or 'g' + elif sign: + if not is_numeric: + raise ValueError("Sign not allowed in string format specifier") + if conversion == 'c': + raise ValueError("Sign not allowed with integer " + "format specifier 'c'") + if comma: + # TODO: thousand separator + pass + try: + if ((is_numeric and conversion == 's') or + (not is_integer and conversion in set('cdoxX'))): + raise ValueError + if conversion == 'c': + conversion = 's' + value = chr(value % 256) + rv = ('%' + prefix + precision + (conversion or 's')) % (value,) + except ValueError: + raise ValueError("Unknown format code %r for object of type %r" % + (conversion, value.__class__.__name__)) + if sign not in '-' and value >= 0: + # sign in (' ', '+') + rv = sign + rv + if width: + zero = (width[0] == '0') + width = int(width) + else: + zero = False + width = 0 + # Fastpath when alignment is not required + if width <= len(rv): + if not is_numeric and (align == '=' or (zero and not align)): + raise ValueError("'=' alignment not allowed in string format " + "specifier") + return rv + fill, align = align[:-1], align[-1:] + if not fill: + fill = zero and '0' or ' ' + if align == '^': + padding = width - len(rv) + # tweak the formatting if the padding is odd + if padding % 2: + rv += fill + rv = rv.center(width, fill) + elif align == '=' or (zero and not align): + if not is_numeric: + raise ValueError("'=' alignment not allowed in string format " + "specifier") + if value < 0 or sign not in '-': + rv = rv[0] + rv[1:].rjust(width - 1, fill) + else: + rv = rv.rjust(width, fill) + elif align in ('>', '=') or (is_numeric and not align): + # numeric value right aligned by default + rv = rv.rjust(width, fill) + else: + rv = rv.ljust(width, fill) + return rv + + +def _format_field(value, parts, conv, spec, want_bytes=False): + """Format a replacement field.""" + for k, part, _ in parts: + if k: + if part.isdigit(): + value = value[int(part)] + else: + value = value[part] + else: + value = getattr(value, part) + if conv: + value = ((conv == 'r') and '%r' or '%s') % (value,) + if hasattr(value, '__format__'): + value = value.__format__(spec) + elif hasattr(value, 'strftime') and spec: + value = value.strftime(str(spec)) + else: + value = _strformat(value, spec) + if want_bytes and isinstance(value, unicode): + return str(value) + return value + + +class FormattableString(object): + """Class which implements method format(). + + The method format() behaves like str.format() in python 2.6+. + + >>> FormattableString(u'{a:5}').format(a=42) + ... # Same as u'{a:5}'.format(a=42) + u' 42' + + """ + + __slots__ = '_index', '_kwords', '_nested', '_string', 'format_string' + + def __init__(self, format_string): + self._index = 0 + self._kwords = {} + self._nested = {} + + self.format_string = format_string + self._string = _format_str_sub(self._prepare, format_string) + + def __eq__(self, other): + if isinstance(other, FormattableString): + return self.format_string == other.format_string + # Compare equal with the original string. + return self.format_string == other + + def _prepare(self, match): + # Called for each replacement field. + part = match.group(0) + if part[0] == part[-1]: + # '{{' or '}}' + assert part == part[0] * len(part) + return part[:len(part) // 2] + repl = part[1:-1] + field, _, format_spec = partition(repl, ':') + literal, sep, conversion = partition(field, '!') + if sep and not conversion: + raise ValueError("end of format while looking for " + "conversion specifier") + if len(conversion) > 1: + raise ValueError("expected ':' after format specifier") + if conversion not in 'rsa': + raise ValueError("Unknown conversion specifier %s" % + str(conversion)) + name_parts = _field_part_re.findall(literal) + if literal[:1] in '.[': + # Auto-numbering + if self._index is None: + raise ValueError("cannot switch from manual field " + "specification to automatic field numbering") + name = str(self._index) + self._index += 1 + if not literal: + del name_parts[0] + else: + name = name_parts.pop(0)[1] + if name.isdigit() and self._index is not None: + # Manual specification + if self._index: + raise ValueError("cannot switch from automatic field " + "numbering to manual field specification") + self._index = None + empty_attribute = False + for k, v, tail in name_parts: + if not v: + empty_attribute = True + if tail: + raise ValueError("Only '.' or '[' may follow ']' " + "in format field specifier") + if name_parts and k == '[' and not literal[-1] == ']': + raise ValueError("Missing ']' in format string") + if empty_attribute: + raise ValueError("Empty attribute in format string") + if '{' in format_spec: + format_spec = _format_sub_re.sub(self._prepare, format_spec) + rv = (name_parts, conversion, format_spec) + self._nested.setdefault(name, []).append(rv) + else: + rv = (name_parts, conversion, format_spec) + self._kwords.setdefault(name, []).append(rv) + return r'%%(%s)s' % id(rv) + + def format(self, *args, **kwargs): + """Same as str.format() and unicode.format() in Python 2.6+.""" + if args: + kwargs.update(dict((str(i), value) + for (i, value) in enumerate(args))) + # Encode arguments to ASCII, if format string is bytes + want_bytes = isinstance(self._string, str) + params = {} + for name, items in self._kwords.items(): + value = kwargs[name] + for item in items: + parts, conv, spec = item + params[str(id(item))] = _format_field(value, parts, conv, spec, + want_bytes) + for name, items in self._nested.items(): + value = kwargs[name] + for item in items: + parts, conv, spec = item + spec = spec % params + params[str(id(item))] = _format_field(value, parts, conv, spec, + want_bytes) + return self._string % params + + +def selftest(): + import datetime + F = FormattableString + + assert F(u"{0:{width}.{precision}s}").format('hello world', + width=8, precision=5) == u'hello ' + + d = datetime.date(2010, 9, 7) + assert F(u"The year is {0.year}").format(d) == u"The year is 2010" + assert F(u"Tested on {0:%Y-%m-%d}").format(d) == u"Tested on 2010-09-07" + print 'Test successful' + +if __name__ == '__main__': + selftest() \ No newline at end of file