1717# documentation root, use os.path.abspath to make it absolute, like shown here.
1818#
1919from __future__ import annotations
20+ from io import StringIO
2021import os
22+ from pathlib import Path
2123import sys
2224import platform
2325import inspect
24- import glob
26+ from typing import List
2527
2628stats = '''
2729
4951os .environ ["BN_DISABLE_USER_PLUGINS" ] = "True"
5052os .environ ["BN_DISABLE_REPOSITORY_PLUGINS" ] = "True"
5153import binaryninja
52- import binaryninja .debugger
54+
55+ try :
56+ import binaryninja .debugger
57+ except ImportError :
58+ pass
59+
5360try :
5461 import binaryninja .collaboration
5562except ImportError :
@@ -85,17 +92,14 @@ def setup(app):
8592 app .add_css_file ('css/other.css' )
8693 app .is_parallel_allowed ('write' )
8794
88- def cleansource ():
89- rstfiles = glob .glob ("*.rst" )
90- for f in rstfiles :
91- try :
92- os .remove (f )
93- except OSError :
94- print (f"Unable to remove { f } " )
95-
9695def generaterst ():
97- pythonrst = open ("index.rst" , "w" )
98- pythonrst .write ('''Binary Ninja Python API Reference
96+ # Generate and index.rst and .rst files for modules, removing any unneeded/old .rst files.
97+ # This should only write to the files if the contents have changed,
98+ # because sphinx uses that .rst mtime to determine if the corresponding .html file should be regenerated.
99+ # Maybe eventually they'll check file contents to see if the file contents changed but until then this works.
100+ used_rst_files : List [Path ] = []
101+ index_rst = StringIO ()
102+ index_rst .write ('''Binary Ninja Python API Reference
99103=====================================
100104
101105Welcome to the Binary Ninja API documentation. The below methods are available
@@ -130,19 +134,20 @@ def generaterst():
130134
131135 # Generate docs for both binaryninja and binaryninja.debugger module
132136 modules = modulelist (binaryninja )
133- modules .extend (modulelist (binaryninja .debugger , basename = "debugger" ))
137+ if hasattr (binaryninja , "debugger" ):
138+ modules .extend (modulelist (binaryninja .debugger , basename = "debugger" ))
134139 if hasattr (binaryninja , "collaboration" ):
135140 modules .extend (modulelist (binaryninja .collaboration , basename = "collaboration" ))
136141 modules = sorted (modules , key = lambda pair : pair [0 ])
137142
138143 # Separate top-level and nested modules for proper TOC structure
139144 nested_modules = {"debugger" : [], "collaboration" : []}
140-
145+
141146 for modulename , module in modules :
142147 filename = f"{ module .__name__ } -module.rst"
143148 if modulename .count ("." ) == 0 :
144149 # Top-level module - always include in main TOC
145- pythonrst .write (f" { modulename } <{ filename } >\n " )
150+ index_rst .write (f" { modulename } <{ filename } >\n " )
146151 else :
147152 # This is a nested module - collect them for parent modules
148153 parent = modulename .split ("." )[0 ]
@@ -153,42 +158,42 @@ def generaterst():
153158 # Since we put debugger python files in a folder, binaryninja.{modulename} is no longer the
154159 # correct name of the module
155160 filename = f"{ module .__name__ } -module.rst"
156- modulefile = open ( filename , "w" )
161+ module_contents = StringIO ( )
157162 underline = "=" * len (f"{ modulename } module" )
158- modulefile .write (f'''{ modulename } module
163+ module_contents .write (f'''{ modulename } module
159164{ underline }
160165
161166''' )
162167
163168 # Add sub-toctree for parent modules that have nested modules
164169 if modulename .count ("." ) == 0 and modulename in nested_modules and nested_modules [modulename ]:
165- modulefile .write (".. toctree::\n " )
166- modulefile .write (" :maxdepth: 1\n " )
167- modulefile .write (" :hidden:\n \n " )
170+ module_contents .write (".. toctree::\n " )
171+ module_contents .write (" :maxdepth: 1\n " )
172+ module_contents .write (" :hidden:\n \n " )
168173 for nested_name , nested_filename in nested_modules [modulename ]:
169- modulefile .write (f" { nested_name } <{ nested_filename } >\n " )
170- modulefile .write ("\n " )
174+ module_contents .write (f" { nested_name } <{ nested_filename } >\n " )
175+ module_contents .write ("\n " )
171176
172177 # Include module-level docstring
173- modulefile .write (f'''.. automodule:: { module .__name__ }
178+ module_contents .write (f'''.. automodule:: { module .__name__ }
174179
175180''' )
176181
177182 # Generate custom summary table
178183 classes = list (classlist (module ))
179184 if classes :
180- modulefile .write (".. list-table::\n " )
181- modulefile .write (" :header-rows: 1\n " )
182- modulefile .write (" :widths: 30 70\n \n " )
183- modulefile .write (" * - Class\n " )
184- modulefile .write (" - Description\n " )
185-
185+ module_contents .write (".. list-table::\n " )
186+ module_contents .write (" :header-rows: 1\n " )
187+ module_contents .write (" :widths: 30 70\n \n " )
188+ module_contents .write (" * - Class\n " )
189+ module_contents .write (" - Description\n " )
190+
186191 for (classname , classref ) in classes :
187192 if inspect .isclass (classref ):
188193 role = 'py:class'
189194 else :
190195 role = 'py:func'
191-
196+
192197 # Get docstring summary (first line)
193198 doc = inspect .getdoc (classref )
194199 summary = ""
@@ -262,18 +267,18 @@ def generaterst():
262267 if last_backtick > 0 :
263268 truncate_at = summary .rfind (' ' , 0 , last_backtick )
264269 summary = summary [:truncate_at ] + "..."
265-
266- modulefile .write (f" * - :{ role } :`{ inspect .getmodule (classref ).__name__ } .{ classname } `\n " )
267- modulefile .write (f" - { summary } \n " )
268270
271+ module_contents .write (f" * - :{ role } :`{ inspect .getmodule (classref ).__name__ } .{ classname } `\n " )
272+ module_contents .write (f" - { summary } \n " )
273+
274+
275+ module_contents .write ('\n \n ' )
269276
270- modulefile .write (f'''\n \n ''' )
271-
272277 # Generate individual class sections with proper headers
273278 for (classname , classref ) in classes :
274279 # Only include classes that actually belong to this module
275280 if inspect .getmodule (classref ).__name__ == module .__name__ :
276- modulefile .write (f'''{ classname }
281+ module_contents .write (f'''{ classname }
277282{ "-" * len (classname )}
278283
279284.. autoclass:: { module .__name__ } .{ classname }
@@ -282,14 +287,31 @@ def generaterst():
282287 :show-inheritance:
283288
284289''' )
285- modulefile .write (stats )
286- modulefile .close ()
287-
288- pythonrst .write (stats )
289- pythonrst .close ()
290-
291-
292- cleansource ()
290+ module_contents .write (stats )
291+
292+ new_module_contents = module_contents .getvalue ()
293+ module_contents .close ()
294+ module_file = Path (filename )
295+ # Only write to the module file if the contents are different
296+ if not module_file .is_file () or module_file .read_text () != new_module_contents :
297+ module_file .write_text (new_module_contents )
298+ used_rst_files .append (module_file )
299+
300+ module_contents .close ()
301+
302+ index_rst .write (stats )
303+ new_index_contents = index_rst .getvalue ()
304+ index_rst .close ()
305+
306+ index_file = Path ("index.rst" )
307+ if not index_file .is_file () or index_file .read_text () != new_index_contents :
308+ index_file .write_text (new_index_contents )
309+ used_rst_files .append (index_file )
310+
311+ # Remove extra .rst files that weren't used
312+ for rst_file in Path ('.' ).glob ('*.rst' ):
313+ if rst_file not in used_rst_files :
314+ rst_file .unlink ()
293315
294316generaterst ()
295317
0 commit comments