Coverage for tests/test_better_dedent.py: 100%

206 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-06 11:04 -0700

1# SPDX-FileCopyrightText: 2025-present Trey Hunner 

2# 

3# SPDX-License-Identifier: MIT 

4import pytest 

5import textwrap 

6from string.templatelib import Template 

7from hypothesis import given, strategies as st 

8 

9from better_dedent import dedent, undent, _convert 

10 

11 

12class TestDedentWithRegularStrings: 

13 """Test dedent() function with regular string inputs.""" 

14 

15 def test_dedent_basic_indented_string(self): 

16 text = textwrap.dedent("""\ 

17 hello 

18 world""") 

19 result = dedent(text) 

20 expected = textwrap.dedent("""\ 

21 hello 

22 world""") 

23 assert result == expected 

24 

25 def test_dedent_mixed_indentation(self): 

26 text = textwrap.dedent("""\ 

27 line1 

28 line2 

29 line3""") 

30 result = dedent(text) 

31 expected = textwrap.dedent("""\ 

32 line1 

33 line2 

34 line3""") 

35 assert result == expected 

36 

37 def test_dedent_no_indentation(self): 

38 text = "hello\nworld" 

39 result = dedent(text) 

40 assert result == "hello\nworld" 

41 

42 def test_dedent_empty_string(self): 

43 result = dedent("") 

44 assert result == "" 

45 

46 def test_dedent_whitespace_only(self): 

47 result = dedent(" \n \n") 

48 assert result == "\n\n" 

49 

50 def test_dedent_single_line(self): 

51 result = dedent(" hello") 

52 assert result == "hello" 

53 

54 def test_dedent_tabs_and_spaces(self): 

55 text = "\t hello\n\t world" 

56 result = dedent(text) 

57 assert result == "hello\nworld" 

58 

59 def test_dedent_preserves_relative_indentation(self): 

60 text = textwrap.dedent("""\ 

61 def func(): 

62 return 42""") 

63 result = dedent(text) 

64 expected = textwrap.dedent("""\ 

65 def func(): 

66 return 42""") 

67 assert result == expected 

68 

69 def test_dedent_compatibility_with_textwrap(self): 

70 text = textwrap.dedent("""\ 

71 hello 

72 world 

73 nested""") 

74 assert dedent(text) == textwrap.dedent(text) 

75 

76 

77class TestDedentWithTStrings: 

78 """Test dedent() function with t-string Template inputs.""" 

79 

80 def test_dedent_tstring_basic(self): 

81 name = "world" 

82 template = t" hello {name}" 

83 result = dedent(template) 

84 expected = "hello world" 

85 assert result == expected 

86 

87 def test_dedent_tstring_multiline(self): 

88 color = "purple" 

89 size = 3 

90 template = t""" 

91 Color: {color} 

92 Size: {size} 

93 """ 

94 result = dedent(template) 

95 expected = textwrap.dedent(""" 

96 Color: purple 

97 Size: 3 

98 """) 

99 assert result == expected 

100 

101 def test_dedent_tstring_with_format_spec(self): 

102 from math import tau 

103 

104 template = t" tau: {tau:.2f}" 

105 result = dedent(template) 

106 expected = "tau: 6.28" 

107 assert result == expected 

108 

109 def test_dedent_tstring_with_conversion(self): 

110 color = "purple" 

111 template = t" Color: {color!r}" 

112 result = dedent(template) 

113 expected = "Color: 'purple'" 

114 assert result == expected 

115 

116 def test_dedent_tstring_preserves_interpolation_indentation(self): 

117 code = textwrap.dedent("""\ 

118 def func(): 

119 return 42""").strip() 

120 template = t"""\ 

121 \ 

122 Code: 

123 {code} 

124 """ 

125 result = dedent(template) 

126 expected = textwrap.dedent("""\ 

127 Code: 

128 def func(): 

129 return 42 

130 """) 

131 assert result == expected 

132 

133 def test_dedent_tstring_multiline_interpolation_indented(self): 

134 lines = textwrap.dedent("""\ 

135 line1 

136 line2 

137 line3""").strip() 

