aboutsummaryrefslogtreecommitdiffstats
path: root/parse_type/cardinality_field.py
blob: 4e892edde28bc39517bb02e8a7e2de60a6ec12a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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)