Сидел вечером дома, думал чем бы заняться. А! У Python есть отладчик, но в нём совершенно некрасивое приглашение ко вводу. Дай‐ка я впилю туда powerline. Дело казалось бы совершенно плёвое: нужно просто создать свой подкласс pdb.Pdb со своим свойством, да?
Итак, вот что нам, получается, нужно:
Результат на github пока есть только в моей ветке, позже будет в
Разумеется, результат не ограничен только pyrepl, а может применяться в различных местах, куда вам нельзя подсунуть не‐ASCII строку, но очень хочется.
¹ При
² То, что вы увидите, если включите autocall в IPython и наберёте
(нижняя строка).
Нет. На Python-3 такой код ещё может работать, но на Python-2 нас уже поджидает проблема: для вывода необходимо превратить юникодную строку в набор байт, что требует указания кодировки. Ну, это просто: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
. Это просто и это работает… пока пользователь не установит pdbpp. Теперь нас приветствуют ряд ошибок, связанных с тем, что pdbpp может использовать pyrepl, а pyrepl не работает с Unicode (причём то, будет ли использоваться pyrepl, как‐то зависит от значения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
$TERM¹). Ошибки, связанные с тем, что в приглашении кто‐то не хочет видеть Unicode, не новы — ещё IPython пытался запретить Unicode в rewrite prompt². Но здесь всё гораздо хуже: 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 символами, так как Python пытается привестиpromptкunicode, а не наоборот.findи схожие функции должны работать с юникодными аргументами так, будто это байтовые строки в текущей кодировке. Это нужно, чтобы они выдавали правильные индексы, но при этом не валились с ошибками из‐за конвертации байтовой строки в юникодную (а здесь‐то почему конвертация не обратная?).__len__должен выдавать длину строки в юникодных codepoint’ах. Эта часть нужна, чтобы pyrepl, считающий, где заканчивается приглашение (и ставящий курсор соответственно), не ошибся и не сделал гиганский пробел между приглашением и курсором. Подозреваю, что нужно на самом деле использовать не codepoint’ы, а ширину строки в экранных ячейках (то, что делает, к примеру, strdisplaywidth() в Vim).__add__должен возвращать наш первый класс‐наследникunicodeпри прибавлении к юникодной строке.__radd__должен делать то же самое. Сложение байтовых строк должно давать наш класс‐наследникstr. Подробнее в следующем пункте.- Ну, и наконец,
__getslice__(внимание:__getitem__не катит,strиспользует deprecated__getslice__для срезов) должен возвращать объект того же самого класса, поскольку pyrepl в самом конце складывает пустую юникодную строку, срез от текущего класса и другой срез от него же. И если эту часть обойти вниманием, то опять получим какую‐то из UnicodeError.
(в Python2class 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)
bytes is str).Результат на github пока есть только в моей ветке, позже будет в
develop основного репозитория.Разумеется, результат не ограничен только pyrepl, а может применяться в различных местах, куда вам нельзя подсунуть не‐ASCII строку, но очень хочется.
¹ При
TERM=xterm-256color я получаю ошибки от pyrepl, а при TERM= или TERM=konsole-256color — нет и всё работает нормально.² То, что вы увидите, если включите autocall в IPython и наберёте
int 42:
(нижняя строка).