Skip to content

Commit d9ace1f

Browse files
authored
fix: resolve several empty line regressions (#330)
* fix: add function to check if docstring is at end of file * fix: handle case where class contains only docstring * chore: fix mypy error * fix: treat ellipses as code line * fix: include workaround to detect f-string in Python < 3.12 * fix: refine newline detection to exclude empty lines
1 parent 7798699 commit d9ace1f

File tree

6 files changed

+136
-39
lines changed

6 files changed

+136
-39
lines changed

src/docformatter/classify.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ def is_code_line(token: tokenize.TokenInfo) -> bool:
266266
bool
267267
True if the token is a code line, False otherwise.
268268
"""
269-
if token.type == tokenize.NAME and not (
269+
if (token.type == tokenize.NAME or token.string == "...") and not (
270270
token.line.strip().startswith("def ")
271271
or token.line.strip().startswith("async ")
272272
or token.line.strip().startswith("class ")
@@ -317,6 +317,15 @@ def is_f_string(token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo) -> bo
317317
if PY312:
318318
if tokenize.FSTRING_MIDDLE in [token.type, prev_token.type]:
319319
return True
320+
elif any(
321+
[
322+
token.string.startswith('f"""'),
323+
prev_token.string.startswith('f"""'),
324+
token.string.startswith("f'''"),
325+
prev_token.string.startswith("f'''"),
326+
]
327+
):
328+
return True
320329

321330
return False
322331

