Использование textwrap.dedent() с байтами в Python 3

#python #python-3.x #indentation #literals #python-unicode

#python #python-3.x #отступ #литералы #python-unicode

Вопрос:

Когда я использую многострочную строку в тройных кавычках в Python, я обычно использую textwrap.dedent, чтобы код оставался читаемым с хорошим отступом:

 some_string = textwrap.dedent("""
    First line
    Second line
    ...
    """).strip()
  

Однако в Python 3.x textwrap.dedent, похоже, не работает с байтовыми строками. Я столкнулся с этим при написании модульного теста для метода, который возвращал длинную многострочную байтовую строку, например:

 # The function to be tested

def some_function():
    return b'Lorem ipsum dolor sit ametn  consectetuer adipiscing elit'

# Unit test

import unittest
import textwrap

class SomeTest(unittest.TestCase):
    def test_some_function(self):
        self.assertEqual(some_function(), textwrap.dedent(b"""
            Lorem ipsum dolor sit amet
              consectetuer adipiscing elit
            """).strip())

if __name__ == '__main__':
    unittest.main()
  

В Python 2.7.10 приведенный выше код работает нормально, но в Python 3.4.3 он терпит неудачу:

 E
======================================================================
ERROR: test_some_function (__main__.SomeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 16, in test_some_function
    """).strip())
  File "/usr/lib64/python3.4/textwrap.py", line 416, in dedent
    text = _whitespace_only_re.sub('', text)
TypeError: can't use a string pattern on a bytes-like object

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)
  

Итак: есть ли альтернатива textwrap.dedent, которая работает с байтовыми строками?

  • Я мог бы написать такую функцию сам, но если есть существующая функция, я бы предпочел использовать ее.
  • Я мог бы преобразовать в юникод, использовать textwrap.dedent и преобразовать обратно в байты. Но это возможно только в том случае, если строка байтов соответствует некоторой кодировке Unicode.

Ответ №1:

Ответ 2: textwrap в первую очередь касается Textwrap класса и функций. dedent находится в разделе

 # -- Loosely related functionality --------------------
  

Насколько я могу судить, единственное, что делает его специфичным для текста (unicode str ), — это литералы re . Я поставил перед всеми 6 префиксами b и вуаля! (Я больше ничего не редактировал, но строка документации функции должна быть скорректирована.)

 import re

_whitespace_only_re = re.compile(b'^[ t] $', re.MULTILINE)
_leading_whitespace_re = re.compile(b'(^[ t]*)(?:[^ tn])', re.MULTILINE)

def dedent_bytes(text):
    """Remove any common leading whitespace from every line in `text`.

    This can be used to make triple-quoted strings line up with the left
    edge of the display, while still presenting them in the source code
    in indented form.

    Note that tabs and spaces are both treated as whitespace, but they
    are not equal: the lines "  hello" and "\thello" are
    considered to have no common leading whitespace.  (This behaviour is
    new in Python 2.5; older versions of this module incorrectly
    expanded tabs before searching for common leading whitespace.)
    """
    # Look for the longest leading string of spaces and tabs common to
    # all lines.
    margin = None
    text = _whitespace_only_re.sub(b'', text)
    indents = _leading_whitespace_re.findall(text)
    for indent in indents:
        if margin is None:
            margin = indent

        # Current line more deeply indented than previous winner:
        # no change (previous winner is still on top).
        elif indent.startswith(margin):
            pass

        # Current line consistent with and no deeper than previous winner:
        # it's the new winner.
        elif margin.startswith(indent):
            margin = indent

        # Find the largest common whitespace between current line
        # and previous winner.
        else:
            for i, (x, y) in enumerate(zip(margin, indent)):
                if x != y:
                    margin = margin[:i]
                    break
            else:
                margin = margin[:len(indent)]

    # sanity check (testing/debugging only)
    if 0 and margin:
        for line in text.split(b"n"):
            assert not line or line.startswith(margin), 
                   "line = %r, margin = %r" % (line, margin)

    if margin:
        text = re.sub(rb'(?m)^'   margin, b'', text)
    return text

print(dedent_bytes(b"""
            Lorem ipsum dolor sit amet
              consectetuer adipiscing elit
            """)
      )

# prints
b'nLorem ipsum dolor sit ametn  consectetuer adipiscing elitn'
  

Ответ №2:

К сожалению, похоже, что dedent он не поддерживает байтовые строки. Однако, если вам нужен кросс-совместимый код, я рекомендую вам воспользоваться six библиотекой:

 import sys, unittest
from textwrap import dedent

import six


def some_function():
    return b'Lorem ipsum dolor sit ametn  consectetuer adipiscing elit'


class SomeTest(unittest.TestCase):
    def test_some_function(self):
        actual = some_function()

        expected = six.b(dedent("""
            Lorem ipsum dolor sit amet
              consectetuer adipiscing elit
            """)).strip()

        self.assertEqual(actual, expected)

if __name__ == '__main__':
    unittest.main()
  

Это похоже на ваше предложение в вопросе

Я мог бы преобразовать в юникод, использовать textwrap.dedent и преобразовать обратно в байты. Но это возможно только в том случае, если строка байтов соответствует некоторой кодировке Unicode.

Но вы что-то недопонимаете в кодировках здесь — если вы можете написать строковый литерал в своем тесте таким образом в первую очередь и успешно проанализировать файл с помощью python (т. Е. Правильное объявление coding находится в модуле), то здесь нет шага «преобразовать в юникод». Файл анализируется в указанной кодировке (или sys.defaultencoding , если вы не указали), а затем, когда строка является переменной python, она уже декодируется.

Комментарии:

1. Хорошая идея использовать six.b вне dedent. Я уже использую six в своем проекте, поэтому использование six.b не добавляет дополнительной зависимости. Мои проблемы с кодированием были связаны не столько с символами, отличными от ascii, в исходном файле, сколько с шестнадцатеричными escape-последовательностями, такими как » xff». Но я протестировал его сейчас, и он работает для всех таких последовательностей (six.b (s) на Python 3 эквивалентно s.encode(«latin-1»)). Я приму этот ответ.

Ответ №3:

Ответ 1: Многострочные строки в тройных кавычках (и dedent) — это удобство (иногда), а не необходимость. Вместо этого вы можете написать отдельный байтовый литерал, заканчивающийся на b’ n’ для каждой строки, и позволить анализатору объединить их. Пример:

 >>> b = (
    b'Lorem ipsum dolor sit ametn' # first line
    b'consectetuer adipiscing elitn' # 2nd line
    )
>>> b
b'Lorem ipsum dolor sit ametnconsectetuer adipiscing elitn'
  

Я намеренно добавил пробелы и комментарии к коду, которые не требуются в результирующих байтах, чтобы они не были включены. Иногда я делаю эквивалент вышеупомянутого с текстовыми строками.

Ответ 2: Преобразовать textwrap.dedent в байт обработки (см. Отдельный ответ)

Ответ 3: Опустите b префикс и добавьте .encode() до или после .strip() .

 print(textwrap.dedent("""
            Lorem ipsum dolor sit amet
              consectetuer adipiscing elit
            """).encode())
# prints (same as Answer 2).
b'nLorem ipsum dolor sit ametn  consectetuer adipiscing elitn'
  

Комментарии:

1. Хороший ответ. Я уже использовал этот подход для небольшого количества строк; думаю, мне не стоит бояться использовать его и для более длинных текстов. Тем не менее, я думаю, что для очень длинных текстов или для текстов, которые регулярно будут обновляться с помощью копирования вставки (что иногда случается в моих модульных тестах), тройные кавычки dedent были бы лучше и имели меньше визуального беспорядка.

2. Ответ 3 не совсем работает во всех случаях, например, textwrap.dedent(«»» xff»»»).encode() — это b’xc3 xbf’, а не b’ xff’ (я хочу последнее). В Python 2 это вызывает ошибку UnicodeDecodeError. Использование six.b() вместо .encode() устраняет эти проблемы, см. Ответ wim.

3. 'xff'.encode('latin1') == b'xff' Вы можете получить любые байты с latin-1 . Предположение textwrap и любого его моделирования заключается в том, что текст / байты имеют случайные разрывы строк.