diff --git a/devtools/prettier.py b/devtools/prettier.py index b603bef..b288438 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -218,7 +218,7 @@ def _str_lines(self, lines: Iterable[str | bytes], indent_current: int, indent_n self._stream.write(indent_current * self._c + ')') def _wrap_lines(self, s: str | bytes, indent_new: int) -> Generator[str | bytes, None, None]: - width = self._width - indent_new - 3 + width = max(1, self._width - indent_new - 3) for line in s.splitlines(True): start = 0 for pos in range(width, len(line), width): @@ -285,13 +285,14 @@ def _format_raw(self, _: Any, value_repr: str, indent_current: int, indent_new: lines = value_repr.splitlines(True) if len(lines) > 1 or (len(value_repr) + indent_current) >= self._width: self._stream.write('(\n') - wrap_at = self._width - indent_new + wrap_at = max(1, self._width - indent_new) prefix = indent_new * self._c from textwrap import wrap for line in lines: - sub_lines = wrap(line, wrap_at) + # At width one, whitespace must occupy its own line instead of being dropped by textwrap. + sub_lines = line.rstrip('\r\n') if wrap_at == 1 else wrap(line, wrap_at) for sline in sub_lines: self._stream.write(prefix + sline + '\n') self._stream.write(indent_current * self._c + ')') diff --git a/tests/test_prettier.py b/tests/test_prettier.py index cf069a9..041add4 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -513,3 +513,55 @@ def test_ast_expr(): def test_ast_module(): assert pformat(ast.parse('print(1, 2, round(3))')).startswith('Module(\n body=[') + + +@pytest.mark.parametrize('width', [7, 11, 15]) +@pytest.mark.parametrize('value', ['abcdefghijk', b'abcdefghijk']) +def test_nested_string_exhausted_width(width: int, value: str | bytes): + output = PrettyFormat(width=width)([value]) + assert ast.literal_eval(output) == [value] + + +@pytest.mark.parametrize('width', [7, 11, 15]) +def test_nested_bytearray_exhausted_width(width: int): + value = bytearray(b'abcdefghijk') + output = PrettyFormat(width=width)([value]) + node = ast.parse(output, mode='eval').body + assert isinstance(node, ast.List) + call = node.elts[0] + assert isinstance(call, ast.Call) + assert ast.literal_eval(call.args[0]) == bytes(value) + + +@pytest.mark.parametrize('width', [12, 120]) +@pytest.mark.parametrize('depth', [2, 30]) +@pytest.mark.parametrize('highlight', [False, True]) +def test_nested_raw_repr_exhausted_width(width: int, depth: int, highlight: bool): + class LongRepr: + def __repr__(self) -> str: + return 'Custom(value=' + 'x' * 120 + ')' + + leaf = LongRepr() + value: Any = leaf + for _ in range(depth): + value = [value] + output = PrettyFormat(width=width)(value, highlight=highlight) + assert repr(leaf) in ''.join(strip_ansi(output).split()) + + +@pytest.mark.parametrize('width,depth', [(12, 2), (13, 2), (120, 30)]) +@pytest.mark.parametrize('highlight', [False, True]) +@pytest.mark.parametrize('payload', ['alpha beta gamma', ' leading and trailing ', 'alpha\tbeta']) +def test_exhausted_raw_repr_preserves_whitespace(width: int, depth: int, highlight: bool, payload: str): + class RawRepr: + def __repr__(self) -> str: + return f"Custom('{payload}')" + + leaf = RawRepr() + value: Any = leaf + for _ in range(depth): + value = [value] + output = strip_ansi(PrettyFormat(width=width)(value, highlight=highlight)) + prefix = ' ' * ((depth + 1) * 4) + content = ''.join(line[len(prefix) :] for line in output.splitlines() if line.startswith(prefix)) + assert content == repr(leaf)