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