diff --git a/src/docformatter/configuration.py b/src/docformatter/configuration.py index 248ddb7..1d0ad72 100644 --- a/src/docformatter/configuration.py +++ b/src/docformatter/configuration.py @@ -152,7 +152,7 @@ def do_parse_arguments(self) -> None: self.args = self.parser.parse_known_args(self.args_lst[1:])[0] - # Default black line length is 88 so use this when not specified + # Default black line length is 88, so use this when not specified # otherwise use PEP-8 defaults if self.args.black: _default_wrap_summaries = 88 diff --git a/src/docformatter/strings.py b/src/docformatter/strings.py index 0eccef0..f396545 100644 --- a/src/docformatter/strings.py +++ b/src/docformatter/strings.py @@ -29,6 +29,18 @@ import re from typing import List, Match, Optional, Union +# TODO: Read this from the configuration file or command line. +ABBREVIATIONS = ( + "e.g.", + "i.e.", + "et. al.", + "etc.", + "Dr.", + "Mr.", + "Mrs.", + "Ms.", +) + def find_shortest_indentation(lines: List[str]) -> str: """Determine the shortest indentation in a list of lines. @@ -184,14 +196,14 @@ def split_first_sentence(text): sentence += previous_delimiter + word - if sentence.endswith(("e.g.", "i.e.", "etc.", "Dr.", "Mr.", "Mrs.", "Ms.")): + if sentence.endswith(ABBREVIATIONS): # Ignore false end of sentence. pass elif sentence.endswith((".", "?", "!")): break elif sentence.endswith(":") and delimiter == "\n": # Break on colon if it ends the line. This is a heuristic to detect - # the beginning of some parameter list afterwards. + # the beginning of some parameter list after wards. break previous_delimiter = delimiter @@ -200,12 +212,48 @@ def split_first_sentence(text): return sentence, delimiter + rest +def split_summary(lines) -> List[str]: + """Split multi-sentence summary into the first sentence and the rest.""" + if not lines or not lines[0].strip(): + return lines + + text = lines[0].strip() + + tokens = re.split(r"(\s+)", text) # Keep whitespace for accurate rejoining + sentence = [] + rest = [] + i = 0 + + while i < len(tokens): + token = tokens[i] + sentence.append(token) + + if token.endswith(".") and not any( + "".join(sentence).strip().endswith(abbr) for abbr in ABBREVIATIONS + ): + i += 1 + break + + i += 1 + + rest = tokens[i:] + first_sentence = "".join(sentence).strip() + rest_text = "".join(rest).strip() + + lines[0] = first_sentence + if rest_text: + lines.insert(2, rest_text) + + return lines + + def split_summary_and_description(contents): """Split docstring into summary and description. Return tuple (summary, description). """ split_lines = contents.rstrip().splitlines() + split_lines = split_summary(split_lines) for index in range(1, len(split_lines)): # Empty line separation would indicate the rest is the description or diff --git a/tests/test_string_functions.py b/tests/test_string_functions.py index 23d23e6..0e83d37 100644 --- a/tests/test_string_functions.py +++ b/tests/test_string_functions.py @@ -36,6 +36,7 @@ normalize_summary() remove_section_headers() split_first_sentence() + split_summary() split_summary_and_description() strip_leading_blank_lines() strip_quotes() @@ -261,6 +262,40 @@ def test_split_first_sentence(self): "\none\ntwo", ) == docformatter.split_first_sentence("This is the first:\none\ntwo") + @pytest.mark.unit + def test_split_one_sentence_summary(self): + """A single sentence summary should be returned as is. + + See issue #283. + """ + assert [ + "This is a sentence.", + "", + ] == docformatter.split_summary(["This is a sentence.", ""]) + + assert [ + "This e.g. a sentence.", + "", + ] == docformatter.split_summary(["This e.g. a sentence.", ""]) + + @pytest.mark.unit + def test_split_multi_sentence_summary(self): + """A multi-sentence summary should return only the first as the summary. + + See issue #283. + """ + assert [ + "This is a sentence.", + "", + "This is another.", + ] == docformatter.split_summary(["This is a sentence. This is another.", ""]) + + assert [ + "This e.g. a sentence.", + "", + "This is another.", + ] == docformatter.split_summary(["This e.g. a sentence. This is another.", ""]) + @pytest.mark.unit def test_split_summary_and_description(self): """""" @@ -317,9 +352,7 @@ def test_split_summary_and_description_with_capital(self): assert ( "This is the first\nWashington", "", - ) == docformatter.split_summary_and_description( - "This is the first\nWashington" - ) + ) == docformatter.split_summary_and_description("This is the first\nWashington") @pytest.mark.unit def test_split_summary_and_description_with_list_on_other_line(self): @@ -351,9 +384,7 @@ def test_split_summary_and_description_with_colon(self): assert ( "This is the first:", "one\ntwo", - ) == docformatter.split_summary_and_description( - "This is the first:\none\ntwo" - ) + ) == docformatter.split_summary_and_description("This is the first:\none\ntwo") @pytest.mark.unit def test_split_summary_and_description_with_exclamation(self): @@ -361,9 +392,7 @@ def test_split_summary_and_description_with_exclamation(self): assert ( "This is the first!", "one\ntwo", - ) == docformatter.split_summary_and_description( - "This is the first!\none\ntwo" - ) + ) == docformatter.split_summary_and_description("This is the first!\none\ntwo") @pytest.mark.unit def test_split_summary_and_description_with_question_mark(self): @@ -371,9 +400,7 @@ def test_split_summary_and_description_with_question_mark(self): assert ( "This is the first?", "one\ntwo", - ) == docformatter.split_summary_and_description( - "This is the first?\none\ntwo" - ) + ) == docformatter.split_summary_and_description("This is the first?\none\ntwo") @pytest.mark.unit def test_split_summary_and_description_with_quote(self): @@ -381,23 +408,17 @@ def test_split_summary_and_description_with_quote(self): assert ( 'This is the first\n"one".', "", - ) == docformatter.split_summary_and_description( - 'This is the first\n"one".' - ) + ) == docformatter.split_summary_and_description('This is the first\n"one".') assert ( "This is the first\n'one'.", "", - ) == docformatter.split_summary_and_description( - "This is the first\n'one'." - ) + ) == docformatter.split_summary_and_description("This is the first\n'one'.") assert ( "This is the first\n``one``.", "", - ) == docformatter.split_summary_and_description( - "This is the first\n``one``." - ) + ) == docformatter.split_summary_and_description("This is the first\n``one``.") @pytest.mark.unit def test_split_summary_and_description_with_punctuation(self): @@ -461,9 +482,7 @@ def test_split_summary_and_description_with_abbreviation(self): "Test Mrs. now", "Test Ms. now", ]: - assert (text, "") == docformatter.split_summary_and_description( - text - ) + assert (text, "") == docformatter.split_summary_and_description(text) @pytest.mark.unit def test_split_summary_and_description_with_url(self): @@ -497,9 +516,7 @@ class TestStrippers: @pytest.mark.unit def test_remove_section_header(self): """Remove section header directives.""" - assert "foo\nbar\n" == docformatter.remove_section_header( - "----\nfoo\nbar\n" - ) + assert "foo\nbar\n" == docformatter.remove_section_header("----\nfoo\nbar\n") line = "foo\nbar\n" assert line == docformatter.remove_section_header(line)