Skip to content

Commit 5e2e9f5

Browse files
authored
Refactor docs (#338)
* First step for new docs structure * Make docstrings start with a oneliner * doc enums and flags * structs and better looking flags and enums * add file * auto cross references * make use of auto cross references * Better cross ref resolving. Adjust docstrings. * refactor GUI docs * refactor utils docs * drop walrus operator * Struct docs has links * Also show default value in structs * fmt * fix canvas args passing * dont run the new example * Move some docs into better places, and write more guide * instructions for Lavapipe * Add note on Lavapipe * sphinx stfu * better
1 parent 60c6d96 commit 5e2e9f5

26 files changed

+1383
-815
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ instance/
7979

8080
# Sphinx documentation
8181
docs/_build/
82+
docs/generated
8283

8384
# PyBuilder
8485
target/

codegen/apiwriter.py

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,42 @@
22
Writes the parts of the API that are simple: flags, enums, structs.
33
"""
44

5+
import re
6+
57
from codegen.utils import print, blacken, to_snake_case
68
from codegen.idlparser import get_idl_parser
79
from codegen.files import file_cache
810

911

12+
ref_pattern = re.compile(r"\W((GPU|flags\.|enums\.|structs\.)\w+?)\W", re.MULTILINE)
13+
14+
15+
def resolve_crossrefs(text):
16+
# Similar code as in docs/conf.py
17+
text += " "
18+
i2 = 0
19+
while True:
20+
m = ref_pattern.search(text, i2)
21+
if not m:
22+
break
23+
i1, i2 = m.start(1), m.end(1)
24+
prefix = m.group(2)
25+
ref_indicator = ":obj:" if prefix.lower() == prefix else ":class:"
26+
name = m.group(1)
27+
if name.startswith("structs."):
28+
link = name.split(".")[1]
29+
else:
30+
link = "wgpu." + name
31+
insertion = f"{ref_indicator}`{name} <{link}>`"
32+
text = text[:i1] + insertion + text[i2:]
33+
i2 += len(insertion) - len(name)
34+
return text.rstrip()
35+
36+
1037
flags_preamble = '''
1138
"""
12-
All wgpu flags. Also available in the root wgpu namespace.
39+
Flags are bitmasks; zero or multiple fields can be set at the same time.
40+
These flags are also available in the root wgpu namespace.
1341
"""
1442
1543
# THIS CODE IS AUTOGENERATED - DO NOT EDIT
@@ -27,9 +55,9 @@ def __iter__(self):
2755
return iter([key for key in dir(self) if not key.startswith("_")])
2856
2957
def __repr__(self):
30-
options = ", ".join(self)
3158
if _use_sphinx_repr: # no-cover
32-
return options
59+
return ""
60+
options = ", ".join(self)
3361
return f"<{self.__class__.__name__} {self._name}: {options}>"
3462
3563
'''.lstrip()
@@ -38,14 +66,18 @@ def __repr__(self):
3866
def write_flags():
3967
idl = get_idl_parser()
4068
n = len(idl.flags)
41-
# Generate code
69+
# Preamble
4270
pylines = [flags_preamble]
4371
pylines.append(f"# There are {n} flags\n")
4472
for name, d in idl.flags.items():
73+
# Object-docstring as a comment
74+
for key, val in d.items():
75+
pylines.append(f'#: * "{key}" ({val})')
76+
# Generate Code
4577
pylines.append(f'{name} = Flags(\n "{name}",')
4678
for key, val in d.items():
4779
pylines.append(f" {key}={val!r},")
48-
pylines.append(") #:\n")
80+
pylines.append(")\n")
4981
# Write
5082
code = blacken("\n".join(pylines))
5183
file_cache.write("flags.py", code)
@@ -54,7 +86,8 @@ def write_flags():
5486

5587
enums_preamble = '''
5688
"""
57-
All wgpu enums. Also available in the root wgpu namespace.
89+
Enums are choices; exactly one field must be selected.
90+
These enums are also available in the root wgpu namespace.
5891
"""
5992
6093
# THIS CODE IS AUTOGENERATED - DO NOT EDIT
@@ -74,9 +107,9 @@ def __iter__(self):
74107
)
75108
76109
def __repr__(self):
77-
options = ", ".join(f"'{x}'" for x in self)
78110
if _use_sphinx_repr: # no-cover
79-
return options
111+
return ""
112+
options = ", ".join(f"'{x}'" for x in self)
80113
return f"<{self.__class__.__name__} {self._name}: {options}>"
81114
82115
'''.lstrip()
@@ -85,14 +118,18 @@ def __repr__(self):
85118
def write_enums():
86119
idl = get_idl_parser()
87120
n = len(idl.enums)
88-
# Generate code
121+
# Preamble
89122
pylines = [enums_preamble]
90123
pylines.append(f"# There are {n} enums\n")
91124
for name, d in idl.enums.items():
125+
# Object-docstring as a comment
126+
for key, val in d.items():
127+
pylines.append(f'#: * "{key}"')
128+
# Generate Code
92129
pylines.append(f'{name} = Enum(\n "{name}",')
93130
for key, val in d.items():
94131
pylines.append(f' {key}="{val}",')
95-
pylines.append(") #:\n") # That #: is for Sphinx
132+
pylines.append(")\n")
96133
# Write
97134
code = blacken("\n".join(pylines))
98135
file_cache.write("enums.py", code)
@@ -101,7 +138,8 @@ def write_enums():
101138

102139
structs_preamble = '''
103140
"""
104-
All wgpu structs.
141+
The sructs in wgpu-py are represented as Python dictionaries.
142+
Fields that have default values (as indicated below) may be omitted.
105143
"""
106144
107145
# THIS CODE IS AUTOGENERATED - DO NOT EDIT
@@ -121,9 +159,9 @@ def __iter__(self):
121159
)
122160
123161
def __repr__(self):
124-
options = ", ".join(f"'{x}'" for x in self)
125162
if _use_sphinx_repr: # no-cover
126-
return options
163+
return ""
164+
options = ", ".join(f"'{x}'" for x in self)
127165
return f"<{self.__class__.__name__} {self._name}: {options}>"
128166
129167
'''.lstrip()
@@ -133,20 +171,30 @@ def write_structs():
133171
ignore = ["ImageCopyTextureTagged"]
134172
idl = get_idl_parser()
135173
n = len(idl.structs)
136-
# Generate code
174+
# Preamble
137175
pylines = [structs_preamble]
138176
pylines.append(f"# There are {n} structs\n")
139177
for name, d in idl.structs.items():
140178
if name in ignore:
141179
continue
180+
# Object-docstring as a comment
181+
for field in d.values():
182+
tp = idl.resolve_type(field.typename).strip("'")
183+
if field.default is not None:
184+
pylines.append(
185+
resolve_crossrefs(f"#: * {field.name} :: {tp} = {field.default}")
186+
)
187+
else:
188+
pylines.append(resolve_crossrefs(f"#: * {field.name} :: {tp}"))
189+
# Generate Code
142190
pylines.append(f'{name} = Struct(\n "{name}",')
143191
for field in d.values():
144192
key = to_snake_case(field.name)
145193
val = idl.resolve_type(field.typename)
146194
if not val.startswith(("'", '"')):
147195
val = f"'{val}'"
148196
pylines.append(f" {key}={val},")
149-
pylines.append(") #:\n") # That #: is for Sphinx
197+
pylines.append(")\n")
150198

151199
# Write
152200
code = blacken("\n".join(pylines))

docs/_templates/wgpu_class_layout.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{ fullname | escape | underline}}
2+
3+
.. currentmodule:: {{ module }}
4+
5+
.. autoclass:: {{ objname }}
6+
:members:
7+
:show-inheritance:

docs/conf.py

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
# add these directories to sys.path here. If the directory is relative to the
1111
# documentation root, use os.path.abspath to make it absolute, like shown here.
1212

13+
import re
1314
import os
1415
import sys
16+
import shutil
1517

1618
ROOT_DIR = os.path.abspath(os.path.join(__file__, "..", ".."))
1719
sys.path.insert(0, ROOT_DIR)
@@ -22,57 +24,105 @@
2224
import wgpu.gui # noqa: E402
2325

2426

25-
# -- Tweak wgpu's docs -------------------------------------------------------
27+
# -- Tests -------------------------------------------------------------------
2628

27-
# Ensure that all classes are in the docs
28-
with open(os.path.join(ROOT_DIR, "docs", "reference_classes.rst"), "rb") as f:
29-
classes_text = f.read().decode()
30-
for cls_name in wgpu.base.__all__:
31-
expected = f".. autoclass:: {cls_name}"
32-
assert (
33-
expected in classes_text
34-
), f"Missing doc entry {cls_name} in reference_classes.rst"
35-
36-
# Ensure that all classes are references in the alphabetic list, and referenced at least one other time
37-
with open(os.path.join(ROOT_DIR, "docs", "reference_wgpu.rst"), "rb") as f:
29+
# Ensure that all classes are references in the alphabetic list,
30+
# and referenced at least one other time as part of the explanatory text.
31+
with open(os.path.join(ROOT_DIR, "docs", "wgpu.rst"), "rb") as f:
3832
wgpu_text = f.read().decode()
33+
wgpu_lines = [line.strip() for line in wgpu_text.splitlines()]
3934
for cls_name in wgpu.base.__all__:
40-
expected1 = f":class:`{cls_name}`"
41-
expected2 = f"* :class:`{cls_name}`"
42-
assert expected2 in wgpu_text, f"Missing doc entry {cls_name} in reference_wgpu.rst"
4335
assert (
44-
wgpu_text.count(expected1) >= 2
45-
), f"Need at least one reference to {cls_name} in reference_wgpu.rst"
36+
f"~{cls_name}" in wgpu_lines
37+
), f"Class {cls_name} not listed in class list in wgpu.rst"
38+
assert (
39+
f":class:`{cls_name}`" in wgpu_text
40+
), f"Class {cls_name} not referenced in the text in wgpu.rst"
41+
4642

47-
# Make flags and enum appear better in docs
43+
# -- Hacks to tweak docstrings -----------------------------------------------
44+
45+
# Make flags and enums appear better in docs
4846
wgpu.enums._use_sphinx_repr = True
4947
wgpu.flags._use_sphinx_repr = True
50-
51-
# Also tweak docstrings of classes and their methods
52-
for cls_name, cls in wgpu.base.__dict__.items():
53-
if cls_name not in wgpu.base.__all__:
54-
continue
55-
56-
# Change class docstring to include a link to the base class,
57-
# and the class' signature is not shown
58-
base_info = ""
59-
base_classes = [f":class:`.{c.__name__}`" for c in cls.mro()[1:-1]]
60-
if base_classes:
61-
base_info = f" *Subclass of* {', '.join(base_classes)}\n\n"
62-
cls.__doc__ = cls.__name__ + "()\n\n" + base_info + " " + cls.__doc__.lstrip()
63-
# Change docstring of methods that dont have positional arguments
64-
for method in cls.__dict__.values():
65-
if not (callable(method) and hasattr(method, "__code__")):
66-
continue
67-
if method.__code__.co_argcount == 1 and method.__code__.co_kwonlyargcount > 0:
68-
sig = method.__name__ + "(**parameters)"
69-
method.__doc__ = sig + "\n\n " + method.__doc__.lstrip()
48+
wgpu.structs._use_sphinx_repr = True
49+
50+
# Build regular expressions to resolve crossrefs
51+
func_ref_pattern = re.compile(r"\ (`\w+?\(\)`)", re.MULTILINE)
52+
ob_ref_pattern = re.compile(
53+
r"\ (`(GPU|gui\.Wgpu|flags\.|enums\.|structs\.)\w+?`)", re.MULTILINE
54+
)
55+
argtype_ref_pattern = re.compile(
56+
r"\(((GPU|gui\.Wgpu|flags\.|enums\.|structs\.)\w+?)\)", re.MULTILINE
57+
)
58+
59+
60+
def resolve_crossrefs(text):
61+
text = (text or "").lstrip()
62+
63+
# Turn references to functions into a crossref.
64+
# E.g. `Foo.bar()`
65+
i2 = 0
66+
while True:
67+
m = func_ref_pattern.search(text, i2)
68+
if not m:
69+
break
70+
i1, i2 = m.start(1), m.end(1)
71+
ref_indicator = ":func:"
72+
text = text[:i1] + ref_indicator + text[i1:]
73+
74+
# Turn references to objects (classes, flags, enums, and structs) into a crossref.
75+
# E.g. `GPUDevice` or `flags.BufferUsage`
76+
i2 = 0
77+
while True:
78+
m = ob_ref_pattern.search(text, i2)
79+
if not m:
80+
break
81+
i1, i2 = m.start(1), m.end(1)
82+
prefix = m.group(2) # e.g. GPU or flags.
83+
ref_indicator = ":obj:" if prefix.lower() == prefix else ":class:"
84+
text = text[:i1] + ref_indicator + text[i1:]
85+
86+
# Turn function arg types into a crossref.
87+
# E.g. (GPUDevice) or (flags.BufferUsage)
88+
i2 = 0
89+
while True:
90+
m = argtype_ref_pattern.search(text)
91+
if not m:
92+
break
93+
i1, i2 = m.start(1), m.end(1)
94+
ref_indicator = ":obj:"
95+
text = text[:i1] + ref_indicator + "`" + text[i1:i2] + "`" + text[i2:]
96+
97+
return text
98+
99+
100+
# Tweak docstrings of classes and their methods
101+
for module, hide_class_signature in [(wgpu.base, True), (wgpu.gui, False)]:
102+
for cls_name in module.__all__:
103+
cls = getattr(module, cls_name)
104+
# Class docstring
105+
docs = resolve_crossrefs(cls.__doc__)
106+
if hide_class_signature:
107+
docs = cls.__name__ + "()\n\n " + docs
108+
cls.__doc__ = docs or None
109+
# Docstring of methods
110+
for method in cls.__dict__.values():
111+
if callable(method) and hasattr(method, "__code__"):
112+
docs = resolve_crossrefs(method.__doc__)
113+
if (
114+
method.__code__.co_argcount == 1
115+
and method.__code__.co_kwonlyargcount > 0
116+
):
117+
sig = method.__name__ + "(**parameters)"
118+
docs = sig + "\n\n " + docs
119+
method.__doc__ = docs or None
70120

71121

72122
# -- Project information -----------------------------------------------------
73123

74124
project = "wgpu-py"
75-
copyright = "2020-2022, Almar Klein, Korijn van Golen"
125+
copyright = "2020-2023, Almar Klein, Korijn van Golen"
76126
author = "Almar Klein, Korijn van Golen"
77127
release = wgpu.__version__
78128

@@ -85,11 +135,15 @@
85135
extensions = [
86136
"sphinx.ext.autodoc",
87137
"sphinx.ext.napoleon",
138+
"sphinx.ext.autosummary",
88139
]
89140

90141
# Add any paths that contain templates here, relative to this directory.
91142
templates_path = ["_templates"]
92143

144+
# Just let autosummary produce a new version each time
145+
shutil.rmtree(os.path.join(os.path.dirname(__file__), "generated"), True)
146+
93147
# List of patterns, relative to source directory, that match files and
94148
# directories to ignore when looking for source files.
95149
# This pattern also affects html_static_path and html_extra_path.
@@ -102,8 +156,9 @@
102156

103157
# The theme to use for HTML and HTML Help pages. See the documentation for
104158
# a list of builtin themes.
105-
#
106-
# html_theme = "sphinx_rtd_theme"
159+
160+
if not (os.getenv("READTHEDOCS") or os.getenv("CI")):
161+
html_theme = "sphinx_rtd_theme"
107162

108163
# Add any paths that contain custom static files (such as style sheets) here,
109164
# relative to this directory. They are copied after the builtin static files,

0 commit comments

Comments
 (0)