138 template = t"""\ 

139 \ 

140 Content: 

141 {lines} 

142 """ 

143 result = dedent(template) 

144 expected = textwrap.dedent("""\ 

145 Content: 

146 line1 

147 line2 

148 line3 

149 """) 

150 assert result == expected 

151 

152 def test_dedent_tstring_mixed_content(self): 

153 header = "Header" 

154 body = textwrap.dedent("""\ 

155 First line 

156 Second line""").strip() 

157 template = t"""\ 

158 \ 

159 {header}: 

160 {body} 

161 End 

162 """ 

163 result = dedent(template) 

164 expected = textwrap.dedent("""\ 

165 Header: 

166 First line 

167 Second line 

168 End 

169 """) 

170 assert result == expected 

171 

172 def test_dedent_tstring_literal_braces(self): 

173 value = "test" 

174 template = t" { 0} {value}" 

175 result = dedent(template) 

176 expected = "{0} test" 

177 assert result == expected 

178 

179 def test_dedent_tstring_empty_interpolation(self): 

180 empty = "" 

181 template = t" Value: '{empty}'" 

182 result = dedent(template) 

183 expected = "Value: ''" 

184 assert result == expected 

185 

186 def test_dedent_tstring_multiple_interpolations_same_line(self): 

187 x, y = 1, 2 

188 template = t" Point: ({x}, {y})" 

189 result = dedent(template) 

190 expected = "Point: (1, 2)" 

191 assert result == expected 

192 

193 

194class TestUndent: 

195 """Test undent() function.""" 

196 

197 def test_undent_basic(self): 

198 text = "\n hello\n world\n" 

199 result = undent(text) 

200 assert result == "hello\nworld" 

201 

202 def test_undent_no_leading_newline(self): 

203 text = " hello\n world\n" 

204 result = undent(text) 

205 assert result == "hello\nworld" 

206 

207 def test_undent_no_trailing_newline(self): 

208 text = "\n hello\n world" 

209 result = undent(text) 

210 assert result == "hello\nworld" 

211 

212 def test_undent_preserve_trailing_newline(self): 

213 text = "\n hello\n world\n" 

214 result = undent(text, strip_trailing=False) 

215 assert result == "hello\nworld\n" 

216 

217 def test_undent_with_tstring(self): 

218 name = "test" 

219 template = t"\n Hello {name}\n" 

220 result = undent(template) 

221 assert result == "Hello test" 

222 

223 def test_undent_empty_string(self): 

224 result = undent("") 

225 assert result == "" 

226 

227 def test_undent_only_newlines(self): 

228 result = undent("\n\n") 

229 assert result == "" 

230 

231 def test_undent_whitespace_only(self): 

232 result = undent("\n \n \n") 

233 assert result == "\n" 

234 

235 

236class TestConvertHelper: 

237 """Test _convert() helper function.""" 

238 

239 def test_convert_no_conversion(self): 

240 assert _convert("hello", None) == "hello" 

241 assert _convert(42, None) == 42 

242 

243 def test_convert_str(self): 

244 assert _convert(42, "s") == "42" 

245 assert _convert([1, 2, 3], "s") == "[1, 2, 3]" 

246 

247 def test_convert_repr(self): 

248 assert _convert("hello", "r") == "'hello'" 

249 assert _convert([1, 2, 3], "r") == "[1, 2, 3]" 

250 

251 def test_convert_ascii(self): 

252 assert _convert("hello", "a") == "'hello'" 

253 assert _convert("héllo", "a") == "'h\\xe9llo'" 

254 

255 

256class TestEdgeCases: 

257 """Test edge cases and complex scenarios.""" 

258 

259 def test_deeply_nested_indentation(self): 

260 text = " " * 5 + "deep" 

261 result = dedent(text) 

262 expected = "deep" 

263 assert result == expected 

264 

265 def test_mixed_tabs_spaces_complex(self): 

266 text = "\t line1\n\t\tline2\n\t line3" 

267 result = dedent(text) 

268 expected = " line1\n\tline2\n line3" 

269 assert result == expected 

270 

271 def test_tstring_with_nested_quotes(self): 

