1# -*- coding: utf-8 -*- 2""" 3This module simplifies to build parse types and regular expressions 4for a data type with the specified cardinality. 5""" 6 7# -- USE: enum34 8from __future__ import absolute_import 9from enum import Enum 10 11 12# ----------------------------------------------------------------------------- 13# FUNCTIONS: 14# ----------------------------------------------------------------------------- 15def pattern_group_count(pattern): 16 """Count the pattern-groups within a regex-pattern (as text).""" 17 return pattern.replace(r"\(", "").count("(") 18 19 20# ----------------------------------------------------------------------------- 21# CLASS: Cardinality (Enum Class) 22# ----------------------------------------------------------------------------- 23class Cardinality(Enum): 24 """Cardinality enumeration class to simplify building regular expression 25 patterns for a data type with the specified cardinality. 26 """ 27 # pylint: disable=bad-whitespace 28 __order__ = "one, zero_or_one, zero_or_more, one_or_more" 29 one = (None, 0) 30 zero_or_one = (r"(%s)?", 1) # SCHEMA: pattern 31 zero_or_more = (r"(%s)?(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern 32 one_or_more = (r"(%s)(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern 33 34 # -- ALIASES: 35 optional = zero_or_one 36 many0 = zero_or_more 37 many = one_or_more 38 39 def __init__(self, schema, group_count=0): 40 self.schema = schema 41 self.group_count = group_count #< Number of match groups. 42 43 def is_many(self): 44 """Checks for a more general interpretation of "many". 45 46 :return: True, if Cardinality.zero_or_more or Cardinality.one_or_more. 47 """ 48 return ((self is Cardinality.zero_or_more) or 49 (self is Cardinality.one_or_more)) 50 51 def make_pattern(self, pattern, listsep=','): 52 """Make pattern for a data type with the specified cardinality. 53 54 .. code-block:: python 55 56 yes_no_pattern = r"yes|no" 57 many_yes_no = Cardinality.one_or_more.make_pattern(yes_no_pattern) 58 59 :param pattern: Regular expression for type (as string). 60 :param listsep: List separator for multiple items (as string, optional) 61 :return: Regular expression pattern for type with cardinality. 62 """ 63 if self is Cardinality.one: 64 return pattern 65 elif self is Cardinality.zero_or_one: 66 return self.schema % pattern 67 # -- OTHERWISE: 68 return self.schema % (pattern, listsep, pattern) 69 70 def compute_group_count(self, pattern): 71 """Compute the number of regexp match groups when the pattern is provided 72 to the :func:`Cardinality.make_pattern()` method. 73 74 :param pattern: Item regexp pattern (as string). 75 :return: Number of regexp match groups in the cardinality pattern. 76 """ 77 group_count = self.group_count 78 pattern_repeated = 1 79 if self.is_many(): 80 pattern_repeated = 2 81 return group_count + pattern_repeated * pattern_group_count(pattern) 82 83 84# ----------------------------------------------------------------------------- 85# CLASS: TypeBuilder 86# ----------------------------------------------------------------------------- 87class TypeBuilder(object): 88 """Provides a utility class to build type-converters (parse_types) for parse. 89 It supports to build new type-converters for different cardinality 90 based on the type-converter for cardinality one. 91 """ 92 anything_pattern = r".+?" 93 default_pattern = anything_pattern 94 95 @classmethod 96 def with_cardinality(cls, cardinality, converter, pattern=None, 97 listsep=','): 98 """Creates a type converter for the specified cardinality 99 by using the type converter for T. 100 101 :param cardinality: Cardinality to use (0..1, 0..*, 1..*). 102 :param converter: Type converter (function) for data type T. 103 :param pattern: Regexp pattern for an item (=converter.pattern). 104 :return: type-converter for optional<T> (T or None). 105 """ 106 if cardinality is Cardinality.one: 107 return converter 108 # -- NORMAL-CASE 109 builder_func = getattr(cls, "with_%s" % cardinality.name) 110 if cardinality is Cardinality.zero_or_one: 111 return builder_func(converter, pattern) 112 # -- MANY CASE: 0..*, 1..* 113 return builder_func(converter, pattern, listsep=listsep) 114 115 @classmethod 116 def with_zero_or_one(cls, converter, pattern=None): 117 """Creates a type converter for a T with 0..1 times 118 by using the type converter for one item of T. 119 120 :param converter: Type converter (function) for data type T. 121 :param pattern: Regexp pattern for an item (=converter.pattern). 122 :return: type-converter for optional<T> (T or None). 123 """ 124 cardinality = Cardinality.zero_or_one 125 if not pattern: 126 pattern = getattr(converter, "pattern", cls.default_pattern) 127 optional_pattern = cardinality.make_pattern(pattern) 128 group_count = cardinality.compute_group_count(pattern) 129 130 def convert_optional(text, m=None): 131 # pylint: disable=invalid-name, unused-argument, missing-docstring 132 if text: 133 text = text.strip() 134 if not text: 135 return None 136 return converter(text) 137 convert_optional.pattern = optional_pattern 138 convert_optional.regex_group_count = group_count 139 return convert_optional 140 141 @classmethod 142 def with_zero_or_more(cls, converter, pattern=None, listsep=","): 143 """Creates a type converter function for a list<T> with 0..N items 144 by using the type converter for one item of T. 145 146 :param converter: Type converter (function) for data type T. 147 :param pattern: Regexp pattern for an item (=converter.pattern). 148 :param listsep: Optional list separator between items (default: ',') 149 :return: type-converter for list<T> 150 """ 151 cardinality = Cardinality.zero_or_more 152 if not pattern: 153 pattern = getattr(converter, "pattern", cls.default_pattern) 154 many0_pattern = cardinality.make_pattern(pattern, listsep) 155 group_count = cardinality.compute_group_count(pattern) 156 157 def convert_list0(text, m=None): 158 # pylint: disable=invalid-name, unused-argument, missing-docstring 159 if text: 160 text = text.strip() 161 if not text: 162 return [] 163 return [converter(part.strip()) for part in text.split(listsep)] 164 convert_list0.pattern = many0_pattern 165 # OLD convert_list0.group_count = group_count 166 convert_list0.regex_group_count = group_count 167 return convert_list0 168 169 @classmethod 170 def with_one_or_more(cls, converter, pattern=None, listsep=","): 171 """Creates a type converter function for a list<T> with 1..N items 172 by using the type converter for one item of T. 173 174 :param converter: Type converter (function) for data type T. 175 :param pattern: Regexp pattern for an item (=converter.pattern). 176 :param listsep: Optional list separator between items (default: ',') 177 :return: Type converter for list<T> 178 """ 179 cardinality = Cardinality.one_or_more 180 if not pattern: 181 pattern = getattr(converter, "pattern", cls.default_pattern) 182 many_pattern = cardinality.make_pattern(pattern, listsep) 183 group_count = cardinality.compute_group_count(pattern) 184 185 def convert_list(text, m=None): 186 # pylint: disable=invalid-name, unused-argument, missing-docstring 187 return [converter(part.strip()) for part in text.split(listsep)] 188 convert_list.pattern = many_pattern 189 # OLD: convert_list.group_count = group_count 190 convert_list.regex_group_count = group_count 191 return convert_list 192 193 # -- ALIAS METHODS: 194 @classmethod 195 def with_optional(cls, converter, pattern=None): 196 """Alias for :py:meth:`with_zero_or_one()` method.""" 197 return cls.with_zero_or_one(converter, pattern) 198 199 @classmethod 200 def with_many(cls, converter, pattern=None, listsep=','): 201 """Alias for :py:meth:`with_one_or_more()` method.""" 202 return cls.with_one_or_more(converter, pattern, listsep) 203 204 @classmethod 205 def with_many0(cls, converter, pattern=None, listsep=','): 206 """Alias for :py:meth:`with_zero_or_more()` method.""" 207 return cls.with_zero_or_more(converter, pattern, listsep) 208