blob: 0e5ee73560a46f34527ce1911e64d82cc6feae6e [file] [log] [blame]
# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
"""
Provides generic utility classes for the :class:`parse.Parser` class.
"""
from __future__ import absolute_import
from collections import namedtuple
import parse
import six
# -- HELPER-CLASS: For format part in a Field.
# REQUIRES: Python 2.6 or newer.
# pylint: disable=redefined-builtin, too-many-arguments
FormatSpec = namedtuple("FormatSpec",
["type", "width", "zero", "align", "fill", "precision"])
def make_format_spec(type=None, width="", zero=False, align=None, fill=None,
precision=None):
return FormatSpec(type, width, zero, align, fill, precision)
# pylint: enable=redefined-builtin
class Field(object):
"""
Provides a ValueObject for a Field in a parse expression.
Examples:
* "{}"
* "{name}"
* "{:format}"
* "{name:format}"
Format specification: [[fill]align][0][width][.precision][type]
"""
# pylint: disable=redefined-builtin
ALIGN_CHARS = '<>=^'
def __init__(self, name="", format=None):
self.name = name
self.format = format
self._format_spec = None
def set_format(self, format):
self.format = format
self._format_spec = None
@property
def has_format(self):
return bool(self.format)
@property
def format_spec(self):
if not self._format_spec and self.format:
self._format_spec = self.extract_format_spec(self.format)
return self._format_spec
def __str__(self):
name = self.name or ""
if self.has_format:
return "{%s:%s}" % (name, self.format)
return "{%s}" % name
def __eq__(self, other):
if isinstance(other, Field):
format1 = self.format or ""
format2 = other.format or ""
return (self.name == other.name) and (format1 == format2)
elif isinstance(other, six.string_types):
return str(self) == other
else:
raise ValueError(other)
def __ne__(self, other):
return not self.__eq__(other)
@staticmethod
def make_format(format_spec):
"""Build format string from a format specification.
:param format_spec: Format specification (as FormatSpec object).
:return: Composed format (as string).
"""
fill = ''
align = ''
zero = ''
width = format_spec.width
if format_spec.align:
align = format_spec.align[0]
if format_spec.fill:
fill = format_spec.fill[0]
if format_spec.zero:
zero = '0'
precision_part = ""
if format_spec.precision:
precision_part = ".%s" % format_spec.precision
# -- FORMAT-SPEC: [[fill]align][0][width][.precision][type]
return "%s%s%s%s%s%s" % (fill, align, zero, width,
precision_part, format_spec.type)
@classmethod
def extract_format_spec(cls, format):
"""Pull apart the format: [[fill]align][0][width][.precision][type]"""
# -- BASED-ON: parse.extract_format()
# pylint: disable=redefined-builtin, unsubscriptable-object
if not format:
raise ValueError("INVALID-FORMAT: %s (empty-string)" % format)
orig_format = format
fill = align = None
if format[0] in cls.ALIGN_CHARS:
align = format[0]
format = format[1:]
elif len(format) > 1 and format[1] in cls.ALIGN_CHARS:
fill = format[0]
align = format[1]
format = format[2:]
zero = False
if format and format[0] == '0':
zero = True
format = format[1:]
width = ''
while format:
if not format[0].isdigit():
break
width += format[0]
format = format[1:]
precision = None
if format.startswith('.'):
# Precision isn't needed but we need to capture it so that
# the ValueError isn't raised.
format = format[1:] # drop the '.'
precision = ''
while format:
if not format[0].isdigit():
break
precision += format[0]
format = format[1:]
# the rest is the type, if present
type = format
if not type:
raise ValueError("INVALID-FORMAT: %s (without type)" % orig_format)
return FormatSpec(type, width, zero, align, fill, precision)
class FieldParser(object):
"""
Utility class that parses/extracts fields in parse expressions.
"""
@classmethod
def parse(cls, text):
if not (text.startswith('{') and text.endswith('}')):
message = "FIELD-SCHEMA MISMATCH: text='%s' (missing braces)" % text
raise ValueError(message)
# first: lose the braces
text = text[1:-1]
if ':' in text:
# -- CASE: Typed field with format.
name, format_ = text.split(':')
else:
name = text
format_ = None
return Field(name, format_)
@classmethod
def extract_fields(cls, schema):
"""Extract fields in a parse expression schema.
:param schema: Parse expression schema/format to use (as string).
:return: Generator for fields in schema (as Field objects).
"""
# -- BASED-ON: parse.Parser._generate_expression()
for part in parse.PARSE_RE.split(schema):
if not part or part == '{{' or part == '}}':
continue
elif part[0] == '{':
# this will be a braces-delimited field to handle
yield cls.parse(part)
@classmethod
def extract_types(cls, schema):
"""Extract types (names) for typed fields (with format/type part).
:param schema: Parser schema/format to use.
:return: Generator for type names (as string).
"""
for field in cls.extract_fields(schema):
if field.has_format:
yield field.format_spec.type