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