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
|
# -*- coding: utf-8 -*-
"""
This module simplifies to build parse types and regular expressions
for a data type with the specified cardinality.
"""
# -- USE: enum34
from __future__ import absolute_import
from enum import Enum
# -----------------------------------------------------------------------------
# FUNCTIONS:
# -----------------------------------------------------------------------------
def pattern_group_count(pattern):
"""Count the pattern-groups within a regex-pattern (as text)."""
return pattern.replace(r"\(", "").count("(")
# -----------------------------------------------------------------------------
# CLASS: Cardinality (Enum Class)
# -----------------------------------------------------------------------------
class Cardinality(Enum):
"""Cardinality enumeration class to simplify building regular expression
patterns for a data type with the specified cardinality.
"""
# pylint: disable=bad-whitespace
__order__ = "one, zero_or_one, zero_or_more, one_or_more"
one = (None, 0)
zero_or_one = (r"(%s)?", 1) # SCHEMA: pattern
zero_or_more = (r"(%s)?(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern
one_or_more = (r"(%s)(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern
# -- ALIASES:
optional = zero_or_one
many0 = zero_or_more
many = one_or_more
def __init__(self, schema, group_count=0):
self.schema = schema
self.group_count = group_count #< Number of match groups.
def is_many(self):
"""Checks for a more general interpretation of "many".
:return: True, if Cardinality.zero_or_more or Cardinality.one_or_more.
"""
return ((self is Cardinality.zero_or_more) or
(self is Cardinality.one_or_more))
def make_pattern(self, pattern, listsep=','):
"""Make pattern for a data type with the specified cardinality.
.. code-block:: python
yes_no_pattern = r"yes|no"
many_yes_no = Cardinality.one_or_more.make_pattern(yes_no_pattern)
:param pattern: Regular expression for type (as string).
:param listsep: List separator for multiple items (as string, optional)
:return: Regular expression pattern for type with cardinality.
"""
if self is Cardinality.one:
return pattern
elif self is Cardinality.zero_or_one:
return self.schema % pattern
# -- OTHERWISE:
return self.schema % (pattern, listsep, pattern)
def compute_group_count(self, pattern):
"""Compute the number of regexp match groups when the pattern is provided
to the :func:`Cardinality.make_pattern()` method.
:param pattern: Item regexp pattern (as string).
:return: Number of regexp match groups in the cardinality pattern.
"""
group_count = self.group_count
pattern_repeated = 1
if self.is_many():
pattern_repeated = 2
return group_count + pattern_repeated * pattern_group_count(pattern)
# -----------------------------------------------------------------------------
# CLASS: TypeBuilder
# -----------------------------------------------------------------------------
class TypeBuilder(object):
"""Provides a utility class to build type-converters (parse_types) for parse.
It supports to build new type-converters for different cardinality
based on the type-converter for cardinality one.
"""
anything_pattern = r".+?"
default_pattern = anything_pattern
@classmethod
def with_cardinality(cls, cardinality, converter, pattern=None,
listsep=','):
"""Creates a type converter for the specified cardinality
by using the type converter for T.
:param cardinality: Cardinality to use (0..1, 0..*, 1..*).
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:return: type-converter for optional<T> (T or None).
"""
if cardinality is Cardinality.one:
return converter
# -- NORMAL-CASE
builder_func = getattr(cls, "with_%s" % cardinality.name)
if cardinality is Cardinality.zero_or_one:
return builder_func(converter, pattern)
# -- MANY CASE: 0..*, 1..*
return builder_func(converter, pattern, listsep=listsep)
@classmethod
def with_zero_or_one(cls, converter, pattern=None):
"""Creates a type converter for a T with 0..1 times
by using the type converter for one item of T.
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:return: type-converter for optional<T> (T or None).
"""
cardinality = Cardinality.zero_or_one
if not pattern:
pattern = getattr(converter, "pattern", cls.default_pattern)
optional_pattern = cardinality.make_pattern(pattern)
group_count = cardinality.compute_group_count(pattern)
def convert_optional(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
if text:
text = text.strip()
if not text:
return None
return converter(text)
convert_optional.pattern = optional_pattern
convert_optional.regex_group_count = group_count
return convert_optional
@classmethod
def with_zero_or_more(cls, converter, pattern=None, listsep=","):
"""Creates a type converter function for a list<T> with 0..N items
by using the type converter for one item of T.
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:param listsep: Optional list separator between items (default: ',')
:return: type-converter for list<T>
"""
cardinality = Cardinality.zero_or_more
if not pattern:
pattern = getattr(converter, "pattern", cls.default_pattern)
many0_pattern = cardinality.make_pattern(pattern, listsep)
group_count = cardinality.compute_group_count(pattern)
def convert_list0(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
if text:
text = text.strip()
if not text:
return []
return [converter(part.strip()) for part in text.split(listsep)]
convert_list0.pattern = many0_pattern
# OLD convert_list0.group_count = group_count
convert_list0.regex_group_count = group_count
return convert_list0
@classmethod
def with_one_or_more(cls, converter, pattern=None, listsep=","):
"""Creates a type converter function for a list<T> with 1..N items
by using the type converter for one item of T.
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:param listsep: Optional list separator between items (default: ',')
:return: Type converter for list<T>
"""
cardinality = Cardinality.one_or_more
if not pattern:
pattern = getattr(converter, "pattern", cls.default_pattern)
many_pattern = cardinality.make_pattern(pattern, listsep)
group_count = cardinality.compute_group_count(pattern)
def convert_list(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
return [converter(part.strip()) for part in text.split(listsep)]
convert_list.pattern = many_pattern
# OLD: convert_list.group_count = group_count
convert_list.regex_group_count = group_count
return convert_list
# -- ALIAS METHODS:
@classmethod
def with_optional(cls, converter, pattern=None):
"""Alias for :py:meth:`with_zero_or_one()` method."""
return cls.with_zero_or_one(converter, pattern)
@classmethod
def with_many(cls, converter, pattern=None, listsep=','):
"""Alias for :py:meth:`with_one_or_more()` method."""
return cls.with_one_or_more(converter, pattern, listsep)
@classmethod
def with_many0(cls, converter, pattern=None, listsep=','):
"""Alias for :py:meth:`with_zero_or_more()` method."""
return cls.with_zero_or_more(converter, pattern, listsep)
|