aboutsummaryrefslogtreecommitdiff
path: root/parse_type/parse_util.py
blob: 0e5ee73560a46f34527ce1911e64d82cc6feae6e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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