build_reference.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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 re
  8. import subprocess
  9. from collections import defaultdict
  10. from pathlib import Path
  11. # Constants
  12. hub_sdk = False
  13. if hub_sdk:
  14. PACKAGE_DIR = Path("/Users/glennjocher/PycharmProjects/hub-sdk/hub_sdk")
  15. REFERENCE_DIR = PACKAGE_DIR.parent / "docs/reference"
  16. GITHUB_REPO = "ultralytics/hub-sdk"
  17. else:
  18. FILE = Path(__file__).resolve()
  19. PACKAGE_DIR = FILE.parents[1] / "ultralytics" # i.e. /Users/glennjocher/PycharmProjects/ultralytics/ultralytics
  20. REFERENCE_DIR = PACKAGE_DIR.parent / "docs/en/reference"
  21. GITHUB_REPO = "ultralytics/ultralytics"
  22. def extract_classes_and_functions(filepath: Path) -> tuple:
  23. """Extracts class and function names from a given Python file."""
  24. content = filepath.read_text()
  25. class_pattern = r"(?:^|\n)class\s(\w+)(?:\(|:)"
  26. func_pattern = r"(?:^|\n)def\s(\w+)\("
  27. classes = re.findall(class_pattern, content)
  28. functions = re.findall(func_pattern, content)
  29. return classes, functions
  30. def create_markdown(py_filepath: Path, module_path: str, classes: list, functions: list):
  31. """Creates a Markdown file containing the API reference for the given Python module."""
  32. md_filepath = py_filepath.with_suffix(".md")
  33. exists = md_filepath.exists()
  34. # Read existing content and keep header content between first two ---
  35. header_content = ""
  36. if exists:
  37. existing_content = md_filepath.read_text()
  38. header_parts = existing_content.split("---")
  39. for part in header_parts:
  40. if "description:" in part or "comments:" in part:
  41. header_content += f"---{part}---\n\n"
  42. if not any(header_content):
  43. header_content = "---\ndescription: TODO ADD DESCRIPTION\nkeywords: TODO ADD KEYWORDS\n---\n\n"
  44. module_name = module_path.replace(".__init__", "")
  45. module_path = module_path.replace(".", "/")
  46. url = f"https://github.com/{GITHUB_REPO}/blob/main/{module_path}.py"
  47. edit = f"https://github.com/{GITHUB_REPO}/edit/main/{module_path}.py"
  48. title_content = (
  49. f"# Reference for `{module_path}.py`\n\n"
  50. f"!!! Note\n\n"
  51. f" This file is available at [{url}]({url}). If you spot a problem please help fix it by [contributing]"
  52. f"(https://docs.ultralytics.com/help/contributing/) a [Pull Request]({edit}) 🛠️. Thank you 🙏!\n\n"
  53. )
  54. md_content = ["<br><br>\n"] + [f"## ::: {module_name}.{class_name}\n\n<br><br>\n" for class_name in classes]
  55. md_content.extend(f"## ::: {module_name}.{func_name}\n\n<br><br>\n" for func_name in functions)
  56. md_content = header_content + title_content + "\n".join(md_content)
  57. if not md_content.endswith("\n"):
  58. md_content += "\n"
  59. md_filepath.parent.mkdir(parents=True, exist_ok=True)
  60. md_filepath.write_text(md_content)
  61. if not exists:
  62. # Add new markdown file to the git staging area
  63. print(f"Created new file '{md_filepath}'")
  64. subprocess.run(["git", "add", "-f", str(md_filepath)], check=True, cwd=PACKAGE_DIR)
  65. return md_filepath.relative_to(PACKAGE_DIR.parent)
  66. def nested_dict() -> defaultdict:
  67. """Creates and returns a nested defaultdict."""
  68. return defaultdict(nested_dict)
  69. def sort_nested_dict(d: dict) -> dict:
  70. """Sorts a nested dictionary recursively."""
  71. return {key: sort_nested_dict(value) if isinstance(value, dict) else value for key, value in sorted(d.items())}
  72. def create_nav_menu_yaml(nav_items: list, save: bool = False):
  73. """Creates a YAML file for the navigation menu based on the provided list of items."""
  74. nav_tree = nested_dict()
  75. for item_str in nav_items:
  76. item = Path(item_str)
  77. parts = item.parts
  78. current_level = nav_tree["reference"]
  79. for part in parts[2:-1]: # skip the first two parts (docs and reference) and the last part (filename)
  80. current_level = current_level[part]
  81. md_file_name = parts[-1].replace(".md", "")
  82. current_level[md_file_name] = item
  83. nav_tree_sorted = sort_nested_dict(nav_tree)
  84. def _dict_to_yaml(d, level=0):
  85. """Converts a nested dictionary to a YAML-formatted string with indentation."""
  86. yaml_str = ""
  87. indent = " " * level
  88. for k, v in d.items():
  89. if isinstance(v, dict):
  90. yaml_str += f"{indent}- {k}:\n{_dict_to_yaml(v, level + 1)}"
  91. else:
  92. yaml_str += f"{indent}- {k}: {str(v).replace('docs/en/', '')}\n"
  93. return yaml_str
  94. # Print updated YAML reference section
  95. print("Scan complete, new mkdocs.yaml reference section is:\n\n", _dict_to_yaml(nav_tree_sorted))
  96. # Save new YAML reference section
  97. if save:
  98. (PACKAGE_DIR.parent / "nav_menu_updated.yml").write_text(_dict_to_yaml(nav_tree_sorted))
  99. def main():
  100. """Main function to extract class and function names, create Markdown files, and generate a YAML navigation menu."""
  101. nav_items = []
  102. for py_filepath in PACKAGE_DIR.rglob("*.py"):
  103. classes, functions = extract_classes_and_functions(py_filepath)
  104. if classes or functions:
  105. py_filepath_rel = py_filepath.relative_to(PACKAGE_DIR)
  106. md_filepath = REFERENCE_DIR / py_filepath_rel
  107. module_path = f"{PACKAGE_DIR.name}.{py_filepath_rel.with_suffix('').as_posix().replace('/', '.')}"
  108. md_rel_filepath = create_markdown(md_filepath, module_path, classes, functions)
  109. nav_items.append(str(md_rel_filepath))
  110. create_nav_menu_yaml(nav_items)
  111. if __name__ == "__main__":
  112. main()