Skip to content

Commit 1ed496b

Browse files
authored
[FEATURE] Decorators for API docs (part 2) (great-expectations#6497)
1 parent 3126e27 commit 1ed496b

10 files changed

+844
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
docstring-parser==0.15
12
myst-parser
23
pydata-sphinx-theme==0.11.0 # Pinned to keep the css styling consistent.
34
sphinx~=4.5.0

docs/sphinx_api_docs_source/tasks.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def _exit_with_error_if_not_run_from_correct_dir(
3939
os.path.realpath(os.path.dirname(os.path.realpath(__file__)))
4040
)
4141
curdir = pathlib.Path(os.path.realpath(os.getcwd()))
42-
exit_message = f"The {task_name} task must be invoked from the same directory as the task.py file at the top of the repo."
42+
exit_message = f"The {task_name} task must be invoked from the same directory as the tasks.py file."
4343
if correct_dir != curdir:
4444
raise invoke.Exit(
4545
exit_message,
+246-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
from typing import Callable
1+
from textwrap import dedent
2+
from typing import Any, Callable, TypeVar
3+
4+
try:
5+
import docstring_parser
6+
except ImportError:
7+
docstring_parser = None
28

39
WHITELISTED_TAG = "--Public API--"
410

11+
F = TypeVar("F", bound=Callable[..., Any])
12+
513

614
def public_api(func) -> Callable:
715
"""Add the public API tag for processing by the auto documentation generator.
816
17+
Used as a decorator:
18+
19+
@public_api
20+
def my_method(some_argument):
21+
...
22+
923
This tag is added at import time.
1024
"""
1125

@@ -14,3 +28,234 @@ def public_api(func) -> Callable:
1428
func.__doc__ = WHITELISTED_TAG + existing_docstring
1529

1630
return func
31+
32+
33+
def deprecated_method(
34+
version: str,
35+
message: str = "",
36+
):
37+
"""Add a deprecation warning to the docstring of the decorated method.
38+
39+
Used as a decorator:
40+
41+
@deprecated_method(version="1.2.3", message="Optional message")
42+
def my_method(some_argument):
43+
...
44+
45+
Args:
46+
version: Version number when the method was deprecated.
47+
message: Optional deprecation message.
48+
"""
49+
50+
text = f".. deprecated:: {version}" "\n" f" {message}"
51+
52+
def wrapper(func: F) -> F:
53+
"""Wrapper method that accepts func, so we can modify the docstring."""
54+
return _add_text_to_function_docstring_after_summary(
55+
func=func,
56+
text=text,
57+
)
58+
59+
return wrapper
60+
61+
62+
def new_method(
63+
version: str,
64+
message: str = "",
65+
):
66+
"""Add a version added note to the docstring of the decorated method.
67+
68+
Used as a decorator:
69+
70+
@new_method(version="1.2.3", message="Optional message")
71+
def my_method(some_argument):
72+
...
73+
74+
Args:
75+
version: Version number when the method was added.
76+
message: Optional message.
77+
"""
78+
79+
text = f".. versionadded:: {version}" "\n" f" {message}"
80+
81+
def wrapper(func: F) -> F:
82+
"""Wrapper method that accepts func, so we can modify the docstring."""
83+
return _add_text_to_function_docstring_after_summary(
84+
func=func,
85+
text=text,
86+
)
87+
88+
return wrapper
89+
90+
91+
def deprecated_argument(
92+
argument_name: str,
93+
version: str,
94+
message: str = "",
95+
):
96+
"""Add an arg-specific deprecation warning to the docstring of the decorated method.
97+
98+
Used as a decorator:
99+
100+
@deprecated_argument(argument_name="some_argument", version="1.2.3", message="Optional message")
101+
def my_method(some_argument):
102+
...
103+
104+
If docstring_parser is not installed, this will not modify the docstring.
105+
106+
Args:
107+
argument_name: Name of the argument to associate with the deprecation note.
108+
version: Version number when the method was deprecated.
109+
message: Optional deprecation message.
110+
"""
111+
112+
text = f".. deprecated:: {version}" "\n" f" {message}"
113+
114+
def wrapper(func: F) -> F:
115+
"""Wrapper method that accepts func, so we can modify the docstring."""
116+
if not docstring_parser:
117+
return func
118+
119+
return _add_text_below_function_docstring_argument(
120+
func=func,
121+
argument_name=argument_name,
122+
text=text,
123+
)
124+
125+
return wrapper
126+
127+
128+
def new_argument(
129+
argument_name: str,
130+
version: str,
131+
message: str = "",
132+
):
133+
"""Add note for new arguments about which version the argument was added.
134+
135+
Used as a decorator:
136+
137+
@new_argument(argument_name="some_argument", version="1.2.3", message="Optional message")
138+
def my_method(some_argument):
139+
...
140+
141+
If docstring_parser is not installed, this will not modify the docstring.
142+
143+
Args:
144+
argument_name: Name of the argument to associate with the note.
145+
version: The version number to associate with the note.
146+
message: Optional message.
147+
"""
148+
149+
text = f".. versionadded:: {version}" "\n" f" {message}"
150+
151+
def wrapper(func: F) -> F:
152+
"""Wrapper method that accepts func, so we can modify the docstring."""
153+
if not docstring_parser:
154+
return func
155+
156+
return _add_text_below_function_docstring_argument(
157+
func=func,
158+
argument_name=argument_name,
159+
text=text,
160+
)
161+
162+
return wrapper
163+
164+
165+
def _add_text_to_function_docstring_after_summary(func: F, text: str) -> F:
166+
"""Insert text into docstring, e.g. rst directive.
167+
168+
Args:
169+
func: Add text to provided func docstring.
170+
text: String to add to the docstring, can be a rst directive e.g.:
171+
text = (
172+
".. versionadded:: 1.2.3\n"
173+
" Added in version 1.2.3\n"
174+
)
175+
176+
Returns:
177+
func with modified docstring.
178+
"""
179+
existing_docstring = func.__doc__ if func.__doc__ else ""
180+
split_docstring = existing_docstring.split("\n", 1)
181+
182+
docstring = ""
183+
if len(split_docstring) == 2:
184+
short_description, docstring = split_docstring
185+
docstring = (
186+
f"{short_description.strip()}\n"
187+
"\n"
188+
f"{text}\n"
189+
"\n"
190+
f"{dedent(docstring)}"
191+
)
192+
elif len(split_docstring) == 1:
193+
short_description = split_docstring[0]
194+
docstring = f"{short_description.strip()}\n" "\n" f"{text}\n"
195+
elif len(split_docstring) == 0:
196+
docstring = f"{text}\n"
197+
198+
func.__doc__ = docstring
199+
200+
return func
201+
202+
203+
def _add_text_below_function_docstring_argument(
204+
func: F,
205+
argument_name: str,
206+
text: str,
207+
) -> F:
208+
"""Add text below specified docstring argument.
209+
210+
Args:
211+
func: Function whose docstring will be modified.
212+
argument_name: Name of the argument to add text to its description.
213+
text: Text to add to the argument description.
214+
215+
Returns:
216+
func with modified docstring.
217+
"""
218+
existing_docstring = func.__doc__ if func.__doc__ else ""
219+
220+
func.__doc__ = _add_text_below_string_docstring_argument(
221+
docstring=existing_docstring, argument_name=argument_name, text=text
222+
)
223+
224+
return func
225+
226+
227+
def _add_text_below_string_docstring_argument(
228+
docstring: str, argument_name: str, text: str
229+
) -> str:
230+
"""Add text below an argument in a docstring.
231+
232+
Note: Can be used for rst directives.
233+
234+
Args:
235+
docstring: Docstring to modify.
236+
argument_name: Argument to place text below.
237+
text: Text to place below argument. Can be an rst directive.
238+
239+
Returns:
240+
Modified docstring.
241+
"""
242+
parsed_docstring = docstring_parser.parse(docstring)
243+
244+
if argument_name not in (param.arg_name for param in parsed_docstring.params):
245+
raise ValueError(
246+
f"Please specify an existing argument, you specified {argument_name}."
247+
)
248+
249+
for param in parsed_docstring.params:
250+
if param.arg_name == argument_name:
251+
if param.description is None:
252+
param.description = text
253+
else:
254+
param.description += "\n\n" + text + "\n\n"
255+
256+
# RenderingStyle.EXPANDED used to make sure any line breaks before and
257+
# after the added text are included (for Sphinx html rendering).
258+
return docstring_parser.compose(
259+
docstring=parsed_docstring,
260+
rendering_style=docstring_parser.RenderingStyle.EXPANDED,
261+
)

great_expectations/core/usage_statistics/package_dependencies.py

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class GXDependencies:
7171
"azure-storage-blob",
7272
"black",
7373
"boto3",
74+
"docstring-parser",
7475
"feather-format",
7576
"flake8",
7677
"flask",
@@ -204,6 +205,8 @@ class GXDependencies:
204205
"uszipcode",
205206
"yahoo_fin",
206207
"zipcodes",
208+
# requirements-dev-api-docs-test.txt
209+
"docstring-parser",
207210
]
208211

209212
GX_DEV_DEPENDENCIES: Set[str] = set(ALL_GX_DEV_DEPENDENCIES) - set(
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docstring-parser==0.15

reqs/requirements-dev-test.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
--requirement requirements-dev-lite.txt
22
--requirement requirements-dev-contrib.txt
3+
--requirement requirements-dev-api-docs-test.txt

requirements-dev.txt

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
--requirement requirements.txt
2-
--requirement reqs/requirements-dev-lite.txt
3-
--requirement reqs/requirements-dev-contrib.txt
2+
--requirement reqs/requirements-dev-test.txt
43
--requirement reqs/requirements-dev-sqlalchemy.txt
54

65
--requirement reqs/requirements-dev-arrow.txt

tasks.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def _exit_with_error_if_not_in_repo_root(task_name: str):
360360
"""Exit if the command was not run from the repository root."""
361361
filedir = os.path.realpath(os.path.dirname(os.path.realpath(__file__)))
362362
curdir = os.path.realpath(os.getcwd())
363-
exit_message = f"The {task_name} task must be invoked from the same directory as the task.py file at the top of the repo."
363+
exit_message = f"The {task_name} task must be invoked from the same directory as the tasks.py file at the top of the repo."
364364
if filedir != curdir:
365365
raise invoke.Exit(
366366
exit_message,

0 commit comments

Comments
 (0)