From 4b4524291adbc78ab880317124803fc37a2e414a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 6 Jun 2014 20:12:23 +0200 Subject: [PATCH] Converted test management command to argparse Keeping backwards compatibility with test_runner.option_list is tricky and would imply transforming an optparse.Option to an argparse.Action. I choose to introduce a backwards incompatible change because it only affects testing, not runtime behavior. --- django/core/management/commands/test.py | 62 +++++++++++++------------ django/test/runner.py | 24 +++++----- docs/releases/1.8.txt | 11 +++++ docs/topics/testing/advanced.txt | 27 +++++++++-- tests/test_runner/tests.py | 12 ++--- 5 files changed, 86 insertions(+), 50 deletions(-) diff --git a/django/core/management/commands/test.py b/django/core/management/commands/test.py index bf5a4b47ea..8dbff62653 100644 --- a/django/core/management/commands/test.py +++ b/django/core/management/commands/test.py @@ -1,7 +1,6 @@ import logging import sys import os -from optparse import make_option, OptionParser from django.conf import settings from django.core.management.base import BaseCommand @@ -9,26 +8,7 @@ from django.test.utils import get_runner class Command(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option('--noinput', - action='store_false', dest='interactive', default=True, - help='Tells Django to NOT prompt the user for input of any kind.'), - make_option('--failfast', - action='store_true', dest='failfast', default=False, - help='Tells Django to stop running the test suite after first ' - 'failed test.'), - make_option('--testrunner', - action='store', dest='testrunner', - help='Tells Django to use specified test runner class instead of ' - 'the one specified by the TEST_RUNNER setting.'), - make_option('--liveserver', - action='store', dest='liveserver', default=None, - help='Overrides the default address where the live server (used ' - 'with LiveServerTestCase) is expected to run from. The ' - 'default value is localhost:8081.'), - ) - help = ('Discover and run tests in the specified modules or the current directory.') - args = '[path.to.modulename|path.to.modulename.TestCase|path.to.modulename.TestCase.test_method]...' + help = 'Discover and run tests in the specified modules or the current directory.' requires_system_checks = False @@ -49,15 +29,40 @@ class Command(BaseCommand): break super(Command, self).run_from_argv(argv) - def create_parser(self, prog_name, subcommand): - parser = super(Command, self).create_parser(prog_name, subcommand) + def add_arguments(self, parser): + parser.add_argument('args', metavar='test_label', nargs='*', + help='Module paths to test; can be modulename, modulename.TestCase or modulename.TestCase.test_method') + parser.add_argument('--noinput', + action='store_false', dest='interactive', default=True, + help='Tells Django to NOT prompt the user for input of any kind.'), + parser.add_argument('--failfast', + action='store_true', dest='failfast', default=False, + help='Tells Django to stop running the test suite after first ' + 'failed test.'), + parser.add_argument('--testrunner', + action='store', dest='testrunner', + help='Tells Django to use specified test runner class instead of ' + 'the one specified by the TEST_RUNNER setting.'), + parser.add_argument('--liveserver', + action='store', dest='liveserver', default=None, + help='Overrides the default address where the live server (used ' + 'with LiveServerTestCase) is expected to run from. The ' + 'default value is localhost:8081.'), + test_runner_class = get_runner(settings, self.test_runner) - for opt in getattr(test_runner_class, 'option_list', ()): - parser.add_option(opt) - return parser + if hasattr(test_runner_class, 'option_list'): + # Keeping compatibility with both optparse and argparse at this level + # would be too heavy for a non-critical item + raise RuntimeError( + "The method to extend accepted command-line arguments by the " + "test management command has changed in Django 1.8. Please " + "create an add_arguments class method to achieve this.") + + if hasattr(test_runner_class, 'add_arguments'): + test_runner_class.add_arguments(parser) def execute(self, *args, **options): - if int(options['verbosity']) > 0: + if options['verbosity'] > 0: # ensure that deprecation warnings are displayed during testing # the following state is assumed: # logging.capturewarnings is true @@ -67,7 +72,7 @@ class Command(BaseCommand): handler = logging.StreamHandler() logger.addHandler(handler) super(Command, self).execute(*args, **options) - if int(options['verbosity']) > 0: + if options['verbosity'] > 0: # remove the testing-specific handler logger.removeHandler(handler) @@ -76,7 +81,6 @@ class Command(BaseCommand): from django.test.utils import get_runner TestRunner = get_runner(settings, options.get('testrunner')) - options['verbosity'] = int(options.get('verbosity')) if options.get('liveserver') is not None: os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options['liveserver'] diff --git a/django/test/runner.py b/django/test/runner.py index f4d0995835..7ec40f3c66 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -1,6 +1,5 @@ from importlib import import_module import os -from optparse import make_option import unittest from unittest import TestSuite, defaultTestLoader @@ -19,17 +18,6 @@ class DiscoverRunner(object): test_runner = unittest.TextTestRunner test_loader = defaultTestLoader reorder_by = (TestCase, SimpleTestCase) - option_list = ( - make_option('-t', '--top-level-directory', - action='store', dest='top_level', default=None, - help='Top level of project for unittest discovery.'), - make_option('-p', '--pattern', action='store', dest='pattern', - default="test*.py", - help='The test matching pattern. Defaults to test*.py.'), - make_option('-k', '--keepdb', action='store_true', dest='keepdb', - default=False, - help='Preserve the test DB between runs. Defaults to False'), - ) def __init__(self, pattern=None, top_level=None, verbosity=1, interactive=True, failfast=False, keepdb=False, @@ -43,6 +31,18 @@ class DiscoverRunner(object): self.failfast = failfast self.keepdb = keepdb + @classmethod + def add_arguments(cls, parser): + parser.add_argument('-t', '--top-level-directory', + action='store', dest='top_level', default=None, + help='Top level of project for unittest discovery.') + parser.add_argument('-p', '--pattern', action='store', dest='pattern', + default="test*.py", + help='The test matching pattern. Defaults to test*.py.') + parser.add_argument('-k', '--keepdb', action='store_true', dest='keepdb', + default=False, + help='Preserve the test DB between runs. Defaults to False') + def setup_test_environment(self, **kwargs): setup_test_environment() settings.DEBUG = False diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 437c27189f..c54e1d5ab0 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -294,6 +294,17 @@ you don't have to keep compatibility with older Django versions, it's better to implement the new :meth:`~django.core.management.BaseCommand.add_arguments` method as described in :doc:`/howto/custom-management-commands`. +Custom test management command arguments through test runner +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The method to add custom arguments to the `test` management command through the +test runner has changed. Previously, you could provide an `option_list` class +variable on the test runner to add more arguments (à la :py:mod:`optparse`). +Now to implement the same behavior, you have to create an +``add_arguments(cls, parser)`` class method on the test runner and call +``parser.add_argument`` to add any custom arguments, as parser is now an +:py:class:`argparse.ArgumentParser` instance. + Miscellaneous ~~~~~~~~~~~~~ diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index ac6e15557a..46d0913084 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -333,9 +333,15 @@ execute and tear down the test suite. runner, ensure it accepts ``**kwargs``. Your test runner may also define additional command-line options. - If you add an ``option_list`` attribute to a subclassed test runner, - those options will be added to the list of command-line options that - the :djadmin:`test` command can use. + Create or override an ``add_arguments(cls, parser)`` class method and add + custom arguments by calling ``parser.add_argument()`` inside the method, so + that the :djadmin:`test` command will be able to use those arguments. + + .. versionchanged:: 1.8 + + Previously, you had to provide an ``option_list`` attribute to a + subclassed test runner to add options to the list of command-line + options that the :djadmin:`test` command could use. Attributes ~~~~~~~~~~ @@ -372,6 +378,12 @@ Attributes management command's ``OptionParser`` for parsing arguments. See the documentation for Python's ``optparse`` module for more details. + .. deprecated:: 1.8 + + You should now override the :meth:`~DiscoverRunner.add_arguments` class + method to add custom arguments accepted by the :djadmin:`test` + management command. + Methods ~~~~~~~ @@ -389,6 +401,15 @@ Methods This method should return the number of tests that failed. +.. classmethod:: DiscoverRunner.add_arguments(parser) + + .. versionadded:: 1.8 + + Override this class method to add custom arguments accepted by the + :djadmin:`test` management command. See + :py:meth:`argparse.ArgumentParser.add_argument()` for details about adding + arguments to a parser. + .. method:: DiscoverRunner.setup_test_environment(**kwargs) Sets up the test environment by calling diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 2a9337302f..e0faf77ebf 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -3,7 +3,6 @@ Tests for django test runner """ from __future__ import unicode_literals -from optparse import make_option import unittest from django.core.exceptions import ImproperlyConfigured @@ -152,11 +151,6 @@ class ManageCommandTests(unittest.TestCase): class CustomOptionsTestRunner(runner.DiscoverRunner): - option_list = ( - make_option('--option_a', '-a', action='store', dest='option_a', default='1'), - make_option('--option_b', '-b', action='store', dest='option_b', default='2'), - make_option('--option_c', '-c', action='store', dest='option_c', default='3'), - ) def __init__(self, verbosity=1, interactive=True, failfast=True, option_a=None, option_b=None, option_c=None, **kwargs): super(CustomOptionsTestRunner, self).__init__(verbosity=verbosity, interactive=interactive, @@ -165,6 +159,12 @@ class CustomOptionsTestRunner(runner.DiscoverRunner): self.option_b = option_b self.option_c = option_c + @classmethod + def add_arguments(cls, parser): + parser.add_argument('--option_a', '-a', action='store', dest='option_a', default='1'), + parser.add_argument('--option_b', '-b', action='store', dest='option_b', default='2'), + parser.add_argument('--option_c', '-c', action='store', dest='option_c', default='3'), + def run_tests(self, test_labels, extra_tests=None, **kwargs): print("%s:%s:%s" % (self.option_a, self.option_b, self.option_c))