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

1# Copyright (c) 2025 Mathias Ertl 

2# Licensed under the MIT License. See LICENSE file for details. 

3 

4"""Module for wrapping text width.""" 

5 

6import re 

7import textwrap 

8from collections.abc import Iterator 

9from typing import Any 

10 

11 

12class CommandLineTextWrapper(textwrap.TextWrapper): 

13 """Subclass of TextWrapper that "unsplits" a short option and its (supposed) value. 

14 

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. 

18 

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 """ 

24 

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 

30 

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 

43 

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 = [] 

51 

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 

60 

61 # yield any remaining chunks 

62 yield from unsplit 

63 

64 def _split(self, text: str) -> list[str]: 

65 chunks = super()._split(text) 

66 unsplit = list(self._unsplit_optargs(chunks)) 

67 return unsplit 

68 

69 

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}") 

74 

75 lines = [f"{line} \\" if i != len(lines) else line for i, line in enumerate(lines, 1)] 

76 return "\n".join(lines)