diff options
Diffstat (limited to 'parse_type/parse_util.py')
-rw-r--r-- | parse_type/parse_util.py | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/parse_type/parse_util.py b/parse_type/parse_util.py new file mode 100644 index 0000000..0e5ee73 --- /dev/null +++ b/parse_type/parse_util.py @@ -0,0 +1,198 @@ +# -*- 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 |