-
Notifications
You must be signed in to change notification settings - Fork 120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Python auto doc #665
Python auto doc #665
Changes from all commits
fc1e9a0
14baa55
9e97798
2f36394
72ab01d
99f0c30
ce63a83
50ab9f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Python Bindings | ||
|
||
## Autogenerated Doc-Strings | ||
|
||
Doc-strings for the python API wrapper are generated by gen_docs.py | ||
The script is run automatically during the build to keep python | ||
doc strings fully up-to-date with c++ sources. | ||
|
||
It scrapes documentation comments and c++ function signatures | ||
from the manifold c++ sources, in order to generate a c++ header file | ||
exposing the comments as string variables named by function names. | ||
This allows python bindings to re-use the c++ comments directly. | ||
|
||
Some snake-casing of params is applied for python use case. | ||
|
||
--- | ||
|
||
When modifying the Manifold C++ sources, you may need to update | ||
gen_docs.py. For example, top-level free functions are white-listed, | ||
so if you add a new one, you will need to add it in gen_docs.py. | ||
|
||
Similarly, the list of source files to parse is also white listed, | ||
so if you define functions in new files that need python wrappers, | ||
you will also need to up gen_docs.py. | ||
|
||
To verify that python docs are correct after changes, you can | ||
run the following commends from the manifold repo root: | ||
``` | ||
pip install . | ||
python -c 'import manifold3d; help(manifold3d)' | ||
``` | ||
|
||
Alternateively you could generate stubs with roughly the same info | ||
``` | ||
pip install nanobind-stubgen | ||
pip install . | ||
nanobind-stubgen manifold3d | ||
``` | ||
It will emit some warnings and write a file `manifold3d.pyi` | ||
which will show all the function signatures and docstrings. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
from os.path import dirname | ||
from hashlib import md5 | ||
import re | ||
|
||
base = dirname(dirname(dirname(__file__))) | ||
|
||
def snake_case(name): | ||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() | ||
|
||
|
||
def python_param_modifier(comment): | ||
# p = f":{snake_case(m[0][1:])}:" | ||
comment = re.sub(r"@(param \w+)", lambda m: f':{snake_case(m[1])}:', comment) | ||
# python API renames `MeshGL` to `Mesh` | ||
comment = re.sub('mesh_gl', 'mesh', comment) | ||
comment = re.sub('MeshGL', 'Mesh', comment) | ||
return comment | ||
|
||
|
||
def method_key(name): | ||
name = re.sub("\+", "_plus", name) | ||
name = re.sub("\-", "_minus", name) | ||
name = re.sub("\^", "_xor", name) | ||
name = re.sub("\=", "_eq", name) | ||
name = re.sub("\:", "_", name) | ||
name = re.sub("\~", "destroy_", name) | ||
return name | ||
|
||
|
||
parens_re = re.compile(r"[^(]+\(([^(]*(\(.*\))*[^(]*\))", flags=re.DOTALL) | ||
args_re = re.compile( | ||
r"^[^,^\(^\)]*(\(.*\))*[^,^\(^\)]*[\s\&\*]([0-9\w]+)\s*[,\)]", flags=re.DOTALL | ||
) | ||
|
||
|
||
def parse_args(s): | ||
par = parens_re.match(s) | ||
if not par: | ||
return None | ||
out = [] | ||
arg_str = par[1] | ||
while m := re.search(args_re, arg_str): | ||
out += [snake_case(m[2])] | ||
arg_str = arg_str[m.span()[1] :] | ||
return out | ||
|
||
|
||
def collect(fname, matcher, param_modifier=python_param_modifier): | ||
comment = "" | ||
with open(fname) as f: | ||
for line in f: | ||
line = line.lstrip() | ||
if line.startswith("/**"): | ||
comment = "" | ||
elif line.startswith("*/"): | ||
pass | ||
elif line.startswith("*") and comment is not None: | ||
comment += line[1:].lstrip() | ||
elif comment and (m := matcher(line)): | ||
while (args := parse_args(line)) is None: | ||
line += next(f) | ||
if len(line) > 500: | ||
break | ||
|
||
method = method_key(snake_case(m[1])) | ||
# comment = re.sub(param_re, param_modifier, comment) | ||
comment = param_modifier(comment) | ||
method = "__".join([method, *args]) | ||
assert method not in comments | ||
comments[method] = comment | ||
comment = "" | ||
|
||
|
||
comments = {} | ||
|
||
method_re = re.compile(r"(\w+::[\w\-\+\^\=\:]+)\(") | ||
function_re = re.compile(r"([\w\-\+\^\=\:]+)\(") | ||
|
||
|
||
# we don't handle inline functions in classes properly | ||
# so instead just white-list functions we want | ||
def select_functions(s): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add something in a README somewhere that when a new non-member function is added to the API it needs to also be added to this list? |
||
m = function_re.search(s) | ||
if m and "Triangulate" in m[0]: | ||
return m | ||
if m and "Circular" in m[0]: | ||
return m | ||
return None | ||
|
||
|
||
collect(f"{base}/src/manifold/src/manifold.cpp", lambda s: method_re.search(s)) | ||
collect(f"{base}/src/manifold/src/constructors.cpp", lambda s: method_re.search(s)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps also add to a README that when new files are added with API documentation, they need to also be added as a |
||
collect( | ||
f"{base}/src/cross_section/src/cross_section.cpp", lambda s: method_re.search(s) | ||
) | ||
collect(f"{base}/src/polygon/src/polygon.cpp", select_functions) | ||
collect(f"{base}/src/utilities/include/public.h", select_functions) | ||
|
||
comments = dict(sorted(comments.items())) | ||
|
||
gen_h = f"autogen_docstrings.inl" | ||
with open(gen_h, "w") as f: | ||
f.write("#pragma once\n\n") | ||
f.write("// --- AUTO GENERATED ---\n") | ||
f.write("// gen_docs.py is run by cmake build\n\n") | ||
f.write("namespace manifold_docstrings {\n") | ||
for key, doc in comments.items(): | ||
f.write(f'const char* {key} = R"___({doc.strip()})___";\n') | ||
f.write("} // namespace manifold_docs") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might need to add a substitution of
MeshGL
tomesh
, since several docs refer to this data structure by its C++ name.