Repository: nose-devs/nose2

Issue:
Enabling [doctest] always-on = true makes nose2 exit with "error: no commands supplied"
I'm trying to migrate check-manifest's test suite from nose to nose2.  I have a couple of doctests I'm trying to include in the test suite, by creating a `unittest2.cfg` with

```ini
[unittest]
plugins = nose2.plugins.doctests

[doctest]
always-on = true
```

However as soon as I do that, running `nose2` with no command-line arguments fails:

```
$ nose2
usage: nose2 [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: nose2 --help [cmd1 cmd2 ...]
   or: nose2 --help-commands
   or: nose2 cmd --help

error: no commands supplied
```

I can comment out the `always-on = true` line and then nose2 runs all the tests (but no doctests).

Comments:

Patch:
diff --git a/nose2/plugins/loader/functions.py b/nose2/plugins/loader/functions.py
index d7b6563..5bd7212 100644
--- a/nose2/plugins/loader/functions.py
+++ b/nose2/plugins/loader/functions.py
@@ -87,6 +87,7 @@ class Functions(Plugin):
 
         parent, obj, name, index = result
         if (isinstance(obj, types.FunctionType) and not
+            isinstance(parent, type) and not
             util.isgenerator(obj) and not
             hasattr(obj, 'paramList') and
             util.num_expected_args(obj) == 0):
diff --git a/nose2/plugins/mp.py b/nose2/plugins/mp.py
index 33f488e..da71f7e 100644
--- a/nose2/plugins/mp.py
+++ b/nose2/plugins/mp.py
@@ -22,7 +22,7 @@ class MultiProcess(events.Plugin):
     configSection = 'multiprocess'
 
     def __init__(self):
-        self.addArgument(self.setProcs, 'N', 'processes', '# o procs')
+        self.addArgument(self.setProcs, 'N', 'processes', 'Number of processes used to run tests (0 = auto)')
         self.testRunTimeout = self.config.as_float('test-run-timeout', 60.0)
         self._procs = self.config.as_int(
             'processes', 0)
@@ -46,7 +46,7 @@ class MultiProcess(events.Plugin):
     def procs(self, value):
         """Setter for procs property"""
         if value < 0:
-            raise AttributeError("Can't set the procs number to less than 0, (0 = Auto)")
+            raise ValueError("Number of processes cannot be less than 0")
         self._procs = value
 
     def setProcs(self, num):


Test patch:
diff --git a/nose2/plugins/doctests.py b/nose2/plugins/doctests.py
index 20a25e6..af16195 100644
--- a/nose2/plugins/doctests.py
+++ b/nose2/plugins/doctests.py
@@ -42,6 +42,9 @@ class DocTestLoader(Plugin):
             return
 
         name, package_path = util.name_from_path(path)
+        # ignore top-level setup.py which cannot be imported
+        if name == 'setup':
+            return
         util.ensure_importable(package_path)
         try:
             module = util.module_from_name(name)
diff --git a/nose2/tests/unit/test_doctest_plugin.py b/nose2/tests/unit/test_doctest_plugin.py
index 79b2967..6ea9344 100644
--- a/nose2/tests/unit/test_doctest_plugin.py
+++ b/nose2/tests/unit/test_doctest_plugin.py
@@ -2,6 +2,8 @@
 import sys
 import doctest
 
+from textwrap import dedent
+
 from nose2 import events, loader, session
 from nose2.plugins import doctests
 from nose2.tests._common import TestCase
@@ -60,6 +62,22 @@ def func():
         else:
             self.assertEqual(event.extraTests, [])
 
+    def test_handle_file_python_setup_py(self):
+        # Test calling handleFile on a top-level setup.py file.
+        # The file should be ignored by the plugin as it cannot safely be
+        # imported.
+
+        setup_py = dedent("""\
+            '''
+            >>> never executed
+            '''
+            from setuptools import setup
+            setup(name='foo')
+            """
+        )
+        event = self._handle_file("setup.py", setup_py)
+        self.assertEqual(event.extraTests, [])
+
     def _handle_file(self, fpath, content):
         """Have plugin handle a file with certain content.
 
diff --git a/nose2/tests/unit/test_functions_loader.py b/nose2/tests/unit/test_functions_loader.py
index cf1d6fd..e6fd25d 100644
--- a/nose2/tests/unit/test_functions_loader.py
+++ b/nose2/tests/unit/test_functions_loader.py
@@ -1,4 +1,11 @@
 import unittest
+
+try:
+    from unittest import mock
+except ImportError:
+    # Python versions older than 3.3 don't have mock by default
+    import mock
+
 from nose2 import events, loader, session
 from nose2.plugins.loader import functions
 from nose2.tests._common import TestCase
@@ -47,3 +54,37 @@ class TestFunctionLoader(TestCase):
         event = events.LoadFromModuleEvent(self.loader, m)
         self.session.hooks.loadTestsFromModule(event)
         self.assertEqual(len(event.extraTests), 0)
+
+    def test_can_load_test_functions_from_name(self):
+        event = events.LoadFromNameEvent(self.loader, __name__+'.func', None)
+        suite = self.session.hooks.loadTestsFromName(event)
+        self.assertNotEqual(suite, None)
+
+    def test_ignores_test_methods_from_name(self):
+        # Should ignore test methods even when specified directly
+        event = events.LoadFromNameEvent(self.loader, __name__+'.Case.test_method', None)
+        suite = self.session.hooks.loadTestsFromName(event)
+        self.assertEqual(suite, None)
+
+    def test_ignores_decorated_test_methods_from_name(self):
+        # Should ignore test methods even when they are of FunctionType
+        event = events.LoadFromNameEvent(self.loader, __name__+'.Case.test_patched', None)
+        suite = self.session.hooks.loadTestsFromName(event)
+        self.assertEqual(suite, None)
+
+
+def func():
+    pass
+
+def dummy():
+    pass
+
+class Case(unittest.TestCase):
+    __test__ = False # do not run this
+
+    def test_method(self):
+        pass
+
+    @mock.patch(__name__+'.dummy')
+    def test_patched(self, mock):
+        pass