• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2# pylint: disable=missing-docstring
3r"""
4Provides support to compose user-defined parse types.
5
6Cardinality
7------------
8
9It is often useful to constrain how often a data type occurs.
10This is also called the cardinality of a data type (in a context).
11The supported cardinality are:
12
13  * 0..1  zero_or_one,  optional<T>: T or None
14  * 0..N  zero_or_more, list_of<T>
15  * 1..N  one_or_more,  list_of<T> (many)
16
17
18.. doctest:: cardinality
19
20    >>> from parse_type import TypeBuilder
21    >>> from parse import Parser
22
23    >>> def parse_number(text):
24    ...     return int(text)
25    >>> parse_number.pattern = r"\d+"
26
27    >>> parse_many_numbers = TypeBuilder.with_many(parse_number)
28    >>> more_types = { "Numbers": parse_many_numbers }
29    >>> parser = Parser("List: {numbers:Numbers}", more_types)
30    >>> parser.parse("List: 1, 2, 3")
31    <Result () {'numbers': [1, 2, 3]}>
32
33
34Enumeration Type (Name-to-Value Mappings)
35-----------------------------------------
36
37An Enumeration data type allows to select one of several enum values by using
38its name. The converter function returns the selected enum value.
39
40.. doctest:: make_enum
41
42    >>> parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False})
43    >>> more_types = { "YesNo": parse_enum_yesno }
44    >>> parser = Parser("Answer: {answer:YesNo}", more_types)
45    >>> parser.parse("Answer: yes")
46    <Result () {'answer': True}>
47
48
49Choice (Name Enumerations)
50-----------------------------
51
52A Choice data type allows to select one of several strings.
53
54.. doctest:: make_choice
55
56    >>> parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"])
57    >>> more_types = { "ChoiceYesNo": parse_choice_yesno }
58    >>> parser = Parser("Answer: {answer:ChoiceYesNo}", more_types)
59    >>> parser.parse("Answer: yes")
60    <Result () {'answer': 'yes'}>
61
62"""
63
64from __future__ import absolute_import
65import inspect
66import re
67import enum
68from parse_type.cardinality import pattern_group_count, \
69    Cardinality, TypeBuilder as CardinalityTypeBuilder
70
71__all__ = ["TypeBuilder", "build_type_dict", "parse_anything"]
72
73
74class TypeBuilder(CardinalityTypeBuilder):
75    """
76    Provides a utility class to build type-converters (parse_types) for
77    the :mod:`parse` module.
78    """
79    default_strict = True
80    default_re_opts = (re.IGNORECASE | re.DOTALL)
81
82    @classmethod
83    def make_list(cls, item_converter=None, listsep=','):
84        """
85        Create a type converter for a list of items (many := 1..*).
86        The parser accepts anything and the converter needs to fail on errors.
87
88        :param item_converter:  Type converter for an item.
89        :param listsep:  List separator to use (as string).
90        :return: Type converter function object for the list.
91        """
92        if not item_converter:
93            item_converter = parse_anything
94        return cls.with_cardinality(Cardinality.many, item_converter,
95                                    pattern=cls.anything_pattern,
96                                    listsep=listsep)
97
98    @staticmethod
99    def make_enum(enum_mappings):
100        """
101        Creates a type converter for an enumeration or text-to-value mapping.
102
103        :param enum_mappings: Defines enumeration names and values.
104        :return: Type converter function object for the enum/mapping.
105        """
106        if (inspect.isclass(enum_mappings) and
107                issubclass(enum_mappings, enum.Enum)):
108            enum_class = enum_mappings
109            enum_mappings = enum_class.__members__
110
111        def convert_enum(text):
112            if text not in convert_enum.mappings:
113                text = text.lower()     # REQUIRED-BY: parse re.IGNORECASE
114            return convert_enum.mappings[text]    #< text.lower() ???
115        convert_enum.pattern = r"|".join(enum_mappings.keys())
116        convert_enum.mappings = enum_mappings
117        return convert_enum
118
119    @staticmethod
120    def _normalize_choices(choices, transform):
121        assert transform is None or callable(transform)
122        if transform:
123            choices = [transform(value)  for value in choices]
124        else:
125            choices = list(choices)
126        return choices
127
128    @classmethod
129    def make_choice(cls, choices, transform=None, strict=None):
130        """
131        Creates a type-converter function to select one from a list of strings.
132        The type-converter function returns the selected choice_text.
133        The :param:`transform()` function is applied in the type converter.
134        It can be used to enforce the case (because parser uses re.IGNORECASE).
135
136        :param choices: List of strings as choice.
137        :param transform: Optional, initial transform function for parsed text.
138        :return: Type converter function object for this choices.
139        """
140        # -- NOTE: Parser uses re.IGNORECASE flag
141        #    => transform may enforce case.
142        choices = cls._normalize_choices(choices, transform)
143        if strict is None:
144            strict = cls.default_strict
145
146        def convert_choice(text):
147            if transform:
148                text = transform(text)
149            if strict and text not in convert_choice.choices:
150                values = ", ".join(convert_choice.choices)
151                raise ValueError("%s not in: %s" % (text, values))
152            return text
153        convert_choice.pattern = r"|".join(choices)
154        convert_choice.choices = choices
155        return convert_choice
156
157    @classmethod
158    def make_choice2(cls, choices, transform=None, strict=None):
159        """
160        Creates a type converter to select one item from a list of strings.
161        The type converter function returns a tuple (index, choice_text).
162
163        :param choices: List of strings as choice.
164        :param transform: Optional, initial transform function for parsed text.
165        :return: Type converter function object for this choices.
166        """
167        choices = cls._normalize_choices(choices, transform)
168        if strict is None:
169            strict = cls.default_strict
170
171        def convert_choice2(text):
172            if transform:
173                text = transform(text)
174            if strict and text not in convert_choice2.choices:
175                values = ", ".join(convert_choice2.choices)
176                raise ValueError("%s not in: %s" % (text, values))
177            index = convert_choice2.choices.index(text)
178            return index, text
179        convert_choice2.pattern = r"|".join(choices)
180        convert_choice2.choices = choices
181        return convert_choice2
182
183    @classmethod
184    def make_variant(cls, converters, re_opts=None, compiled=False, strict=True):
185        """
186        Creates a type converter for a number of type converter alternatives.
187        The first matching type converter is used.
188
189        REQUIRES: type_converter.pattern attribute
190
191        :param converters: List of type converters as alternatives.
192        :param re_opts:  Regular expression options zu use (=default_re_opts).
193        :param compiled: Use compiled regexp matcher, if true (=False).
194        :param strict:   Enable assertion checks.
195        :return: Type converter function object.
196
197        .. note::
198
199            Works only with named fields in :class:`parse.Parser`.
200            Parser needs group_index delta for unnamed/fixed fields.
201            This is not supported for user-defined types.
202            Otherwise, you need to use :class:`parse_type.parse.Parser`
203            (patched version of the :mod:`parse` module).
204        """
205        # -- NOTE: Uses double-dispatch with regex pattern rematch because
206        #          match is not passed through to primary type converter.
207        assert converters, "REQUIRE: Non-empty list."
208        if len(converters) == 1:
209            return converters[0]
210        if re_opts is None:
211            re_opts = cls.default_re_opts
212
213        pattern = r")|(".join([tc.pattern for tc in converters])
214        pattern = r"("+ pattern + ")"
215        group_count = len(converters)
216        for converter in converters:
217            group_count += pattern_group_count(converter.pattern)
218
219        if compiled:
220            convert_variant = cls.__create_convert_variant_compiled(converters,
221                                                                    re_opts,
222                                                                    strict)
223        else:
224            convert_variant = cls.__create_convert_variant(re_opts, strict)
225        convert_variant.pattern = pattern
226        convert_variant.converters = tuple(converters)
227        convert_variant.regex_group_count = group_count
228        return convert_variant
229
230    @staticmethod
231    def __create_convert_variant(re_opts, strict):
232        # -- USE: Regular expression pattern (compiled on use).
233        def convert_variant(text, m=None):
234            # pylint: disable=invalid-name, unused-argument, missing-docstring
235            for converter in convert_variant.converters:
236                if re.match(converter.pattern, text, re_opts):
237                    return converter(text)
238            # -- pragma: no cover
239            assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
240            return None
241        return convert_variant
242
243    @staticmethod
244    def __create_convert_variant_compiled(converters, re_opts, strict):
245        # -- USE: Compiled regular expression matcher.
246        for converter in converters:
247            matcher = getattr(converter, "matcher", None)
248            if not matcher:
249                converter.matcher = re.compile(converter.pattern, re_opts)
250
251        def convert_variant(text, m=None):
252            # pylint: disable=invalid-name, unused-argument, missing-docstring
253            for converter in convert_variant.converters:
254                if converter.matcher.match(text):
255                    return converter(text)
256            # -- pragma: no cover
257            assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
258            return None
259        return convert_variant
260
261
262def build_type_dict(converters):
263    """
264    Builds type dictionary for user-defined type converters,
265    used by :mod:`parse` module.
266    This requires that each type converter has a "name" attribute.
267
268    :param converters: List of type converters (parse_types)
269    :return: Type converter dictionary
270    """
271    more_types = {}
272    for converter in converters:
273        assert callable(converter)
274        more_types[converter.name] = converter
275    return more_types
276
277# -----------------------------------------------------------------------------
278# COMMON TYPE CONVERTERS
279# -----------------------------------------------------------------------------
280def parse_anything(text, match=None, match_start=0):
281    """
282    Provides a generic type converter that accepts anything and returns
283    the text (unchanged).
284
285    :param text:  Text to convert (as string).
286    :return: Same text (as string).
287    """
288    # pylint: disable=unused-argument
289    return text
290parse_anything.pattern = TypeBuilder.anything_pattern
291
292
293# -----------------------------------------------------------------------------
294# Copyright (c) 2012-2017 by Jens Engel (https://github/jenisys/parse_type)
295#
296# Permission is hereby granted, free of charge, to any person obtaining a copy
297# of this software and associated documentation files (the "Software"), to deal
298# in the Software without restriction, including without limitation the rights
299# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
300# copies of the Software, and to permit persons to whom the Software is
301# furnished to do so, subject to the following conditions:
302#
303#  The above copyright notice and this permission notice shall be included in
304#  all copies or substantial portions of the Software.
305#
306# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
307# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
308# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
309# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
310# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
311# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
312# SOFTWARE.
313