diff --git a/python/helpers/pycharm/django_manage_commands_provider/_optparse.py b/python/helpers/pycharm/django_manage_commands_provider/_optparse.py deleted file mode 100644 index 1ef84ab96dd2..000000000000 --- a/python/helpers/pycharm/django_manage_commands_provider/_optparse.py +++ /dev/null @@ -1,53 +0,0 @@ -# coding=utf-8 -""" -Exports data from optparse-based manage.py commands and reports it to _xml.XmlDumper. -This module encapsulates Django semi-public API knowledge, and not very stable because of it. -""" -from optparse import Option -import django -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.core.management import ManagementUtility, get_commands, BaseCommand - -__author__ = 'Ilya.Kazakevich' - - -def report_data(dumper): - """ - Fetches data from management commands and reports it to dumper. - - :type dumper _xml.XmlDumper - :param dumper: destination to report - """ - utility = ManagementUtility() - for command_name in get_commands().keys(): - try: - command = utility.fetch_command(command_name) - except ImproperlyConfigured: - continue # TODO: Log somehow - - assert isinstance(command, BaseCommand) - dumper.start_command(command_name=command_name, - command_help_text=str(command.usage("").replace("%prog", command_name)), - # TODO: support subcommands - command_args_text=str(command.args)) - for opt in command.option_list: - num_of_args = int(opt.nargs) if opt.nargs else 0 - opt_type = None - if num_of_args > 0: - # If option accepts arg, we need to determine its type. It could be int, choices, or something other - # See https://docs.python.org/2/library/optparse.html#standard-option-types - if opt.type in ["int", "long"]: - opt_type = "int" - elif opt.choices: - assert isinstance(opt.choices, list), "Choices should be list" - opt_type = opt.choices - - # There is no official way to access this field, so I use protected one. At least it is public API. - # noinspection PyProtectedMember - dumper.add_command_option( - long_opt_names=opt._long_opts, - short_opt_names=opt._short_opts, - help_text=opt.help, - argument_info=(num_of_args, opt_type) if num_of_args else None) - dumper.close_command() \ No newline at end of file diff --git a/python/helpers/pycharm/django_manage_commands_provider/_parser/__init__.py b/python/helpers/pycharm/django_manage_commands_provider/_parser/__init__.py new file mode 100644 index 000000000000..ed2654ecf306 --- /dev/null +++ b/python/helpers/pycharm/django_manage_commands_provider/_parser/__init__.py @@ -0,0 +1,6 @@ +# coding=utf-8 +""" +This package hides differences between argparse and optparse. +Use "parser" module as entry point +""" +__author__ = 'Ilya.Kazakevich' diff --git a/python/helpers/pycharm/django_manage_commands_provider/_parser/_argparse.py b/python/helpers/pycharm/django_manage_commands_provider/_parser/_argparse.py new file mode 100644 index 000000000000..b5bceee3260a --- /dev/null +++ b/python/helpers/pycharm/django_manage_commands_provider/_parser/_argparse.py @@ -0,0 +1,50 @@ +# coding=utf-8 +""" +Fetches arguments from argparse-based Django (1.8+) +""" +from argparse import Action, _StoreTrueAction, _StoreFalseAction + +from _parser import _utils + + +__author__ = 'Ilya.Kazakevich' + + +# noinspection PyUnusedLocal +# Command here by contract +def process_command(dumper, command, parser): + """ + Fetches arguments and options from command and parser and reports em to dumper. + + :param dumper dumper to output data to + :param parser arg parser to use + :param command django command + + :type dumper _xml.XmlDumper + :type parser argparse.ArgumentParser + :type command django.core.management.base.BaseCommand + """ + + argument_names = [] + # No public API to fetch actions from argparse. + # noinspection PyProtectedMember + for action in parser._actions: + assert isinstance(action, Action) + if action.option_strings: + # Long opts start with -- + long_opt_names = set(filter(lambda opt_name: str(opt_name).startswith("--"), action.option_strings)) + # All other opts are short + short_opt_names = set(action.option_strings) - long_opt_names + argument_info = None + # The only difference between bool option and argument-based option is the one has store=True + bool_option = isinstance(action, _StoreTrueAction) or isinstance(action, _StoreFalseAction) + if not bool_option: + # TODO: Support nargs. It can be +, ?, * and number. Not only 1. + argument_info = (1, _utils.get_opt_type(action)) + + dumper.add_command_option(long_opt_names, short_opt_names, str(action.help), argument_info) + else: + # TODO: Fetch optionality/mandatority from argument info because it has nargs field + argument_names.append("[" + str(action.metavar if action.metavar else action.dest) + "]") + + dumper.set_arguments(" ".join(argument_names)) \ No newline at end of file diff --git a/python/helpers/pycharm/django_manage_commands_provider/_parser/_optparse.py b/python/helpers/pycharm/django_manage_commands_provider/_parser/_optparse.py new file mode 100644 index 000000000000..31ffcc58f2ed --- /dev/null +++ b/python/helpers/pycharm/django_manage_commands_provider/_parser/_optparse.py @@ -0,0 +1,37 @@ +# coding=utf-8 +""" +Fetches arguments from optparse-based Django (< 1.8) +""" +__author__ = 'Ilya.Kazakevich' +from _parser import _utils + + +# noinspection PyUnusedLocal +# Parser here by contract +def process_command(dumper, command, parser): + """ + Fetches arguments and options from command and parser and reports em to dumper. + + :param dumper dumper to output data to + :param parser opt parser to use + :param command django command + + :type dumper _xml.XmlDumper + :type parser optparse.OptionParser + :type command django.core.management.base.BaseCommand + """ + dumper.set_arguments(command.args) + # TODO: support subcommands + for opt in command.option_list: + num_of_args = int(opt.nargs) if opt.nargs else 0 + opt_type = None + if num_of_args > 0: + opt_type = _utils.get_opt_type(opt) + + # There is no official way to access this field, so I use protected one. At least it is public API. + # noinspection PyProtectedMember + dumper.add_command_option( + long_opt_names=opt._long_opts, + short_opt_names=opt._short_opts, + help_text=opt.help, + argument_info=(num_of_args, opt_type) if num_of_args else None) diff --git a/python/helpers/pycharm/django_manage_commands_provider/_parser/_utils.py b/python/helpers/pycharm/django_manage_commands_provider/_parser/_utils.py new file mode 100644 index 000000000000..1ced921cb3c7 --- /dev/null +++ b/python/helpers/pycharm/django_manage_commands_provider/_parser/_utils.py @@ -0,0 +1,22 @@ +# coding=utf-8 +""" +Internal package tools shared between argparse and optparse +""" +__author__ = 'Ilya.Kazakevich' + + +def get_opt_type(opt): + """ + If option accepts arg, we need to determine its type. It could be int, choices, or something other. + Accepts option (from arg or opt) and returns its type (as scalar or list in case of choices). + Arg should have "type" and "choices" field. + + :param opt option or action from argparse or optparse that has type and choices field + :return: type + """ + if opt.type in ["int", "long"]: + return "int" + elif opt.choices: + assert isinstance(opt.choices, list), "Choices should be list" + return opt.choices + return "str" diff --git a/python/helpers/pycharm/django_manage_commands_provider/_parser/parser.py b/python/helpers/pycharm/django_manage_commands_provider/_parser/parser.py new file mode 100644 index 000000000000..c11ac3919a3d --- /dev/null +++ b/python/helpers/pycharm/django_manage_commands_provider/_parser/parser.py @@ -0,0 +1,41 @@ +# coding=utf-8 +""" +Exports data from optparse or argparse based manage.py commands and reports it to _xml.XmlDumper. +This module encapsulates Django semi-public API knowledge, and not very stable because of it. +""" +from django.core.exceptions import ImproperlyConfigured +from django.core.management import ManagementUtility, get_commands, BaseCommand + +from _parser import _optparse, _argparse + + +__author__ = 'Ilya.Kazakevich' + + +def report_data(dumper): + """ + Fetches data from management commands and reports it to dumper. + + :type dumper _xml.XmlDumper + :param dumper: destination to report + """ + utility = ManagementUtility() + for command_name in get_commands().keys(): + try: + command = utility.fetch_command(command_name) + except ImproperlyConfigured: + continue # TODO: Log somehow + + assert isinstance(command, BaseCommand) + + use_argparse = False + try: + use_argparse = command.use_argparse + except AttributeError: + pass + dumper.start_command(command_name=command_name, + command_help_text=str(command.usage("").replace("%prog", command_name))) + module_to_use = _argparse if use_argparse else _optparse # Choose appropriate module: argparse, optparse + module_to_use.process_command(dumper, command, command.create_parser("", command_name)) + dumper.close_command() + diff --git a/python/helpers/pycharm/django_manage_commands_provider/_xml.py b/python/helpers/pycharm/django_manage_commands_provider/_xml.py index 4f034026dd4d..c2cebfb4df50 100644 --- a/python/helpers/pycharm/django_manage_commands_provider/_xml.py +++ b/python/helpers/pycharm/django_manage_commands_provider/_xml.py @@ -19,6 +19,8 @@ It does not have schema (yet!) but here is XML format it uses. Classes like DjangoCommandsInfo is used on Java side. +TODO: Since Django 1.8 we can fetch much more info from argparse like positional argument names, nargs etc. Use it! + """ from xml.dom import minidom from xml.dom.minidom import Element @@ -60,17 +62,16 @@ class XmlDumper(object): """ for value in values: tag = self.__document.createElement(tag_name) - text = self.__document.createTextNode(value) + text = self.__document.createTextNode(str(value)) tag.appendChild(text) parent.appendChild(tag) - def start_command(self, command_name, command_help_text, command_args_text): + def start_command(self, command_name, command_help_text): """ Starts manage command :param command_name: command name :param command_help_text: command help - :param command_args_text: command text for args """ @@ -78,9 +79,20 @@ class XmlDumper(object): self.__command_element = self.__document.createElement(XmlDumper.__command_info_tag) self.__command_element.setAttribute("name", command_name) self.__command_element.setAttribute("help", command_help_text) - self.__command_element.setAttribute("args", command_args_text) self.__root.appendChild(self.__command_element) + def set_arguments(self, command_args_text): + """ + Adds "arguments help" to command. + + TODO: Use real list of arguments instead of this text when people migrate to argparse (Dj. 1.8) + + :param command_args_text: command text for args + :type command_args_text str + """ + assert bool(self.__command_element), "Not in a a command" + self.__command_element.setAttribute("args", command_args_text) + def add_command_option(self, long_opt_names, short_opt_names, help_text, argument_info): """ Adds command option @@ -93,10 +105,10 @@ class XmlDumper(object): :param short_opt_names: list of short opt names :param help_text: help text - :type long_opt_names list of str - :type short_opt_names list of str + :type long_opt_names iterable of str + :type short_opt_names iterable of str :type help_text str - :type argument_info tuple + :type argument_info tuple or None """ assert isinstance(self.__command_element, Element), "Add option in command only" diff --git a/python/helpers/pycharm/django_manage_commands_provider/provider.py b/python/helpers/pycharm/django_manage_commands_provider/provider.py index bf0ca6e8fa69..8f8d3d80125c 100644 --- a/python/helpers/pycharm/django_manage_commands_provider/provider.py +++ b/python/helpers/pycharm/django_manage_commands_provider/provider.py @@ -1,25 +1,23 @@ # coding=utf-8 """ This is an entry point of this helper. -It fetches data from Django manage commands via _optparse module and report is via _xml module. +It fetches data from Django manage commands delegating calles to _parser package report it via _xml module. See _xml module and readme.txt for more info. Module can be called directly, but be sure env var DJANGO_SETTINGS_MODULE is set to something like "mysite.settings" """ -from distutils.version import LooseVersion import django -import _optparse + +from _parser import parser import _xml + __author__ = 'Ilya.Kazakevich' -# TODO: Support Django 1.8 as well, it uses argparse, not optparse -version = LooseVersion(django.get_version()) -assert version < LooseVersion('1.8a'), "Only Django <1.8 is supported now" # Some django versions require setup if hasattr(django, 'setup'): django.setup() dumper = _xml.XmlDumper() -_optparse.report_data(dumper) +parser.report_data(dumper) print(dumper.xml) \ No newline at end of file