Skip to content

Commit 0fecc43

Browse files
thatlittleboypawamoy
authored andcommitted
feat: Add trim_doctest_flag to google and numpy parsers
1 parent 9ea40b0 commit 0fecc43

4 files changed

Lines changed: 142 additions & 16 deletions

File tree

src/pytkdocs/parsers/docstrings/google.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,26 @@
2828

2929
RE_GOOGLE_STYLE_ADMONITION: Pattern = re.compile(r"^(?P<indent>\s*)(?P<type>[\w-]+):((?:\s+)(?P<title>.+))?$")
3030
"""Regular expressions to match lines starting admonitions, of the form `TYPE: [TITLE]`."""
31+
RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
32+
"""Regular expression to match lines of the form `<BLANKLINE>`."""
33+
RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
34+
"""Regular expression to match lines containing doctest flags of the form `# doctest: +FLAG`."""
3135

3236

3337
class Google(Parser):
3438
"""A Google-style docstrings parser."""
3539

36-
def __init__(self, replace_admonitions: bool = True) -> None:
40+
def __init__(self, replace_admonitions: bool = True, trim_doctest_flags: bool = True) -> None:
3741
"""
3842
Initialize the object.
3943
4044
Arguments:
4145
replace_admonitions: Whether to replace admonitions by their Markdown equivalent.
46+
trim_doctest_flags: Whether to remove doctest flags.
4247
"""
4348
super().__init__()
4449
self.replace_admonitions = replace_admonitions
50+
self.trim_doctest_flags = trim_doctest_flags
4551
self.section_reader = {
4652
Section.Type.PARAMETERS: self.read_parameters_section,
4753
Section.Type.KEYWORD_ARGS: self.read_keyword_arguments_section,
@@ -492,6 +498,9 @@ def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Opt
492498
current_text.append(line)
493499

494500
elif in_code_example:
501+
if self.trim_doctest_flags:
502+
line = RE_DOCTEST_FLAGS.sub("", line)
503+
line = RE_DOCTEST_BLANKLINE.sub("", line)
495504
current_example.append(line)
496505

497506
elif line.startswith("```"):
@@ -506,6 +515,9 @@ def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Opt
506515
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
507516
current_text = []
508517
in_code_example = True
518+
519+
if self.trim_doctest_flags:
520+
line = RE_DOCTEST_FLAGS.sub("", line)
509521
current_example.append(line)
510522

511523
else:

src/pytkdocs/parsers/docstrings/numpy.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
"""This module defines functions and classes to parse docstrings into structured data."""
22
import re
3-
from typing import List, Optional
3+
from typing import List, Optional, Pattern
44

55
from docstring_parser import parse
66
from docstring_parser.common import Docstring, DocstringMeta
77

88
from pytkdocs.parsers.docstrings.base import AnnotatedObject, Attribute, Parameter, Parser, Section, empty
99

10+
RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
11+
"""Regular expression to match lines of the form `<BLANKLINE>`."""
12+
RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
13+
"""Regular expression to match lines containing doctest flags of the form `# doctest: +FLAG`."""
14+
1015

1116
class Numpy(Parser):
1217
"""A Numpy-style docstrings parser."""
1318

14-
def __init__(self) -> None:
19+
def __init__(self, trim_doctest_flags: bool = True) -> None:
1520
"""
1621
Initialize the objects.
22+
23+
Arguments:
24+
trim_doctest_flags: Whether to remove doctest flags.
1725
"""
1826
super().__init__()
27+
self.trim_doctest_flags = trim_doctest_flags
1928
self.section_reader = {
2029
Section.Type.PARAMETERS: self.read_parameters_section,
2130
Section.Type.EXCEPTIONS: self.read_exceptions_section,
@@ -229,6 +238,9 @@ def read_examples_section(
229238
current_text.append(line)
230239

231240
elif in_code_example:
241+
if self.trim_doctest_flags:
242+
line = RE_DOCTEST_FLAGS.sub("", line)
243+
line = RE_DOCTEST_BLANKLINE.sub("", line)
232244
current_example.append(line)
233245

234246
elif line.startswith("```"):
@@ -243,15 +255,21 @@ def read_examples_section(
243255
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
244256
current_text = []
245257
in_code_example = True
258+
259+
if self.trim_doctest_flags:
260+
line = RE_DOCTEST_FLAGS.sub("", line)
246261
current_example.append(line)
247262
else:
248263
current_text.append(line)
264+
249265
if current_text:
250266
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
251267
elif current_example:
252268
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
269+
253270
if sub_sections:
254271
return Section(Section.Type.EXAMPLES, sub_sections)
272+
255273
if re.search("Examples\n", docstring):
256274
self.error("Empty examples section")
257275
return None

tests/test_parsers/test_docstrings/test_google.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,19 @@ class DummyObject:
1414
path = "o"
1515

1616

17-
def parse(docstring, signature=None, return_type=inspect.Signature.empty, admonitions=True):
17+
def parse(
18+
docstring,
19+
signature=None,
20+
return_type=inspect.Signature.empty,
21+
admonitions=True,
22+
trim_doctest=False,
23+
):
1824
"""Helper to parse a doctring."""
19-
return Google(replace_admonitions=admonitions).parse(
20-
dedent(docstring).strip(), {"obj": DummyObject(), "signature": signature, "type": return_type}
25+
parser = Google(replace_admonitions=admonitions, trim_doctest_flags=trim_doctest)
26+
27+
return parser.parse(
28+
dedent(docstring).strip(),
29+
context={"obj": DummyObject(), "signature": signature, "type": return_type},
2130
)
2231

2332

@@ -129,8 +138,48 @@ def f(x: int, y: int, *, z: int) -> int:
129138
assert not errors
130139

131140

141+
def test_function_with_examples_trim_doctest():
142+
"""Parse example docstring with trim_doctest_flags option."""
143+
144+
def f(x: int) -> int:
145+
"""Test function.
146+
147+
Example:
148+
149+
We want to skip the following test.
150+
>>> 1 + 1 == 3 # doctest: +SKIP
151+
True
152+
153+
And then a few more examples here:
154+
>>> print("a\\n\\nb")
155+
a
156+
<BLANKLINE>
157+
b
158+
>>> 1 + 1 == 2 # doctest: +SKIP
159+
>>> print(list(range(1, 100))) # doctest: +ELLIPSIS
160+
[1, 2, ..., 98, 99]
161+
"""
162+
return x
163+
164+
sections, errors = parse(
165+
inspect.getdoc(f),
166+
inspect.signature(f),
167+
trim_doctest=True,
168+
)
169+
assert len(sections) == 2
170+
assert len(sections[1].value) == 4
171+
assert not errors
172+
173+
# Verify that doctest flags have indeed been trimmed
174+
example_str = sections[1].value[1][1]
175+
assert "# doctest: +SKIP" not in example_str
176+
example_str = sections[1].value[3][1]
177+
assert "<BLANKLINE>" not in example_str
178+
assert "\n>>> print(list(range(1, 100)))\n" in example_str
179+
180+
132181
def test_function_with_examples():
133-
"""Parse a function docstring with signature annotations."""
182+
"""Parse a function docstring with examples."""
134183

135184
def f(x: int, y: int) -> int:
136185
"""

tests/test_parsers/test_docstrings/test_numpy.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ class DummyObject:
1111
path = "o"
1212

1313

14-
def parse(docstring, signature=None, return_type=inspect.Signature.empty):
14+
def parse(
15+
docstring,
16+
signature=None,
17+
return_type=inspect.Signature.empty,
18+
trim_doctest=False,
19+
):
1520
"""Helper to parse a doctring."""
16-
return Numpy().parse(
21+
parser = Numpy(trim_doctest_flags=trim_doctest)
22+
23+
return parser.parse(
1724
dedent(docstring).strip(),
18-
{"obj": DummyObject(), "signature": signature, "type": return_type},
25+
context={"obj": DummyObject(), "signature": signature, "type": return_type},
1926
)
2027

2128

@@ -48,13 +55,13 @@ def test_sections_without_signature():
4855
4956
Parameters
5057
----------
51-
void :
58+
void :
5259
SEGFAULT.
53-
niet :
60+
niet :
5461
SEGFAULT.
55-
nada :
62+
nada :
5663
SEGFAULT.
57-
rien :
64+
rien :
5865
SEGFAULT.
5966
6067
Raises
@@ -63,7 +70,7 @@ def test_sections_without_signature():
6370
when nothing works as expected.
6471
6572
Returns
66-
-------
73+
-------
6774
bool
6875
Itself.
6976
"""
@@ -135,8 +142,48 @@ def f(x: int, y: int) -> int:
135142
assert not errors
136143

137144

145+
def test_function_with_examples_trim_doctest():
146+
"""Parse example docstring with trim_doctest_flags option."""
147+
148+
def f(x: int) -> int:
149+
"""Test function.
150+
151+
Example
152+
-------
153+
We want to skip the following test.
154+
>>> 1 + 1 == 3 # doctest: +SKIP
155+
True
156+
157+
And then a few more examples here:
158+
>>> print("a\\n\\nb")
159+
a
160+
<BLANKLINE>
161+
b
162+
>>> 1 + 1 == 2 # doctest: +SKIP
163+
>>> print(list(range(1, 100))) # doctest: +ELLIPSIS
164+
[1, 2, ..., 98, 99]
165+
"""
166+
return x
167+
168+
sections, errors = parse(
169+
inspect.getdoc(f),
170+
inspect.signature(f),
171+
trim_doctest=True,
172+
)
173+
assert len(sections) == 2
174+
assert len(sections[1].value) == 4
175+
assert not errors
176+
177+
# Verify that doctest flags have indeed been trimmed
178+
example_str = sections[1].value[1][1]
179+
assert "# doctest: +SKIP" not in example_str
180+
example_str = sections[1].value[3][1]
181+
assert "<BLANKLINE>" not in example_str
182+
assert "\n>>> print(list(range(1, 100)))\n" in example_str
183+
184+
138185
def test_function_with_examples():
139-
"""Parse a function docstring with signature annotations."""
186+
"""Parse a function docstring with examples."""
140187

141188
def f(x: int, y: int) -> int:
142189
"""

0 commit comments

Comments
 (0)