Initial support for argparse (django 1.8) for PY-11855 (manage.py).

This commit is contained in:
Ilya.Kazakevich
2015-04-08 21:29:21 +03:00
parent 55620c2463
commit 9df9071511
8 changed files with 180 additions and 67 deletions
@@ -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()
@@ -0,0 +1,6 @@
# coding=utf-8
"""
This package hides differences between argparse and optparse.
Use "parser" module as entry point
"""
__author__ = 'Ilya.Kazakevich'
@@ -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))
@@ -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)
@@ -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"
@@ -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()
@@ -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"
@@ -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)