diff options
Diffstat (limited to 'parse_type/cardinality_field.py')
-rw-r--r-- | parse_type/cardinality_field.py | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/parse_type/cardinality_field.py b/parse_type/cardinality_field.py new file mode 100644 index 0000000..4e892ed --- /dev/null +++ b/parse_type/cardinality_field.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +""" +Provides support for cardinality fields. +A cardinality field is a type suffix for parse format expression, ala: + + "{person:Person?}" #< Cardinality: 0..1 = zero or one = optional + "{persons:Person*}" #< Cardinality: 0..* = zero or more = many0 + "{persons:Person+}" #< Cardinality: 1..* = one or more = many +""" + +from __future__ import absolute_import +import six +from parse_type.cardinality import Cardinality, TypeBuilder + + +class MissingTypeError(KeyError): # pylint: disable=missing-docstring + pass + +# ----------------------------------------------------------------------------- +# CLASS: Cardinality (Field Part) +# ----------------------------------------------------------------------------- +class CardinalityField(object): + """Cardinality field for parse format expression, ala: + + "{person:Person?}" #< Cardinality: 0..1 = zero or one = optional + "{persons:Person*}" #< Cardinality: 0..* = zero or more = many0 + "{persons:Person+}" #< Cardinality: 1..* = one or more = many + """ + + # -- MAPPING SUPPORT: + pattern_chars = "?*+" + from_char_map = { + '?': Cardinality.zero_or_one, + '*': Cardinality.zero_or_more, + '+': Cardinality.one_or_more, + } + to_char_map = dict([(value, key) for key, value in from_char_map.items()]) + + @classmethod + def matches_type(cls, type_name): + """Checks if a type name uses the CardinalityField naming scheme. + + :param type_name: Type name to check (as string). + :return: True, if type name has CardinalityField name suffix. + """ + return type_name and type_name[-1] in CardinalityField.pattern_chars + + @classmethod + def split_type(cls, type_name): + """Split type of a type name with CardinalityField suffix into its parts. + + :param type_name: Type name (as string). + :return: Tuple (type_basename, cardinality) + """ + if cls.matches_type(type_name): + basename = type_name[:-1] + cardinality = cls.from_char_map[type_name[-1]] + else: + # -- ASSUME: Cardinality.one + cardinality = Cardinality.one + basename = type_name + return (basename, cardinality) + + @classmethod + def make_type(cls, basename, cardinality): + """Build new type name according to CardinalityField naming scheme. + + :param basename: Type basename of primary type (as string). + :param cardinality: Cardinality of the new type (as Cardinality item). + :return: Type name with CardinalityField suffix (if needed) + """ + if cardinality is Cardinality.one: + # -- POSTCONDITION: assert not cls.make_type(type_name) + return basename + # -- NORMAL CASE: type with CardinalityField suffix. + type_name = "%s%s" % (basename, cls.to_char_map[cardinality]) + # -- POSTCONDITION: assert cls.make_type(type_name) + return type_name + + +# ----------------------------------------------------------------------------- +# CLASS: CardinalityFieldTypeBuilder +# ----------------------------------------------------------------------------- +class CardinalityFieldTypeBuilder(object): + """Utility class to create type converters based on: + + * the CardinalityField naming scheme and + * type converter for cardinality=1 + """ + + listsep = ',' + + @classmethod + def create_type_variant(cls, type_name, type_converter): + r"""Create type variants for types with a cardinality field. + The new type converters are based on the type converter with + cardinality=1. + + .. code-block:: python + + import parse + + @parse.with_pattern(r'\d+') + def parse_number(text): + return int(text) + + new_type = CardinalityFieldTypeBuilder.create_type_variant( + "Number+", parse_number) + new_type = CardinalityFieldTypeBuilder.create_type_variant( + "Number+", dict(Number=parse_number)) + + :param type_name: Type name with cardinality field suffix. + :param type_converter: Type converter or type dictionary. + :return: Type converter variant (function). + :raises: ValueError, if type_name does not end with CardinalityField + :raises: MissingTypeError, if type_converter is missing in type_dict + """ + assert isinstance(type_name, six.string_types) + if not CardinalityField.matches_type(type_name): + message = "type_name='%s' has no CardinalityField" % type_name + raise ValueError(message) + + primary_name, cardinality = CardinalityField.split_type(type_name) + if isinstance(type_converter, dict): + type_dict = type_converter + type_converter = type_dict.get(primary_name, None) + if not type_converter: + raise MissingTypeError(primary_name) + + assert callable(type_converter) + type_variant = TypeBuilder.with_cardinality(cardinality, + type_converter, + listsep=cls.listsep) + type_variant.name = type_name + return type_variant + + + @classmethod + def create_type_variants(cls, type_names, type_dict): + """Create type variants for types with a cardinality field. + The new type converters are based on the type converter with + cardinality=1. + + .. code-block:: python + + # -- USE: parse_number() type converter function. + new_types = CardinalityFieldTypeBuilder.create_type_variants( + ["Number?", "Number+"], dict(Number=parse_number)) + + :param type_names: List of type names with cardinality field suffix. + :param type_dict: Type dictionary with named type converters. + :return: Type dictionary with type converter variants. + """ + type_variant_dict = {} + for type_name in type_names: + type_variant = cls.create_type_variant(type_name, type_dict) + type_variant_dict[type_name] = type_variant + return type_variant_dict + + # MAYBE: Check if really needed. + @classmethod + def create_missing_type_variants(cls, type_names, type_dict): + """Create missing type variants for types with a cardinality field. + + :param type_names: List of type names with cardinality field suffix. + :param type_dict: Type dictionary with named type converters. + :return: Type dictionary with missing type converter variants. + """ + missing_type_names = [name for name in type_names + if name not in type_dict] + return cls.create_type_variants(missing_type_names, type_dict) |