http://habrahabr.ru/post/249129/
Сидел вечером дома, думал чем бы заняться. А: у 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 строку, но очень хочется.