Coverage for structured_tutorials / sphinx / textwrap.py: 70%
40 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-11 20:35 +0100
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-11 20:35 +0100
1# Copyright (c) 2025 Mathias Ertl
2# Licensed under the MIT License. See LICENSE file for details.
4"""Module for wrapping text width."""
6import re
7import textwrap
8from collections.abc import Iterator
9from typing import Any
12class CommandLineTextWrapper(textwrap.TextWrapper):
13 """Subclass of TextWrapper that "unsplits" a short option and its (supposed) value.
15 This makes sure that a command with many options will not break between short options and their value,
16 e.g. for ``docker run -e FOO=foo -e BAR=bar ...``, the text wrapper will never insert a line split between
17 ``-e`` and its respective option value.
19 Note that the class of course does not know the semantics of the command it renders. A short option
20 followed by a value is always considered a reason not to break. For example, for ``docker run ... -d
21 image``, the wrapper will never split between ``-d`` and ``image``, despite the latter being unrelated to
22 the former.
23 """
25 def __init__(self, *args: Any, **kwargs: Any) -> None:
26 super().__init__(*args, **kwargs)
27 self.subsequent_indent = "> "
28 self.break_on_hyphens = False
29 self.break_long_words = False
31 def _unsplit_optargs(self, chunks: list[str]) -> Iterator[str]:
32 unsplit: list[str] = []
33 for chunk in chunks:
34 if re.match("-[a-z]$", chunk): # chunk appears to be an option 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 if unsplit: # previous option was also an optarg, so yield what was there
36 yield from unsplit
37 unsplit = [chunk]
38 elif chunk == " ":
39 if unsplit: # this is the whitespace after an option 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 unsplit.append(chunk)
41 else: # a whitespace not preceeded by an option
42 yield chunk
44 # The unsplit buffer has two values (short option and space) and this chunk looks like its
45 # value, so yield the buffer and this value as split
46 elif len(unsplit) == 2 and re.match("[a-zA-Z0-9`]", chunk): 46 ↛ 48line 46 didn't jump to line 48 because the condition on line 46 was never true
47 # unsplit option, whitespace and option value
48 unsplit.append(chunk)
49 yield "".join(unsplit)
50 unsplit = []
52 # There is something in the unsplit buffer, but this chunk does not look like a value (maybe
53 # it's a long option?), so we yield tokens from the buffer and then this chunk.
54 elif unsplit: 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true
55 yield from unsplit
56 unsplit = []
57 yield chunk
58 else:
59 yield chunk
61 # yield any remaining chunks
62 yield from unsplit
64 def _split(self, text: str) -> list[str]:
65 chunks = super()._split(text)
66 unsplit = list(self._unsplit_optargs(chunks))
67 return unsplit
70def wrap_command_filter(command: str, prompt: str, text_width: int) -> str:
71 """Filter to wrap a command based on the given text width."""
72 wrapper = CommandLineTextWrapper(width=text_width)
73 lines = wrapper.wrap(f"{prompt}{command}")
75 lines = [f"{line} \\" if i != len(lines) else line for i, line in enumerate(lines, 1)]
76 return "\n".join(lines)