Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New script to generate the command docs #4664

Merged
merged 15 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/4664.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changes:
- description: Added new script and a pre-commit hook to generate commands documentation.
Shellyber marked this conversation as resolved.
Show resolved Hide resolved
type: internal
pr_number: 4664
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ repos:
- id: mypy
exclude: tests/.*|demisto_sdk/commands/init/templates/.*
language: system
- repo: local
hooks:
- id: generate-command-docs
name: Generate Documentation for Changed Commands # Required when using local hooks
entry: generate-command-docs
language: python
files: ^.*_setup\.py$
pass_filenames: false
7 changes: 7 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,10 @@
entry: prevent-mypy-global-ignore
language: python
files: .*Packs/.*/(?:Integrations|Scripts)/.*.py$

- id: generate-command-docs
name: Generate Documentation for Changed Commands
description: Generates documentation for commands when a `_setup.py` file is modified.
entry: generate-command-docs
language: python
files: ^.*_setup\.py$
199 changes: 199 additions & 0 deletions demisto_sdk/scripts/generate_commands_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env python3

import inspect
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import List, Optional, Union

import typer
from typer.main import get_command

os.environ["DEMISTO_SDK_IGNORE_CONTENT_WARNING"] = "True"

from demisto_sdk.__main__ import app

# Initialize Typer app
docs_app = typer.Typer()

EXCLUDED_BRANCHES_REGEX = r"^(master|[0-9]+\.[0-9]+\.[0-9]+)$"


def get_current_branch() -> str:
"""Returns the current Git branch name."""
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True
)
return result.stdout.strip()


def get_modified_files() -> List[Path]:
"""
Returns a list of files modified in the current commit as Path objects.
If no files are modified, it returns an empty list.
"""
result = subprocess.run(
["git", "diff", "--cached", "--name-only"], capture_output=True, text=True
)
files = result.stdout.splitlines()
return [Path(file) for file in files]


def extract_changed_commands(modified_files: List[Union[str, Path]]) -> List[str]:
"""Extract command names from modified _setup.py files."""
return [
Path(file).stem.replace("_setup", "").replace("_", "-")
for file in modified_files
if str(file).endswith("_setup.py")
]


def get_sdk_command(command_name: str) -> Union[str, object]:
"""Retrieve the command object from the Typer app."""
click_app = get_command(app)
command = click_app.commands.get(command_name) # type: ignore[attr-defined]
if command is None:
return f"No README found for command: {command_name}"
return command


def get_command_overview(command_name: str) -> str:
"""Retrieve the overview (docstring) for the command."""
command = get_sdk_command(command_name)

if isinstance(command, str):
typer.secho(f"Error: {command}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)

command_func = command.callback # type: ignore[attr-defined]
return inspect.getdoc(command_func) or "No overview provided."


def get_command_options(command_name: str) -> str:
"""Generate the options section for the command."""
command = get_sdk_command(command_name)
if isinstance(command, str):
return command

options_text = ""
for param in command.params: # type: ignore[attr-defined]
param_name = (
f"--{param.name.replace('_', '-')}"
if param.param_type_name == "option"
else param.name
)
options_text += f"- **{param_name}**: {param.help or ''}\n"
if param.default is not None:
options_text += f" - Default: `{param.default}`\n"
options_text += "\n"
return options_text


def update_readme(command_name: str, description: str, options: str) -> None:
"""Update or create the README.md file for the command."""
# Normalize the command name to match the folder naming convention
normalized_command_name = command_name.replace("-", "_")

command_doc_path = (
Path("demisto_sdk") / "commands" / normalized_command_name / "README.md"
)
command_doc_path.parent.mkdir(parents=True, exist_ok=True)

# Read the current README content if it exists, otherwise initialize it
if command_doc_path.exists():
with command_doc_path.open("r") as f:
readme_content = f.read()
else:
readme_content = f"## {command_name}\n"

# Function to update or insert a section in the README
def update_section(header: str, content: str, readme: str) -> str:
"""Update or add a section to the README content."""
section_header = f"### {header}"

# Check if the section exists
if section_header in readme:
# Replace the content of the existing section
start_index = readme.find(section_header) + len(section_header)
end_index = (
readme.find("###", start_index)
if "###" in readme[start_index:]
else len(readme)
)
readme = (
readme[:start_index] + f"\n\n{content.strip()}\n" + readme[end_index:]
)
else:
# Append the new section
readme = readme.strip() + f"\n\n{section_header}\n\n{content.strip()}"

return readme

# Update or add the Overview and Options sections
updated_readme = update_section("Overview", description, readme_content)
updated_readme = update_section("Options", options, updated_readme)

# Write the updated or new README file
with command_doc_path.open("w") as f:
f.write(updated_readme)

print(f"README.md updated for command: {command_name}") # noqa: T201


def generate_docs_for_command(command_name: str) -> None:
"""Generate documentation for a specific command."""
overview = get_command_overview(command_name)
options = get_command_options(command_name)
update_readme(command_name, overview, options)


# @command_docs.command()
def generate_docs(modified_files: Optional[List[Path]] = typer.Argument(None)) -> None:
"""
Generate documentation for the given list of modified files.

If no files are provided, the script will check Git for modified `_setup.py` files.
"""
# Check if modified_files is None, and if so, get the modified files from git
if not modified_files:
modified_files = get_modified_files()
else:
# Ensure that modified_files is a list of Path objects
modified_files = [Path(file) for file in modified_files]
changed_commands = extract_changed_commands(modified_files) # type: ignore[arg-type]
if not changed_commands:
print("No modified commands detected.") # noqa: T201
return

print(f"Generating documentation for modified commands: {changed_commands}") # noqa: T201
for command_name in changed_commands:
generate_docs_for_command(command_name)

print("Documentation generation and Git commits completed.") # noqa: T201


@docs_app.command()
def pre_commit() -> None:
"""
Pre-commit hook to generate docs for changed commands.
"""
current_branch = get_current_branch()
if re.match(EXCLUDED_BRANCHES_REGEX, current_branch):
print(f"Generate docs pre-commit hook skipped on branch '{current_branch}'") # noqa: T201
sys.exit(0)

# Get the modified files (no need to pass as an argument)
modified_files = get_modified_files()

# Call generate_docs with the list of modified files
generate_docs(modified_files)


def main():
docs_app()


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ init-validation = "demisto_sdk.scripts.init_validation_script:main"
validate-deleted-files = "demisto_sdk.scripts.validate_deleted_files:main"
validate-file-permission-changes = "demisto_sdk.scripts.validate_file_permission_changes:main"
prevent-mypy-global-ignore = "demisto_sdk.scripts.prevent_mypy_global_ignore:main"
generate-command-docs = "demisto_sdk.scripts.generate_commands_docs:main"

[tool.ruff]
exclude = [
Expand Down
Loading