diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py index ee1187d4..6292b64e 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -14,6 +14,10 @@ IMPORT_MATPLOTLIB_RE = r"\b(import +matplotlib|from +matplotlib +import)\b" +# Used to help identify the first sentence of an object's description. These +# values are based on sphinx.ext.autosummary. +WELL_KNOWN_ABBREVIATIONS = ("et al.", "e.g.", "i.e.", "vs.") + class SphinxDocString(NumpyDocString): def __init__(self, docstring, config=None): @@ -159,16 +163,22 @@ def _process_param(self, param, desc, fake_autosummary): # Referenced object has a docstring display_param = f":obj:`{param} <{link_prefix}{param}>`" if obj_doc: - # Overwrite desc. Take summary logic of autosummary - desc = re.split(r"\n\s*\n", obj_doc.strip(), maxsplit=1)[0] - # XXX: Should this have DOTALL? - # It does not in autosummary - m = re.search(r"^([A-Z].*?\.)(?:\s|$)", " ".join(desc.split())) - if m: - desc = m.group(1).strip() + # Overwrite desc with the first sentence. Based on + # sphinx.ext.autosummary's `extract_summary()`. + stanza = re.split(r"\n\s*\n", obj_doc.strip(), maxsplit=1)[0] + sentences = re.split(r"\.(?:\s+|$)", " ".join(stanza.split())) + if len(sentences) == 1: + # If there are no periods, use only the first line. This + # differs from autosummary, which takes the whole stanza. + desc = stanza.partition("\n")[0] else: - desc = desc.partition("\n")[0] - desc = desc.split("\n") + desc = "" + for i in range(len(sentences)): + desc = ". ".join(sentences[: i + 1]).rstrip(".") + "." + if not desc.endswith(WELL_KNOWN_ABBREVIATIONS): + break + desc = [desc] + return display_param, desc def _str_param_list(self, name, fake_autosummary=False): diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index acf5194c..715e8717 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1198,6 +1198,7 @@ def test_duplicate_signature(): But a description no_docstring2 : str multiline_sentence + multiline_single_sentence midword_period no_period @@ -1253,6 +1254,7 @@ def test_class_members_doc(): But a description no_docstring2 : str multiline_sentence + multiline_single_sentence midword_period no_period @@ -1300,6 +1302,12 @@ def multiline_sentence(self): sentence. It spans multiple lines.""" return + @property + def multiline_single_sentence(self): + """This is a + sentence.""" + return + @property def midword_period(self): """The sentence for numpy.org.""" @@ -1351,6 +1359,9 @@ def no_period(self): :obj:`multiline_sentence ` This is a sentence. + :obj:`multiline_single_sentence ` + This is a sentence. + :obj:`midword_period ` The sentence for numpy.org. @@ -1419,6 +1430,53 @@ def an_attribute(self): assert "Another description" not in str(SphinxClassDoc(Foo, config=cfg)) +def test_class_properties_with_abbreviations(): + class Foo: + """ + Class docstring. + + Attributes + ---------- + using_ie + using_ie_in_parens + using_others + """ + + @property + def using_ie(self): + """ + Test property, i.e. a method that works like an attribute. More. + """ + return + + @property + def using_ie_in_parens(self): + """ + Test property (i.e. a method that works like an attribute). More. + """ + return + + @property + def using_others(self): + """ + Test others et al. and e.g. and vs. are ok. More. + """ + return + + attr_doc = """:Attributes: + + :obj:`using_ie ` + Test property, i.e. a method that works like an attribute. + + :obj:`using_ie_in_parens ` + Test property (i.e. a method that works like an attribute). + + :obj:`using_others ` + Test others et al. and e.g. and vs. are ok.""" + + assert attr_doc in str(SphinxClassDoc(Foo)) + + def test_templated_sections(): doc = SphinxClassDoc( None,