@@ -432,7 +441,7 @@ def is_newline_continuation(
432441
if (
433442
token.type in (tokenize.NEWLINE, tokenize.NL)
434443
and token.line.strip() in prev_token.line.strip()
435-
and token.line != "\n"
444+
and token.line not in {"\n", "\r\n"}
436445
):
437446
return True
438447

@@ -460,14 +469,29 @@ def is_string_variable(
460469
# TODO: The AWAIT token is removed in Python 3.13 and later. Only Python 3.9
461470
# seems to generate the AWAIT token, so we can safely remove the check for it when
462471
# support for Python 3.9 is dropped in April 2026.
463-
try:
472+
if sys.version_info <= (3, 12):
464473
_token_types = (tokenize.AWAIT, tokenize.OP)
465-
except AttributeError:
466-
_token_types = (tokenize.OP,) # type: ignore
474+
else:
475+
_token_types = (tokenize.OP,)
467476

468477
if prev_token.type in _token_types and (
469478
'= """' in token.line or token.line in prev_token.line
470479
):
471480
return True
472481

473482
return False
483+
484+
485+
def is_docstring_at_end_of_file(tokens: list[tokenize.TokenInfo], index: int) -> bool:
486+
"""Determine if the docstring is at the end of the file."""
487+
for i in range(index + 1, len(tokens)):
488+
tok = tokens[i]
489+
if tok.type not in (
490+
tokenize.NL,
491+
tokenize.NEWLINE,
492+
tokenize.DEDENT,
493+
tokenize.ENDMARKER,
494+
):
495+
return False
496+
497+
return True

src/docformatter/format.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,22 @@ def _get_class_docstring_newlines(
260260
The number of newlines to insert after the docstring.
261261
"""
262262
j = index + 1
263+
indention_level = tokens[index].start[1]
263264

264265
# The docstring is followed by a comment.
265266
if tokens[j].string.startswith("#"):
266267
return 0
267268

269+
while j < len(tokens):
270+
if tokens[j].type in (tokenize.NL, tokenize.NEWLINE):
271+
j += 1
272+
continue
273+
274+
if tokens[j].start[1] < indention_level:
275+
return 2
276+
277+
break
278+
268279
return 1
269280

270281

@@ -379,7 +390,10 @@ def _get_newlines_by_type(
379390
int
380391
The number of newlines to insert after the docstring.
381392
"""
382-
if _classify.is_module_docstring(tokens, index):
393+
if _classify.is_docstring_at_end_of_file(tokens, index):
394+
# print("End of file")
395+
return 0
396+
elif _classify.is_module_docstring(tokens, index):
383397
# print("Module")
384398
return _get_module_docstring_newlines(black)
385399
elif _classify.is_class_docstring(tokens, index):

tests/_data/string_files/do_format_code.toml

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ source='''
4242
expected='''
4343
CONST = 123
4444
"""Docstring for CONST."""
45-
4645
'''
4746

4847
[class_docstring]
@@ -60,7 +59,6 @@ expected='''
6059
:cvar test_int: a class attribute.
6160
.. py:method:: big_method()
6261
"""
63-
6462
'''
6563

6664
[newline_class_variable]
@@ -87,7 +85,6 @@ expected='''
8785
8886
test_var2 = 1
8987
"""This is a second class variable docstring."""
90-
9188
'''
9289

9390
[class_attribute_wrap]
@@ -102,7 +99,6 @@ expected='''class TestClass:
10299
test_int = 1
103100
"""This is a very, very, very long docstring that should really be
104101
reformatted nicely by docformatter."""
105-
106102
'''
107103

108104
[newline_outside_docstring]
@@ -364,7 +360,6 @@ expected='''class Foo:
364360
365361
More stuff.
366362
"""
367-
368363
'''
369364

370365
[class_empty_lines_2]
@@ -688,7 +683,6 @@ class TestClass:
688683
:cvar test_int: a class attribute.
689684
..py:method:: big_method()
690685
"""
691-
692686
'''
693687

694688
[issue_139_2]
@@ -1134,7 +1128,6 @@ expected='''
11341128
#!/usr/bin/env python
11351129
11361130
"""a.py."""
1137-
11381131
'''
11391132

11401133
[issue_203]
@@ -1167,3 +1160,76 @@ expected='''def foo(bar):
11671160
Description.
11681161
"""
11691162
'''
1163+
1164+
[two_lines_between_stub_classes]
1165+
source='''class Foo:
1166+
"""Foo class."""
1167+
class Bar:
1168+
"""Bar class."""
1169+
'''
1170+
expected='''class Foo:
1171+
"""Foo class."""
1172+
1173+
1174+
class Bar:
1175+
"""Bar class."""
1176+
'''
1177+
1178+
[two_lines_between_stub_classes_with_preceding_comment]
1179+
source='''class Foo:
1180+
"""Foo class."""
1181+
1182+
# A comment for class Bar
1183+
class Bar:
1184+
"""Bar class."""
1185+
'''
1186+
expected='''class Foo:
1187+
"""Foo class."""
1188+
1189+
1190+
# A comment for class Bar
1191+
class Bar:
1192+
"""Bar class."""
1193+
'''
1194+
1195+
[ellipses_is_code_line]
1196+
source='''class Foo:
1197+
def bar() -> str:
1198+
"""Bar."""
1199+
1200+
...
1201+
1202+
def baz() -> None:
1203+
"""Baz."""
1204+
1205+
...
1206+
'''
1207+
expected='''class Foo:
1208+
def bar() -> str:
1209+
"""Bar."""
1210+
...
1211+
1212+
def baz() -> None:
1213+
"""Baz."""
1214+
...
1215+
'''
1216+
1217+
[do_not_break_f_string_double_quotes]
1218+
source='''foo = f"""
1219+
bar
1220+
"""
1221+
'''
1222+
expected='''foo = f"""
1223+
bar
1224+
"""
1225+
'''
1226+
1227+
[do_not_break_f_string_single_quotes]
1228+
source="""foo = f'''
1229+
bar
1230+
'''
1231+
"""
1232+
expected="""foo = f'''
1233+
bar
1234+
'''
1235+
"""

tests/_data/string_files/format_functions.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,11 @@ expected = 1
169169

170170
[get_newlines_by_type_module_docstring]
171171
source = '"""Module docstring."""'
172-
expected = 1
172+
expected = 0
173173

174174
[get_newlines_by_type_module_docstring_black]
175175
source = '"""Module docstring."""'
176-
expected = 2
176+
expected = 0
177177

178178
[get_newlines_by_type_class_docstring]
179179
source = '''
@@ -195,7 +195,7 @@ expected = 0
195195
source = '''x = 1
196196
"""Docstring for x."""
197197
'''
198-
expected = 1
198+
expected = 0
199199

200200
[get_num_rows_columns]
201201
token = [5, " ", [3, 10], [3, 40], ''' This is

tests/formatter/test_do_format_code.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@
135135
("issue_187", NO_ARGS),
136136
("issue_203", NO_ARGS),
137137
("issue_243", NO_ARGS),
138+
("two_lines_between_stub_classes", NO_ARGS),
139+
("two_lines_between_stub_classes_with_preceding_comment", NO_ARGS),
140+
("ellipses_is_code_line", NO_ARGS),
141+
("do_not_break_f_string_double_quotes", NO_ARGS),
142+
("do_not_break_f_string_single_quotes", NO_ARGS),
138143
],
139144
)
140145
def test_do_format_code(test_key, test_args, args):

0 commit comments

Comments
 (0)