Source code for py2store.key_mappers.tuples

"""
Tools to map tuple-structured keys.
That is, converting from any of the following kinds of keys:
    * tuples (or list-like)
    * dicts
    * formatted/templated strings
    * dsv (Delimiter-Separated Values)

"""

# TODO: Add short docs
# TODO: Add SIMPLE (just one or two tests) doctests.
# TODO: Add randomized "bijectivity" tests (see _test_dsv_of_list for what I mean) if easy.

from functools import partial
from py2store.errors import KeyValidationError, _assert_condition

__assert_condition = partial(_assert_condition, err_cls=KeyValidationError)


def tuple_of_dict(d, fields):
    __assert_condition(
        len(fields) == len(d), f'len(d)={len(d)} but len(fields)={len(fields)}'
    )
    return tuple(d[f] for f in fields)


def dict_of_tuple(d, fields):
    __assert_condition(
        len(fields) == len(d), f'len(d)={len(d)} but len(fields)={len(fields)}'
    )
    return {f: x for f, x in zip(fields, d)}


[docs]def str_of_tuple(d, str_format): """Convert tuple to str. It's just str_format.format(*d). Why even write such a function? (1) To have a consistent interface for key conversions (2) We want a KeyValidationError to occur here Args: d: tuple if params to str_format str_format: Auto fields format string. If you have manual fields, consider auto_field_format_str to convert. Returns: parametrized string >>> str_of_tuple(('hello', 'world'), "Well, {} dear {}!") 'Well, hello dear world!' """ try: return str_format.format(*d) except Exception as e: raise KeyValidationError(e)
def tuple_of_str(d, compiled_regex): m = compiled_regex.match(d) if m: return m.groups() else: raise KeyValidationError( f"The string {d} didn't match the pattern {compiled_regex}" ) def str_of_dict(d, str_format): try: return str_format.format(**d) except Exception as e: raise KeyValidationError(e) def dict_of_str(d, compiled_regex): m = compiled_regex.match(d) if m: return m.groupdict() else: raise KeyValidationError( f"The string {d} didn't match the pattern {compiled_regex}" )
[docs]def mk_str_of_obj(attrs): """Make a function that transforms objects to strings, using specific attributes of object. Args: attrs: Attributes that should be read off of the object to make the parameters of the string Returns: A transformation function >>> from dataclasses import dataclass >>> @dataclass ... class A: ... foo: int ... bar: str >>> a = A(foo=0, bar='rin') >>> a A(foo=0, bar='rin') >>> >>> str_from_obj = mk_str_of_obj(['foo', 'bar']) >>> str_from_obj(a, 'ST{foo}/{bar}/G') 'ST0/rin/G' """ def dict_of_obj(o): return {k: getattr(o, k) for k in attrs} def str_of_obj(d, str_format): return str_of_dict(dict_of_obj(d), str_format) return str_of_obj
[docs]def mk_obj_of_str(constructor): """Make a function that transforms a string to an object. The factory making inverses of what mk_str_from_obj makes. Args: constructor: The function (or class) that will be used to make objects from the **kwargs parsed out of the string. Returns: A function factory. """ def obj_of_str(d, compiled_regex): constructor(**dict_of_str(d, compiled_regex)) return obj_of_str
[docs]def dsv_of_list(d, sep=','): """ Converting a list of strings to a dsv (delimiter-separated values) string. Note that unlike most key mappers, there is no schema imposing size here. If you wish to impose a size validation, do so externally (we suggest using a decorator for that). Args: d: A list of component strings sep: The delimiter text used to separate a string into a list of component strings Returns: The delimiter-separated values (dsv) string for the input tuple >>> dsv_of_list(['a', 'brown', 'fox'], sep=' ') 'a brown fox' >>> dsv_of_list(('jumps', 'over'), sep='/') # for filepaths (and see that tuple inputs work too!) 'jumps/over' >>> dsv_of_list(['Sat', 'Jan', '1', '1983'], sep=',') # csv: the usual delimiter-separated values format 'Sat,Jan,1,1983' >>> dsv_of_list(['First', 'Last'], sep=':::') # a longer delimiter 'First:::Last' >>> dsv_of_list(['singleton'], sep='@') # when the list has only one element 'singleton' >>> dsv_of_list([], sep='@') # when the list is empty '' """ return sep.join(d)
[docs]def list_of_dsv(d, sep=','): """ Converting a dsv (delimiter-separated values) string to the list of it's components. Args: d: A (delimiter-separated values) string sep: The delimiter text used to separate the string into a list of component strings Returns: A list of component strings corresponding to the input delimiter-separated values (dsv) string >>> list_of_dsv('a brown fox', sep=' ') ['a', 'brown', 'fox'] >>> tuple(list_of_dsv('jumps/over', sep='/')) # for filepaths ('jumps', 'over') >>> list_of_dsv('Sat,Jan,1,1983', sep=',') # csv: the usual delimiter-separated values format ['Sat', 'Jan', '1', '1983'] >>> list_of_dsv('First:::Last', sep=':::') # a longer delimiter ['First', 'Last'] >>> list_of_dsv('singleton', sep='@') # when the list has only one element ['singleton'] >>> list_of_dsv('', sep='@') # when the string is empty [] """ if ( not d ): # doing this, because split returns [''] on an empty string (bad choice if you ask me!) return [] else: return d.split(sep)
def _test_dsv_of_list(n_tests=100, max_n_elements=10, max_sep_length=3): import random import string alphanumeric = string.digits + string.ascii_lowercase non_alphanumeric = ''.join(set(string.printable).difference(alphanumeric)) def random_string(length=7, character_set=alphanumeric): return ''.join(random.choice(character_set) for _ in range(length)) for i in range(n_tests): for n_elements in random.choice(range(1, max_n_elements + 1)): words = [x for x in random_string(n_elements, alphanumeric)] sep_length = random.choice(range(1, max_sep_length + 1)) sep = random_string(sep_length, non_alphanumeric) dsv_line = dsv_of_list(words, sep) dsv_words = list_of_dsv(dsv_line, sep) assert all( dsv_words == words ), f'Expected:\n\t{words}\nGot:\n\t{dsv_words}' if __name__ == '__main__': _test_dsv_of_list()