• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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