Spaces:
Sleeping
Sleeping
| """Base utilities for documentation generation.""" | |
| import re | |
| from collections.abc import Sequence | |
| from typing import TYPE_CHECKING, Any | |
| if TYPE_CHECKING: | |
| from cyclopts.core import App | |
| from cyclopts.help import HelpPanel | |
| from cyclopts.command_spec import CommandSpec | |
| from cyclopts.help import format_doc, format_usage | |
| def should_show_usage(app: "App") -> bool: | |
| """Determine if usage should be shown for an app. | |
| Root apps always show usage (even without default_command, showing "app COMMAND"). | |
| Subcommands only show usage if they have a default_command. | |
| This skips usage for command groups that can't be invoked directly. | |
| The determination is made by checking the app_stack depth: | |
| - Stack length of 1 means root app (just the initial frame) | |
| - Stack length > 1 means we're in a subcommand context (frames were pushed) | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance to check. | |
| Returns | |
| ------- | |
| bool | |
| True if usage should be shown. | |
| """ | |
| # Check if we're in a subcommand context by examining the stack depth | |
| is_root = len(app.app_stack.stack) == 1 | |
| if is_root: | |
| # Root app: always show usage | |
| return True | |
| else: | |
| # Subcommand: only show if it has a default_command | |
| return app.default_command is not None | |
| def should_show_commands_list(app: "App") -> bool: | |
| """Determine if commands list should be shown for an app. | |
| Only show commands list for apps with a default_command. | |
| Command groups (apps without default_command) skip the list | |
| since their commands will be documented recursively anyway. | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance to check. | |
| Returns | |
| ------- | |
| bool | |
| True if commands list should be shown. | |
| """ | |
| return app.default_command is not None | |
| def _is_builtin_flag(app: "App", name: str) -> bool: | |
| """Check if a flag name is a built-in help or version flag. | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance to check against. | |
| name : str | |
| The flag name to check. | |
| Returns | |
| ------- | |
| bool | |
| True if this is a built-in help or version flag. | |
| """ | |
| help_flags = set(app.app_stack.resolve("help_flags", fallback=())) | |
| version_flags = set(app.app_stack.resolve("version_flags", fallback=())) | |
| builtin_flags = help_flags | version_flags | |
| return name in builtin_flags | |
| def is_all_builtin_flags(app: "App", names: Sequence[str]) -> bool: | |
| """Check if all names in the sequence are builtin help or version flags. | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance to check against. | |
| names : Sequence[str] | |
| Sequence of flag names to check. | |
| Returns | |
| ------- | |
| bool | |
| True if all names are builtin flags. | |
| """ | |
| if not names: | |
| return False | |
| return all(_is_builtin_flag(app, name) for name in names) | |
| def normalize_command_filters( | |
| commands_filter: list[str] | None = None, | |
| exclude_commands: list[str] | None = None, | |
| ) -> tuple[set[str] | None, set[str] | None]: | |
| """Normalize command filter lists by converting underscores to dashes. | |
| Parameters | |
| ---------- | |
| commands_filter : list[str] | None | |
| List of commands to include. | |
| exclude_commands : list[str] | None | |
| List of commands to exclude. | |
| Returns | |
| ------- | |
| tuple[set[str] | None, set[str] | None] | |
| Normalized include and exclude sets for O(1) lookup. | |
| """ | |
| normalized_include = None | |
| if commands_filter is not None: | |
| normalized_include = {cmd.replace("_", "-") for cmd in commands_filter} | |
| normalized_exclude = None | |
| if exclude_commands: | |
| normalized_exclude = {cmd.replace("_", "-") for cmd in exclude_commands} | |
| return normalized_include, normalized_exclude | |
| def should_include_command( | |
| name: str, | |
| parent_path: list[str], | |
| normalized_commands_filter: set[str] | None, | |
| normalized_exclude_commands: set[str] | None, | |
| subapp: "App", | |
| ) -> bool: | |
| """Determine if a command should be included based on filters. | |
| Parameters | |
| ---------- | |
| name : str | |
| The command name. | |
| parent_path : list[str] | |
| Path to parent commands. | |
| normalized_commands_filter : set[str] | None | |
| Set of commands to include (already normalized). | |
| normalized_exclude_commands : set[str] | None | |
| Set of commands to exclude (already normalized). | |
| subapp : App | |
| The subcommand App instance. | |
| Returns | |
| ------- | |
| bool | |
| True if the command should be included, False otherwise. | |
| """ | |
| full_path = ".".join(parent_path + [name]) if parent_path else name | |
| if normalized_exclude_commands: | |
| if name in normalized_exclude_commands or full_path in normalized_exclude_commands: | |
| return False | |
| for i in range(len(parent_path)): | |
| parent_segment = ".".join(parent_path[: i + 1]) | |
| if parent_segment in normalized_exclude_commands: | |
| return False | |
| if normalized_commands_filter is not None: | |
| if name in normalized_commands_filter or full_path in normalized_commands_filter: | |
| return True | |
| for i in range(len(parent_path)): | |
| parent_segment = ".".join(parent_path[: i + 1]) | |
| if parent_segment in normalized_commands_filter: | |
| return True | |
| if hasattr(subapp, "_commands") and subapp._commands: | |
| for filter_cmd in normalized_commands_filter: | |
| if filter_cmd.startswith(full_path + "."): | |
| return True | |
| return False | |
| return True | |
| def adjust_filters_for_subcommand( | |
| name: str, | |
| normalized_commands_filter: set[str] | None, | |
| normalized_exclude_commands: set[str] | None, | |
| ) -> tuple[list[str] | None, list[str] | None]: | |
| """Adjust filter lists for subcommand context. | |
| Parameters | |
| ---------- | |
| name : str | |
| The current command name. | |
| normalized_commands_filter : set[str] | None | |
| Set of commands to include (already normalized). | |
| normalized_exclude_commands : set[str] | None | |
| Set of commands to exclude (already normalized). | |
| Returns | |
| ------- | |
| tuple[list[str] | None, list[str] | None] | |
| Adjusted commands_filter and exclude_commands lists (denormalized). | |
| """ | |
| sub_commands_filter = None | |
| if normalized_commands_filter is not None: | |
| sub_commands_filter = [] | |
| for filter_cmd in normalized_commands_filter: | |
| if filter_cmd.startswith(name + "."): | |
| sub_filter = filter_cmd[len(name) + 1 :] | |
| sub_commands_filter.append(sub_filter.replace("-", "_")) | |
| elif filter_cmd == name: | |
| sub_commands_filter = None | |
| break | |
| if sub_commands_filter is not None and not sub_commands_filter: | |
| sub_commands_filter = [] | |
| sub_exclude_commands = None | |
| if normalized_exclude_commands: | |
| sub_exclude_commands = [] | |
| for exclude_cmd in normalized_exclude_commands: | |
| if exclude_cmd.startswith(name + "."): | |
| sub_exclude = exclude_cmd[len(name) + 1 :] | |
| sub_exclude_commands.append(sub_exclude.replace("-", "_")) | |
| else: | |
| sub_exclude_commands.append(exclude_cmd.replace("-", "_")) | |
| return sub_commands_filter, sub_exclude_commands | |
| def get_app_info(app: "App", command_chain: list[str] | None = None) -> tuple[str, str, str]: | |
| """Get app name, full command path, and title. | |
| Parameters | |
| ---------- | |
| app : App | |
| The cyclopts App instance. | |
| command_chain : Optional[List[str]] | |
| Chain of parent commands leading to this app. | |
| Returns | |
| ------- | |
| Tuple[str, str, str] | |
| (app_name, full_command, title) | |
| """ | |
| if not command_chain: | |
| app_name = app.name[0] | |
| full_command = app_name | |
| title = app_name | |
| else: | |
| app_name = command_chain[0] | |
| full_command = " ".join(command_chain) | |
| title = full_command | |
| return app_name, full_command, title | |
| def build_command_chain(command_chain: list[str] | None, command_name: str, app_name: str) -> list[str]: | |
| """Build command chain for a subcommand. | |
| Parameters | |
| ---------- | |
| command_chain : Optional[List[str]] | |
| Current command chain. | |
| command_name : str | |
| Name of the subcommand. | |
| app_name : str | |
| Name of the root app. | |
| Returns | |
| ------- | |
| List[str] | |
| Updated command chain. | |
| """ | |
| if command_chain: | |
| return command_chain + [command_name] | |
| else: | |
| return [app_name, command_name] | |
| def apply_usage_name(command_chain: list[str], usage_name: str | None) -> list[str]: | |
| """Return a display command chain with the root replaced by ``usage_name``. | |
| When ``usage_name`` is ``None``, returns ``command_chain`` unchanged so callers | |
| can use this helper unconditionally. When ``usage_name`` is ``""``, the root | |
| token is dropped rather than substituted, so downstream formatters never see | |
| an empty element (which would render as stray leading/internal whitespace). | |
| When the chain is empty and ``usage_name`` is a non-empty string, returns a | |
| single-element list containing ``usage_name``. | |
| Parameters | |
| ---------- | |
| command_chain : list[str] | |
| The logical command chain (root app name first). | |
| usage_name : str | None | |
| Replacement for the chain's root element used in Usage: lines only. | |
| An empty string drops the root token entirely. | |
| Returns | |
| ------- | |
| list[str] | |
| A new list with the root replaced/dropped, or the original chain when | |
| ``usage_name`` is ``None``. | |
| """ | |
| if usage_name is None: | |
| return command_chain | |
| if usage_name == "": | |
| return command_chain[1:] | |
| if not command_chain: | |
| return [usage_name] | |
| return [usage_name, *command_chain[1:]] | |
| def generate_anchor(command_path: str) -> str: | |
| """Generate a URL-friendly anchor from a command path. | |
| Converts spaces to hyphens and lowercases the string to match | |
| how markdown/HTML processors generate anchors from headings. | |
| Strips leading dashes to match markdown processor behavior. | |
| Parameters | |
| ---------- | |
| command_path : str | |
| Full command path (e.g., "myapp files cp"). | |
| Returns | |
| ------- | |
| str | |
| Anchor string (e.g., "myapp-files-cp"). | |
| Examples | |
| -------- | |
| >>> generate_anchor("myapp files cp") | |
| 'myapp-files-cp' | |
| >>> generate_anchor("myapp --install-completion") | |
| 'myapp-install-completion' | |
| """ | |
| anchor = command_path.lower().replace(" ", "-") | |
| # Collapse consecutive dashes to single dash (markdown processors do this) | |
| anchor = re.sub(r"-+", "-", anchor) | |
| return anchor | |
| def should_skip_command(command_name: str, subapp: "App", parent_app: "App", include_hidden: bool) -> bool: | |
| """Check if a command should be skipped. | |
| Parameters | |
| ---------- | |
| command_name : str | |
| Name of the command. | |
| subapp : App | |
| The subcommand App instance. | |
| parent_app : App | |
| The parent App instance. | |
| include_hidden : bool | |
| Whether to include hidden commands. | |
| Returns | |
| ------- | |
| bool | |
| True if command should be skipped. | |
| """ | |
| if _is_builtin_flag(parent_app, command_name): | |
| return True | |
| if not isinstance(subapp, type(parent_app)): | |
| return True | |
| if not include_hidden and not subapp.show: | |
| return True | |
| return False | |
| def filter_help_entries(app: "App", panel: "HelpPanel", include_hidden: bool) -> list[Any]: | |
| """Filter help panel entries based on visibility settings. | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance to check against. | |
| panel : HelpPanel | |
| The help panel to filter. | |
| include_hidden : bool | |
| Whether to include hidden entries. | |
| Returns | |
| ------- | |
| List[Any] | |
| Filtered panel entries. | |
| """ | |
| if include_hidden: | |
| return panel.entries | |
| return [e for e in panel.entries if not (e.names and is_all_builtin_flags(app, e.names))] | |
| def extract_description(app: "App", help_format: str) -> Any | None: | |
| """Extract app description. | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance. | |
| help_format : str | |
| Help format type. | |
| Returns | |
| ------- | |
| Optional[Any] | |
| The extracted description object, or None. | |
| """ | |
| description = format_doc(app, help_format) | |
| return description | |
| def extract_usage(app: "App") -> Any | None: | |
| """Extract usage string. | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance. | |
| Returns | |
| ------- | |
| Optional[Any] | |
| The extracted usage object, or None. | |
| """ | |
| if app.usage is not None: | |
| return app.usage if app.usage else None | |
| usage = format_usage(app, []) | |
| return usage | |
| def format_usage_line(usage_text: str, command_chain: list[str], prefix: str = "") -> str: | |
| """Format usage line with proper command path. | |
| Parameters | |
| ---------- | |
| usage_text : str | |
| Raw usage text. | |
| command_chain : List[str] | |
| Command chain for the app. | |
| prefix : str | |
| Optional prefix for the usage line (e.g., "$"). | |
| Returns | |
| ------- | |
| str | |
| Formatted usage line. | |
| """ | |
| if not usage_text: | |
| return "" | |
| if "Usage:" in usage_text: | |
| usage_text = usage_text.replace("Usage:", "").strip() | |
| full_command = " ".join(command_chain) if command_chain else "" | |
| parts = usage_text.split(None, 1) | |
| if len(parts) > 1 and command_chain: | |
| usage_line = f"{prefix} {full_command} {parts[1]}" if prefix else f"{full_command} {parts[1]}" | |
| elif command_chain: | |
| usage_line = f"{prefix} {full_command}" if prefix else full_command | |
| else: | |
| usage_line = f"{prefix} {usage_text}" if prefix else usage_text | |
| return usage_line.strip() | |
| def iterate_commands(app: "App", include_hidden: bool = False, resolve_lazy: bool = True): | |
| """Iterate through app commands, yielding valid resolved subapps. | |
| Automatically resolves CommandSpec instances to App instances. | |
| Each unique subapp is yielded only once (first occurrence wins). | |
| Parameters | |
| ---------- | |
| app : App | |
| The App instance. | |
| include_hidden : bool | |
| Whether to include hidden commands. | |
| resolve_lazy : bool | |
| If ``True`` (default), resolve lazy commands (import their modules) to | |
| include them in the output. If ``False``, skip unresolved lazy commands. | |
| Set to ``True`` when generating static artifacts that need all commands, | |
| such as documentation or shell completion scripts. | |
| Yields | |
| ------ | |
| Tuple[str, App] | |
| (command_name, resolved_subapp) for each valid command. | |
| """ | |
| if not app._commands: | |
| return | |
| seen: set[int] = set() | |
| for name, app_or_spec in app._commands.items(): | |
| if _is_builtin_flag(app, name): | |
| continue | |
| if isinstance(app_or_spec, CommandSpec): | |
| if not app_or_spec.is_resolved and not resolve_lazy: | |
| continue | |
| subapp = app_or_spec.resolve(app) | |
| else: | |
| subapp = app_or_spec | |
| if not isinstance(subapp, type(app)): | |
| continue | |
| if not include_hidden and not subapp.show: | |
| continue | |
| # Skip if we've already yielded this app (alias) | |
| app_id = id(subapp) | |
| if app_id in seen: | |
| continue | |
| seen.add(app_id) | |
| yield name, subapp | |