iterative closest points
CEV ICP algorithm library
Loading...
Searching...
No Matches
icp_doc_builder.py
Go to the documentation of this file.
1# Copyright (C) 2024 Ethan Uppal. All rights reserved.
2
3import os
4import re
5import sys
6
7
8def change_extension(filename, new_extension):
9 name, _ = os.path.splitext(filename)
10 return f"{name}.{new_extension}"
11
12
13# yes this documentation was mostly generated by ChatGPT
14# no I don't care, it's Python
16 """
17 A class used to build documentation for an ICP implementation.
18
19 ...
20
21 Attributes
22 ----------
23 search_dir : str
24 The directory to search for source files.
25 out_dir : str
26 The directory to write the output markdown files.
27 main_file : str
28 The main documentation file.
29 all_sources : set
30 A set of all sources found in the source files.
31 registration_names : dict
32 Associates each ICP type name with its registration name.
33
34 Methods
35 -------
36 extract(file, contents):
37 Extracts the documentation from the given file contents and writes it to a markdown file.
38 build():
39 Searches for source files in the search directory and extracts their documentation.
40 """
41
42 def __init__(self, search_dir, out_dir, main_file):
43 """
44 Constructs a new ICPDocumentationBuilder.
45
46 Parameters
47 ----------
48 search_dir : str
49 The directory to search for source files.
50 out_dir : str
51 The directory to write the output markdown files.
52 main_file : str
53 A path to the main markdown file.
54 """
55 self.search_dir = search_dir
56 self.out_dir = out_dir
57 self.main_file = main_file
58 self.all_sources = set()
59
60 def extract(self, file, contents):
61 """
62 Extracts the documentation from the given file contents and writes it to a markdown file.
63
64 Parameters
65 ----------
66 file : str
67 The name of the file to extract documentation from.
68 contents : str
69 The contents of the file.
70 """
71
72 pattern = r"/\*\s*(#step|#name|#desc)(.*?)\*/"
73 comments = re.findall(pattern, contents, re.DOTALL)
74 if not comments:
75 return
76 md_filename = "icp_" + change_extension(file, "md")
77 made_description = False
78 with open(self.out_dir + "/" + md_filename, "w") as md_file:
79 step_cnt = 1
80 for kind, comment in comments:
81 comment_parts = comment.strip().split("Sources:", 1)
82 comment_text = (
83 re.sub(r"\n *", "\n", comment_parts[0])
84 .replace("\n*", "\n")
85 .replace("\n *", "\n")
86 )
87 sources = (
88 re.findall(r"https?://[^\s]+", comment_parts[1])
89 if len(comment_parts) > 1
90 else []
91 )
92 self.all_sources.update(sources)
93 if kind == "#name":
94 doxygen_ref = f"{comment_text.lower()}_icp".replace("-", "_")
95 md_file.write(f"\\page {doxygen_ref} {comment_text} ICP\n")
96
97 register_pattern = r"/\*\s*#register(.*?)\*/"
98 registers = re.findall(register_pattern, contents, re.DOTALL)
99 assert len(registers) == 1
100 register_name = registers[0].strip()
101
102 conf_pattern = r'/\*\s*#conf\s+"([^"]*)"\s+(.*?)\*/'
103 confs = re.findall(conf_pattern, contents, re.DOTALL)
104
105 if not confs:
106 md_file.write(
107 f'\\par Usage\nYou can construct a new instance of {comment_text} ICP with `icp::ICP::from_method("{register_name}")`.'
108 )
109 else:
110 md_file.write(
111 f'\\par Usage\nYou can construct a new instance of {comment_text} ICP with `icp::ICP::from_method("{register_name}", config)`. Supply the following parameters to `config` (via icp::ICP::Config::set):\n'
112 )
113 md_file.write("\nKey | Description\n--- | ---\n")
114 for key, descr in confs:
115 descr_lines = descr.split("\n")
116 cleaned_lines = [
117 line.strip().removeprefix("*") for line in descr_lines
118 ]
119 descr = " ".join(cleaned_lines)
120 md_file.write(f'`"{key}"` | {descr}\n')
121 elif kind == "#step":
122 if not made_description:
123 md_file.write("\n\\par Description\n")
124 made_description = True
125 lines = comment_text.splitlines()
126 first_line_parts = lines[0].split(":", 1)
127 if len(first_line_parts) == 2:
128 lines[0] = f"**{first_line_parts[0]}**:{first_line_parts[1]}"
129 else:
130 lines[0] = f"**{first_line_parts[0]}**"
131 step_text = "\n".join(" " + line for line in lines)
132 md_file.write(f"\n{step_cnt}. {step_text}\n")
133 step_cnt += 1
134 if sources:
135 md_file.write(" Sources: \n")
136 for source in sources:
137 md_file.write(f" - {source}\n")
138 md_file.write("\n")
139 elif kind == "#desc":
140 icp_description = comment_text
141 md_file.write(f"\n\\par Description\n{comment_text}\n")
142 made_description = True
143 md_file.write(
144 "\n\nRead \\ref icp_sources for a list of all resources used in this project."
145 )
146 md_file.write(
147 f"\nThis page was automatically generated from {file} with {os.path.basename(__file__)}."
148 )
149 return doxygen_ref, icp_description
150
151 def update_main(self, doxygen_refs):
152 with open(self.main_file, "r") as file:
153 content = file.read()
154
155 start_marker = "<!-- ICP_DOCS_BUILDER EDIT MARKER START -->"
156 end_marker = "<!-- ICP_DOCS_BUILDER EDIT MARKER END -->"
157
158 start_index = content.find(start_marker)
159 end_index = content.find(end_marker)
160
161 if start_index != -1 and end_index != -1:
162 new_content = (
163 content[: start_index + len(start_marker) + 1]
164 + "\n".join(
165 sorted(
166 [
167 f"- \\ref {doxygen_ref} ({icp_description})"
168 for (doxygen_ref, icp_description) in doxygen_refs
169 ]
170 )
171 )
172 + "\n"
173 + content[end_index:]
174 )
175
176 with open(self.main_file, "w") as file:
177 file.write(new_content)
178
179 def build(self):
180 """
181 Searches for source files in the search directory and extracts their documentation.
182 """
183 doxygen_refs = []
184 for root, _, files in os.walk(self.search_dir):
185 for file in files:
186 if file.endswith(".cpp"):
187 with open(os.path.join(root, file), "r") as f:
188 result = self.extract(file, f.read())
189 if result:
190 doxygen_ref, icp_description = result
191 doxygen_refs.append((doxygen_ref, icp_description))
192 self.update_main(doxygen_refs)
193 os.makedirs(self.out_dir + "/extra", exist_ok=True)
194 with open(self.out_dir + "/extra/sources.md", "w") as md_file:
195 md_file.write("\\page icp_sources ICP Sources\n\n")
196 md_file.write(
197 "This list contains all resources used in implementing ICP for this project in alphabetical order.\n\n"
198 )
199 for source in sorted(list(self.all_sources)):
200 md_file.write(f" - {source}\n")
201
202
203if __name__ == "__main__":
204 if len(sys.argv) != 4:
205 print(
206 f"usage: python3 {os.path.basename(__file__)} search_dir out_dir main_file"
207 )
208 sys.exit(1)
209
210 os.makedirs(sys.argv[2], exist_ok=True)
211
212 doc_builder = ICPDocumentationBuilder(sys.argv[1], sys.argv[2], sys.argv[3])
213 doc_builder.build()
__init__(self, search_dir, out_dir, main_file)
change_extension(filename, new_extension)