Repository: spyder-ide/spyder-unittest (language: python)

Issue:
Remove support for nose
Nose has seen no updates since 2015, numpy switched away in 2018, I don't remember an explicit mention of nose in connection with this plugin in a long time, so I propose that we remove support for nose in v0.6 of this plugin.


Discussion / hints:
There is nose2 as a potential alternative: https://docs.nose2.io/en/latest/

Dmitry Shachnev (a Debian Python maintainer) has just proposed a mass-bug filing against packages still using nose: https://lists.debian.org/debian-python/2022/08/msg00065.html

So I'd be OK with seeing nose support removed from this plugin, though if nose2 is a reasonable substitute and it is relatively easy to switch the code from nose support to nose2 support, that might be preferable to a wholesale removal.

Best wishes, Julian
I saw that Dmitry went ahead and filed a bug against the Debian package. Since the nose dependency is optional (except for running the test suite), I guess you can just disable the tests that run nose in the Debian package if you want to get rid of the Build-Depends.

Regarding nose2, there has never been a demand for it and I don't use it myself so I am not super keen. However, it looks lie it may be really easy to change the code from nose to nose2. There are apparently thousands of repos on Github that use nose2 so it does see some use. I will try it and see how it goes. Thanks for the suggestion.

Fix patch:
diff --git a/.github/issue_template.md b/.github/issue_template.md
index 8bc43705..15aac278 100644
--- a/.github/issue_template.md
+++ b/.github/issue_template.md
@@ -29,6 +29,6 @@
 * Version of spyder-unittest plugin:
 * Installation method for Spyder and the unittest plugin: Anaconda / pip / ...
 * Python version:
-* Testing framework used: nose / py.test / unittest
+* Testing framework used: nose2 / pytest / unittest
 * Testing framework version:
 * Operating system:
diff --git a/README.md b/README.md
index 304d76db..d0930b5f 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Spyder-unittest is a plugin that integrates popular unit test frameworks
 with Spyder, allowing you to run test suites and view the results in the IDE.
 
 The plugin supports the `unittest` module in the Python standard library
-as well as the `pytest` and `nose` testing frameworks.
+as well as the `pytest` and `nose2` testing frameworks.
 Support for `pytest` is most complete at the moment.
 
 
