aboutsummaryrefslogtreecommitdiffstats
path: root/parse_type/builder.py
blob: 4bde1c8f42c21ff63a604a56f9080917746d2c83 (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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
r"""
Provides support to compose user-defined parse types.

Cardinality
------------

It is often useful to constrain how often a data type occurs.
This is also called the cardinality of a data type (in a context).
The supported cardinality are:

  * 0..1  zero_or_one,  optional<T>: T or None
  * 0..N  zero_or_more, list_of<T>
  * 1..N  one_or_more,  list_of<T> (many)


.. doctest:: cardinality

    >>> from parse_type import TypeBuilder
    >>> from parse import Parser

    >>> def parse_number(text):
    ...     return int(text)
    >>> parse_number.pattern = r"\d+"

    >>> parse_many_numbers = TypeBuilder.with_many(parse_number)
    >>> more_types = { "Numbers": parse_many_numbers }
    >>> parser = Parser("List: {numbers:Numbers}", more_types)
    >>> parser.parse("List: 1, 2, 3")
    <Result () {'numbers': [1, 2, 3]}>


Enumeration Type (Name-to-Value Mappings)
-----------------------------------------

An Enumeration data type allows to select one of several enum values by using
its name. The converter function returns the selected enum value.

.. doctest:: make_enum

    >>> parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False})
    >>> more_types = { "YesNo": parse_enum_yesno }
    >>> parser = Parser("Answer: {answer:YesNo}", more_types)
    >>> parser.parse("Answer: yes")
    <Result () {'answer': True}>


Choice (Name Enumerations)
-----------------------------

A Choice data type allows to select one of several strings.

.. doctest:: make_choice

    >>> parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"])
    >>> more_types = { "ChoiceYesNo": parse_choice_yesno }
    >>> parser = Parser("Answer: {answer:ChoiceYesNo}", more_types)
    >>> parser.parse("Answer: yes")
    <Result () {'answer': 'yes'}>

"""

from __future__ import absolute_import
import inspect
import re
import enum
from parse_type.cardinality import pattern_group_count, \
    Cardinality, TypeBuilder as CardinalityTypeBuilder

__all__ = ["TypeBuilder", "build_type_dict", "parse_anything"]


class TypeBuilder(CardinalityTypeBuilder):
    """
    Provides a utility class to build type-converters (parse_types) for
    the :mod:`parse` module.
    """
    default_strict = True
    default_re_opts = (re.IGNORECASE | re.DOTALL)

    @classmethod
    def make_list(cls, item_converter=None, listsep=','):
        """
        Create a type converter for a list of items (many := 1..*).
        The parser accepts anything and the converter needs to fail on errors.

        :param item_converter:  Type converter for an item.
        :param listsep:  List separator to use (as string).
        :return: Type converter function object for the list.
        """
        if not item_converter:
            item_converter = parse_anything
        return cls.with_cardinality(Cardinality.many, item_converter,
                                    pattern=cls.anything_pattern,
                                    listsep=listsep)

    @staticmethod
    def make_enum(enum_mappings):
        """
        Creates a type converter for an enumeration or text-to-value mapping.

        :param enum_mappings: Defines enumeration names and values.
        :return: Type converter function object for the enum/mapping.
        """
        if (inspect.isclass(enum_mappings) and
                issubclass(enum_mappings, enum.Enum)):
            enum_class = enum_mappings
            enum_mappings = enum_class.__members__

        def convert_enum(text):
            if text not in convert_enum.mappings:
                text = text.lower()     # REQUIRED-BY: parse re.IGNORECASE
            return convert_enum.mappings[text]    #< text.lower() ???
        convert_enum.pattern = r"|".join(enum_mappings.keys())
        convert_enum.mappings = enum_mappings
        return convert_enum

    @staticmethod
    def _normalize_choices(choices, transform):
        assert transform is None or callable(transform)
        if transform:
            choices = [transform(value)  for value in choices]
        else:
            choices = list(choices)
        return choices

    @classmethod
    def make_choice(cls, choices, transform=None, strict=None):
        """
        Creates a type-converter function to select one from a list of strings.
        The type-converter function returns the selected choice_text.
        The :param:`transform()` function is applied in the type converter.
        It can be used to enforce the case (because parser uses re.IGNORECASE).

        :param choices: List of strings as choice.
        :param transform: Optional, initial transform function for parsed text.
        :return: Type converter function object for this choices.
        """
        # -- NOTE: Parser uses re.IGNORECASE flag
        #    => transform may enforce case.
        choices = cls._normalize_choices(choices, transform)
        if strict is None:
            strict = cls.default_strict

        def convert_choice(text):
            if transform:
                text = transform(text)
            if strict and text not in convert_choice.choices:
                values = ", ".join(convert_choice.choices)
                raise ValueError("%s not in: %s" % (text, values))
            return text
        convert_choice.pattern = r"|".join(choices)
        convert_choice.choices = choices
        return convert_choice

    @classmethod
    def make_choice2(cls, choices, transform=None, strict=None):
        """
        Creates a type converter to select one item from a list of strings.
        The type converter function returns a tuple (index, choice_text).

        :param choices: List of strings as choice.
        :param transform: Optional, initial transform function for parsed text.
        :return: Type converter function object for this choices.
        """
        choices = cls._normalize_choices(choices, transform)
        if strict is None:
            strict = cls.default_strict

        def convert_choice2(text):
            if transform:
                text = transform(text)
            if strict and text not in convert_choice2.choices:
                values = ", ".join(convert_choice2.choices)
                raise ValueError("%s not in: %s" % (text, values))
            index = convert_choice2.choices.index(text)
            return index, text
        convert_choice2.pattern = r"|".join(choices)
        convert_choice2.choices = choices
        return convert_choice2

    @classmethod
    def make_variant(cls, converters, re_opts=None, compiled=False, strict=True):
        """
        Creates a type converter for a number of type converter alternatives.
        The first matching type converter is used.

        REQUIRES: type_converter.pattern attribute

        :param converters: List of type converters as alternatives.
        :param re_opts:  Regular expression options zu use (=default_re_opts).
        :param compiled: Use compiled regexp matcher, if true (=False).
        :param strict:   Enable assertion checks.
        :return: Type converter function object.

        .. note::

            Works only with named fields in :class:`parse.Parser`.
            Parser needs group_index delta for unnamed/fixed fields.
            This is not supported for user-defined types.
            Otherwise, you need to use :class:`parse_type.parse.Parser`
            (patched version of the :mod:`parse` module).
        """
        # -- NOTE: Uses double-dispatch with regex pattern rematch because
        #          match is not passed through to primary type converter.
        assert converters, "REQUIRE: Non-empty list."
        if len(converters) == 1:
            return converters[0]
        if re_opts is None:
            re_opts = cls.default_re_opts

        pattern = r")|(".join([tc.pattern for tc in converters])
        pattern = r"("+ pattern + ")"
        group_count = len(converters)
        for converter in converters:
            group_count += pattern_group_count(converter.pattern)

        if compiled:
            convert_variant = cls.__create_convert_variant_compiled(converters,
                                                                    re_opts,
                                                                    strict)
        else:
            convert_variant = cls.__create_convert_variant(re_opts, strict)
        convert_variant.pattern = pattern
        convert_variant.converters = tuple(converters)
        convert_variant.regex_group_count = group_count
        return convert_variant

    @staticmethod
    def __create_convert_variant(re_opts, strict):
        # -- USE: Regular expression pattern (compiled on use).
        def convert_variant(text, m=None):
            # pylint: disable=invalid-name, unused-argument, missing-docstring
            for converter in convert_variant.converters:
                if re.match(converter.pattern, text, re_opts):
                    return converter(text)
            # -- pragma: no cover
            assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
            return None
        return convert_variant

    @staticmethod
    def __create_convert_variant_compiled(converters, re_opts, strict):
        # -- USE: Compiled regular expression matcher.
        for converter in converters:
            matcher = getattr(converter, "matcher", None)
            if not matcher:
                converter.matcher = re.compile(converter.pattern, re_opts)

        def convert_variant(text, m=None):
            # pylint: disable=invalid-name, unused-argument, missing-docstring
            for converter in convert_variant.converters:
                if converter.matcher.match(text):
                    return converter(text)
            # -- pragma: no cover
            assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
            return None
        return convert_variant


def build_type_dict(converters):
    """
    Builds type dictionary for user-defined type converters,
    used by :mod:`parse` module.
    This requires that each type converter has a "name" attribute.

    :param converters: List of type converters (parse_types)
    :return: Type converter dictionary
    """
    more_types = {}
    for converter in converters:
        assert callable(converter)
        more_types[converter.name] = converter
    return more_types

# -----------------------------------------------------------------------------
# COMMON TYPE CONVERTERS
# -----------------------------------------------------------------------------
def parse_anything(text, match=None, match_start=0):
    """
    Provides a generic type converter that accepts anything and returns
    the text (unchanged).

    :param text:  Text to convert (as string).
    :return: Same text (as string).
    """
    # pylint: disable=unused-argument
    return text
parse_anything.pattern = TypeBuilder.anything_pattern


# -----------------------------------------------------------------------------
# Copyright (c) 2012-2017 by Jens Engel (https://github/jenisys/parse_type)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.