diff --git a/manage.py b/manage.py index 8be3ed3152..b0dcd5a4d9 100755 --- a/manage.py +++ b/manage.py @@ -2,6 +2,7 @@ import configparser import os import sys +from typing import Dict, List, Optional if sys.version_info <= (3, 0): print("Error: Zulip is a Python 3 project, and cannot be run with Python 2.") @@ -14,8 +15,112 @@ from scripts.lib.setup_path import setup_path setup_path() +from collections import defaultdict + +from django.core.management import ManagementUtility, get_commands +from django.core.management.color import color_style + from scripts.lib.zulip_tools import assert_not_running_as_root + +def get_filtered_commands() -> Dict[str, str]: + """Because Zulip uses management commands in production, `manage.py + help` is a form of documentation for users. Here we exclude from + that documentation built-in commands that are not constructive for + end users or even Zulip developers to run. + + Ideally, we'd do further customization to display management + commands with more organization in the help text, and also hide + development-focused management commands in production. + """ + all_commands = get_commands() + documented_commands = dict() + documented_apps = [ + # "auth" removed because its commands are not applicable to Zulip. + # "contenttypes" removed because we don't use that subsystem, and + # even if we did. + "django.core", + "analytics", + # "otp_static" removed because it's a 2FA internals detail. + # "sessions" removed because it's just a cron job with a misleading + # name, since all it does is delete expired sessions. + # "social_django" removed for similar reasons to sessions. + # "staticfiles" removed because its commands are only usefully run when + # wrapped by Zulip tooling. + # "two_factor" removed because it's a 2FA internals detail. + "zerver", + "zilencer", + ] + documented_command_subsets = { + "django.core": { + "dbshell", + "makemigrations", + "migrate", + "shell", + "showmigrations", + }, + } + for command, app in all_commands.items(): + if app not in documented_apps: + continue + if app in documented_command_subsets: + if command not in documented_command_subsets[app]: + continue + + documented_commands[command] = app + return documented_commands + + +class FilteredManagementUtility(ManagementUtility): + """Replaces the main_help_text function of ManagementUtility with one + that calls our get_filtered_commands(), rather than the default + get_commands() function. + + All other change are just code style differences to pass the Zulip linter. + """ + + def main_help_text(self, commands_only: bool = False) -> str: + """Return the script's main help text, as a string.""" + if commands_only: + usage = sorted(get_filtered_commands()) + else: + usage = [ + "", + f"Type '{self.prog_name} help ' for help on a specific subcommand.", + "", + "Available subcommands:", + ] + commands_dict = defaultdict(lambda: []) + for name, app in get_filtered_commands().items(): + if app == "django.core": + app = "django" + else: + app = app.rpartition(".")[-1] + commands_dict[app].append(name) + style = color_style() + for app in sorted(commands_dict): + usage.append("") + usage.append(style.NOTICE(f"[{app}]")) + for name in sorted(commands_dict[app]): + usage.append(f" {name}") + # Output an extra note if settings are not properly configured + if self.settings_exception is not None: + usage.append( + style.NOTICE( + "Note that only Django core commands are listed " + f"as settings are not properly configured (error: {self.settings_exception})." + ) + ) + + return "\n".join(usage) + + +def execute_from_command_line(argv: Optional[List[str]] = None) -> None: + """Run a FilteredManagementUtility.""" + utility = FilteredManagementUtility(argv) + utility.execute() + + if __name__ == "__main__": assert_not_running_as_root() @@ -38,7 +143,6 @@ if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zproject.settings") from django.conf import settings - from django.core.management import execute_from_command_line from django.core.management.base import CommandError from scripts.lib.zulip_tools import log_management_command