# -*- coding: utf-8 -*- from __future__ import absolute_import import sys import six from six import PY2, binary_type if PY2: string_types = (str, unicode) basestring = basestring def implements_to_string(cls): """Class decorator that renames __str__ to __unicode__ and modifies __str__ that returns utf-8. """ cls.__unicode__ = cls.__str__ cls.__str__ = lambda x: x.__unicode__().encode('utf-8') return cls else: # PY3 string_types = (str,) basestring = (str, bytes) implements_to_string = lambda x: x class ComparableMixin(object): '''Implements rich operators for an object.''' def _compare(self, other, method): try: return method(self._cmpkey(), other._cmpkey()) except (AttributeError, TypeError): # _cmpkey not implemented, or return different type, # so I can't compare with "other". Try the reverse comparison return NotImplemented def __lt__(self, other): return self._compare(other, lambda s, o: s < o) def __le__(self, other): return self._compare(other, lambda s, o: s <= o) def __eq__(self, other): return self._compare(other, lambda s, o: s == o) def __ge__(self, other): return self._compare(other, lambda s, o: s >= o) def __gt__(self, other): return self._compare(other, lambda s, o: s > o) def __ne__(self, other): return self._compare(other, lambda s, o: s != o) class BlobComparableMixin(ComparableMixin): '''Allow blob objects to be comparable with both strings and blobs.''' def _compare(self, other, method): if isinstance(other, basestring): # Just compare with the other string return method(self._cmpkey(), other) return super(BlobComparableMixin, self)._compare(other, method) @implements_to_string class StringlikeMixin(object): '''Make blob objects behave like Python strings. Expects that classes that use this mixin to have a _strkey() method that returns the string to apply string methods to. Using _strkey() instead of __str__ ensures consistent behavior between Python 2 and 3. ''' def __repr__(self): '''Returns a string representation for debugging.''' class_name = self.__class__.__name__ text = self.__unicode__().encode("utf-8") if PY2 else str(self) ret = '{cls}("{text}")'.format(cls=class_name, text=text) return binary_type(ret) if PY2 else ret def __str__(self): '''Returns a string representation used in print statements or str(my_blob).''' return self._strkey() def __len__(self): '''Returns the length of the raw text.''' return len(self._strkey()) def __iter__(self): '''Makes the object iterable as if it were a string, iterating through the raw string's characters. ''' return iter(self._strkey()) def __contains__(self, sub): '''Implements the `in` keyword like a Python string.''' return sub in self._strkey() def __getitem__(self, index): '''Returns a substring. If index is an integer, returns a Python string of a single character. If a range is given, e.g. `blob[3:5]`, a new instance of the class is returned. ''' if isinstance(index, int): return self._strkey()[index] # Just return a single character else: # Return a new blob object return self.__class__(self._strkey()[index]) def find(self, sub, start=0, end=sys.maxsize): '''Behaves like the built-in str.find() method. Returns an integer, the index of the first occurrence of the substring argument sub in the sub-string given by [start:end]. ''' return self._strkey().find(sub, start, end) def rfind(self, sub, start=0, end=sys.maxsize): '''Behaves like the built-in str.rfind() method. Returns an integer, the index of he last (right-most) occurence of the substring argument sub in the sub-sequence given by [start:end]. ''' return self._strkey().rfind(sub, start, end) def index(self, sub, start=0, end=sys.maxsize): '''Like blob.find() but raise ValueError when the substring is not found. ''' return self._strkey().index(sub, start, end) def rindex(self, sub, start=0, end=sys.maxsize): '''Like blob.rfind() but raise ValueError when substring is not found. ''' return self._strkey().rindex(sub, start, end) def startswith(self, prefix, start=0, end=sys.maxsize): """Returns True if the blob starts with the given prefix.""" return self._strkey().startswith(prefix, start, end) def endswith(self, suffix, start=0, end=sys.maxsize): """Returns True if the blob ends with the given suffix.""" return self._strkey().endswith(suffix, start, end) # PEP8 aliases starts_with = startswith ends_with = endswith def title(self): """Returns a blob object with the text in title-case.""" return self.__class__(self._strkey().title()) def format(self, *args, **kwargs): """Perform a string formatting operation, like the built-in `str.format(*args, **kwargs)`. Returns a blob object. """ return self.__class__(self._strkey().format(*args, **kwargs)) def split(self, sep=None, maxsplit=sys.maxsize): """Behaves like the built-in str.split(). """ return self._strkey().split(sep, maxsplit) def strip(self, chars=None): """Behaves like the built-in str.strip([chars]) method. Returns an object with leading and trailing whitespace removed. """ return self.__class__(self._strkey().strip(chars)) def upper(self): """Like str.upper(), returns new object with all upper-cased characters. """ return self.__class__(self._strkey().upper()) def lower(self): """Like str.lower(), returns new object with all lower-cased characters. """ return self.__class__(self._strkey().lower()) def join(self, iterable): """Behaves like the built-in `str.join(iterable)` method, except returns a blob object. Returns a blob which is the concatenation of the strings or blobs in the iterable. """ return self.__class__(self._strkey().join(iterable)) def replace(self, old, new, count=sys.maxsize): """Return a new blob object with all the occurence of `old` replaced by `new`. """ return self.__class__(self._strkey().replace(old, new, count))