| # -*- 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) |