aboutsummaryrefslogtreecommitdiff
path: root/parse_type/parse_util.py
diff options
context:
space:
mode:
Diffstat (limited to 'parse_type/parse_util.py')
-rw-r--r--parse_type/parse_util.py198
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