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