Coverage for tests / test_cli / test_help.py: 100%

67 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1import subprocess 1abcdefg

2import sys 1abcdefg

3 

4import typer 1abcdefg

5from typer.testing import CliRunner 1abcdefg

6 

7runner = CliRunner() 1abcdefg

8 

9 

10def test_script_help(): 1abcdefg

11 result = subprocess.run( 1abcdefg

12 [ 

13 sys.executable, 

14 "-m", 

15 "coverage", 

16 "run", 

17 "-m", 

18 "typer", 

19 "tests/assets/cli/sample.py", 

20 "--help", 

21 ], 

22 capture_output=True, 

23 encoding="utf-8", 

24 ) 

25 assert "run" in result.stdout 1abcdefg

26 

27 

28def test_not_python(): 1abcdefg

29 result = subprocess.run( 1abcdefg

30 [ 

31 sys.executable, 

32 "-m", 

33 "coverage", 

34 "run", 

35 "-m", 

36 "typer", 

37 "tests/assets/cli/not_python.txt", 

38 "run", 

39 ], 

40 capture_output=True, 

41 encoding="utf-8", 

42 ) 

43 assert "Could not import as Python file" in result.stderr 1abcdefg

44 

45 

46def test_short_help() -> None: 1abcdefg

47 app = typer.Typer( 1abcdefg

48 rich_markup_mode=None, 

49 context_settings={"max_content_width": 50}, 

50 ) 

51 

52 @app.command(help=" \n\t ") 1abcdefg

53 def empty() -> None: 1abcdefg

54 pass # pragma: no cover 

55 

56 @app.command(help="\b first sentence.") 1abcdefg

57 def marker() -> None: 1abcdefg

58 pass # pragma: no cover 

59 

60 # Forcing truncation 

61 @app.command(help=f"{'x' * 30} {'y' * 5} z trailing") 1abcdefg

62 def long() -> None: 1abcdefg

63 pass # pragma: no cover 

64 

65 result = runner.invoke(app, ["--help"], terminal_width=50) 1abcdefg

66 assert result.exit_code == 0 1abcdefg

67 assert "empty" in result.output 1abcdefg

68 assert "marker" in result.output 1abcdefg

69 assert "long" in result.output 1abcdefg

70 assert "first sentence." in result.output 1abcdefg

71 assert f"{'x' * 30}..." in result.output 1abcdefg

72 

73 

74def test_help_wrapping() -> None: 1abcdefg

75 app = typer.Typer( 1abcdefg

76 rich_markup_mode=None, 

77 context_settings={"max_content_width": 50}, 

78 ) 

79 

80 @app.command( 1abcdefg

81 help=( 

82 "Wrapped paragraph has enough words to wrap in help output.\n" 

83 "\n" 

84 "\n" 

85 "\b\n" 

86 "RAW-LINE-ONE stays on one line even with many many many words.\n" 

87 "RAW-LINE-TWO keeps original formatting.\n" 

88 "\n" 

89 "Final paragraph wraps normally as well." 

90 ) 

91 ) 

92 def cmd() -> None: 1abcdefg

93 pass # pragma: no cover 

94 

95 result = runner.invoke(app, ["cmd", "--help"], terminal_width=50) 1abcdefg

96 assert result.exit_code == 0 1abcdefg

97 assert "Wrapped paragraph has enough words to wrap" in result.output 1abcdefg

98 assert ( 1abcdefg

99 "RAW-LINE-ONE stays on one line even with many many many words." 

100 in result.output 

101 ) 

102 assert "RAW-LINE-TWO keeps original formatting." in result.output 1abcdefg

103 assert "Final paragraph wraps normally as well." in result.output 1abcdefg

104 

105 

106def test_help_wrapping_long_name() -> None: 1abcdefg

107 app = typer.Typer(rich_markup_mode=None) 1abcdefg

108 

109 @app.command() 1abcdefg

110 def cmd(value: str) -> None: 1abcdefg

111 pass # pragma: no cover 

112 

113 result = runner.invoke( 1abcdefg

114 app, 

115 ["cmd", "--help"], 

116 terminal_width=40, 

117 prog_name="very-long-program-name-that-forces-wrap", 

118 ) 

119 assert result.exit_code == 0 1abcdefg

120 

121 output_lines = result.output.splitlines() 1abcdefg

122 usage_idx = output_lines.index("Usage: very-long-program-name-that-forces-wrap ") 1abcdefg

123 args_line = output_lines[usage_idx + 1] 1abcdefg

124 assert args_line.lstrip() == "[OPTIONS] {value}" 1abcdefg

125 assert args_line.startswith(" ") 1abcdefg

126 

127 

128def test_format_long_help_option() -> None: 1abcdefg

129 app = typer.Typer(rich_markup_mode=None) 1abcdefg

130 

131 @app.command() 1abcdefg

132 def cmd( 1abcdefg

133 very_long: str = typer.Option( 

134 ..., 

135 "--this-is-a-very-very-very-long-option-name", 

136 help="Description is rendered in the next line for long option labels.", 

137 ), 

138 ) -> None: 

139 pass # pragma: no cover 

140 

141 result = runner.invoke(app, ["cmd", "--help"], terminal_width=80) 1abcdefg

142 assert result.exit_code == 0 1abcdefg

143 

144 output_lines = result.output.splitlines() 1abcdefg

145 option_idx = next( 1abcdefg

146 i 

147 for i, line in enumerate(output_lines) 

148 if "--this-is-a-very-very-very-long-option-name" in line 

149 ) 

150 assert "Description is rendered" not in output_lines[option_idx] 1abcdefg

151 first_desc_line = output_lines[option_idx + 1] 1abcdefg

152 assert first_desc_line.lstrip().startswith("Description is rendered") 1abcdefg

153 continuation_block = " ".join( 1abcdefg

154 line.strip() for line in output_lines[option_idx + 1 :] if line.startswith(" ") 

155 ) 

156 assert ( 1abcdefg

157 "Description is rendered in the next line for long option labels." 

158 in continuation_block 

159 ) 

160 

161 

162def test_typer_run_usage() -> None: 1abcdefg

163 from typer.cli import app as typer_cli_app 1abcdefg

164 

165 result = runner.invoke( 1abcdefg

166 typer_cli_app, 

167 ["tests/assets/cli/minimum_main.py", "run"], 

168 prog_name="typer", 

169 ) 

170 

171 assert result.exit_code == 2 1abcdefg

172 usage_line = result.output.splitlines()[0] 1abcdefg

173 assert usage_line.startswith("Usage: typer [PATH_OR_MODULE] run [OPTIONS] {name}") 1abcdefg

174 assert "Try 'typer [PATH_OR_MODULE] run --help' for help." in result.output 1abcdefg