Skip to content

Commit befc4bb

Browse files
authored
feat: Google style docstring parsing (robotframework#5684)
Implements (and tests) the parser component of robotframework#5604. Still needs to be integrated with rest of the code..
1 parent fd8076d commit befc4bb

4 files changed

Lines changed: 620 additions & 2 deletions

File tree

‎atest/requirements.txt‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
# Dependencies required by acceptance tests.
22
# See atest/README.rst for more information.
33

4-
markdown
54
pygments
6-
pyyaml
75
lxml
86
pillow >= 7.1.0; platform_system == 'Windows'
97
telnetlib-313-and-up; python_version >= '3.13'
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Copyright 2008-2015 Nokia Networks
2+
# Copyright 2016- Robot Framework Foundation
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import re
17+
import textwrap
18+
from collections.abc import Iterable
19+
from dataclasses import dataclass, field
20+
from enum import auto, Enum
21+
22+
__all__ = ["parse_docstring", "DocInfo"]
23+
24+
25+
@dataclass
26+
class DocInfo:
27+
doc: str = ""
28+
args: "dict[str, str]" = field(default_factory=dict)
29+
returns: str = ""
30+
31+
32+
def parse_docstring(doc: str) -> DocInfo:
33+
"""Parses a docstring into documentation, arguments and return value.
34+
35+
Parsing is done according to the Google Python Style Guide.
36+
https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings
37+
38+
Example::
39+
40+
def keyword(arg) -> str:
41+
\"""Example keyword documentation.
42+
43+
Args:
44+
arg: Doc for the argument.
45+
46+
Returns:
47+
Argument in uppercase.
48+
49+
This continues the documentation.
50+
\"""
51+
return arg.upper()
52+
53+
It is assumed that the given `doc` is already normalized using `inspect.cleandoc`
54+
or otherwise.
55+
"""
56+
return DocStringParser().parse(doc)
57+
58+
59+
class SectionKind(Enum):
60+
ARGS = auto()
61+
RETURNS = auto()
62+
NONE = auto()
63+
64+
65+
class Section:
66+
args = ("args", "arguments", "parameters")
67+
returns = ("returns", "return", "yields")
68+
69+
def __init__(self, header: "str | None", body: "list[str]"):
70+
self.header = header
71+
self.body = body
72+
73+
@property
74+
def kind(self) -> SectionKind:
75+
if self.header is None:
76+
return SectionKind.NONE
77+
header = self.header.lower()
78+
if header in self.args:
79+
return SectionKind.ARGS
80+
if header in self.returns:
81+
return SectionKind.RETURNS
82+
return SectionKind.NONE
83+
84+
def dedent(self):
85+
self.body = textwrap.dedent("\n".join(self.body)).splitlines()
86+
87+
def accepts(self, line: str) -> bool:
88+
if not self.header:
89+
return True
90+
if not line or line[:2].isspace():
91+
return True
92+
return False
93+
94+
95+
class DocStringParser:
96+
header_re = re.compile(
97+
rf"({'|'.join(Section.args + Section.returns)}):(.*)?",
98+
re.IGNORECASE,
99+
)
100+
arg_re = re.compile(r"\s?(\S.*?):(\s.*)?")
101+
102+
def parse(self, doc: str) -> DocInfo:
103+
info = DocInfo()
104+
for section in self._parse_sections(doc):
105+
if section.header:
106+
section.dedent()
107+
if section.kind is SectionKind.ARGS:
108+
info.args.update(self._parse_args(section.body))
109+
elif section.kind is SectionKind.RETURNS:
110+
info.returns = self._parse_returns(section.body)
111+
else:
112+
info.doc += "\n".join(section.body) + "\n"
113+
info.doc = info.doc.strip()
114+
return info
115+
116+
def _parse_sections(self, doc: str) -> "Iterable[Section]":
117+
section = None
118+
can_start = True
119+
for line in doc.splitlines():
120+
match = self.header_re.fullmatch(line) if can_start else None
121+
if match:
122+
if section:
123+
yield section
124+
header, inline_text = match.groups()
125+
body = [inline_text.strip() if inline_text else ""]
126+
section = Section(header, body)
127+
elif section:
128+
if section.accepts(line):
129+
section.body.append(line)
130+
else:
131+
yield section
132+
section = Section(None, [line])
133+
else:
134+
section = Section(None, [line])
135+
can_start = not line.strip()
136+
if section:
137+
yield section
138+
139+
def _parse_args(self, lines: "list[str]") -> "Iterable[tuple[str, str]]":
140+
name, desc = "", []
141+
for line in lines:
142+
match = self.arg_re.fullmatch(line)
143+
if match:
144+
yield from self._yield_arg(name, desc)
145+
name, inline_text = match.groups()
146+
desc = [inline_text.strip() if inline_text else ""]
147+
else:
148+
desc.append(line)
149+
yield from self._yield_arg(name, desc)
150+
151+
def _yield_arg(self, name: str, desc: "list[str]") -> "Iterable[tuple[str, str]]":
152+
if (name or any(line.strip() for line in desc)) and name != "*":
153+
yield self._normalize_name(name), self._format(desc)
154+
155+
def _normalize_name(self, name: str) -> str:
156+
name = name.split("(")[0].strip()
157+
if name.startswith("*"):
158+
name = name.lstrip("*").strip()
159+
elif name.startswith(("${", "@{", "&{")) and name.endswith("}"):
160+
name = name[2:-1].strip()
161+
return name or "<no-name>"
162+
163+
def _format(self, lines: "list[str]") -> str:
164+
if not lines:
165+
return ""
166+
inline = lines[0]
167+
body = textwrap.dedent("\n".join(lines[1:]))
168+
if inline and body:
169+
result = inline + "\n" + body
170+
elif inline:
171+
result = inline
172+
else:
173+
result = body
174+
return result.rstrip()
175+
176+
def _parse_returns(self, lines: "list[str]") -> str:
177+
return self._format(lines)

‎utest/requirements.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ docutils >= 0.10
44
jsonschema
55
typing_extensions >= 4.13
66
markdown
7+
pyyaml

0 commit comments

Comments
 (0)