aboutsummaryrefslogtreecommitdiffstats
path: root/parse_type/cardinality_field.py
diff options
context:
space:
mode:
Diffstat (limited to 'parse_type/cardinality_field.py')
-rw-r--r--parse_type/cardinality_field.py171
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)