Source code for retype.extras.str_subclasses

import logging
from copy import deepcopy
from collections import UserString


logger = logging.getLogger('str_subclasses')
# Suppress our rather verbose logs by default. They can be shown in tests by
#  setting level to DEBUG.
logger.setLevel(logging.WARNING)


[docs]class AnyStr(UserString): """A string that is equal to all `possibilities'. Each possibility should be equal in length."""
[docs] def __init__(self, *possibilities): super().__init__(possibilities[0]) self.base = possibilities[0] self.possibilities = [] # Get rid of duplicate possibilities for possibility in possibilities: if possibility not in self.possibilities: self.possibilities.append(possibility)
def __eq__(self, s): if s in self.possibilities: return True return False def _add(self, s, order=1): args = None if order == 1: args = (self, s) elif order == -1: args = (s, self) else: raise ValueError("order is expected to be 1 or -1, not {}" .format(order)) if isinstance(s, str): return ManifoldStr.from_str_and_anystr(s, self, -order) if isinstance(s, AnyStr): return ManifoldStr.from_anystr_and_anystr(*args) else: return NotImplemented # raise TypeError('can only concatenate str or AnyStr (not "{}")\ # to AnyStr'.format(type(s))) def __add__(self, s): return self._add(s, 1) def __radd__(self, s): return self._add(s, -1)
[docs] def join(self, iterable): """Not like the str method because we don’t put anything in between""" result = None for s in iterable: if not result: result = s else: result += s return result
def __getitem__(self, i): if isinstance(i, int): if i < 0: self.__getitem__(len(self.base) + i) return AnyStr(*[possibility[i] for possibility in self.possibilities]) elif isinstance(i, slice): start, stop, step = i.indices(len(self)) if step == 1 and start == 0 and stop == len(self): return self # Breaking up AnyStrs is unsupported, revert to str else: logger.debug("__getitem__ call that breaks up an AnyStr") return ''.join([self.base[j] for j in range(start, stop, step)]) else: raise TypeError("AnyStr indices must be integers or slices, not {}" .format(type(i))) def __str__(self): # this is kind of weird but i am doing this for ease of use in manifold # when we calculate combined. but it isn’t ideal, returning self.base # would make more sense (but break our usage in manifold) if len(self.possibilities) > 1: return str(self.possibilities[1]) else: return str(self.base) def __repr__(self): return (f'{self.__class__.__name__}' f'{*self.possibilities,}')
[docs] def strip(self, chars=" ", directions=[1, -1]): new = AnyStr(*self.possibilities) for i in directions: if i == 1: sl = slice(1, len(self)) k = 0 elif i == -1: sl = slice(0, -1) k = -1 else: raise ValueError("each direction is expected to be 1 or -1, \ not {}".format(i)) for char in chars: while new and new[k] == char: new = new[sl] if new is None: new = '' break return new
[docs] def lstrip(self, chars=" "): return self.strip(chars, directions=[1])
[docs] def rstrip(self, chars=" "): return self.strip(chars, directions=[-1])
[docs] def isspace(self): for possibility in self.possibilities: if possibility.strip() == '': return True return False
[docs]class ManifoldStr(UserString):
[docs] def __init__(self, data, rdict): """A string structure that takes an str `data' and a dictionary `rdict' where each key is a substring to be replaced, and corresponding value is an array of possible replacements of equal length. Each key in `replacement_dict' is replaced where found in `data' with an AnyStr containing key and replacements.""" super().__init__(data) self.base = data self.rdict = rdict # A dictionary of strs and AnyStrs with the index of where each begins self.manifold = {0: data} def makeCombined(): keys = sorted([*self.manifold]) combined = ''.join([str(self.manifold[key]) for key in keys]) return combined logger.debug("Initialisation start. Initial value of manifold: {}" .format(self.manifold)) for replace_me, replacements in rdict.items(): logger.debug('--- New iteration on replacements loop ---') logger.debug("Current replace: '{}'".format(replace_me)) logger.debug('Current value of manifold: {}'.format(self.manifold)) combined = makeCombined() logger.debug("Searching through combined: '{}'".format(combined)) index = combined.find(replace_me) logger.debug("First find at index: {}".format(index)) while index != -1: manifold_copy = deepcopy(self.manifold) # Go through the substrings that are at or before our position for i, substring in manifold_copy.items(): if i > index: continue # Skip if it is an AnyStr, thus already had a replacement; # nothing we can do if type(substring) is not str: continue logger.debug("Looking at substring: {} '{}'" .format(i, substring)) del self.manifold[i] before_us = substring[:index - i] logger.debug("before us: '{}'".format(before_us)) if before_us: self.manifold[i] = before_us after_us = substring[len(before_us) + len(replace_me):] logger.debug("after us: '{}'".format(after_us)) if after_us: new_key = index + len(replace_me) self.manifold[new_key] = after_us # Place new AnyStr self.manifold[index] = AnyStr(replace_me, *replacements) # Construct new string to search through for what still needs # replacing combined = makeCombined() logger.debug("New combined: '{}'".format(combined)) index = combined.find(replace_me) logger.debug("Next index: {}".format(index)) logger.debug("Initialisation complete. Final value of manifold: {}\n" .format(self.manifold))
def __eq__(self, s): if s == self.data: return True else: # else check against substrings for i, substring in self.manifold.items(): if s[i: i + len(substring)] != substring: return False return True return False def _add(self, s, order=1): args = None if order == 1: args = (self, s) elif order == -1: args = (s, self) else: raise ValueError("order is expected to be 1 or -1, not {}" .format(order)) if isinstance(s, str): return ManifoldStr.from_ms_and_str(self, s, order) if isinstance(s, AnyStr): return ManifoldStr.from_ms_and_anystr(self, s, order) if isinstance(s, ManifoldStr): return ManifoldStr.from_ms_and_ms(*args) else: raise TypeError('can only concatenate str, AnyStr, or ManifoldStr\ (not "{}") to ManifoldStr'.format(type(s))) def __add__(self, s): return self._add(s, 1) def __radd__(self, s): return self._add(s, -1)
[docs] def join(self, iterable): result = None for s in iterable: if not result: result = s else: result += s return result
def __getitem__(self, i): if isinstance(i, int): if i < 0: return self.__getitem__(len(self.data) + i) elif i in self.manifold: if len(self.manifold[i]) > 1: return self.manifold[i][0] else: return self.manifold[i] else: # Find substring before i, and extract just the index required descending_keys = sorted([*self.manifold], reverse=True) substring = k = None for k in descending_keys: if k < i: substring, k = (self.manifold[k], k) break if substring is None: raise KeyError return substring[i - k] elif isinstance(i, slice): start, stop, step = i.indices(len(self)) if step == 1: descending_keys = sorted([*self.manifold], reverse=True) substring_by_start = substring_by_stop = None new_manifold = {} for k in descending_keys: index = 0 if k == 0 else k-start if start <= k < stop: new_manifold[index] = self.manifold[k] if not substring_by_stop: if k < stop: substring_by_stop = self.manifold[k] new_manifold[index] = substring_by_stop[:stop-k] if k < start: substring_by_start = self.manifold[k] new_manifold[index] = substring_by_start[start-k:] break return ManifoldStr.by_parts(self.data[start:stop], self.rdict, new_manifold) else: return self.join([self[j] for j in range(start, stop, step)]) else: raise TypeError("ManifoldStr indices must be integers or slices,\ not {}".format(type(i))) def __repr__(self): return (f'{self.__class__.__name__}' f'(\'{self.base}\', {self.rdict})')
[docs] def strip(self, chars=" ", directions=[1, -1]): new = deepcopy(self) for i in directions: if i == 1: sl = slice(1, len(self)) k = 0 elif i == -1: sl = slice(0, -1) k = -1 else: raise ValueError("each direction is expected to be 1 or -1, \ not {}".format(i)) for char in chars: while new and new[k] == char: new = new[sl] if new is None: new = '' break return new
[docs] def lstrip(self, chars=" "): return self.strip(chars, directions=[1])
[docs] def rstrip(self, chars=" "): return self.strip(chars, directions=[-1])
[docs] def isspace(self): if self.strip() == '': return True return False
[docs] @classmethod def by_parts(cls, data, rdict, manifold): new = cls.__new__(cls) super().__init__(cls, data) new.base = data new.rdict = rdict new.manifold = manifold return new
[docs] @classmethod def from_anystr_and_anystr(cls, anystr1, anystr2): data = anystr1.base + anystr2.base rdict = {anystr1.base: anystr1.possibilities[1:], anystr2.base: anystr2.possibilities[1:]} manifold = {0: anystr1, len(anystr1): anystr2} new = cls.by_parts(data, rdict, manifold) logger.debug("New ManifoldStr from AnyStr '{}' and AnyStr '{}':\ '{}'".format(anystr1, anystr2, new)) return new
[docs] @classmethod def from_str_and_anystr(cls, str_, anystr, order=1): data = manifold = None if order == 1: data = str_ + anystr.base manifold = {0: str_, len(str_): anystr} elif order == -1: data = anystr.base + str_ manifold = {0: anystr, len(anystr): str_} else: raise ValueError("order is expected to be 1 or -1, not {}" .format(order)) rdict = {anystr.base: anystr.possibilities[1:]} new = cls.by_parts(data, rdict, manifold) logger.debug("New ManifoldStr from str '{}' and AnyStr '{}'\ (order {}): '{}'".format(str_, anystr, order, new)) return new
[docs] @classmethod def from_anystr_and_str(cls, anystr, str_): return cls.from_str_and_anystr(str_, anystr, -1)
[docs] @classmethod def from_ms_and_anystr(cls, ms, anystr, order=1): data = manifold = None if order == 1: data = ms.base + anystr.base manifold = {**ms.manifold, len(ms): anystr} elif order == -1: data = anystr.base + ms.base manifold = {0: anystr} for key, value in ms.manifold.items(): manifold[key + len(anystr)] = value else: raise ValueError("order is expected to be 1 or -1, not {}" .format(order)) rdict = {**deepcopy(ms.rdict), anystr.base: anystr.possibilities[1:]} new = cls.by_parts(data, rdict, manifold) logger.debug("New ManifoldStr from ManifoldStr '{}' and AnyStr '{}':\ '{}'".format(ms, anystr, new)) return new
[docs] @classmethod def from_anystr_and_ms(cls, anystr, ms): cls.from_ms_and_anystr(ms, anystr, -1)
[docs] @classmethod def from_ms_and_str(cls, ms, str_, order=1): rdict = deepcopy(ms.rdict) if order == 1: return ms + ManifoldStr(str_, rdict) elif order == -1: return ManifoldStr(str_, rdict) + ms else: raise ValueError("order is expected to be 1 or -1, not {}" .format(order))
[docs] @classmethod def from_str_and_ms(cls, str_, ms): cls.from_ms_and_str(ms, str_, -1)
[docs] @classmethod def from_ms_and_ms(cls, ms1, ms2): data = ms1.base + ms2.base rdict = deepcopy(ms1.rdict) for k, v in ms2.rdict.items(): rdict[k] = v return ManifoldStr(data, rdict)