Blob Blame History Raw
From 5c936915b3964e7f71c568219693e43f319b50ca Mon Sep 17 00:00:00 2001
From: John Whitlock <John-Whitlock@ieee.org>
Date: Wed, 8 Apr 2015 17:19:43 -0500
Subject: [PATCH] Convert nose optparse options to argparse

When django.core.management.base.BaseCommand includes 'use_argparse',
then nose's optparse options are merged using argparse's
parser.add_argument in BaseCommand's overriden add_arguments method.

For Django 1.7 and earlier, the current .options method is used to set
the options.

Fixes #178.
---
 django_nose/runner.py | 134 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 126 insertions(+), 8 deletions(-)

diff --git a/django_nose/runner.py b/django_nose/runner.py
index b99d7fb..b30fdb3 100644
--- a/django_nose/runner.py
+++ b/django_nose/runner.py
@@ -143,7 +143,132 @@ def _get_options():
                                        o.action != 'help')
 
 
-class BasicNoseRunner(DiscoverRunner):
+if hasattr(BaseCommand, 'use_argparse'):
+    # Django 1.8 and later uses argparse.ArgumentParser
+    # Translate nose optparse arguments to argparse
+    class BaseRunner(DiscoverRunner):
+
+        # Don't pass the following options to nosetests
+        django_opts = [
+            '--noinput', '--liveserver', '-p', '--pattern', '--testrunner',
+            '--settings']
+
+        #
+        # For optparse -> argparse conversion
+        #
+        # Option strings to remove from Django options if found
+        _argparse_remove_options = (
+            '-p',  # Short arg for nose's --plugins, not Django's --patterns
+            '-d',  # Short arg for nose's --detailed-errors, not Django's
+                   #  --debug-sql
+        )
+
+        # Convert nose optparse options to argparse options
+        _argparse_type = {
+            'int': int,
+            'float': float,
+            'complex': complex,
+            'string': str,
+        }
+        # If optparse has a None argument, omit from call to add_argument
+        _argparse_omit_if_none = (
+            'action', 'nargs', 'const', 'default', 'type', 'choices',
+            'required', 'help', 'metavar', 'dest', 'callback', 'callback_args',
+            'callback_kwargs')
+
+        # Translating callbacks is not supported, because none of the built-in
+        # plugins uses one.  If you have a plugin that uses a callback, please
+        # open a ticket or submit a working implementation.
+        _argparse_fail_if_not_none = (
+            'callback', 'callback_args', 'callback_kwargs')
+
+        @classmethod
+        def add_arguments(cls, parser):
+            """Convert nose's optparse arguments to argparse"""
+            super(BaseRunner, cls).add_arguments(parser)
+
+            # Read optparse options for nose and plugins
+            cfg_files = nose.core.all_config_files()
+            manager = nose.core.DefaultPluginManager()
+            config = nose.core.Config(
+                env=os.environ, files=cfg_files, plugins=manager)
+            config.plugins.addPlugins(list(_get_plugins_from_settings()))
+            options = config.getParser()._get_all_options()
+
+            # Gather existing option strings`
+            django_options = set()
+            for action in parser._actions:
+                for override in cls._argparse_remove_options:
+                    if override in action.option_strings:
+                        # Emulate parser.conflict_handler='resolve'
+                        parser._handle_conflict_resolve(
+                            None, ((override, action),))
+                django_options.update(action.option_strings)
+
+            # Process nose optparse options
+            for option in options:
+                # Skip any options also in Django options
+                opt_long = option.get_opt_string()
+                if opt_long in django_options:
+                    continue
+                if option._short_opts:
+                    opt_short = option._short_opts[0]
+                    if opt_short in django_options:
+                        continue
+                else:
+                    opt_short = None
+
+                # Rename nose's --verbosity to --nose-verbosity
+                if opt_long == '--verbosity':
+                    opt_long = '--nose-verbosity'
+
+                # Convert optparse attributes to argparse attributes
+                option_attrs = {}
+                for attr in option.ATTRS:
+                    value = getattr(option, attr)
+
+                    # Rename options for nose's --verbosity
+                    if opt_long == '--nose-verbosity':
+                        if attr == 'dest':
+                            value = 'nose_verbosity'
+                        elif attr == 'metavar':
+                            value = 'NOSE_VERBOSITY'
+
+                    # Omit arguments that are None, use default
+                    if attr in cls._argparse_omit_if_none and value is None:
+                        continue
+
+                    # Translating callbacks is not supported
+                    if attr in cls._argparse_fail_if_not_none:
+                        assert value is None, (
+                            'argparse option %s=%s is not supported' %
+                            (attr, value))
+                        continue
+
+                    # Convert type from optparse string to argparse type
+                    if attr == 'type':
+                        value = cls._argparse_type[value]
+
+                    # Pass converted attribute to optparse option
+                    option_attrs[attr] = value
+
+                # Add the optparse argument
+                if opt_short:
+                    parser.add_argument(opt_short, opt_long, **option_attrs)
+                else:
+                    parser.add_argument(opt_long, **option_attrs)
+else:
+    # Django 1.7 and earlier use optparse
+    class BaseRunner(DiscoverRunner):
+        # Replace the builtin options with the merged django/nose options:
+        options = _get_options()
+
+        # Not add following options to nosetests
+        django_opts = ['--noinput', '--liveserver', '-p', '--pattern',
+            '--testrunner']
+
+
+class BasicNoseRunner(BaseRunner):
     """Facade that implements a nose runner in the guise of a Django runner
 
     You shouldn't have to use this directly unless the additions made by
@@ -153,13 +278,6 @@ class BasicNoseRunner(DiscoverRunner):
     """
     __test__ = False
 
-    # Replace the builtin command options with the merged django/nose options:
-    options = _get_options()
-
-    # Not add following options to nosetests
-    django_opts = ['--noinput', '--liveserver', '-p', '--pattern',
-        '--testrunner']
-
     def run_suite(self, nose_argv):
         result_plugin = ResultPlugin()
         plugins_to_add = [DjangoSetUpPlugin(self),