Coverage for tests/test_better_dedent.py: 100%

1# SPDX-FileCopyrightText: 2025-present Trey Hunner

2#

3# SPDX-License-Identifier: MIT

4 import pytest

5 import textwrap

6 from string.templatelib import Template

7 from hypothesis import given, strategies as st

8

9 from better_dedent import dedent, undent, _convert

10

11

12 class 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

77 class 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

194 class 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

236 class 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

256 class 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

350 class 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)