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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ repos:
- id: mypy
exclude: tests/.*|demisto_sdk/commands/init/templates/.*
language: system
- repo: local
hooks:
- id: generate-docs
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-docs
name: Generate Documentation for Changed Commands
description: Generates documentation for commands when a _setup.py file is modified.
entry: python pre_commit_generate_docs.py
language: python
files: ^.*_setup\.py$
Empty file.
131 changes: 131 additions & 0 deletions demisto_sdk/scripts/generate_commands_docs/generate_commands_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import inspect
import sys
from pathlib import Path
from typing import List

from typer.main import get_command

from demisto_sdk.__main__ import app


def extract_changed_commands(modified_files: List[str]) -> List[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this duplicate?

"""Extract the command names from the list of modified files."""
changed_commands = []
for file in modified_files:
# Check if the modified file ends with '_setup.py'
if file.endswith("_setup.py"):
command_name = Path(file).stem.replace("_setup", "")
changed_commands.append(command_name)
return changed_commands


def get_sdk_command(command_name: str):
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_description(command_name: str) -> str:
"""Retrieve the description (docstring) for the command."""
command = get_sdk_command(command_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the response is "No README found for command..."?

if isinstance(command, str):
return command

command_func = command.callback
return inspect.getdoc(command_func) or "No description 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 = "### Options\n\n"
for param in command.params:
param_name = (
f"--{param.name.replace('_', '-')}"
if param.param_type_name == "option"
else param.name
)
options_text += (
f"- **{param_name}**: {param.help or 'No description provided'}\n"
)
if param.default is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is not None:? Isn't if param.default enough? if for example it is'' you still don't want the condition to apply right?

options_text += f" - Default: `{param.default}`\n"
options_text += "\n"
return options_text


def update_readme(command_name: str, description: str, options: str):
"""Update the README.md file for the command with the given description and options."""
command_doc_path = Path("demisto_sdk/commands") / command_name / "README.md"

if not command_doc_path.exists():
print(f"README.md not found for command: {command_name}") # noqa: T201
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not generate a new one?

return

# Read the existing README.md
with command_doc_path.open("r") as f:
readme_content = f.read()

# Update the Description section
description_start = readme_content.find("## Description")
if description_start != -1:
description_end = readme_content.find(
"##", description_start + len("## Description")
)
if description_end == -1:
description_end = len(readme_content)
updated_readme = (
readme_content[:description_start]
+ f"## Description\n{description}\n\n"
+ readme_content[description_end:]
)
else:
updated_readme = f"{readme_content}\n## Description\n{description}\n\n"

# Update the Options section
options_start = updated_readme.find("### Options")
if options_start != -1:
options_end = updated_readme.find("##", options_start + len("### Options"))
if options_end == -1:
options_end = len(updated_readme)
updated_readme = (
updated_readme[:options_start] + options + updated_readme[options_end:]
)
else:
updated_readme += "\n" + options

# Write the updated content back into the README.md
with command_doc_path.open("w") as f:
f.write(updated_readme)

print(f"Description and options section updated for command: {command_name}") # noqa: T201


def generate_docs_for_command(command_name: str):
"""Generate documentation for a specific command."""
description = get_command_description(command_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we ok with the description being 'No description provided'? Where is it going be added to?

options = get_command_options(command_name)
update_readme(command_name, description, options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it enough to write the file? what about committing etc?



def main():
if len(sys.argv) < 2:
print("Usage: python generate_docs.py <modified_file1> <modified_file2> ...") # noqa: T201
sys.exit(1)

# Receive the list of modified files from command-line arguments
modified_files = sys.argv[1:]
changed_commands = extract_changed_commands(modified_files)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, you called this script with changed commands (not changed files) - subprocess.run([sys.executable, "generate_docs.py", *changed_commands])
Why are we trying to re-extract the command names?


# Generate documentation for each modified command
for command_name in changed_commands:
generate_docs_for_command(command_name)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import os
import re
import subprocess
import sys
from pathlib import Path

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


def get_current_branch():
"""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():
"""Returns a list of files modified in the current commit."""
result = subprocess.run(["git", "diff", "--cached", "--name-only"], capture_output=True, text=True)
return result.stdout.splitlines()


def extract_changed_commands(modified_files):
"""Extract command names from modified _setup.py files."""
changed_commands = []
for file in modified_files:
if file.endswith("_setup.py"):
command_name = Path(file).stem.replace("_setup", "")
changed_commands.append(command_name)
return changed_commands
Comment on lines +10 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add return argument and return value docstring as well as a return type: (-> str and so on)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True for all functions



def main():
# Check if the branch should be excluded
current_branch = get_current_branch()
if re.match(EXCLUDED_BRANCHES_REGEX, current_branch):
print(f"Pre-commit hook skipped on branch '{current_branch}'")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
print(f"Pre-commit hook skipped on branch '{current_branch}'")
print(f"Generate docs pre-commit hook skipped on branch '{current_branch}'")

sys.exit(0)

# Get the list of modified files
modified_files = get_modified_files()

# Filter for _setup.py files to determine which commands changed
changed_commands = extract_changed_commands(modified_files)
if not changed_commands:
print("No modified _setup.py files found. Skipping documentation generation.")
sys.exit(0)
Comment on lines +44 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it make sense tho? Per the hook config (files: ^.*_setup\.py$) the hook only runs when the setup file change no?


# Run the documentation generation script with all changed commands
print(f"Generating documentation for modified commands: {changed_commands}")
subprocess.run([sys.executable, "generate_docs.py", *changed_commands])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using a subprocess here? why not just import and run the relevant python module?


# Stage the newly generated or updated README files for each command
for command_name in changed_commands:
readme_file = Path("demisto-sdk/commands") / command_name / "README.md"
if readme_file.exists():
subprocess.run(["git", "add", str(readme_file)])

print("Pre-commit hook completed successfully.")


if __name__ == "__main__":
main()
Loading