Source code for declarative_parser.types

from abc import ABC, abstractmethod
from argparse import ArgumentTypeError
from typing import Iterable, Any


def abstract_property(method):
    return property(abstractmethod(method))


[docs]class StringHandlingMixin(ABC): """Turn string provided on initialization into `data_type`.""" @abstract_property def separator(self): """Separator for split operation""" pass @abstract_property def item_type(self): pass @abstract_property def data_type(self): pass @property def require_separator(self): """If True and the string has no separator ArgumentTypeError will be raised.""" return False def __init__(self, string): if self.require_separator and self.separator not in string: name = self.__class__.__name__ raise ArgumentTypeError( f'Given string {string} does not look like a ' f'{name} (no {self.separator}, which is required)' ) try: self.data = self.data_type( [ self.item_type(value) if value != '' else None for value in string.split(self.separator) ] if self.separator else self.item_type(string) ) except (TypeError, ValueError) as e: raise ArgumentTypeError(*e.args)
[docs]class Subset(ABC): @abstractmethod def get_iterator(self, iterable: Iterable[Any]) -> Iterable: return iterable def get(self, iterable: Iterable[Any]): return list(self.get_iterator(iterable))
def positive_int(value): value = int(value) if value < 0: raise ValueError('Indices need to be positive integers') return value
[docs]def n_tuple(n): """Factory for n-tuples.""" def custom_tuple(data): if len(data) != n: raise TypeError( f'{n}-tuple requires exactly {n} items ' f'({len(data)} received).' ) return tuple(data) return custom_tuple
[docs]def dsv(value_type, delimiter=','): """Delimiter Separated Values""" def closure(value): return [ value_type(y) for y in value.split(delimiter) ] return closure
[docs]def one_of(*types): """Create a function which attempts to cast input to any of provided types. The order of provided `types` is meaningful - if two types accept given input value, the first one on list will be used. Types should be able to accept a string (if correct) as input value for their constructors. """ def one_of_types(string): exceptions = [] for type_constructor in types: try: return type_constructor(string) except (ArgumentTypeError, TypeError, ValueError) as e: exceptions.append(f'{type_constructor.__name__}: {e}') names = ', '.join(t.__name__ for t in types) exceptions = ''.join('\n\t' + e for e in exceptions) raise ArgumentTypeError( f'Argument {string} does not match any of allowed types: {names}.\n' + f'Following exceptions has been raised: {exceptions}' ) return one_of_types
static = staticmethod
[docs]class Indices(Subset, StringHandlingMixin): separator = ',' # negative indices may be ambiguous item_type = static(positive_int) # each column should be used once data_type = set def get_iterator(self, iterable): for i, value in enumerate(iterable): if i in self.data: yield value
[docs]class Slice(Subset, StringHandlingMixin): require_separator = True separator = ':' item_type = int data_type = static(one_of(n_tuple(2), n_tuple(3))) def get_iterator(self, iterable): return iterable[slice(*self.data)]
[docs]class Range(Subset, StringHandlingMixin): """Simplified slice with '-' as separator. Handles only start and end, does not support negative numbers. """ require_separator = True separator = '-' item_type = int # if user provides '1-3-5' or '1--3' we will not handle that # (such values are ambiguous, possibly typos) data_type = static(n_tuple(2)) def get_iterator(self, iterable): return iterable[slice(*self.data)]