Проталкиваем не‐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¹). Ошибки, связанные с тем, что в приглашении кто‐то не хочет видеть Unicode, не новы — ещё IPython пытался запретить Unicode в rewrite prompt². Но здесь всё гораздо хуже: pyrepl использует from __future__ import unicode_literals, при этом делая с использованием обычных строк (превращённых этим импортом в юникодные) различные операции на строке приглашения, в явном виде конвертируемой в str в самом начале.

    Итак, вот что нам, получается, нужно:
    1. Класс‐наследник unicode, который бы конвертировался в str без выбрасывания ошибок на не‐ASCII символах (конвертация осуществляется просто в виде str(prompt)). Эта часть очень проста: нужно переопределить методы __str__ и __new__ (без второго можно, в принципе, и обойтись, но так удобнее при конвертации в этот класс из следующего и для возможности явного указания кодировки, которая будет использована).
    2. Класс‐наследник str, в который бы и конвертировался предыдущий класс. Здесь переопределения двух методов категорически недостаточно:
      1. __new__ нужен для удобного сохранения кодировки и отсутствие необходимости в явном преобразовании unicodestr.
      2. __contains__ и несколько других методов должны работать с юникодными аргументами так, будто текущий класс есть unicode (для неюникодных аргументов ничего менять не нужно). Дело в том, что при наличиии unicode_literals '\n' in prompt выбрасывает исключение, если prompt — байтовая строка с не‐ASCII символами, так как Python пытается привести prompt к unicode, а не наоборот.
      3. find и схожие функции должны работать с юникодными аргументами так, будто это байтовые строки в текущей кодировке. Это нужно, чтобы они выдавали правильные индексы, но при этом не валились с ошибками из‐за конвертации байтовой строки в юникодную (а здесь‐то почему конвертация не обратная?).
      4. __len__ должен выдавать длину строки в юникодных codepoint’ах. Эта часть нужна, чтобы pyrepl, считающий, где заканчивается приглашение (и ставящий курсор соответственно), не ошибся и не сделал гиганский пробел между приглашением и курсором. Подозреваю, что нужно на самом деле использовать не codepoint’ы, а ширину строки в экранных ячейках (то, что делает, к примеру, strdisplaywidth() в Vim).
      5. __add__ должен возвращать наш первый класс‐наследник unicode при прибавлении к юникодной строке. __radd__ должен делать то же самое. Сложение байтовых строк должно давать наш класс‐наследник str. Подробнее в следующем пункте.
      6. Ну, и наконец, __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 строку, но очень хочется.


    ¹ При TERM=xterm-256color я получаю ошибки от pyrepl, а при TERM= или TERM=konsole-256color — нет и всё работает нормально.
    ² То, что вы увидите, если включите autocall в IPython и наберёте int 42: Powerline IPython in and rewrite prompt (нижняя строка).
    • +11
    • 5,2k
    • 2
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 2
    • +1
      Вы бы скриншот показали, а то не все знаю что такое powerline
      • 0
        Так статья всё же не реклама Powerline. Снимки экрана есть на github по первой ссылке. Добавил ещё один в статью для пояснения, что же такое rewrite prompt.

        Надо бы, кстати, в README добавить снимков экрана, не относящихся к Vim.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.