Проталкиваем не‐ASCII в непредназначенные для этого места

Сидел вечером дома, думал чем бы заняться. А: у Python есть отладчик, но в нём совершенно некрасивое приглашение ко вводу. Дай‐ка я впилю туда powerline. Дело казалось бы совершенно плёвое: нужно просто создать свой подкласс pdb.Pdb со своим свойством, да? def use_powerline_prompt (cls): '''Decorator that installs powerline prompt to the class ''' @property def prompt (self): try: powerline = self.powerline except AttributeError: powerline = PDBPowerline () powerline.setup (self) self.powerline = powerline return powerline.render (side='left')

@prompt.setter def prompt (self, _): pass

cls.prompt = prompt

return cls Нет. На Python-3 такой код ещё работать, но на Python-2 нас уже поджидает проблема: для вывода необходимо превратить юникодную строку в набор байт, что требует указания кодировки. Ну, это просто: encoding = get_preferred_output_encoding ()

def prompt (self): … ret = powerline.render (side='left') if not isinstance (ret, str): # Python-2 ret = ret.encode (encoding) return ret . Это просто и это работает… пока пользователь не установит pdbpp. Теперь нас приветствуют ряд ошибок, связанных с тем, что pdbpp может использовать pyrepl, а pyrepl не работает с Unicode (при чём будет ли использоваться pyrepl как‐то зависит от значения $TERM: при TERM=xterm-256color я получаю ошибки от pyrepl, а при TERM= или TERM=konsole-256color — нет и всё работает нормально). Ошибки, связанные с тем, что в приглашении кто‐то не хочет видеть Unicode, не новы — ещё IPython пытался запретить Unicode в rewrite prompt (эта то, что вы увидите, если вы включите autocall в IPython и наберёте int 42). Но здесь всё гораздо хуже: pyrepl использует from __future__ import unicode_literals, при этом делая с использованием обычных строк (превращённых этим импортом в юникодные) различные операции на строке приглашения, в явном виде конвертируемой в str.Итак, вот что нам получается нужно:

Класс‐наследник unicode, который бы конвертировался в str без выбрасывания ошибок на не‐ASCII символах (конвертация осуществляется просто в виде str (prompt)). Эта часть очень проста: нужно переопределить методы __str__ и __new__ (без второго можно, в принципе, и обойтись, но так удобнее при конвертации в этот класс из следующего и для возможности явного указания кодировки, которая будет использована). Класс‐наследник str, в который бы и конвертировался предыдущий класс. Здесь переопределения двух методов категорически недостаточно: __new__нужен для удобного сохранения кодировки и отсутствие необходимости в явном преобразовании unicode→str. __contains__ и несколько других методов должны работать с юникодными аргументами так, будто текущий класс есть unicode (для неюникодных аргументов ничего менять не нужно). Дело в том, что при наличиии unicode_literals '\n' in prompt выбрасывает исключение, если prompt — байтовая строка с не‐ASCII символами, так как пытается привести prompt к unicode, а не наоборот. find и схожие функции должны работать с юникодными аргументами так, будто это байтовые строки в текущей кодировке. Это нужно, чтобы они выдавали правильные индексы, но при этом не валились с ошибками из‐за конвертации байтовой строки в юникодную (а здесь‐то почему конвертация не обратная?). __len__ должен выдавать длину строки в юникодных codepoint«ах. Эта часть нужна, чтобы pyrepl, считающий, где заканчивается приглашение (и ставящий курсор соответственно), не ошибся и не сделал гиганский пробел между приглашением и курсором. Подозреваю, что нужно на самом деле использовать не codepoint«ы, а ширину строки в экранных ячейках (то, что делает, к примеру, strdisplaywidth () в Vim). __add__ должен возвращать наш первый класс‐наследник unicode при прибавлении к юникодной строке. __radd__ должен делать то же самое. Сложение байтовых строк должно давать наш класс‐наследник str. Подробнее в следующем пункте. Ну, и наконец, __getslice__ (внимание: __getitem__ не катит, str использует deprecated __getslice__ для срезов) должен возвращать объект того же самого класса, поскольку pyrepl в самом конце складывает пустую юникодную строку, срез от текущего класса и другой срез от него же. И если эту часть обойти вниманием, то опять получим какую‐то из UnicodeError. В результате получатся следующие два уродца: class PowerlineRenderBytesResult (bytes): def __new__(cls, s, encoding=None): encoding = encoding or s.encoding self = bytes.__new__(cls, s.encode (encoding) if isinstance (s, unicode) else s) self.encoding = encoding return self

for meth in ( '__contains__', 'partition', 'rpartition', 'split', 'rsplit', 'count', 'join', ): exec (( 'def {0}(self, *args):\n' ' if any ((isinstance (arg, unicode) for arg in args)):\n' ' return self.__unicode__().{0}(*args)\n' ' else:\n' ' return bytes.{0}(self, *args)' ).format (meth))

for meth in ( 'find', 'rfind', 'index', 'rindex', ): exec (( 'def {0}(self, *args):\n' ' if any ((isinstance (arg, unicode) for arg in args)):\n' ' args = [arg.encode (self.encoding) if isinstance (arg, unicode) else arg for arg in args]\n' ' return bytes.{0}(self, *args)' ).format (meth))

def __len__(self): return len (self.decode (self.encoding))

def __getitem__(self, *args): return PowerlineRenderBytesResult (bytes.__getitem__(self, *args), encoding=self.encoding)

def __getslice__(self, *args): return PowerlineRenderBytesResult (bytes.__getslice__(self, *args), encoding=self.encoding)

@staticmethod def add (encoding, *args): if any ((isinstance (arg, unicode) for arg in args)): return ''.join (( arg if isinstance (arg, unicode) else arg.decode (encoding) for arg in args )) else: return PowerlineRenderBytesResult (b''.join (args), encoding=encoding)

def __add__(self, other): return self.add (self.encoding, self, other)

def __radd__(self, other): return self.add (self.encoding, other, self)

def __unicode__(self): return PowerlineRenderResult (self)

class PowerlineRenderResult (unicode): def __new__(cls, s, encoding=None): encoding = ( encoding or getattr (s, 'encoding', None) or get_preferred_output_encoding () ) if isinstance (s, unicode): self = unicode.__new__(cls, s) else: self = unicode.__new__(cls, s, encoding, 'replace') self.encoding = encoding return self

def __str__(self): return PowerlineRenderBytesResult (self) (в Python2 bytes is str).Результат на github пока есть только в моей ветке, позже будет в develop основного репозитория.Разумеется, результат не ограничен только pyrepl, а может применяться в различных местах, куда вам нельзя подсунуть не‐ASCII строку, но очень хочется.

© Habrahabr.ru