From e81dc6197862b74706817c9247b27b551e43aef3 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 11:23:10 -0700 Subject: [PATCH 01/18] Working on modifications for using Google-style docstrings for Python. --- pybind11_mkdoc/mkdoc_lib.py | 152 +++++++++++++++++++-- tests/sample_header_docs/sample_header_2.h | 23 ++++ 2 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 tests/sample_header_docs/sample_header_2.h diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index c34c82a..ee87886 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -107,6 +107,9 @@ def sanitize_name(name): name = re.sub("_$", "", re.sub("_+", "_", name)) return "mkd_doc_" + name +param_re = re.compile(r"[\\@]param\s+([\w:]+)\s*(.*)") +t_param_re = re.compile(r"[\\@]tparam\s+([\w:]+)\s*(.*)") +return_re = re.compile(rf"[\\@]returns?\s+(.*)") def process_comment(comment): result = "" @@ -135,7 +138,6 @@ def process_comment(comment): # Doxygen tags cpp_group = r"([^\s]+)" - param_group = r"([\[\w:,\]]+)" s = result s = re.sub(rf"[\\@][cp]\s+{cpp_group}", r"``\1``", s) @@ -144,15 +146,56 @@ def process_comment(comment): s = re.sub(rf"[\\@]em\s+{cpp_group}", r"*\1*", s) s = re.sub(rf"[\\@]b\s+{cpp_group}", r"**\1**", s) s = re.sub(rf"[\\@]ingroup\s+{cpp_group}", r"", s) - s = re.sub(rf"[\\@]param{param_group}?\s+{cpp_group}", r"\n\n$Parameter ``\2``:\n\n", s) - s = re.sub(rf"[\\@]tparam{param_group}?\s+{cpp_group}", r"\n\n$Template parameter ``\2``:\n\n", s) + + # Add arguments and return type + lines = s.splitlines() + rm_lines = [] + params = {} + t_params = {} + ret = [] + for k,line in enumerate(lines): + if m := param_re.match(line): + name,text = m.groups() + params[name] = text.strip() + rm_lines.append(k) + elif m := t_param_re.match(line): + name,text = m.groups() + t_params[name] = text.strip() + rm_lines.append(k) + elif m := return_re.match(line): + text = m.groups()[0] + ret.append(text.strip()) + rm_lines.append(k) + rm_lines.append(k) + + # If we had any hits, then remove the old lines, fill with the new lines, and convert back to s + if rm_lines: + rm_lines.sort(reverse=True) + for k in rm_lines: + lines.pop(k) + + new_lines = [] + if params: + new_lines.append("Args:") + new_lines += [f" {name}: {text}" for name,text in params.items()] + new_lines.append("") + if t_params: + new_lines.append("Template Args:") + new_lines += [f" {name}: {text}" for name,text in t_params.items()] + new_lines.append("") + if ret: + new_lines.append("Returns:") + new_lines += [f" {text}" for text in ret] + new_lines.append("") + + idx = rm_lines[-1] + lines = lines[0:idx] + new_lines + lines[idx:] + s = "\n".join(lines) # Remove class and struct tags s = re.sub(r"[\\@](class|struct)\s+.*", "", s) for in_, out_ in { - "returns": "Returns", - "return": "Returns", "authors": "Authors", "author": "Author", "copyright": "Copyright", @@ -214,15 +257,98 @@ def process_comment(comment): elif in_code_segment: result += x.strip() else: - for y in re.split(r"(?: *\n *){2,}", x): - wrapped = wrapper.fill(re.sub(r"\s+", " ", y).strip()) - if len(wrapped) > 0 and wrapped[0] == "$": - result += wrapped[1:] + "\n" - wrapper.initial_indent = wrapper.subsequent_indent = " " * 4 + wrapped = [] + + paragraph = [] + + def get_prefix_and_indent(line) -> tuple[str|None, str]: + indent = len(line) - len(line.lstrip()) + indent_str = " " * indent + m = re.match( + rf"{indent_str}(" + r"(?:[*\-•]\s)|(?:\(?\d+[\.)]\s)|(?:\w+:)" + r")", + line + ) + if m: + g = m.group(0) + return g, indent_str + " " * len(g) + else: + return None, indent_str + + def flush_paragraph(): + if not paragraph: + return + + # Detect bullet/number from first line + first_line = paragraph[0] + prefix, indent_str = get_prefix_and_indent(first_line) + + # Combine paragraph into single string (replace internal line breaks with space) + para_text = " ".join(line.strip() for line in paragraph) + + if prefix: + content = para_text[len(prefix.strip()):] + wrapper.initial_indent=prefix + wrapper.subsequent_indent=indent_str + if content == "": + # This paragraph is just the prefix + wrapped.append(prefix) + paragraph.clear() + return else: - if len(wrapped) > 0: - result += wrapped + "\n\n" - wrapper.initial_indent = wrapper.subsequent_indent = "" + content = para_text.lstrip() + wrapper.initial_indent=indent_str + wrapper.subsequent_indent=indent_str + + wrapped.append(wrapper.fill(content)) + paragraph.clear() + + current_prefix = None + current_indent = "" + for line in lines: + if not line.strip(): + flush_paragraph() + wrapped.append(line) # preserve blank lines + continue + + prefix,indent = get_prefix_and_indent(line) + if paragraph and ((indent != current_indent) or (prefix and prefix != current_prefix)): + # Prefix/indent changed → start new paragraph + flush_paragraph() + + paragraph.append(line) + current_prefix = prefix + current_indent = indent + + flush_paragraph() + result += "\n".join(wrapped) + # wrapped_lines = [] + #for line in x.splitlines(): + # if not line.strip(): + # wrapped_lines.append(line) + # continue + + # # Detect indentation + # indent = len(line) - len(line.lstrip()) + # indent_str = " " * indent + + # # Detect bullet or numbered list prefix + # # Examples matched: "* ", "- ", "• ", "1. ", "2) ", "(3) " + # m = re.match(rf"{indent_str}((?:[\*\-•]\s)|(?:\(?\d+[\.\)]\s))", line) + # if m: + # prefix = indent_str + m.group(1) + # content = line[len(prefix):] + # wrapper.initial_indent=prefix + # wrapper.subsequent_indent=" " * len(prefix) + # else: + # content = line.lstrip() + # wrapper.initial_indent=indent_str + # wrapper.subsequent_indent=indent_str + + # wrapped_lines.append(wrapper.fill(content)) + + #result += "\n".join(wrapped_lines) return result.rstrip().lstrip("\n") diff --git a/tests/sample_header_docs/sample_header_2.h b/tests/sample_header_docs/sample_header_2.h new file mode 100644 index 0000000..a23bae9 --- /dev/null +++ b/tests/sample_header_docs/sample_header_2.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +/** + * @class Base + * @brief A simple base class. + */ +class Base { + public: + /** + * @brief Description for method1. + * + * This is the extended description for method1. + * + * @param p1 I am the first parameter. + * @param p2 I am the second parameter. + * @return An integer is what I return. + */ + int method1(std::vector p1, std::map p2); +}; From 2a341016feea5b085f5bed1ffeb4598b7cc90818 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 12:44:18 -0700 Subject: [PATCH 02/18] Adding logic for parsing exceptions for google doc style. --- pybind11_mkdoc/mkdoc_lib.py | 44 ++++++---------------- tests/sample_header_docs/sample_header_2.h | 2 + 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index ee87886..b87461f 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -110,6 +110,7 @@ def sanitize_name(name): param_re = re.compile(r"[\\@]param\s+([\w:]+)\s*(.*)") t_param_re = re.compile(r"[\\@]tparam\s+([\w:]+)\s*(.*)") return_re = re.compile(rf"[\\@]returns?\s+(.*)") +raises_re = re.compile(rf"[\\@](?:exception|throws?)\s+([\w:]+)(.*)") def process_comment(comment): result = "" @@ -147,11 +148,12 @@ def process_comment(comment): s = re.sub(rf"[\\@]b\s+{cpp_group}", r"**\1**", s) s = re.sub(rf"[\\@]ingroup\s+{cpp_group}", r"", s) - # Add arguments and return type + # Add arguments, return type, and exceptions lines = s.splitlines() rm_lines = [] params = {} t_params = {} + raises = {} ret = [] for k,line in enumerate(lines): if m := param_re.match(line): @@ -167,6 +169,10 @@ def process_comment(comment): ret.append(text.strip()) rm_lines.append(k) rm_lines.append(k) + elif m := raises_re.match(line): + name,text = m.groups() + raises[name] = text.strip() + rm_lines.append(k) # If we had any hits, then remove the old lines, fill with the new lines, and convert back to s if rm_lines: @@ -187,6 +193,10 @@ def process_comment(comment): new_lines.append("Returns:") new_lines += [f" {text}" for text in ret] new_lines.append("") + if raises: + new_lines.append("Raises:") + new_lines += [f" {name}: {text}" for name,text in raises.items()] + new_lines.append("") idx = rm_lines[-1] lines = lines[0:idx] + new_lines + lines[idx:] @@ -204,9 +214,6 @@ def process_comment(comment): "sa": "See also", "see": "See also", "extends": "Extends", - "exception": "Throws", - "throws": "Throws", - "throw": "Throws", }.items(): s = re.sub(rf"[\\@]{in_}\s*", rf"\n\n${out_}:\n\n", s) @@ -258,7 +265,6 @@ def process_comment(comment): result += x.strip() else: wrapped = [] - paragraph = [] def get_prefix_and_indent(line) -> tuple[str|None, str]: @@ -306,7 +312,7 @@ def flush_paragraph(): current_prefix = None current_indent = "" - for line in lines: + for line in x.splitlines(): if not line.strip(): flush_paragraph() wrapped.append(line) # preserve blank lines @@ -323,32 +329,6 @@ def flush_paragraph(): flush_paragraph() result += "\n".join(wrapped) - # wrapped_lines = [] - #for line in x.splitlines(): - # if not line.strip(): - # wrapped_lines.append(line) - # continue - - # # Detect indentation - # indent = len(line) - len(line.lstrip()) - # indent_str = " " * indent - - # # Detect bullet or numbered list prefix - # # Examples matched: "* ", "- ", "• ", "1. ", "2) ", "(3) " - # m = re.match(rf"{indent_str}((?:[\*\-•]\s)|(?:\(?\d+[\.\)]\s))", line) - # if m: - # prefix = indent_str + m.group(1) - # content = line[len(prefix):] - # wrapper.initial_indent=prefix - # wrapper.subsequent_indent=" " * len(prefix) - # else: - # content = line.lstrip() - # wrapper.initial_indent=indent_str - # wrapper.subsequent_indent=indent_str - - # wrapped_lines.append(wrapper.fill(content)) - - #result += "\n".join(wrapped_lines) return result.rstrip().lstrip("\n") diff --git a/tests/sample_header_docs/sample_header_2.h b/tests/sample_header_docs/sample_header_2.h index a23bae9..b2de942 100644 --- a/tests/sample_header_docs/sample_header_2.h +++ b/tests/sample_header_docs/sample_header_2.h @@ -18,6 +18,8 @@ class Base { * @param p1 I am the first parameter. * @param p2 I am the second parameter. * @return An integer is what I return. + * + * @throws runtime_error Throws runtime error if p1 is empty. */ int method1(std::vector p1, std::map p2); }; From e5a285d6a7bbf39ee9a921923aa932f66b6ae62c Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 12:48:46 -0700 Subject: [PATCH 03/18] Updating tests due to new parsing format. --- tests/cli_test.py | 15 ++++++++------- tests/long_parameter_test.py | 6 +++--- tests/sample_header_test.py | 15 ++++++++------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/cli_test.py b/tests/cli_test.py index 6829cd5..810cfe7 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -36,13 +36,14 @@ vestibulum.)doc"; static const char *mkd_doc_drake_MidLevelSymbol = -R"doc(1. Begin first ordered list element. Rutrum quisque non tellus orci ac -auctor. End first ordered list element. 2. Begin second ordered list -element. Ipsum faucibus vitae aliquet nec. Ligula ullamcorper -malesuada proin libero. End second ordered list element. 3. Begin -third ordered list element. Dictum sit amet justo donec enim. Pharetra -convallis posuere morbi leo urna molestie. End third ordered list -element. +R"doc(1. Begin first ordered list element. Rutrum quisque non tellus orci + ac auctor. End first ordered list element. +2. Begin second ordered list element. Ipsum faucibus vitae aliquet + nec. Ligula ullamcorper malesuada proin libero. End second ordered + list element. +3. Begin third ordered list element. Dictum sit amet justo donec + enim. Pharetra convallis posuere morbi leo urna molestie. End third + ordered list element. Senectus et netus et malesuada fames ac. Tincidunt lobortis feugiat vivamus at augue eget arcu dictum varius.)doc"; diff --git a/tests/long_parameter_test.py b/tests/long_parameter_test.py index 5b93992..71ad255 100644 --- a/tests/long_parameter_test.py +++ b/tests/long_parameter_test.py @@ -12,9 +12,9 @@ def test_long_parameter(capsys): res = capsys.readouterr() expected = """\ -Parameter ``x``: - - Begin first parameter description. Senectus et netus et - malesuada fames ac. End first parameter description.)doc"; +Args: + x: - Begin first parameter description. Senectus et netus et + malesuada fames ac. End first parameter description.)doc"; """ assert expected in res.out diff --git a/tests/sample_header_test.py b/tests/sample_header_test.py index 5b58d6e..6eb3d40 100644 --- a/tests/sample_header_test.py +++ b/tests/sample_header_test.py @@ -49,13 +49,14 @@ def test_generate_headers(capsys, tmp_path): vestibulum.)doc"; static const char *mkd_doc_drake_MidLevelSymbol = -R"doc(1. Begin first ordered list element. Rutrum quisque non tellus orci ac -auctor. End first ordered list element. 2. Begin second ordered list -element. Ipsum faucibus vitae aliquet nec. Ligula ullamcorper -malesuada proin libero. End second ordered list element. 3. Begin -third ordered list element. Dictum sit amet justo donec enim. Pharetra -convallis posuere morbi leo urna molestie. End third ordered list -element. +R"doc(1. Begin first ordered list element. Rutrum quisque non tellus orci + ac auctor. End first ordered list element. +2. Begin second ordered list element. Ipsum faucibus vitae aliquet + nec. Ligula ullamcorper malesuada proin libero. End second ordered + list element. +3. Begin third ordered list element. Dictum sit amet justo donec + enim. Pharetra convallis posuere morbi leo urna molestie. End third + ordered list element. Senectus et netus et malesuada fames ac. Tincidunt lobortis feugiat vivamus at augue eget arcu dictum varius.)doc"; From 5b36d9aec83dbe2d6c3068f13f813250a72a20a3 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 12:53:04 -0700 Subject: [PATCH 04/18] Adding some cases that break. We need to parse parameters/returns/exceptions line-by-line like we do when writing them out. --- tests/sample_header_docs/sample_header_2.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/sample_header_docs/sample_header_2.h b/tests/sample_header_docs/sample_header_2.h index b2de942..984599f 100644 --- a/tests/sample_header_docs/sample_header_2.h +++ b/tests/sample_header_docs/sample_header_2.h @@ -22,4 +22,20 @@ class Base { * @throws runtime_error Throws runtime error if p1 is empty. */ int method1(std::vector p1, std::map p2); + + /** + * @brief Description for method1. + * + * This is the extended description for method1. + * + * @param p1 I am a very long description for parameter 1. Let's ensure that this gets wrapped properly. + * @param p2 I am a very long descripton for paramet 2. + * However, I'm broken out onto two lines. Will this be parsed correctly? + * + * @return An integer is what I return. + * + * @throw runtime_error Throws runtime error if p1 is 0. + * @exception invalid_argument Throws invalid_argument error if p2 is 0. + */ + void method2(int p1, int p2); }; From f75abb21d9fd766c61c1eb3e0587404d463e9ca8 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 13:15:42 -0700 Subject: [PATCH 05/18] Updating to work with longer parameter and return descriptions. --- pybind11_mkdoc/mkdoc_lib.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index b87461f..0aced1f 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -111,6 +111,7 @@ def sanitize_name(name): t_param_re = re.compile(r"[\\@]tparam\s+([\w:]+)\s*(.*)") return_re = re.compile(rf"[\\@]returns?\s+(.*)") raises_re = re.compile(rf"[\\@](?:exception|throws?)\s+([\w:]+)(.*)") +any_dox_re = re.compile(rf"[\\@].*") def process_comment(comment): result = "" @@ -155,24 +156,35 @@ def process_comment(comment): t_params = {} raises = {} ret = [] + add_to = None for k,line in enumerate(lines): if m := param_re.match(line): name,text = m.groups() params[name] = text.strip() rm_lines.append(k) + add_to = (params,name) elif m := t_param_re.match(line): name,text = m.groups() t_params[name] = text.strip() rm_lines.append(k) + add_to = (t_params,name) elif m := return_re.match(line): text = m.groups()[0] ret.append(text.strip()) rm_lines.append(k) rm_lines.append(k) + add_to = (rm_lines,len(rm_lines)-1) elif m := raises_re.match(line): name,text = m.groups() raises[name] = text.strip() rm_lines.append(k) + add_to = (raises, name) + elif (m := any_dox_re.match(line)) or line == "": + add_to = None + else: + if add_to is not None: + add_to[0][add_to[1]] += " " + line.strip() + rm_lines.append(k) # If we had any hits, then remove the old lines, fill with the new lines, and convert back to s if rm_lines: @@ -273,12 +285,12 @@ def get_prefix_and_indent(line) -> tuple[str|None, str]: m = re.match( rf"{indent_str}(" r"(?:[*\-•]\s)|(?:\(?\d+[\.)]\s)|(?:\w+:)" - r")", + r"\s*)", line ) if m: g = m.group(0) - return g, indent_str + " " * len(g) + return g, " " * len(g) else: return None, indent_str @@ -294,7 +306,7 @@ def flush_paragraph(): para_text = " ".join(line.strip() for line in paragraph) if prefix: - content = para_text[len(prefix.strip()):] + content = para_text[len(prefix.lstrip()):] wrapper.initial_indent=prefix wrapper.subsequent_indent=indent_str if content == "": From 794144863328f13d14670d760ddbed5e3a860221 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 13:22:06 -0700 Subject: [PATCH 06/18] Updating tests now that we are parsing longer parameter descriptions correctly. --- tests/cli_test.py | 50 +--------------- tests/long_parameter_test.py | 2 +- .../sample_header_docs/sample_header_truth.h | 45 ++++++++++++++ tests/sample_header_test.py | 60 +++---------------- 4 files changed, 55 insertions(+), 102 deletions(-) create mode 100644 tests/sample_header_docs/sample_header_truth.h diff --git a/tests/cli_test.py b/tests/cli_test.py index 810cfe7..dc9948d 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -6,54 +6,8 @@ DIR = Path(__file__).resolve().parent -expected = """\ -/* - This file contains docstrings for use in the Python bindings. - Do not edit! They were automatically extracted by pybind11_mkdoc. - */ - -#define MKD_EXPAND(x) x -#define MKD_COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT -#define MKD_VA_SIZE(...) MKD_EXPAND(MKD_COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1, 0)) -#define MKD_CAT1(a, b) a ## b -#define MKD_CAT2(a, b) MKD_CAT1(a, b) -#define MKD_DOC1(n1) mkd_doc_##n1 -#define MKD_DOC2(n1, n2) mkd_doc_##n1##_##n2 -#define MKD_DOC3(n1, n2, n3) mkd_doc_##n1##_##n2##_##n3 -#define MKD_DOC4(n1, n2, n3, n4) mkd_doc_##n1##_##n2##_##n3##_##n4 -#define MKD_DOC5(n1, n2, n3, n4, n5) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5 -#define MKD_DOC7(n1, n2, n3, n4, n5, n6, n7) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 -#define DOC(...) MKD_EXPAND(MKD_EXPAND(MKD_CAT2(MKD_DOC, MKD_VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) - -#if defined(__GNUG__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-variable" -#endif - - -static const char *mkd_doc_RootLevelSymbol = -R"doc(Root-level symbol. Magna fermentum iaculis eu non diam phasellus -vestibulum.)doc"; - -static const char *mkd_doc_drake_MidLevelSymbol = -R"doc(1. Begin first ordered list element. Rutrum quisque non tellus orci - ac auctor. End first ordered list element. -2. Begin second ordered list element. Ipsum faucibus vitae aliquet - nec. Ligula ullamcorper malesuada proin libero. End second ordered - list element. -3. Begin third ordered list element. Dictum sit amet justo donec - enim. Pharetra convallis posuere morbi leo urna molestie. End third - ordered list element. - -Senectus et netus et malesuada fames ac. Tincidunt lobortis feugiat -vivamus at augue eget arcu dictum varius.)doc"; - -#if defined(__GNUG__) -#pragma GCC diagnostic pop -#endif - -""" - +with open(DIR / "sample_header_docs" / "sample_header_truth.h") as f: + expected = f.read() @pytest.mark.parametrize( "name", diff --git a/tests/long_parameter_test.py b/tests/long_parameter_test.py index 71ad255..530fc37 100644 --- a/tests/long_parameter_test.py +++ b/tests/long_parameter_test.py @@ -14,7 +14,7 @@ def test_long_parameter(capsys): expected = """\ Args: x: - Begin first parameter description. Senectus et netus et - malesuada fames ac. End first parameter description.)doc"; + malesuada fames ac. End first parameter description.)doc"; """ assert expected in res.out diff --git a/tests/sample_header_docs/sample_header_truth.h b/tests/sample_header_docs/sample_header_truth.h new file mode 100644 index 0000000..e4c16d2 --- /dev/null +++ b/tests/sample_header_docs/sample_header_truth.h @@ -0,0 +1,45 @@ +/* + This file contains docstrings for use in the Python bindings. + Do not edit! They were automatically extracted by pybind11_mkdoc. + */ + +#define MKD_EXPAND(x) x +#define MKD_COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT +#define MKD_VA_SIZE(...) MKD_EXPAND(MKD_COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1, 0)) +#define MKD_CAT1(a, b) a ## b +#define MKD_CAT2(a, b) MKD_CAT1(a, b) +#define MKD_DOC1(n1) mkd_doc_##n1 +#define MKD_DOC2(n1, n2) mkd_doc_##n1##_##n2 +#define MKD_DOC3(n1, n2, n3) mkd_doc_##n1##_##n2##_##n3 +#define MKD_DOC4(n1, n2, n3, n4) mkd_doc_##n1##_##n2##_##n3##_##n4 +#define MKD_DOC5(n1, n2, n3, n4, n5) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define MKD_DOC7(n1, n2, n3, n4, n5, n6, n7) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 +#define DOC(...) MKD_EXPAND(MKD_EXPAND(MKD_CAT2(MKD_DOC, MKD_VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) + +#if defined(__GNUG__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + + +static const char *mkd_doc_RootLevelSymbol = +R"doc(Root-level symbol. Magna fermentum iaculis eu non diam phasellus +vestibulum.)doc"; + +static const char *mkd_doc_drake_MidLevelSymbol = +R"doc(1. Begin first ordered list element. Rutrum quisque non tellus orci ac + auctor. End first ordered list element. +2. Begin second ordered list element. Ipsum faucibus vitae aliquet + nec. Ligula ullamcorper malesuada proin libero. End second ordered + list element. +3. Begin third ordered list element. Dictum sit amet justo donec enim. + Pharetra convallis posuere morbi leo urna molestie. End third + ordered list element. + +Senectus et netus et malesuada fames ac. Tincidunt lobortis feugiat +vivamus at augue eget arcu dictum varius.)doc"; + +#if defined(__GNUG__) +#pragma GCC diagnostic pop +#endif + diff --git a/tests/sample_header_test.py b/tests/sample_header_test.py index 6eb3d40..cae194e 100644 --- a/tests/sample_header_test.py +++ b/tests/sample_header_test.py @@ -1,12 +1,15 @@ -import os +from pathlib import Path import pybind11_mkdoc -DIR = os.path.abspath(os.path.dirname(__file__)) +DIR = Path(__file__).resolve().parent + +with open(DIR / "sample_header_docs" / "sample_header_truth.h") as f: + expected = f.read() def test_generate_headers(capsys, tmp_path): - comments = pybind11_mkdoc.mkdoc_lib.extract_all([os.path.join(DIR, "sample_header_docs", "sample_header.h")]) + comments = pybind11_mkdoc.mkdoc_lib.extract_all([str(DIR / "sample_header_docs" / "sample_header.h")]) assert ["mkd_doc_RootLevelSymbol", "mkd_doc_drake_MidLevelSymbol"] == [c[0] for c in comments] output = tmp_path / "docs.h" @@ -17,53 +20,4 @@ def test_generate_headers(capsys, tmp_path): assert "warning" not in res.err assert "error" not in res.err - assert ( - output.read_text() - == """\ -/* - This file contains docstrings for use in the Python bindings. - Do not edit! They were automatically extracted by pybind11_mkdoc. - */ - -#define MKD_EXPAND(x) x -#define MKD_COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT -#define MKD_VA_SIZE(...) MKD_EXPAND(MKD_COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1, 0)) -#define MKD_CAT1(a, b) a ## b -#define MKD_CAT2(a, b) MKD_CAT1(a, b) -#define MKD_DOC1(n1) mkd_doc_##n1 -#define MKD_DOC2(n1, n2) mkd_doc_##n1##_##n2 -#define MKD_DOC3(n1, n2, n3) mkd_doc_##n1##_##n2##_##n3 -#define MKD_DOC4(n1, n2, n3, n4) mkd_doc_##n1##_##n2##_##n3##_##n4 -#define MKD_DOC5(n1, n2, n3, n4, n5) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5 -#define MKD_DOC7(n1, n2, n3, n4, n5, n6, n7) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 -#define DOC(...) MKD_EXPAND(MKD_EXPAND(MKD_CAT2(MKD_DOC, MKD_VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) - -#if defined(__GNUG__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-variable" -#endif - - -static const char *mkd_doc_RootLevelSymbol = -R"doc(Root-level symbol. Magna fermentum iaculis eu non diam phasellus -vestibulum.)doc"; - -static const char *mkd_doc_drake_MidLevelSymbol = -R"doc(1. Begin first ordered list element. Rutrum quisque non tellus orci - ac auctor. End first ordered list element. -2. Begin second ordered list element. Ipsum faucibus vitae aliquet - nec. Ligula ullamcorper malesuada proin libero. End second ordered - list element. -3. Begin third ordered list element. Dictum sit amet justo donec - enim. Pharetra convallis posuere morbi leo urna molestie. End third - ordered list element. - -Senectus et netus et malesuada fames ac. Tincidunt lobortis feugiat -vivamus at augue eget arcu dictum varius.)doc"; - -#if defined(__GNUG__) -#pragma GCC diagnostic pop -#endif - -""" - ) + assert (output.read_text() == expected) From 290871858086b2686a2cc4dc33edf250563e7b13 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 13:26:33 -0700 Subject: [PATCH 07/18] Adding test for new header with exceptions, params, and returns. --- .../sample_header_2_truth.h | 63 +++++++++++++++++++ tests/sample_header_test.py | 21 ++++++- 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/sample_header_docs/sample_header_2_truth.h diff --git a/tests/sample_header_docs/sample_header_2_truth.h b/tests/sample_header_docs/sample_header_2_truth.h new file mode 100644 index 0000000..f97e2b0 --- /dev/null +++ b/tests/sample_header_docs/sample_header_2_truth.h @@ -0,0 +1,63 @@ +/* + This file contains docstrings for use in the Python bindings. + Do not edit! They were automatically extracted by pybind11_mkdoc. + */ + +#define MKD_EXPAND(x) x +#define MKD_COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT +#define MKD_VA_SIZE(...) MKD_EXPAND(MKD_COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1, 0)) +#define MKD_CAT1(a, b) a ## b +#define MKD_CAT2(a, b) MKD_CAT1(a, b) +#define MKD_DOC1(n1) mkd_doc_##n1 +#define MKD_DOC2(n1, n2) mkd_doc_##n1##_##n2 +#define MKD_DOC3(n1, n2, n3) mkd_doc_##n1##_##n2##_##n3 +#define MKD_DOC4(n1, n2, n3, n4) mkd_doc_##n1##_##n2##_##n3##_##n4 +#define MKD_DOC5(n1, n2, n3, n4, n5) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define MKD_DOC7(n1, n2, n3, n4, n5, n6, n7) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 +#define DOC(...) MKD_EXPAND(MKD_EXPAND(MKD_CAT2(MKD_DOC, MKD_VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) + +#if defined(__GNUG__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + + +static const char *mkd_doc_Base = R"doc(A simple base class.)doc"; + +static const char *mkd_doc_Base_method1 = +R"doc(Description for method1. + +This is the extended description for method1. + +Args: + p1: I am the first parameter. + p2: I am the second parameter. + +Returns: + An integer is what I return. + +Raises: + runtime_error: Throws runtime error if p1 is empty.)doc"; + +static const char *mkd_doc_Base_method2 = +R"doc(Description for method1. + +This is the extended description for method1. + +Args: + p1: I am a very long description for parameter 1. Let's ensure + that this gets wrapped properly. + p2: I am a very long descripton for paramet 2. However, I'm broken + out onto two lines. Will this be parsed correctly? + +Returns: + An integer is what I return. + +Raises: + runtime_error: Throws runtime error if p1 is 0. + invalid_argument: Throws invalid_argument error if p2 is 0.)doc"; + +#if defined(__GNUG__) +#pragma GCC diagnostic pop +#endif + diff --git a/tests/sample_header_test.py b/tests/sample_header_test.py index cae194e..e8ddab6 100644 --- a/tests/sample_header_test.py +++ b/tests/sample_header_test.py @@ -4,11 +4,12 @@ DIR = Path(__file__).resolve().parent -with open(DIR / "sample_header_docs" / "sample_header_truth.h") as f: - expected = f.read() def test_generate_headers(capsys, tmp_path): + with open(DIR / "sample_header_docs" / "sample_header_truth.h") as f: + expected = f.read() + comments = pybind11_mkdoc.mkdoc_lib.extract_all([str(DIR / "sample_header_docs" / "sample_header.h")]) assert ["mkd_doc_RootLevelSymbol", "mkd_doc_drake_MidLevelSymbol"] == [c[0] for c in comments] @@ -21,3 +22,19 @@ def test_generate_headers(capsys, tmp_path): assert "warning" not in res.err assert "error" not in res.err assert (output.read_text() == expected) + +def test_generate_headers_2(capsys, tmp_path): + with open(DIR / "sample_header_docs" / "sample_header_2_truth.h") as f: + expected = f.read() + + comments = pybind11_mkdoc.mkdoc_lib.extract_all([str(DIR / "sample_header_docs" / "sample_header_2.h")]) + + output = tmp_path / "docs.h" + with output.open("w") as fd: + pybind11_mkdoc.mkdoc_lib.write_header(comments, fd) + + res = capsys.readouterr() + + assert "warning" not in res.err + assert "error" not in res.err + assert (output.read_text() == expected) From 08d2e9aa8dae91fa6ef514c160a8842f8b46eff7 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 13:53:12 -0700 Subject: [PATCH 08/18] Adding MKDOC6 macro that was missing. --- pybind11_mkdoc/mkdoc_lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index 0aced1f..1bed247 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -541,6 +541,7 @@ def write_header(comments, out_file=sys.stdout): #define MKD_DOC3(n1, n2, n3) mkd_doc_##n1##_##n2##_##n3 #define MKD_DOC4(n1, n2, n3, n4) mkd_doc_##n1##_##n2##_##n3##_##n4 #define MKD_DOC5(n1, n2, n3, n4, n5) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define MKD_DOC6(n1, n2, n3, n4, n5, n6) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 #define MKD_DOC7(n1, n2, n3, n4, n5, n6, n7) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 #define DOC(...) MKD_EXPAND(MKD_EXPAND(MKD_CAT2(MKD_DOC, MKD_VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) From da0c257bfd6b1c7c9871ab13fe9b32237c0378c5 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 13:53:29 -0700 Subject: [PATCH 09/18] Fixing bug with add_to in return statement. --- pybind11_mkdoc/mkdoc_lib.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index 1bed247..ce5347d 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -171,15 +171,14 @@ def process_comment(comment): elif m := return_re.match(line): text = m.groups()[0] ret.append(text.strip()) + add_to = (ret,len(ret)-1) rm_lines.append(k) - rm_lines.append(k) - add_to = (rm_lines,len(rm_lines)-1) elif m := raises_re.match(line): name,text = m.groups() raises[name] = text.strip() - rm_lines.append(k) add_to = (raises, name) - elif (m := any_dox_re.match(line)) or line == "": + rm_lines.append(k) + elif m := any_dox_re.match(line): add_to = None else: if add_to is not None: @@ -285,12 +284,12 @@ def get_prefix_and_indent(line) -> tuple[str|None, str]: m = re.match( rf"{indent_str}(" r"(?:[*\-•]\s)|(?:\(?\d+[\.)]\s)|(?:\w+:)" - r"\s*)", + r")", line ) if m: g = m.group(0) - return g, " " * len(g) + return g, indent_str + " " * len(g) else: return None, indent_str @@ -306,7 +305,7 @@ def flush_paragraph(): para_text = " ".join(line.strip() for line in paragraph) if prefix: - content = para_text[len(prefix.lstrip()):] + content = para_text[len(prefix.strip()):] wrapper.initial_indent=prefix wrapper.subsequent_indent=indent_str if content == "": From 33a654ad1e1b4d491a35d335a327d5045712bc76 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 14:05:37 -0700 Subject: [PATCH 10/18] Formatting with ruff. --- pybind11_mkdoc/mkdoc_lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index ce5347d..c2b9347 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -109,9 +109,9 @@ def sanitize_name(name): param_re = re.compile(r"[\\@]param\s+([\w:]+)\s*(.*)") t_param_re = re.compile(r"[\\@]tparam\s+([\w:]+)\s*(.*)") -return_re = re.compile(rf"[\\@]returns?\s+(.*)") -raises_re = re.compile(rf"[\\@](?:exception|throws?)\s+([\w:]+)(.*)") -any_dox_re = re.compile(rf"[\\@].*") +return_re = re.compile(r"[\\@]returns?\s+(.*)") +raises_re = re.compile(r"[\\@](?:exception|throws?)\s+([\w:]+)(.*)") +any_dox_re = re.compile(r"[\\@].*") def process_comment(comment): result = "" From bc3ab78595db589c0aeefd360127916027d96b91 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 14:26:20 -0700 Subject: [PATCH 11/18] Adding back in lstrip only. --- pybind11_mkdoc/mkdoc_lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index c2b9347..4d4f0a2 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -284,12 +284,12 @@ def get_prefix_and_indent(line) -> tuple[str|None, str]: m = re.match( rf"{indent_str}(" r"(?:[*\-•]\s)|(?:\(?\d+[\.)]\s)|(?:\w+:)" - r")", + r"\s*)", line ) if m: g = m.group(0) - return g, indent_str + " " * len(g) + return g, " " * len(g) else: return None, indent_str @@ -305,7 +305,7 @@ def flush_paragraph(): para_text = " ".join(line.strip() for line in paragraph) if prefix: - content = para_text[len(prefix.strip()):] + content = para_text[len(prefix.lstrip()):] wrapper.initial_indent=prefix wrapper.subsequent_indent=indent_str if content == "": From 54220967fcc97e457c20123950a751069f04b63a Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 14:28:19 -0700 Subject: [PATCH 12/18] Updating to include MKDOC6. --- tests/sample_header_docs/sample_header_2_truth.h | 1 + tests/sample_header_docs/sample_header_truth.h | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/sample_header_docs/sample_header_2_truth.h b/tests/sample_header_docs/sample_header_2_truth.h index f97e2b0..47f8b84 100644 --- a/tests/sample_header_docs/sample_header_2_truth.h +++ b/tests/sample_header_docs/sample_header_2_truth.h @@ -13,6 +13,7 @@ #define MKD_DOC3(n1, n2, n3) mkd_doc_##n1##_##n2##_##n3 #define MKD_DOC4(n1, n2, n3, n4) mkd_doc_##n1##_##n2##_##n3##_##n4 #define MKD_DOC5(n1, n2, n3, n4, n5) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define MKD_DOC6(n1, n2, n3, n4, n5, n6) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 #define MKD_DOC7(n1, n2, n3, n4, n5, n6, n7) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 #define DOC(...) MKD_EXPAND(MKD_EXPAND(MKD_CAT2(MKD_DOC, MKD_VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) diff --git a/tests/sample_header_docs/sample_header_truth.h b/tests/sample_header_docs/sample_header_truth.h index e4c16d2..d752cc5 100644 --- a/tests/sample_header_docs/sample_header_truth.h +++ b/tests/sample_header_docs/sample_header_truth.h @@ -13,6 +13,7 @@ #define MKD_DOC3(n1, n2, n3) mkd_doc_##n1##_##n2##_##n3 #define MKD_DOC4(n1, n2, n3, n4) mkd_doc_##n1##_##n2##_##n3##_##n4 #define MKD_DOC5(n1, n2, n3, n4, n5) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define MKD_DOC6(n1, n2, n3, n4, n5, n6) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 #define MKD_DOC7(n1, n2, n3, n4, n5, n6, n7) mkd_doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 #define DOC(...) MKD_EXPAND(MKD_EXPAND(MKD_CAT2(MKD_DOC, MKD_VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) From 1899742904d6714fada7f88de7bcd1a0109ded99 Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 14:52:51 -0700 Subject: [PATCH 13/18] uvx hatch fmt. --- pybind11_mkdoc/mkdoc_lib.py | 67 ++++++++++++++++++------------------- tests/cli_test.py | 3 +- tests/sample_header_test.py | 8 ++--- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index 4d4f0a2..cfdee63 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -5,6 +5,8 @@ # Extract documentation from C++ header files to use it in Python bindings # +from __future__ import annotations + import contextlib import ctypes.util import os @@ -107,12 +109,14 @@ def sanitize_name(name): name = re.sub("_$", "", re.sub("_+", "_", name)) return "mkd_doc_" + name + param_re = re.compile(r"[\\@]param\s+([\w:]+)\s*(.*)") t_param_re = re.compile(r"[\\@]tparam\s+([\w:]+)\s*(.*)") return_re = re.compile(r"[\\@]returns?\s+(.*)") raises_re = re.compile(r"[\\@](?:exception|throws?)\s+([\w:]+)(.*)") any_dox_re = re.compile(r"[\\@].*") + def process_comment(comment): result = "" @@ -157,33 +161,32 @@ def process_comment(comment): raises = {} ret = [] add_to = None - for k,line in enumerate(lines): + for k, line in enumerate(lines): if m := param_re.match(line): - name,text = m.groups() + name, text = m.groups() params[name] = text.strip() rm_lines.append(k) - add_to = (params,name) + add_to = (params, name) elif m := t_param_re.match(line): - name,text = m.groups() + name, text = m.groups() t_params[name] = text.strip() rm_lines.append(k) - add_to = (t_params,name) + add_to = (t_params, name) elif m := return_re.match(line): text = m.groups()[0] ret.append(text.strip()) - add_to = (ret,len(ret)-1) + add_to = (ret, len(ret) - 1) rm_lines.append(k) elif m := raises_re.match(line): - name,text = m.groups() + name, text = m.groups() raises[name] = text.strip() add_to = (raises, name) rm_lines.append(k) elif m := any_dox_re.match(line): add_to = None - else: - if add_to is not None: - add_to[0][add_to[1]] += " " + line.strip() - rm_lines.append(k) + elif add_to is not None: + add_to[0][add_to[1]] += " " + line.strip() + rm_lines.append(k) # If we had any hits, then remove the old lines, fill with the new lines, and convert back to s if rm_lines: @@ -194,11 +197,11 @@ def process_comment(comment): new_lines = [] if params: new_lines.append("Args:") - new_lines += [f" {name}: {text}" for name,text in params.items()] + new_lines += [f" {name}: {text}" for name, text in params.items()] new_lines.append("") if t_params: new_lines.append("Template Args:") - new_lines += [f" {name}: {text}" for name,text in t_params.items()] + new_lines += [f" {name}: {text}" for name, text in t_params.items()] new_lines.append("") if ret: new_lines.append("Returns:") @@ -206,9 +209,9 @@ def process_comment(comment): new_lines.append("") if raises: new_lines.append("Raises:") - new_lines += [f" {name}: {text}" for name,text in raises.items()] + new_lines += [f" {name}: {text}" for name, text in raises.items()] new_lines.append("") - + idx = rm_lines[-1] lines = lines[0:idx] + new_lines + lines[idx:] s = "\n".join(lines) @@ -278,22 +281,21 @@ def process_comment(comment): wrapped = [] paragraph = [] - def get_prefix_and_indent(line) -> tuple[str|None, str]: + def get_prefix_and_indent(line) -> tuple[str | None, str]: indent = len(line) - len(line.lstrip()) indent_str = " " * indent m = re.match( rf"{indent_str}(" r"(?:[*\-•]\s)|(?:\(?\d+[\.)]\s)|(?:\w+:)" r"\s*)", - line + line, ) - if m: + if m: g = m.group(0) return g, " " * len(g) - else: - return None, indent_str + return None, indent_str - def flush_paragraph(): + def flush_paragraph(paragraph=paragraph, wrapped=wrapped): if not paragraph: return @@ -305,9 +307,9 @@ def flush_paragraph(): para_text = " ".join(line.strip() for line in paragraph) if prefix: - content = para_text[len(prefix.lstrip()):] - wrapper.initial_indent=prefix - wrapper.subsequent_indent=indent_str + content = para_text[len(prefix.lstrip()) :] + wrapper.initial_indent = prefix + wrapper.subsequent_indent = indent_str if content == "": # This paragraph is just the prefix wrapped.append(prefix) @@ -315,28 +317,28 @@ def flush_paragraph(): return else: content = para_text.lstrip() - wrapper.initial_indent=indent_str - wrapper.subsequent_indent=indent_str + wrapper.initial_indent = indent_str + wrapper.subsequent_indent = indent_str wrapped.append(wrapper.fill(content)) paragraph.clear() current_prefix = None - current_indent = "" + current_indent = "" for line in x.splitlines(): if not line.strip(): flush_paragraph() wrapped.append(line) # preserve blank lines continue - prefix,indent = get_prefix_and_indent(line) + prefix, indent = get_prefix_and_indent(line) if paragraph and ((indent != current_indent) or (prefix and prefix != current_prefix)): # Prefix/indent changed → start new paragraph flush_paragraph() paragraph.append(line) current_prefix = prefix - current_indent = indent + current_indent = indent flush_paragraph() result += "\n".join(wrapped) @@ -417,10 +419,7 @@ def read_args(args): if os.path.isfile(library_file): cindex.Config.set_library_file(library_file) else: - msg = ( - "Failed to find libclang.dll! " - "Set the LIBCLANG_PATH environment variable to provide a path to it." - ) + msg = "Failed to find libclang.dll! Set the LIBCLANG_PATH environment variable to provide a path to it." raise FileNotFoundError(msg) else: library_file = ctypes.util.find_library("libclang.dll") @@ -557,7 +556,7 @@ def write_header(comments, out_file=sys.stdout): for name, _, comment in sorted(comments, key=lambda x: (x[0], x[1])): if name == name_prev: name_ctr += 1 - name = name + "_%i" % name_ctr + name = name + f"_{name_ctr}" else: name_prev = name name_ctr = 1 diff --git a/tests/cli_test.py b/tests/cli_test.py index dc9948d..491479e 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,5 +1,5 @@ -import sys import subprocess +import sys from pathlib import Path import pytest @@ -9,6 +9,7 @@ with open(DIR / "sample_header_docs" / "sample_header_truth.h") as f: expected = f.read() + @pytest.mark.parametrize( "name", ["sample_header.h", "sample header with spaces.h"], diff --git a/tests/sample_header_test.py b/tests/sample_header_test.py index e8ddab6..64f992f 100644 --- a/tests/sample_header_test.py +++ b/tests/sample_header_test.py @@ -5,13 +5,12 @@ DIR = Path(__file__).resolve().parent - def test_generate_headers(capsys, tmp_path): with open(DIR / "sample_header_docs" / "sample_header_truth.h") as f: expected = f.read() comments = pybind11_mkdoc.mkdoc_lib.extract_all([str(DIR / "sample_header_docs" / "sample_header.h")]) - assert ["mkd_doc_RootLevelSymbol", "mkd_doc_drake_MidLevelSymbol"] == [c[0] for c in comments] + assert [c[0] for c in comments] == ["mkd_doc_RootLevelSymbol", "mkd_doc_drake_MidLevelSymbol"] output = tmp_path / "docs.h" with output.open("w") as fd: @@ -21,7 +20,8 @@ def test_generate_headers(capsys, tmp_path): assert "warning" not in res.err assert "error" not in res.err - assert (output.read_text() == expected) + assert output.read_text() == expected + def test_generate_headers_2(capsys, tmp_path): with open(DIR / "sample_header_docs" / "sample_header_2_truth.h") as f: @@ -37,4 +37,4 @@ def test_generate_headers_2(capsys, tmp_path): assert "warning" not in res.err assert "error" not in res.err - assert (output.read_text() == expected) + assert output.read_text() == expected From cc95cfd9aaca0cb51724e1d7a8102f806ff9a331 Mon Sep 17 00:00:00 2001 From: Carl Leake <46822212+leakec@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:01:13 -0800 Subject: [PATCH 14/18] Fixing typo. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/sample_header_docs/sample_header_2.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sample_header_docs/sample_header_2.h b/tests/sample_header_docs/sample_header_2.h index 984599f..c62cb80 100644 --- a/tests/sample_header_docs/sample_header_2.h +++ b/tests/sample_header_docs/sample_header_2.h @@ -29,7 +29,7 @@ class Base { * This is the extended description for method1. * * @param p1 I am a very long description for parameter 1. Let's ensure that this gets wrapped properly. - * @param p2 I am a very long descripton for paramet 2. + * @param p2 I am a very long description for parameter 2. * However, I'm broken out onto two lines. Will this be parsed correctly? * * @return An integer is what I return. From 737f6cf8f1a6db609bd6433dd457f714ddae8957 Mon Sep 17 00:00:00 2001 From: Carl Leake <46822212+leakec@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:01:26 -0800 Subject: [PATCH 15/18] Fixing typo. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/sample_header_docs/sample_header_2_truth.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sample_header_docs/sample_header_2_truth.h b/tests/sample_header_docs/sample_header_2_truth.h index 47f8b84..ba16b30 100644 --- a/tests/sample_header_docs/sample_header_2_truth.h +++ b/tests/sample_header_docs/sample_header_2_truth.h @@ -48,7 +48,7 @@ This is the extended description for method1. Args: p1: I am a very long description for parameter 1. Let's ensure that this gets wrapped properly. - p2: I am a very long descripton for paramet 2. However, I'm broken + p2: I am a very long description for parameter 2. However, I'm broken out onto two lines. Will this be parsed correctly? Returns: From 75f86bde47f4b10689f25595125f5c9316856a6f Mon Sep 17 00:00:00 2001 From: Carl Leake <46822212+leakec@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:02:27 -0800 Subject: [PATCH 16/18] Fully expanding groups() Co-authored-by: Henry Schreiner --- pybind11_mkdoc/mkdoc_lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index cfdee63..c378ec8 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -173,7 +173,7 @@ def process_comment(comment): rm_lines.append(k) add_to = (t_params, name) elif m := return_re.match(line): - text = m.groups()[0] + text, = m.groups() ret.append(text.strip()) add_to = (ret, len(ret) - 1) rm_lines.append(k) From 022eba004c19b4396217ce78692f1563d6fdf744 Mon Sep 17 00:00:00 2001 From: Carl Leake <46822212+leakec@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:03:03 -0800 Subject: [PATCH 17/18] Combining list by unpacking other lists rather than using +. Co-authored-by: Henry Schreiner --- pybind11_mkdoc/mkdoc_lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index c378ec8..6f57054 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -213,7 +213,7 @@ def process_comment(comment): new_lines.append("") idx = rm_lines[-1] - lines = lines[0:idx] + new_lines + lines[idx:] + lines = [*lines[0:idx], *new_lines, *lines[idx:]] s = "\n".join(lines) # Remove class and struct tags From 43125061b39c2899e15a05f5a792130a373f612f Mon Sep 17 00:00:00 2001 From: leake Date: Wed, 3 Dec 2025 16:04:47 -0700 Subject: [PATCH 18/18] Fixing after fixing typos. --- tests/sample_header_docs/sample_header_2_truth.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sample_header_docs/sample_header_2_truth.h b/tests/sample_header_docs/sample_header_2_truth.h index ba16b30..c514b61 100644 --- a/tests/sample_header_docs/sample_header_2_truth.h +++ b/tests/sample_header_docs/sample_header_2_truth.h @@ -48,8 +48,8 @@ This is the extended description for method1. Args: p1: I am a very long description for parameter 1. Let's ensure that this gets wrapped properly. - p2: I am a very long description for parameter 2. However, I'm broken - out onto two lines. Will this be parsed correctly? + p2: I am a very long description for parameter 2. However, I'm + broken out onto two lines. Will this be parsed correctly? Returns: An integer is what I return.