blob: 4e892edde28bc39517bb02e8a7e2de60a6ec12a0 [file] [log] [blame]
# -*- 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)