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
« 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
9from better_dedent import dedent, undent, _convert
12class TestDedentWithRegularStrings:
13 """Test dedent() function with regular string inputs."""
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
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
37 def test_dedent_no_indentation(self):
38 text = "hello\nworld"
39 result = dedent(text)
40 assert result == "hello\nworld"
42 def test_dedent_empty_string(self):
43 result = dedent("")
44 assert result == ""
46 def test_dedent_whitespace_only(self):
47 result = dedent(" \n \n")
48 assert result == "\n\n"
50 def test_dedent_single_line(self):
51 result = dedent(" hello")
52 assert result == "hello"
54 def test_dedent_tabs_and_spaces(self):
55 text = "\t hello\n\t world"
56 result = dedent(text)
57 assert result == "hello\nworld"
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
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)
77class TestDedentWithTStrings:
78 """Test dedent() function with t-string Template inputs."""
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
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
101 def test_dedent_tstring_with_format_spec(self):
102 from math import tau
104 template = t" tau: {tau:.2f}"
105 result = dedent(template)
106 expected = "tau: 6.28"
107 assert result == expected
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
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
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
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
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
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
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
194class TestUndent:
195 """Test undent() function."""
197 def test_undent_basic(self):
198 text = "\n hello\n world\n"
199 result = undent(text)
200 assert result == "hello\nworld"
202 def test_undent_no_leading_newline(self):
203 text = " hello\n world\n"
204 result = undent(text)
205 assert result == "hello\nworld"
207 def test_undent_no_trailing_newline(self):
208 text = "\n hello\n world"
209 result = undent(text)
210 assert result == "hello\nworld"
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"
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"
223 def test_undent_empty_string(self):
224 result = undent("")
225 assert result == ""
227 def test_undent_only_newlines(self):
228 result = undent("\n\n")
229 assert result == ""
231 def test_undent_whitespace_only(self):
232 result = undent("\n \n \n")
233 assert result == "\n"
236class TestConvertHelper:
237 """Test _convert() helper function."""
239 def test_convert_no_conversion(self):
240 assert _convert("hello", None) == "hello"
241 assert _convert(42, None) == 42
243 def test_convert_str(self):
244 assert _convert(42, "s") == "42"
245 assert _convert([1, 2, 3], "s") == "[1, 2, 3]"
247 def test_convert_repr(self):
248 assert _convert("hello", "r") == "'hello'"
249 assert _convert([1, 2, 3], "r") == "[1, 2, 3]"
251 def test_convert_ascii(self):
252 assert _convert("hello", "a") == "'hello'"
253 assert _convert("héllo", "a") == "'h\\xe9llo'"
256class TestEdgeCases:
257 """Test edge cases and complex scenarios."""
259 def test_deeply_nested_indentation(self):
260 text = " " * 5 + "deep"
261 result = dedent(text)
262 expected = "deep"
263 assert result == expected
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
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\'"'
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"
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}"
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"
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
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}
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
346 That function was indented properly!""").strip()
347 assert result == expected
350class TestPropertyBasedTests:
351 """Property-based tests using Hypothesis."""
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)
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)
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)
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)