272 quote = "He said 'hello'" 

273 template = t' Message: "{quote}"' 

274 result = dedent(template) 

275 assert result == 'Message: "He said \'hello\'"' 

276 

277 def test_tstring_with_complex_format_specs(self): 

278 data = {"name": "Alice", "score": 95.678} 

279 template = t" {data['name']:>10} scored {data['score']:8.2f}" 

280 result = dedent(template) 

281 assert result == " Alice scored 95.68" 

282 

283 def test_very_long_interpolation(self): 

284 long_text = "x" * 1000 

285 template = t" Long: {long_text}" 

286 result = dedent(template) 

287 assert result == f"Long: {'x' * 1000}" 

288 

289 def test_no_indentation_before_replacement(self): 

290 greeting = "Hello" 

291 name = "Trey" 

292 template = t"{greeting}{name}" 

293 result = dedent(template) 

294 assert result == "HelloTrey" 

295 

296 def test_curly_braces_before_replacement(self): 

297 header = "Header" 

298 body = textwrap.dedent("""\ 

299 First line 

300 Second line""").strip() 

301 template = t"""\ 

302 \ 

303 {header}: 

304 { {body}} 

305 And 

306 { { {body}} } 

307 End 

308 False replacement: { 3} 

309 """ 

310 result = dedent(template) 

311 expected = textwrap.dedent("""\ 

312 Header: 

313 {First line 

314 Second line} 

315 And 

316 {{First line 

317 Second line}} 

318 End 

319 False replacement: {3} 

320 """) 

321 assert result == expected 

322 

323 def test_interpolation_preserves_multiline_indentation_complex(self): 

324 code = textwrap.dedent(r""" 

325 def strip_each(lines): 

326 new_lines = [] 

327 for line in lines: 

328 new_lines.append(line.rstrip("\n")) 

329 return new_lines""" 

330 ).strip("\n") 

331 template = t"""\ 

332 \ 

333 Example function: 

334 {code} 

335 

336 That function was indented properly!""" 

337 result = dedent(template) 

338 expected = textwrap.dedent("""\ 

339 Example function: 

340 def strip_each(lines): 

341 new_lines = [] 

342 for line in lines: 

343 new_lines.append(line.rstrip("\\n")) 

344 return new_lines 

345 

346 That function was indented properly!""").strip() 

347 assert result == expected 

348 

349 

350class TestPropertyBasedTests: 

351 """Property-based tests using Hypothesis.""" 

352 

353 @given(st.text()) 

354 def test_dedent_string_compatibility_with_textwrap(self, text): 

355 """Property test: dedent(string) should always equal textwrap.dedent(string).""" 

356 assert dedent(text) == textwrap.dedent(text) 

357 

358 @given( 

359 st.text( 

360 alphabet=st.characters( 

361 whitelist_categories=("Zs", "Cc"), max_codepoint=127 

362 ), 

363 min_size=0, 

364 max_size=1000, 

365 ) 

366 ) 

367 def test_dedent_whitespace_strings(self, text): 

368 """Property test: dedent should handle whitespace-only strings correctly.""" 

369 assert dedent(text) == textwrap.dedent(text) 

370 

371 @given( 

372 st.lists( 

373 st.text( 

374 alphabet=st.characters(blacklist_characters="\n"), 

375 min_size=0, 

376 max_size=50, 

377 ), 

378 min_size=1, 

379 max_size=20, 

380 ).map(lambda lines: "\n".join(lines)) 

381 ) 

382 def test_dedent_multiline_strings(self, text): 

383 """Property test: dedent should handle multiline strings correctly.""" 

384 assert dedent(text) == textwrap.dedent(text) 

385 

386 @given( 

387 st.text( 

388 alphabet=st.characters( 

389 whitelist_categories=("Lu", "Ll", "Nd", "Zs"), max_codepoint=127 

390 ), 

391 min_size=0, 

392 max_size=200, 

393 ) 

394 ) 

395 def test_dedent_alphanumeric_strings(self, text): 

396 """Property test: dedent should handle alphanumeric strings correctly.""" 

397 assert dedent(text) == textwrap.dedent(text)