build_reference.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. # Ultralytics YOLO 🚀, AGPL-3.0 license
  2. """
  3. Helper file to build Ultralytics Docs reference section. Recursively walks through ultralytics dir and builds an MkDocs
  4. reference section of *.md files composed of classes and functions, and also creates a nav menu for use in mkdocs.yaml.
  5. Note: Must be run from repository root directory. Do not run from docs directory.
  6. """
  7. import os
  8. import re
  9. from collections import defaultdict
  10. from pathlib import Path
  11. from ultralytics.utils import ROOT
  12. NEW_YAML_DIR = ROOT.parent
  13. CODE_DIR = ROOT
  14. REFERENCE_DIR = ROOT.parent / 'docs/reference'
  15. def extract_classes_and_functions(filepath):
  16. with open(filepath, 'r') as file:
  17. content = file.read()
  18. class_pattern = r'(?:^|\n)class\s(\w+)(?:\(|:)'
  19. func_pattern = r'(?:^|\n)def\s(\w+)\('
  20. classes = re.findall(class_pattern, content)
  21. functions = re.findall(func_pattern, content)
  22. return classes, functions
  23. def create_markdown(py_filepath, module_path, classes, functions):
  24. md_filepath = py_filepath.with_suffix('.md')
  25. # Read existing content and keep header content between first two ---
  26. header_content = ''
  27. if md_filepath.exists():
  28. with open(md_filepath, 'r') as file:
  29. existing_content = file.read()
  30. header_parts = existing_content.split('---')
  31. for part in header_parts:
  32. if 'description:' in part or 'comments:' in part:
  33. header_content += f'---{part}---\n\n'
  34. module_name = module_path.replace('.__init__', '')
  35. module_path = module_path.replace(".", "/")
  36. url = f'https://github.com/ultralytics/ultralytics/blob/main/{module_path}.py'
  37. title_content = (f'# Reference for `{module_path}.py`\n\n'
  38. f'!!! note\n\n'
  39. f' Full source code for this file is available at [{url}]({url}). Help us fix any issues you see by submitting a [Pull Request](https://docs.ultralytics.com/help/contributing/) 🛠️. Thank you 🙏!\n\n')
  40. md_content = [f'---\n## ::: {module_name}.{class_name}\n<br><br>\n' for class_name in classes]
  41. md_content.extend(f'---\n## ::: {module_name}.{func_name}\n<br><br>\n' for func_name in functions)
  42. md_content = header_content + title_content + '\n'.join(md_content)
  43. if not md_content.endswith('\n'):
  44. md_content += '\n'
  45. os.makedirs(os.path.dirname(md_filepath), exist_ok=True)
  46. with open(md_filepath, 'w') as file:
  47. file.write(md_content)
  48. return md_filepath.relative_to(NEW_YAML_DIR)
  49. def nested_dict():
  50. return defaultdict(nested_dict)
  51. def sort_nested_dict(d):
  52. return {
  53. key: sort_nested_dict(value) if isinstance(value, dict) else value
  54. for key, value in sorted(d.items())
  55. }
  56. def create_nav_menu_yaml(nav_items):
  57. nav_tree = nested_dict()
  58. for item_str in nav_items:
  59. item = Path(item_str)
  60. parts = item.parts
  61. current_level = nav_tree['reference']
  62. for part in parts[2:-1]: # skip the first two parts (docs and reference) and the last part (filename)
  63. current_level = current_level[part]
  64. md_file_name = parts[-1].replace('.md', '')
  65. current_level[md_file_name] = item
  66. nav_tree_sorted = sort_nested_dict(nav_tree)
  67. def _dict_to_yaml(d, level=0):
  68. yaml_str = ''
  69. indent = ' ' * level
  70. for k, v in d.items():
  71. if isinstance(v, dict):
  72. yaml_str += f'{indent}- {k}:\n{_dict_to_yaml(v, level + 1)}'
  73. else:
  74. yaml_str += f"{indent}- {k}: {str(v).replace('docs/', '')}\n"
  75. return yaml_str
  76. with open(NEW_YAML_DIR / 'nav_menu_updated.yml', 'w') as file:
  77. yaml_str = _dict_to_yaml(nav_tree_sorted)
  78. file.write(yaml_str)
  79. def main():
  80. nav_items = []
  81. for root, _, files in os.walk(CODE_DIR):
  82. for file in files:
  83. if file.endswith('.py'):
  84. py_filepath = Path(root) / file
  85. classes, functions = extract_classes_and_functions(py_filepath)
  86. if classes or functions:
  87. py_filepath_rel = py_filepath.relative_to(CODE_DIR)
  88. md_filepath = REFERENCE_DIR / py_filepath_rel
  89. module_path = f"ultralytics.{py_filepath_rel.with_suffix('').as_posix().replace('/', '.')}"
  90. md_rel_filepath = create_markdown(md_filepath, module_path, classes, functions)
  91. nav_items.append(str(md_rel_filepath))
  92. create_nav_menu_yaml(nav_items)
  93. if __name__ == '__main__':
  94. main()