@@ -76,10 +76,10 @@ The plugin has the following dependencies:
 * [spyder](https://github.com/spyder-ide/spyder) (obviously), at least version 4.0
 * [lxml](http://lxml.de/)
 * the testing framework that you will be using: [pytest](https://pytest.org)
-  and/or [nose](https://nose.readthedocs.io)
+  and/or [nose2](https://docs.nose2.io)
 
 In order to run the tests distributed with this plugin, you need
-[nose](https://nose.readthedocs.io), [pytest](https://pytest.org)
+[nose2](https://docs.nose2.io), [pytest](https://pytest.org)
 and [pytest-qt](https://github.com/pytest-dev/pytest-qt). If you use Python 2,
 you also need [mock](https://github.com/testing-cabal/mock).
 
diff --git a/setup.py b/setup.py
index 6fa09873..e4401799 100644
--- a/setup.py
+++ b/setup.py
@@ -47,7 +47,7 @@ def get_package_data(name, extlist):
 frameworks. It allows you to run tests and view the results.
 
 The plugin supports the `unittest` framework in the Python
-standard library and the `pytest` and `nose` testing frameworks.
+standard library and the `pytest` and `nose2` testing frameworks.
 """
 
 setup(


Test patch:
diff --git a/requirements/tests.txt b/requirements/tests.txt
index 79b5c8fb..384c08b0 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -1,6 +1,6 @@
 codecov
 flaky
-nose
+nose2
 pytest>=5
 pytest-cov
 pytest-qt
diff --git a/spyder_unittest/backend/noserunner.py b/spyder_unittest/backend/nose2runner.py
similarity index 90%
rename from spyder_unittest/backend/noserunner.py
rename to spyder_unittest/backend/nose2runner.py
index 22d0a954..5ad8dd82 100644
--- a/spyder_unittest/backend/noserunner.py
+++ b/spyder_unittest/backend/nose2runner.py
@@ -19,18 +19,18 @@
     _ = gettext.gettext
 
 
-class NoseRunner(RunnerBase):
+class Nose2Runner(RunnerBase):
     """Class for running tests within Nose framework."""
 
-    module = 'nose'
-    name = 'nose'
+    module = 'nose2'
+    name = 'nose2'
 
     def create_argument_list(self, config, cov_path):
         """Create argument list for testing process."""
         return [
-            '-m', self.module, '--with-xunit',
-            '--xunit-file={}'.format(self.resultfilename),
-            ]
+            '-m', self.module, '--plugin=nose2.plugins.junitxml',
+            '--junit-xml', '--junit-xml-path={}'.format(self.resultfilename)
+        ]
 
     def finished(self):
         """Called when the unit test process has finished."""
@@ -81,7 +81,7 @@ def load_data(self):
                         message = type_
                     if child.text:
                         extras.append(child.text)
-                elif child.tag in ('system-out', 'system-err'):
+                elif child.tag in ('system-out', 'system-err') and child.text:
                     if child.tag == 'system-out':
                         heading = _('Captured stdout')
                     else:
diff --git a/spyder_unittest/backend/tests/test_nose2runner.py b/spyder_unittest/backend/tests/test_nose2runner.py
new file mode 100644
index 00000000..bc1eec57
--- /dev/null
+++ b/spyder_unittest/backend/tests/test_nose2runner.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2013 Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see LICENSE.txt for details)
+"""Tests for nose2runner.py"""
+
+# Local imports
+from spyder_unittest.backend.nose2runner import Nose2Runner
+from spyder_unittest.backend.runnerbase import Category
+
+
+def test_nose2runner_load_data(tmpdir):
+    result_file = tmpdir.join('results')
+    result_txt = """
+<testsuite name="nose2-junit" errors="0" failures="1" skipped="0" tests="2" time="0.000">
+  <testcase time="0.04" classname="test_foo" name="test1">
+    <system-out />
+  </testcase>
+  <testcase time="0.01" classname="test_foo" name="test2">
+    <failure message="test failure">text</failure>
+    <system-out />
+  </testcase>
+</testsuite>"""
+    result_file.write(result_txt)
+    runner = Nose2Runner(None, result_file.strpath)
+    results = runner.load_data()
+    assert len(results) == 2
+
+    assert results[0].category == Category.OK
+    assert results[0].status == 'ok'
+    assert results[0].name == 'test_foo.test1'
+    assert results[0].message == ''
+    assert results[0].time == 0.04
+    assert results[0].extra_text == []
+
+    assert results[1].category == Category.FAIL
+    assert results[1].status == 'failure'
+    assert results[1].name == 'test_foo.test2'
+    assert results[1].message == 'test failure'
+    assert results[1].time == 0.01
+    assert results[1].extra_text == ['text']
diff --git a/spyder_unittest/backend/tests/test_noserunner.py b/spyder_unittest/backend/tests/test_noserunner.py
deleted file mode 100644
index af594914..00000000
--- a/spyder_unittest/backend/tests/test_noserunner.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2013 Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see LICENSE.txt for details)
-"""Tests for noserunner.py"""
-
-# Local imports
-from spyder_unittest.backend.noserunner import NoseRunner
-from spyder_unittest.backend.runnerbase import Category
-
-
-def test_noserunner_load_data(tmpdir):
-    result_file = tmpdir.join('results')
-    result_txt = """<?xml version="1.0" encoding="utf-8"?>
-<testsuite errors="0" failures="1" name="pytest" skips="1" tests="3" time="0.1">
-<testcase classname="test_foo" file="test_foo.py" line="2" name="test1" time="0.04"></testcase>
-<testcase classname="test_foo" file="test_foo.py" line="5" name="test2" time="0.01">
-    <failure message="failure message">text</failure>
-</testcase>
-<testcase classname="test_foo" file="test_foo.py" line="8" name="test3" time="0.05">
-    <skipped message="skip message">text2</skipped>
-</testcase></testsuite>"""
-    result_file.write(result_txt)
-    runner = NoseRunner(None, result_file.strpath)
-    results = runner.load_data()
-    assert len(results) == 3
-
-    assert results[0].category == Category.OK
-    assert results[0].status == 'ok'
-    assert results[0].name == 'test_foo.test1'
-    assert results[0].message == ''
-    assert results[0].time == 0.04
-    assert results[0].extra_text == []
-
-    assert results[1].category == Category.FAIL
-    assert results[1].status == 'failure'
-    assert results[1].name == 'test_foo.test2'
-    assert results[1].message == 'failure message'
-    assert results[1].time == 0.01
-    assert results[1].extra_text == ['text']
-
-    assert results[2].category == Category.SKIP
-    assert results[2].status == 'skipped'
-    assert results[2].name == 'test_foo.test3'
-    assert results[2].message == 'skip message'
-    assert results[2].time == 0.05
-    assert results[2].extra_text == ['text2']
-
-
-def test_noserunner_load_data_failing_test_with_stdout(tmpdir):
-    result_file = tmpdir.join('results')
-    result_txt = """<?xml version="1.0" encoding="utf-8"?>
-<testsuite errors="0" failures="1" name="pytest" skips="0" tests="1" time="0.1">
-<testcase classname="test_foo" file="test_foo.py" line="2" name="test1" time="0.04">
-<failure message="failure message">text</failure>
-<system-out>stdout text
-</system-out></testcase></testsuite>"""
-    result_file.write(result_txt)
-    runner = NoseRunner(None, result_file.strpath)
-    results = runner.load_data()
-    assert results[0].extra_text == ['text', '', '----- Captured stdout -----', 'stdout text']
-
-
-def test_noserunner_load_data_passing_test_with_stdout(tmpdir):
-    result_file = tmpdir.join('results')
-    result_txt = """<?xml version="1.0" encoding="utf-8"?>
-<testsuite errors="0" failures="0" name="pytest" skips="0" tests="1" time="0.1">
-<testcase classname="test_foo" file="test_foo.py" line="2" name="test1" time="0.04">
-<system-out>stdout text
-</system-out></testcase></testsuite>"""
-    result_file.write(result_txt)
-    runner = NoseRunner(None, result_file.strpath)
-    results = runner.load_data()
-    assert results[0].extra_text == ['----- Captured stdout -----', 'stdout text']
-
diff --git a/spyder_unittest/backend/workers/print_versions.py b/spyder_unittest/backend/workers/print_versions.py
index 6d049c27..0bc5809c 100644
--- a/spyder_unittest/backend/workers/print_versions.py
+++ b/spyder_unittest/backend/workers/print_versions.py
@@ -37,23 +37,21 @@ def pytest_cmdline_main(self, config):
             'plugins': plugins}
 
 
-def get_nose_info():
-    """Return information about nose."""
-    from pkg_resources import iter_entry_points
+def get_nose2_info():
+    """
+    Return information about nose2.
+
+    This only returns the version of nose2. The function does not gather any
+    information about plugins.
+    """
     try:
-        import nose
+        import nose2
     except ImportError:
         return {'available': False}
 
-    plugins = {}
-    for entry_point, _ in (nose.plugins.manager.EntryPointPluginManager
-                           .entry_points):
-        for ep in iter_entry_points(entry_point):
-            plugins[ep.dist.project_name] = ep.dist.version
-
     return {'available': True,
-            'version': nose.__version__,
-            'plugins': plugins}
+            'version': nose2.__version__,
+            'plugins': {}}
 
 
 def get_unittest_info():
@@ -75,11 +73,11 @@ def get_all_info():
     Information is returned as a dictionary like the following:
     {'pytest': {'available': True, 'version': '7.1.1',
                 'plugins': {'flaky': '3.7.0', 'pytest-mock': '3.6.1'}},
-     'nose': {'available': False},
+     'nose2': {'available': False},
      'unittest': {'available': True, 'version': '3.10.5', 'plugins': {}}}
     """
     return {'pytest': get_pytest_info(),
-            'nose': get_nose_info(),
+            'nose2': get_nose2_info(),
             'unittest': get_unittest_info()}
 
 
diff --git a/spyder_unittest/backend/workers/tests/test_print_versions.py b/spyder_unittest/backend/workers/tests/test_print_versions.py
index 06977931..3fd4ddb2 100644
--- a/spyder_unittest/backend/workers/tests/test_print_versions.py
+++ b/spyder_unittest/backend/workers/tests/test_print_versions.py
@@ -6,7 +6,7 @@
 """Tests for print_versions.py"""
 
 from spyder_unittest.backend.workers.print_versions import (
-    get_nose_info, get_pytest_info, get_unittest_info)
+    get_nose2_info, get_pytest_info, get_unittest_info)
 
 
 def test_get_pytest_info_without_plugins(monkeypatch):
@@ -37,30 +37,11 @@ def test_get_pytest_info_with_plugins(monkeypatch):
     assert get_pytest_info() == expected
 
 
-def test_get_nose_info_without_plugins(monkeypatch):
-    import nose
-    import pkg_resources
-    monkeypatch.setattr(nose, '__version__', '1.2.3')
-    monkeypatch.setattr(pkg_resources, 'iter_entry_points', lambda x: ())
+def test_get_nose2_info(monkeypatch):
+    import nose2
+    monkeypatch.setattr(nose2, '__version__', '1.2.3')
     expected = {'available': True, 'version': '1.2.3', 'plugins': {}}
-    assert get_nose_info() == expected
-
-
-def test_get_nose_info_with_plugins(monkeypatch):
-    import nose
-    import pkg_resources
-    monkeypatch.setattr(nose, '__version__', '1.2.3')
-    dist = pkg_resources.Distribution(project_name='myPlugin',
-                                      version='4.5.6')
-    ep = pkg_resources.EntryPoint('name', 'module_name', dist=dist)
-    monkeypatch.setattr(pkg_resources,
-                        'iter_entry_points',
-                        lambda ept: (x for x in (ep,) if ept == nose.plugins
-                                     .manager.EntryPointPluginManager
-                                     .entry_points[0][0]))
-    expected = {'available': True, 'version': '1.2.3',
-                'plugins': {'myPlugin': '4.5.6'}}
-    assert get_nose_info() == expected
+    assert get_nose2_info() == expected
 
 
 def test_get_unittest_imfo(monkeypatch):
diff --git a/spyder_unittest/widgets/tests/test_unittestgui.py b/spyder_unittest/widgets/tests/test_unittestgui.py
index cecf1284..946f21d6 100644
--- a/spyder_unittest/widgets/tests/test_unittestgui.py
+++ b/spyder_unittest/widgets/tests/test_unittestgui.py
@@ -151,15 +151,15 @@ def test_unittestwidget_process_finished_abnormally_status_label(widget):
     expected_text = '<b>{}</b>'.format('Test process exited abnormally')
     assert widget.status_label.text() == expected_text
 
-@pytest.mark.parametrize('framework', ['pytest', 'nose'])
+@pytest.mark.parametrize('framework', ['pytest', 'nose2'])
 def test_run_tests_and_display_results(qtbot, widget, tmpdir, monkeypatch, framework):
     """Basic integration test."""
     os.chdir(tmpdir.strpath)
     testfilename = tmpdir.join('test_foo.py').strpath
 
     with open(testfilename, 'w') as f:
-        f.write("def test_ok(): assert 1+1 == 2\n"
-                "def test_fail(): assert 1+1 == 3\n")
+        f.write("def test_fail(): assert 1+1 == 3\n"
+                "def test_ok(): assert 1+1 == 2\n")
 
     MockQMessageBox = Mock()
     monkeypatch.setattr('spyder_unittest.widgets.unittestgui.QMessageBox',
@@ -173,14 +173,14 @@ def test_run_tests_and_display_results(qtbot, widget, tmpdir, monkeypatch, frame
     model = widget.testdatamodel
     assert model.rowCount() == 2
     assert model.index(0, 0).data(
-        Qt.DisplayRole) == 'ok' if framework == 'nose' else 'passed'
-    assert model.index(0, 1).data(Qt.DisplayRole) == 't.test_ok'
-    assert model.index(0, 1).data(Qt.ToolTipRole) == 'test_foo.test_ok'
-    assert model.index(0, 2).data(Qt.DisplayRole) == ''
+        Qt.DisplayRole) == 'failure' if framework == 'nose2' else 'failed'
+    assert model.index(0, 1).data(Qt.DisplayRole) == 't.test_fail'
+    assert model.index(0, 1).data(Qt.ToolTipRole) == 'test_foo.test_fail'
     assert model.index(1, 0).data(
-        Qt.DisplayRole) == 'failure' if framework == 'nose' else 'failed'
-    assert model.index(1, 1).data(Qt.DisplayRole) == 't.test_fail'
-    assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.test_fail'
+        Qt.DisplayRole) == 'ok' if framework == 'nose2' else 'passed'
+    assert model.index(1, 1).data(Qt.DisplayRole) == 't.test_ok'
+    assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.test_ok'
+    assert model.index(1, 2).data(Qt.DisplayRole) == ''
 
 
 def test_run_tests_using_unittest_and_display_results(
@@ -215,7 +215,7 @@ def test_run_tests_using_unittest_and_display_results(
     assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.MyTest.test_ok'
     assert model.index(1, 2).data(Qt.DisplayRole) == ''
 
-@pytest.mark.parametrize('framework', ['unittest', 'pytest', 'nose'])
+@pytest.mark.parametrize('framework', ['unittest', 'pytest', 'nose2'])
 def test_run_with_no_tests_discovered_and_display_results(
         qtbot, widget, tmpdir, monkeypatch, framework):
     """Basic integration test."""
diff --git a/spyder_unittest/widgets/unittestgui.py b/spyder_unittest/widgets/unittestgui.py
index b81e5f69..3125b8ef 100644
--- a/spyder_unittest/widgets/unittestgui.py
+++ b/spyder_unittest/widgets/unittestgui.py
@@ -22,7 +22,7 @@
 
 # Local imports
 from spyder_unittest.backend.frameworkregistry import FrameworkRegistry
-from spyder_unittest.backend.noserunner import NoseRunner
+from spyder_unittest.backend.nose2runner import Nose2Runner
 from spyder_unittest.backend.pytestrunner import PyTestRunner
 from spyder_unittest.backend.runnerbase import Category, TestResult
 from spyder_unittest.backend.unittestrunner import UnittestRunner
@@ -37,7 +37,7 @@
     _ = gettext.gettext
 
 # Supported testing frameworks
-FRAMEWORKS = {NoseRunner, PyTestRunner, UnittestRunner}
+FRAMEWORKS = {Nose2Runner, PyTestRunner, UnittestRunner}
 
 
 class UnitTestWidgetActions: