File switch-to-pytest.patch of Package i18nspector

From b84035ee74cc646ddbabbb99d565e50973fe7b1d Mon Sep 17 00:00:00 2001
From: Stuart Prescott <stuart@debian.org>
Date: Thu, 28 Oct 2021 16:39:36 +1100
Subject: [PATCH 01/10] Change tests from nose to pytest

Some of the 'yield' tests are easy to refactor into parameterised tests
while others would take a fair amount of rethinking and/or additional
helper functions. At this stage, the relevant functions are simply decorated
with a 'collect_yielded' decorator that collects all the yield tests at
collection time and sets them up as a parameterised test to be run later.
---
 pytest.ini                        |   4 +
 tests/conftest.py                 |  21 ++
 tests/run-tests                   |  21 +-
 tests/test_changelog.py           |  33 +-
 tests/test_domains.py             |  23 +-
 tests/test_encodings.py           | 117 +++---
 tests/test_gettext.py             | 134 ++++---
 tests/test_iconv.py               |  11 +-
 tests/test_ling.py                | 566 +++++++++++++++---------------
 tests/test_misc.py                |  49 ++-
 tests/test_moparser.py            |  21 +-
 tests/test_polib4us.py            |  14 +-
 tests/test_strformat_c.py         | 404 +++++++++++----------
 tests/test_strformat_perlbrace.py |  19 +-
 tests/test_strformat_pybrace.py   |  99 +++---
 tests/test_strformat_python.py    | 198 ++++++-----
 tests/test_tags.py                |  27 +-
 tests/test_terminal.py            |   8 +-
 tests/test_version.py             |   8 +-
 tests/test_xml.py                 |  12 +-
 tests/tools.py                    |  41 ++-
 21 files changed, 935 insertions(+), 895 deletions(-)
 create mode 100644 pytest.ini
 create mode 100644 tests/conftest.py

Index: i18nspector-0.26/pytest.ini
===================================================================
--- /dev/null
+++ i18nspector-0.26/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+testpaths = tests
+python_classes = test_*
+python_functions = test *_test test_*
Index: i18nspector-0.26/tests/conftest.py
===================================================================
--- /dev/null
+++ i18nspector-0.26/tests/conftest.py
@@ -0,0 +1,19 @@
+
+import os
+import tempfile
+
+
+def pytest_sessionstart(session):
+    envvar = 'XDG_CACHE_HOME'
+    old_xdg_cache_home = os.environ.get(envvar, None)
+    xdg_temp_dir = tempfile.TemporaryDirectory(prefix='i18nspector.tests.')  # pylint: disable=consider-using-with
+    os.environ[envvar] = xdg_temp_dir.name
+
+    def cleanup():
+        xdg_temp_dir.cleanup()
+        if old_xdg_cache_home is None:
+            del os.environ[envvar]
+        else:
+            os.environ[envvar] = old_xdg_cache_home
+
+    session.config.add_cleanup(cleanup)
Index: i18nspector-0.26/tests/run-tests
===================================================================
--- i18nspector-0.26.orig/tests/run-tests
+++ i18nspector-0.26/tests/run-tests
@@ -1,6 +1,6 @@
-#!/usr/bin/env python3
+#!/bin/bash
 
-# Copyright © 2013-2016 Jakub Wilk <jwilk@jwilk.net>
+# Copyright © 2021 Stuart Prescott <stuart@debian.org>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the “Software”), to deal
@@ -20,19 +20,6 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-import os
-import sys
-import tempfile
+set -e
 
-import nose
-
-sys.path[0] += '/..'
-
-from tests import blackbox_tests
-
-if __name__ == '__main__':
-    with tempfile.TemporaryDirectory(prefix='i18nspector.tests.') as tmpdir:
-        os.environ['XDG_CACHE_HOME'] = tmpdir
-        nose.main(addplugins=[blackbox_tests.Plugin()])
-
-# vim:ts=4 sts=4 sw=4 et
+pytest -rsx "$@"
Index: i18nspector-0.26/tests/test_changelog.py
===================================================================
--- i18nspector-0.26.orig/tests/test_changelog.py
+++ i18nspector-0.26/tests/test_changelog.py
@@ -21,11 +21,9 @@
 import os
 import re
 
-from nose.tools import (
-    assert_not_equal,
-)
-
 import lib.tags
+from . import tools
+
 
 here = os.path.dirname(__file__)
 docdir = os.path.join(here, os.pardir, 'doc')
@@ -52,32 +50,39 @@ rename_re = re.compile(
     r'([\w-]+) [(]from ([\w-]+)[)]'
 )
 
-def test_tags():
-    path = os.path.join(docdir, 'changelog')
-    with open(path, 'rt', encoding='UTF-8') as file:
-        changelog = file.read()
-    summaries = summary_re.findall(changelog)
-    changelog_tags = set()
+
+@tools.collect_yielded
+def test_tag_paramerisation():
     def add(info, tag):
         del info
         if tag in changelog_tags:
             raise AssertionError('changelog adds tag twice: ' + tag)
         changelog_tags.add(tag)
+
     def remove(info, tag):
         del info
         if tag not in changelog_tags:
             raise AssertionError('changelog removes non-existent tag: ' + tag)
         changelog_tags.remove(tag)
+
     def rename(info, removed_tag, added_tag):
-        assert_not_equal(removed_tag, added_tag)
+        assert removed_tag != added_tag
         remove(info, removed_tag)
         add(info, added_tag)
+
     def check(info, tag):
         del info
         if tag not in changelog_tags:
             raise AssertionError('tag not in changelog: ' + tag)
         if tag not in data_tags:
             raise AssertionError('changelog adds unknown tag: ' + tag)
+
+    path = os.path.join(docdir, 'changelog')
+    with open(path, 'rt', encoding='UTF-8') as file:
+        changelog = file.read()
+    summaries = summary_re.findall(changelog)
+    changelog_tags = set()
+
     for summary in reversed(summaries):
         match = summary_details_re.match(summary)
         for key, lines in match.groupdict().items():
@@ -86,16 +91,17 @@ def test_tags():
             lines = [l[8:] for l in lines.splitlines()]
             if key == 'added':
                 for tag in lines:
-                    yield add, 'add', tag
+                    yield add, ('add', tag)
             elif key == 'renamed':
                 for line in lines:
                     added_tag, removed_tag = rename_re.match(line).groups()
-                    yield rename, 'rename', removed_tag, added_tag
+                    yield rename, ('rename', removed_tag, added_tag)
             else:
                 assert False
     data_tags = frozenset(tag.name for tag in lib.tags.iter_tags())
     for tag in sorted(changelog_tags | data_tags):
-        yield check, 'check', tag
+        yield check, ('check', tag)
+
 
 def test_trailing_whitespace():
     path = os.path.join(docdir, 'changelog')
Index: i18nspector-0.26/tests/test_domains.py
===================================================================
--- i18nspector-0.26.orig/tests/test_domains.py
+++ i18nspector-0.26/tests/test_domains.py
@@ -18,12 +18,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from nose.tools import (
-    assert_false,
-    assert_is,
-    assert_raises,
-    assert_true,
-)
+import pytest
 
 import lib.domains as M
 
@@ -32,9 +27,9 @@ class test_special_domains:
     def t(self, domain, special=True):
         result = M.is_special_domain(domain)
         if special:
-            assert_true(result)
+            assert result
         else:
-            assert_false(result)
+            assert not result
 
     def test_ok(self):
         self.t('test.jwilk.net', False)
@@ -68,9 +63,9 @@ class test_special_domain_emails:
     def t(self, email, special=True):
         result = M.is_email_in_special_domain(email)
         if special:
-            assert_true(result)
+            assert result
         else:
-            assert_false(result)
+            assert not result
 
     def test_valid(self):
         self.t('jwilk@test.jwilk.net', False)
@@ -79,14 +74,14 @@ class test_special_domain_emails:
         self.t('jwilk@example.net')
 
     def test_no_at(self):
-        with assert_raises(ValueError):
+        with pytest.raises(ValueError):
             self.t('jwilk%jwilk.net')
 
 class test_dotless_domains:
 
     def t(self, domain, dotless=True):
         result = M.is_dotless_domain(domain)
-        assert_is(result, dotless)
+        assert result is dotless
 
     def test_dotless(self):
         self.t('net')
@@ -99,7 +94,7 @@ class test_dotless_emails:
 
     def t(self, email, dotless=True):
         result = M.is_email_in_dotless_domain(email)
-        assert_is(result, dotless)
+        assert result is dotless
 
     def test_dotless(self):
         self.t('jwilk@net')
@@ -108,7 +103,7 @@ class test_dotless_emails:
         self.t('jwilk@example.net', False)
 
     def test_no_at(self):
-        with assert_raises(ValueError):
+        with pytest.raises(ValueError):
             self.t('jwilk%jwilk.net')
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_encodings.py
===================================================================
--- i18nspector-0.26.orig/tests/test_encodings.py
+++ i18nspector-0.26/tests/test_encodings.py
@@ -20,79 +20,85 @@
 
 import curses.ascii
 import sys
+import unittest
 
-import nose
-from nose.tools import (
-    assert_equal,
-    assert_false,
-    assert_is_none,
-    assert_not_in,
-    assert_raises,
-    assert_true,
-)
+import pytest
 
 import lib.encodings as E
 
 from . import tools
 
+
+# methods using the tools.collect_yielded decorator don't have a 'self'
+# since they end up being run before 'self' exists. pylint doesn't
+# understand this unusual situation
+# pylint: disable=no-method-argument
+
+
 class test_is_portable_encoding:
 
     def test_found(self):
-        assert_true(E.is_portable_encoding('ISO-8859-2'))
+        assert E.is_portable_encoding('ISO-8859-2')
 
     def test_found_(self):
-        assert_true(E.is_portable_encoding('ISO_8859-2'))
+        assert E.is_portable_encoding('ISO_8859-2')
 
     def test_found_nonpython(self):
-        assert_false(E.is_portable_encoding('KOI8-T'))
-        assert_true(E.is_portable_encoding('KOI8-T', python=False))
+        assert not E.is_portable_encoding('KOI8-T')
+        assert E.is_portable_encoding('KOI8-T', python=False)
 
     def test_notfound(self):
-        assert_false(E.is_portable_encoding('ISO-8859-16'))
-        assert_false(E.is_portable_encoding('ISO-8859-16', python=False))
+        assert not E.is_portable_encoding('ISO-8859-16')
+        assert not E.is_portable_encoding('ISO-8859-16', python=False)
 
 class test_propose_portable_encoding:
 
     def test_identity(self):
         encoding = 'ISO-8859-2'
         portable_encoding = E.propose_portable_encoding(encoding)
-        assert_equal(portable_encoding, encoding)
+        assert portable_encoding == encoding
 
-    def test_found(self):
+    @tools.collect_yielded
+    def test_found():
         def t(encoding, expected_portable_encoding):
             portable_encoding = E.propose_portable_encoding(encoding)
-            assert_equal(portable_encoding, expected_portable_encoding)
-        yield t, 'ISO8859-2', 'ISO-8859-2'
-        yield t, 'ISO_8859-2', 'ISO-8859-2'
-        yield t, 'Windows-1250', 'CP1250'
+            assert portable_encoding == expected_portable_encoding
+        yield t, ('ISO8859-2', 'ISO-8859-2')
+        yield t, ('ISO_8859-2', 'ISO-8859-2')
+        yield t, ('Windows-1250', 'CP1250')
 
     def test_notfound(self):
         portable_encoding = E.propose_portable_encoding('ISO-8859-16')
-        assert_is_none(portable_encoding)
+        assert portable_encoding is None
+
+
+def _test_missing(encoding):
+    assert not E.is_ascii_compatible_encoding(encoding)
+    with pytest.raises(E.EncodingLookupError):
+        E.is_ascii_compatible_encoding(encoding, missing_ok=False)
+
 
 class test_ascii_compatibility:
 
-    def test_portable(self):
+    @tools.collect_yielded
+    def test_portable():
         def t(encoding):
-            assert_true(E.is_ascii_compatible_encoding(encoding))
-            assert_true(E.is_ascii_compatible_encoding(encoding, missing_ok=False))
+            assert E.is_ascii_compatible_encoding(encoding)
+            assert E.is_ascii_compatible_encoding(encoding, missing_ok=False)
         for encoding in E.get_portable_encodings():
             yield t, encoding
 
-    def test_incompatible(self):
+    @tools.collect_yielded
+    def test_incompatible():
         def t(encoding):
-            assert_false(E.is_ascii_compatible_encoding(encoding))
-            assert_false(E.is_ascii_compatible_encoding(encoding, missing_ok=False))
+            assert not E.is_ascii_compatible_encoding(encoding)
+            assert not E.is_ascii_compatible_encoding(encoding, missing_ok=False)
         yield t, 'UTF-7'
         yield t, 'UTF-16'
 
-    def _test_missing(self, encoding):
-        assert_false(E.is_ascii_compatible_encoding(encoding))
-        with assert_raises(E.EncodingLookupError):
-            E.is_ascii_compatible_encoding(encoding, missing_ok=False)
-
-    def test_non_text(self):
-        t = self._test_missing
+    @tools.collect_yielded
+    def test_non_text():
+        t = _test_missing
         yield t, 'base64_codec'
         yield t, 'bz2_codec'
         yield t, 'hex_codec'
@@ -102,7 +108,8 @@ class test_ascii_compatibility:
         yield t, 'zlib_codec'
 
     def test_missing(self):
-        self._test_missing('eggs')
+        _test_missing('eggs')
+
 
 class test_get_character_name:
 
@@ -110,44 +117,44 @@ class test_get_character_name:
         for i in range(ord('a'), ord('z')):
             u = chr(i)
             name = E.get_character_name(u)
-            assert_equal(name, 'LATIN SMALL LETTER ' + u.upper())
+            assert name == 'LATIN SMALL LETTER ' + u.upper()
             u = chr(i).upper()
             name = E.get_character_name(u)
-            assert_equal(name, 'LATIN CAPITAL LETTER ' + u)
+            assert name == 'LATIN CAPITAL LETTER ' + u
 
     def test_c0(self):
         for i, curses_name in zip(range(0, 0x20), curses.ascii.controlnames):
             u = chr(i)
             name = E.get_character_name(u)
             expected_name = 'control character ' + curses_name
-            assert_equal(name, expected_name)
+            assert name == expected_name
 
     def test_del(self):
         name = E.get_character_name('\x7F')
-        assert_equal(name, 'control character DEL')
+        assert name == 'control character DEL'
 
     def test_c1(self):
         for i in range(0x80, 0xA0):
             u = chr(i)
             name = E.get_character_name(u)
-            assert_true(name.startswith('control character '))
+            assert name.startswith('control character ')
 
     def test_uniqueness(self):
         names = set()
         for i in range(0, 0x100):
             u = chr(i)
             name = E.get_character_name(u)
-            assert_not_in(name, names)
+            assert name not in names
             names.add(name)
 
     def test_non_character(self):
         name = E.get_character_name('\uFFFE')
-        assert_equal(name, 'non-character')
+        assert name == 'non-character'
         name = E.get_character_name('\uFFFF')
-        assert_equal(name, 'non-character')
+        assert name == 'non-character'
 
     def test_lookup_error(self):
-        with assert_raises(ValueError):
+        with pytest.raises(ValueError):
             E.get_character_name('\uE000')
 
 class test_extra_encoding:
@@ -164,13 +171,13 @@ class test_extra_encoding:
         except LookupError:
             pass
         else:
-            raise nose.SkipTest(
+            raise unittest.SkipTest(
                 'python{ver[0]}.{ver[1]} supports the {enc} encoding'.format(
                     ver=sys.version_info,
                     enc=encoding
                 )
             )
-        with assert_raises(LookupError):
+        with pytest.raises(LookupError):
             dec()
         E.install_extra_encodings()
         enc()
@@ -181,7 +188,7 @@ class test_extra_encoding:
         E.install_extra_encodings()
         encoding = '8859-2'
         portable_encoding = E.propose_portable_encoding(encoding)
-        assert_equal('ISO-' + encoding, portable_encoding)
+        assert 'ISO-' + encoding == portable_encoding
 
     @tools.fork_isolation
     def test_not_allowed(self):
@@ -193,14 +200,14 @@ class test_extra_encoding:
         except LookupError:
             pass
         else:
-            raise nose.SkipTest(
+            raise unittest.SkipTest(
                 'python{ver[0]}.{ver[1]} supports the {enc} encoding'.format(
                     ver=sys.version_info,
                     enc=encoding
                 )
             )
         E.install_extra_encodings()
-        with assert_raises(LookupError):
+        with pytest.raises(LookupError):
             enc()
 
     _viscii_unicode = 'Ti\u1EBFng Vi\u1EC7t'
@@ -211,13 +218,13 @@ class test_extra_encoding:
         E.install_extra_encodings()
         u = self._viscii_unicode
         b = u.encode('VISCII')
-        assert_equal(b, self._viscii_bytes)
+        assert b == self._viscii_bytes
 
     @tools.fork_isolation
     def test_8b_encode_error(self):
         E.install_extra_encodings()
         u = self._viscii_unicode
-        with assert_raises(UnicodeEncodeError):
+        with pytest.raises(UnicodeEncodeError):
             u.encode('KOI8-RU')
 
     @tools.fork_isolation
@@ -225,13 +232,13 @@ class test_extra_encoding:
         E.install_extra_encodings()
         b = self._viscii_bytes
         u = b.decode('VISCII')
-        assert_equal(u, self._viscii_unicode)
+        assert u == self._viscii_unicode
 
     @tools.fork_isolation
     def test_8b_decode_error(self):
         E.install_extra_encodings()
         b = self._viscii_bytes
-        with assert_raises(UnicodeDecodeError):
+        with pytest.raises(UnicodeDecodeError):
             b.decode('KOI8-T')
 
     _euc_tw_unicode = '\u4E2D\u6587'
@@ -242,13 +249,13 @@ class test_extra_encoding:
         E.install_extra_encodings()
         u = self._euc_tw_unicode
         b = u.encode('EUC-TW')
-        assert_equal(b, self._euc_tw_bytes)
+        assert b == self._euc_tw_bytes
 
     @tools.fork_isolation
     def test_mb_encode_error(self):
         E.install_extra_encodings()
         u = self._viscii_unicode
-        with assert_raises(UnicodeEncodeError):
+        with pytest.raises(UnicodeEncodeError):
             u.encode('EUC-TW')
 
     @tools.fork_isolation
@@ -256,13 +263,13 @@ class test_extra_encoding:
         E.install_extra_encodings()
         b = self._euc_tw_bytes
         u = b.decode('EUC-TW')
-        assert_equal(u, self._euc_tw_unicode)
+        assert u == self._euc_tw_unicode
 
     @tools.fork_isolation
     def test_mb_decode_error(self):
         E.install_extra_encodings()
         b = self._viscii_bytes
-        with assert_raises(UnicodeDecodeError):
+        with pytest.raises(UnicodeDecodeError):
             b.decode('EUC-TW')
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_gettext.py
===================================================================
--- i18nspector-0.26.orig/tests/test_gettext.py
+++ i18nspector-0.26/tests/test_gettext.py
@@ -20,16 +20,7 @@
 
 import datetime
 
-from nose.tools import (
-    assert_equal,
-    assert_false,
-    assert_is_instance,
-    assert_is_none,
-    assert_is_not_none,
-    assert_less,
-    assert_raises,
-    assert_true,
-)
+import pytest
 
 import lib.gettext as M
 
@@ -38,21 +29,21 @@ class test_header_fields:
     def test_nonempty(self):
         # XXX Update this number after editing data/header-fields:
         expected = 12
-        assert_equal(len(M.header_fields), expected)
+        assert len(M.header_fields) == expected
 
     def test_no_x(self):
         for field in M.header_fields:
-            assert_false(field.startswith('X-'))
+            assert not field.startswith('X-')
 
     def test_valid(self):
         for field in M.header_fields:
-            assert_true(M.is_valid_field_name(field))
+            assert M.is_valid_field_name(field)
 
 class test_header_parser:
 
     def t(self, message, expected):
         parsed = list(M.parse_header(message))
-        assert_equal(parsed, expected)
+        assert parsed == expected
 
     def test_ok(self):
         self.t(
@@ -91,8 +82,8 @@ class test_plural_exp:
     def t(self, s, n=None, fn=None):
         f = M.parse_plural_expression(s)
         if n is not None:
-            assert_is_not_none(fn)
-            assert_equal(f(n), fn)
+            assert fn is not None
+            assert f(n) == fn
 
     def test_const(self):
         n = 42
@@ -101,7 +92,7 @@ class test_plural_exp:
     def test_const_overflow(self):
         m = (1 << 32) - 1
         self.t(str(m), m, m)
-        with assert_raises(OverflowError):
+        with pytest.raises(OverflowError):
             self.t(str(m + 1), m + 1, False)
             self.t(str(m + 1), m + 42, False)
 
@@ -112,7 +103,7 @@ class test_plural_exp:
     def test_var_overflow(self):
         m = (1 << 32) - 1
         self.t('n', m, m)
-        with assert_raises(OverflowError):
+        with pytest.raises(OverflowError):
             self.t('n', m + 1, False)
         self.t('42', m + 1, 42)
 
@@ -122,7 +113,7 @@ class test_plural_exp:
     def test_add_overflow(self):
         m = (1 << 32) - 1
         self.t('n + 42', m - 42, m)
-        with assert_raises(OverflowError):
+        with pytest.raises(OverflowError):
             self.t('n + 42', m - 41, False)
             self.t('n + 42', m - 23, False)
 
@@ -130,7 +121,7 @@ class test_plural_exp:
         self.t('n - 23', 37, 14)
 
     def test_sub_overflow(self):
-        with assert_raises(OverflowError):
+        with pytest.raises(OverflowError):
             self.t('n - 23', 6, False)
 
     def test_mul(self):
@@ -140,7 +131,7 @@ class test_plural_exp:
         m = (1 << 32) - 1
         assert m % 17 == 0
         self.t('n * 17', m / 17, m)
-        with assert_raises(OverflowError):
+        with pytest.raises(OverflowError):
             self.t('n * 17', (m / 17) + 1, False)
             self.t('n * 2', (m + 1) / 2, False)
 
@@ -148,14 +139,14 @@ class test_plural_exp:
         self.t('105 / n', 17, 6)
 
     def test_div_by_0(self):
-        with assert_raises(ZeroDivisionError):
+        with pytest.raises(ZeroDivisionError):
             self.t('105 / n', 0, False)
 
     def test_mod(self):
         self.t('105 % n', 17, 3)
 
     def test_mod_by_0(self):
-        with assert_raises(ZeroDivisionError):
+        with pytest.raises(ZeroDivisionError):
             self.t('105 % n', 0, False)
 
     def test_and(self):
@@ -228,75 +219,75 @@ class test_plural_exp:
         self.t('(2 ? 3 : 7) ? 23 : 37')
 
     def test_badly_nested_conditional(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('2 ? (3 : 7 ? ) : 23')
 
     def test_unary_minus(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('-37')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('23 + (-37)')
 
     def test_unary_plus(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('+42')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('23 + (+37)')
 
     def test_func_call(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('n(42)')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('42(n)')
 
     def test_unbalanced_parentheses(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('(6 * 7')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 * 7)')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6) * (7')
 
     def test_dangling_binop(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 +')
 
     def test_junk_token(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 # 7')
 
     def test_shift(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 << 7')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 >> 7')
 
     def test_pow(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 ** 7')
 
     def test_floor_div(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 // 7')
 
     def test_tuple(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('()')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('(6, 7)')
 
     def test_starred(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('*42')
 
     def test_exotic_whitespace(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('6 *\xA07')
 
     def test_empty(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('')
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t(' ')
 
 class test_codomain:
@@ -308,9 +299,9 @@ class test_codomain:
         cd = f.codomain()
         if min_ is None:
             assert max_ is None
-            assert_is_none(cd)
+            assert cd is None
         else:
-            assert_equal(cd, (min_, max_))
+            assert cd == (min_, max_)
 
     def test_num(self):
         self.t('0', 0, 0)
@@ -530,9 +521,9 @@ class test_period:
         op = f.period()
         if offset is None:
             assert period is None
-            assert_is_none(op)
+            assert op is None
         else:
-            assert_equal(op, (offset, period))
+            assert op == (offset, period)
 
     def test_num(self):
         self.t('42', 0, 1)
@@ -619,20 +610,20 @@ class test_plural_forms:
 
     def t(self, s, *, n, ljunk='', rjunk=''):
         if ljunk or rjunk:
-            with assert_raises(self.error):
+            with pytest.raises(self.error):
                 M.parse_plural_forms(s)
         else:
             (n0, expr0) = M.parse_plural_forms(s)
             del expr0
-            assert_equal(n0, n)
+            assert n0 == n
         (n1, expr1, ljunk1, rjunk1) = M.parse_plural_forms(s, strict=False)  # pylint: disable=unbalanced-tuple-unpacking
         del expr1
-        assert_equal(n1, n)
-        assert_equal(ljunk1, ljunk)
-        assert_equal(rjunk1, rjunk)
+        assert n1 == n
+        assert ljunk1 == ljunk
+        assert rjunk1 == rjunk
 
     def test_nplurals_0(self):
-        with assert_raises(self.error):
+        with pytest.raises(self.error):
             self.t('nplurals=0; plural=0;', n=0)
 
     def test_nplurals_positive(self):
@@ -651,15 +642,15 @@ class test_fix_date_format:
 
     def t(self, old, expected):
         if expected is None:
-            with assert_raises(M.DateSyntaxError):
+            with pytest.raises(M.DateSyntaxError):
                 M.fix_date_format(old)
         else:
             new = M.fix_date_format(old)
-            assert_is_not_none(new)
-            assert_equal(new, expected)
+            assert new is not None
+            assert new == expected
 
     def tbp(self, old):
-        with assert_raises(M.BoilerplateDate):
+        with pytest.raises(M.BoilerplateDate):
             M.fix_date_format(old)
 
     def test_boilerplate(self):
@@ -710,10 +701,9 @@ class test_fix_date_format:
         self.t('2002-01-01 03:05', None)
 
     def test_tz_hint(self):
-        assert_equal(
-            M.fix_date_format('2002-01-01 03:05', tz_hint='+0900'),
-            '2002-01-01 03:05+0900',
-        )
+        assert (
+            M.fix_date_format('2002-01-01 03:05', tz_hint='+0900') ==
+            '2002-01-01 03:05+0900')
 
     def test_gmt_before_tz(self):
         self.t(
@@ -762,30 +752,30 @@ class test_parse_date:
     t = staticmethod(M.parse_date)
 
     def test_nonexistent(self):
-        with assert_raises(M.DateSyntaxError):
+        with pytest.raises(M.DateSyntaxError):
             self.t('2010-02-29 19:49+0200')
 
     def test_existent(self):
         d = self.t('2003-09-08 21:26+0200')
-        assert_equal(d.second, 0)
-        assert_is_instance(d, datetime.datetime)
-        assert_equal(str(d), '2003-09-08 21:26:00+02:00')
+        assert d.second == 0
+        assert isinstance(d, datetime.datetime)
+        assert str(d) == '2003-09-08 21:26:00+02:00'
 
     def test_epoch(self):
         d = self.t('2008-04-03 16:06+0300')
-        assert_less(M.epoch, d)
+        assert M.epoch < d
 
 class test_string_formats:
 
     def test_nonempty(self):
         # XXX Update this number after editing data/string-formats:
         expected = 28
-        assert_equal(len(M.string_formats), expected)
+        assert len(M.string_formats) == expected
 
     def test_lowercase(self):
         for s in M.string_formats:
-            assert_is_instance(s, str)
-            assert_true(s)
-            assert_equal(s, s.lower())
+            assert isinstance(s, str)
+            assert s
+            assert s == s.lower()
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_iconv.py
===================================================================
--- i18nspector-0.26.orig/tests/test_iconv.py
+++ i18nspector-0.26/tests/test_iconv.py
@@ -18,10 +18,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from nose.tools import (
-    assert_equal,
-    assert_raises,
-)
+import pytest
 
 import lib.iconv as M
 
@@ -32,11 +29,11 @@ class _test:
 
     def test_encode(self):
         b = M.encode(self.u, self.e)
-        assert_equal(b, self.b)
+        assert b == self.b
 
     def test_decode(self):
         u = M.decode(self.b, self.e)
-        assert_equal(u, self.u)
+        assert u == self.u
 
 class test_iso2(_test):
     u = 'Żrą łódź? Część miń!'
@@ -50,7 +47,7 @@ class test_tcvn(_test):
 
 def test_incomplete_char():
     b = 'Ę'.encode('UTF-8')[:1]
-    with assert_raises(UnicodeDecodeError):
+    with pytest.raises(UnicodeDecodeError):
         M.decode(b, 'UTF-8')
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_ling.py
===================================================================
--- i18nspector-0.26.orig/tests/test_ling.py
+++ i18nspector-0.26/tests/test_ling.py
@@ -18,19 +18,9 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-import nose
-from nose.tools import (
-    assert_equal,
-    assert_false,
-    assert_in,
-    assert_is,
-    assert_is_instance,
-    assert_is_none,
-    assert_not_equal,
-    assert_not_in,
-    assert_raises,
-    assert_true,
-)
+import unittest
+
+import pytest
 
 import lib.encodings
 import lib.ling
@@ -45,12 +35,12 @@ class test_fix_codes:
 
     def t(self, l1, l2):
         lang = L.parse_language(l1)
-        assert_equal(str(lang), l1)
+        assert str(lang) == l1
         if l1 == l2:
-            assert_is_none(lang.fix_codes())
+            assert lang.fix_codes() is None
         else:
-            assert_is(lang.fix_codes(), True)
-        assert_equal(str(lang), l2)
+            assert lang.fix_codes() is True
+        assert str(lang) == l2
 
     def test_2_to_2(self):
         self.t('grc', 'grc')
@@ -69,18 +59,18 @@ class test_fix_codes:
         self.t('gre_GR', 'el_GR')
 
     def test_ll_not_found(self):
-        with assert_raises(L.FixingLanguageCodesFailed):
+        with pytest.raises(L.FixingLanguageCodesFailed):
             self.t('ry', '')
 
     def test_cc_not_found(self):
-        with assert_raises(L.FixingLanguageCodesFailed):
+        with pytest.raises(L.FixingLanguageCodesFailed):
             self.t('el_RY', '')
 
 def test_language_repr():
     # Language.__repr__() is never used by i18nspector itself,
     # but it's useful for debugging test failures.
     lng = T('el')
-    assert_equal(repr(lng), '<Language el>')
+    assert repr(lng) == '<Language el>'
 
 class test_language_equality:
 
@@ -89,47 +79,47 @@ class test_language_equality:
     def test_eq(self):
         l1 = T('el', 'GR')
         l2 = T('el', 'GR')
-        assert_equal(l1, l2)
-        assert_equal(l2, l1)
+        assert l1 == l2
+        assert l2 == l1
 
     def test_ne(self):
         l1 = T('el')
         l2 = T('el', 'GR')
-        assert_not_equal(l1, l2)
-        assert_not_equal(l2, l1)
+        assert l1 != l2
+        assert l2 != l1
 
     def test_ne_other_type(self):
         l1 = T('el')
-        assert_not_equal(l1, 42)
-        assert_not_equal(42, l1)
+        assert l1 != 42
+        assert 42 != l1   # pylint: disable=misplaced-comparison-constant
 
     def test_almost_equal(self):
         l1 = T('el')
         l2 = T('el', 'GR')
-        assert_true(l1.is_almost_equal(l2))
-        assert_true(l2.is_almost_equal(l1))
+        assert l1.is_almost_equal(l2)
+        assert l2.is_almost_equal(l1)
 
     def test_not_almost_equal(self):
         l1 = T('el', 'GR')
         l2 = T('grc', 'GR')
-        assert_false(l1.is_almost_equal(l2))
-        assert_false(l2.is_almost_equal(l1))
+        assert not l1.is_almost_equal(l2)
+        assert not l2.is_almost_equal(l1)
 
     def test_not_almost_equal_other_type(self):
         l1 = T('el')
-        with assert_raises(TypeError):
+        with pytest.raises(TypeError):
             l1.is_almost_equal(42)
 
 class test_remove_encoding:
 
     def t(self, l1, l2):
         lang = L.parse_language(l1)
-        assert_equal(str(lang), l1)
+        assert str(lang) == l1
         if l1 == l2:
-            assert_is_none(lang.remove_encoding())
+            assert lang.remove_encoding() is None
         else:
-            assert_is(lang.remove_encoding(), True)
-        assert_equal(str(lang), l2)
+            assert lang.remove_encoding() is True
+        assert str(lang) == l2
 
     def test_without_encoding(self):
         self.t('el', 'el')
@@ -141,12 +131,12 @@ class test_remove_nonlinguistic_modifier
 
     def t(self, l1, l2):
         lang = L.parse_language(l1)
-        assert_equal(str(lang), l1)
+        assert str(lang) == l1
         if l1 == l2:
-            assert_is_none(lang.remove_nonlinguistic_modifier())
+            assert lang.remove_nonlinguistic_modifier() is None
         else:
-            assert_is(lang.remove_nonlinguistic_modifier(), True)
-        assert_equal(str(lang), l2)
+            assert lang.remove_nonlinguistic_modifier() is True
+        assert str(lang) == l2
 
     def test_quot(self):
         self.t('en@quot', 'en@quot')
@@ -162,18 +152,18 @@ class test_lookup_territory_code:
 
     def test_found(self):
         cc = L.lookup_territory_code('GR')
-        assert_equal(cc, 'GR')
+        assert cc == 'GR'
 
     def test_not_found(self):
         cc = L.lookup_territory_code('RG')
-        assert_is_none(cc)
+        assert cc is None
 
 class test_get_language_for_name:
 
     def t(self, name, expected):
         lang = L.get_language_for_name(name)
-        assert_is_instance(lang, T)
-        assert_equal(str(lang), expected)
+        assert isinstance(lang, T)
+        assert str(lang) == expected
 
     def test_found(self):
         self.t('Greek', 'el')
@@ -194,69 +184,75 @@ class test_get_language_for_name:
         self.t('Pashto, Pushto', 'ps')
 
     def test_lone_comma(self):
-        with assert_raises(LookupError):
+        with pytest.raises(LookupError):
             self.t(',', None)
 
     def test_not_found(self):
-        with assert_raises(LookupError):
+        with pytest.raises(LookupError):
             self.t('Nadsat', None)
 
 class test_parse_language:
 
     def test_ll(self):
         lang = L.parse_language('el')
-        assert_equal(lang.language_code, 'el')
-        assert_is_none(lang.territory_code)
-        assert_is_none(lang.encoding)
-        assert_is_none(lang.modifier)
+        assert lang.language_code == 'el'
+        assert lang.territory_code is None
+        assert lang.encoding is None
+        assert lang.modifier is None
 
     def test_lll(self):
         lang = L.parse_language('ell')
-        assert_equal(lang.language_code, 'ell')
-        assert_is_none(lang.territory_code)
-        assert_is_none(lang.encoding)
-        assert_is_none(lang.modifier)
+        assert lang.language_code == 'ell'
+        assert lang.territory_code is None
+        assert lang.encoding is None
+        assert lang.modifier is None
 
     def test_ll_cc(self):
         lang = L.parse_language('el_GR')
-        assert_equal(lang.language_code, 'el')
-        assert_equal(lang.territory_code, 'GR')
-        assert_is_none(lang.encoding)
-        assert_is_none(lang.modifier)
+        assert lang.language_code == 'el'
+        assert lang.territory_code == 'GR'
+        assert lang.encoding is None
+        assert lang.modifier is None
 
     def test_ll_cc_enc(self):
         lang = L.parse_language('el_GR.UTF-8')
-        assert_equal(lang.language_code, 'el')
-        assert_equal(lang.territory_code, 'GR')
-        assert_equal(lang.encoding, 'UTF-8')
-        assert_is_none(lang.modifier)
+        assert lang.language_code == 'el'
+        assert lang.territory_code == 'GR'
+        assert lang.encoding == 'UTF-8'
+        assert lang.modifier is None
 
     def test_ll_cc_modifier(self):
         lang = L.parse_language('en_US@quot')
-        assert_equal(lang.language_code, 'en')
-        assert_equal(lang.territory_code, 'US')
-        assert_is_none(lang.encoding)
-        assert_equal(lang.modifier, 'quot')
+        assert lang.language_code == 'en'
+        assert lang.territory_code == 'US'
+        assert lang.encoding is None
+        assert lang.modifier == 'quot'
 
     def test_syntax_error(self):
-        with assert_raises(L.LanguageSyntaxError):
+        with pytest.raises(L.LanguageSyntaxError):
             L.parse_language('GR')
 
 class test_get_primary_languages:
 
     def test_found(self):
         langs = L.get_primary_languages()
-        assert_in('el', langs)
+        assert 'el' in langs
 
     def test_not_found(self):
         langs = L.get_primary_languages()
-        assert_not_in('ry', langs)
+        assert 'ry' not in langs
 
-    def test_iso_639(self):
+    # methods using the tools.collect_yielded decorator don't have a 'self'
+    # since they end up being run before 'self' exists. pylint doesn't
+    # understand this unusual situation
+
+    @tools.collect_yielded
+    def test_iso_639():
+        # pylint: disable=no-method-argument
         def t(lang_str):
             lang = L.parse_language(lang_str)
-            assert_is_none(lang.fix_codes())
-            assert_equal(str(lang), lang_str)
+            assert lang.fix_codes() is None
+            assert str(lang) == lang_str
         for lang_str in L.get_primary_languages():
             yield t, lang_str
 
@@ -267,34 +263,30 @@ class test_get_plural_forms:
         return lang.get_plural_forms()
 
     def test_found_ll(self):
-        assert_equal(
-            self.t('el'),
-            ['nplurals=2; plural=n != 1;']
-        )
+        assert (
+            self.t('el') ==
+            ['nplurals=2; plural=n != 1;'])
 
     def test_found_ll_cc(self):
-        assert_equal(
-            self.t('el_GR'),
-            ['nplurals=2; plural=n != 1;']
-        )
+        assert (
+            self.t('el_GR') ==
+            ['nplurals=2; plural=n != 1;'])
 
     def test_en_ca(self):
-        assert_equal(
-            self.t('en'),
-            self.t('en_CA'),
-        )
+        assert (
+            self.t('en') ==
+            self.t('en_CA'))
 
     def test_pt_br(self):
-        assert_not_equal(
-            self.t('pt'),
-            self.t('pt_BR'),
-        )
+        assert (
+            self.t('pt') !=
+            self.t('pt_BR'))
 
     def test_not_known(self):
-        assert_is_none(self.t('la'))
+        assert self.t('la') is None
 
     def test_not_found(self):
-        assert_is_none(self.t('ry'))
+        assert self.t('ry') is None
 
 class test_principal_territory:
 
@@ -302,115 +294,116 @@ class test_principal_territory:
         # el -> el_GR
         lang = L.parse_language('el')
         cc = lang.get_principal_territory_code()
-        assert_equal(cc, 'GR')
+        assert cc == 'GR'
 
     def test_remove_2(self):
         # el_GR -> el
         lang = L.parse_language('el_GR')
-        assert_equal(str(lang), 'el_GR')
+        assert str(lang) == 'el_GR'
         rc = lang.remove_principal_territory_code()
-        assert_is(rc, True)
-        assert_equal(str(lang), 'el')
+        assert rc is True
+        assert str(lang) == 'el'
 
     def test_found_3(self):
         # ang -> ang_GB
         lang = L.parse_language('ang')
         cc = lang.get_principal_territory_code()
-        assert_equal(cc, 'GB')
+        assert cc == 'GB'
 
     def test_remove_3(self):
         # ang_GB -> ang
         lang = L.parse_language('ang_GB')
-        assert_equal(str(lang), 'ang_GB')
+        assert str(lang) == 'ang_GB'
         rc = lang.remove_principal_territory_code()
-        assert_is(rc, True)
-        assert_equal(str(lang), 'ang')
+        assert rc is True
+        assert str(lang) == 'ang'
 
     def test_no_principal_territory_code(self):
         # en -/-> en_US
         lang = L.parse_language('en')
         cc = lang.get_principal_territory_code()
-        assert_is_none(cc)
+        assert cc is None
 
     def test_no_remove_principal_territory_code(self):
         # en_US -/-> en
         lang = L.parse_language('en_US')
-        assert_equal(str(lang), 'en_US')
+        assert str(lang) == 'en_US'
         rc = lang.remove_principal_territory_code()
-        assert_is_none(rc)
-        assert_equal(str(lang), 'en_US')
+        assert rc is None
+        assert str(lang) == 'en_US'
 
     def test_not_found(self):
         lang = L.parse_language('ry')
         cc = lang.get_principal_territory_code()
-        assert_equal(cc, None)
+        assert cc is None
 
 class test_unrepresentable_characters:
 
     def test_ll_bad(self):
         lang = L.parse_language('pl')
         result = lang.get_unrepresentable_characters('ISO-8859-1')
-        assert_not_equal(result, [])
+        assert result != []
 
     def test_ll_ok(self):
         lang = L.parse_language('pl')
         result = lang.get_unrepresentable_characters('ISO-8859-2')
-        assert_equal(result, [])
+        assert result == []
 
     def test_ll_cc_bad(self):
         lang = L.parse_language('pl_PL')
         result = lang.get_unrepresentable_characters('ISO-8859-1')
-        assert_not_equal(result, [])
+        assert result != []
 
     def test_ll_cc_ok(self):
         lang = L.parse_language('pl_PL')
         result = lang.get_unrepresentable_characters('ISO-8859-2')
-        assert_equal(result, [])
+        assert result == []
 
     def test_ll_mod_bad(self):
         lang = L.parse_language('en@quot')
         result = lang.get_unrepresentable_characters('ISO-8859-1')
-        assert_not_equal(result, [])
+        assert result != []
 
     def test_ll_mod_ok(self):
         lang = L.parse_language('en@quot')
         result = lang.get_unrepresentable_characters('UTF-8')
-        assert_equal(result, [])
+        assert result == []
 
     def test_ll_cc_mod_bad(self):
         lang = L.parse_language('en_US@quot')
         result = lang.get_unrepresentable_characters('ISO-8859-1')
-        assert_not_equal(result, [])
+        assert result != []
 
     def test_ll_cc_mod_ok(self):
         lang = L.parse_language('en_US@quot')
         result = lang.get_unrepresentable_characters('UTF-8')
-        assert_equal(result, [])
+        assert result == []
 
     def test_ll_optional(self):
         # U+0178 (LATIN CAPITAL LETTER Y WITH DIAERESIS) is not representable
         # in ISO-8859-1, but we normally turn a blind eye to this.
         lang = L.parse_language('fr')
         result = lang.get_unrepresentable_characters('ISO-8859-1')
-        assert_equal(result, [])
+        assert result == []
         result = lang.get_unrepresentable_characters('ISO-8859-1', strict=True)
-        assert_not_equal(result, [])
+        assert result != []
 
     def test_ll_not_found(self):
         lang = L.parse_language('ry')
         result = lang.get_unrepresentable_characters('ISO-8859-1')
-        assert_is_none(result)
+        assert result is None
 
     @tools.fork_isolation
     def test_extra_encoding(self):
         encoding = 'GEORGIAN-PS'
         lang = L.parse_language('pl')
-        with assert_raises(LookupError):
+        with pytest.raises(LookupError):
             ''.encode(encoding)
         E.install_extra_encodings()
         result = lang.get_unrepresentable_characters(encoding)
-        assert_not_equal(result, [])
+        assert result != []
 
+@tools.collect_yielded
 def test_glibc_supported():
     def t(l):
         lang = L.parse_language(l)
@@ -419,16 +412,16 @@ def test_glibc_supported():
         except L.FixingLanguageCodesFailed:
             # FIXME: some ISO-639-3 codes are not recognized yet
             if len(l.split('_')[0]) == 3:
-                raise nose.SkipTest('expected failure')
+                raise unittest.SkipTest('expected failure')
             reason = locales_to_skip.get(l)
             if reason is not None:
-                raise nose.SkipTest(reason)
+                raise unittest.SkipTest(reason)
             raise
         assert_equal(str(lang), l)
     try:
         file = open('/usr/share/i18n/SUPPORTED', encoding='ASCII')
     except OSError as exc:
-        raise nose.SkipTest(exc)
+        raise unittest.SkipTest(exc)
     locales = set()
     with file:
         for line in file:
@@ -452,6 +445,7 @@ def test_glibc_supported():
     for l in sorted(locales):
         yield t, l
 
+@tools.collect_yielded
 def test_poedit():
     # https://github.com/vslavik/poedit/blob/v1.8.1-oss/src/language_impl_legacy.h
     # There won't be any new names in this table,
@@ -459,178 +453,178 @@ def test_poedit():
     def t(name, poedit_ll):
         poedit_ll = L.parse_language(poedit_ll)
         ll = L.get_language_for_name(name)
-        assert_equal(ll, poedit_ll)
+        assert ll == poedit_ll
     def x(name, poedit_ll):
         poedit_ll = L.parse_language(poedit_ll)
-        with assert_raises(LookupError):
+        with pytest.raises(LookupError):
             L.get_language_for_name(name)
-        raise nose.SkipTest('expected failure')
-    yield t, 'Abkhazian', 'ab'
-    yield t, 'Afar', 'aa'
-    yield t, 'Afrikaans', 'af'
-    yield t, 'Albanian', 'sq'
-    yield t, 'Amharic', 'am'
-    yield t, 'Arabic', 'ar'
-    yield t, 'Armenian', 'hy'
-    yield t, 'Assamese', 'as'
-    yield t, 'Avestan', 'ae'
-    yield t, 'Aymara', 'ay'
-    yield t, 'Azerbaijani', 'az'
-    yield t, 'Bashkir', 'ba'
-    yield t, 'Basque', 'eu'
-    yield t, 'Belarusian', 'be'
-    yield t, 'Bengali', 'bn'
-    yield t, 'Bislama', 'bi'
-    yield t, 'Bosnian', 'bs'
-    yield t, 'Breton', 'br'
-    yield t, 'Bulgarian', 'bg'
-    yield t, 'Burmese', 'my'
-    yield t, 'Catalan', 'ca'
-    yield t, 'Chamorro', 'ch'
-    yield t, 'Chechen', 'ce'
-    yield t, 'Chichewa; Nyanja', 'ny'
-    yield t, 'Chinese', 'zh'
-    yield t, 'Church Slavic', 'cu'
-    yield t, 'Chuvash', 'cv'
-    yield t, 'Cornish', 'kw'
-    yield t, 'Corsican', 'co'
-    yield t, 'Croatian', 'hr'
-    yield t, 'Czech', 'cs'
-    yield t, 'Danish', 'da'
-    yield t, 'Dutch', 'nl'
-    yield t, 'Dzongkha', 'dz'
-    yield t, 'English', 'en'
-    yield t, 'Esperanto', 'eo'
-    yield t, 'Estonian', 'et'
-    yield t, 'Faroese', 'fo'
-    yield t, 'Fijian', 'fj'
-    yield t, 'Finnish', 'fi'
-    yield t, 'French', 'fr'
-    yield t, 'Frisian', 'fy'
-    yield t, 'Friulian', 'fur'
-    yield t, 'Gaelic', 'gd'
-    yield t, 'Galician', 'gl'
-    yield t, 'Georgian', 'ka'
-    yield t, 'German', 'de'
-    yield t, 'Greek', 'el'
-    yield t, 'Guarani', 'gn'
-    yield t, 'Gujarati', 'gu'
-    yield t, 'Hausa', 'ha'
-    yield t, 'Hebrew', 'he'
-    yield t, 'Herero', 'hz'
-    yield t, 'Hindi', 'hi'
-    yield t, 'Hiri Motu', 'ho'
-    yield t, 'Hungarian', 'hu'
-    yield t, 'Icelandic', 'is'
-    yield t, 'Indonesian', 'id'
-    yield t, 'Interlingua', 'ia'
-    yield t, 'Interlingue', 'ie'
-    yield t, 'Inuktitut', 'iu'
-    yield t, 'Inupiaq', 'ik'
-    yield t, 'Irish', 'ga'
-    yield t, 'Italian', 'it'
-    yield t, 'Japanese', 'ja'
-    yield t, 'Javanese', 'jv'  # https://github.com/vslavik/poedit/pull/193
-    yield t, 'Kalaallisut', 'kl'
-    yield t, 'Kannada', 'kn'
-    yield t, 'Kashmiri', 'ks'
-    yield t, 'Kazakh', 'kk'
-    yield t, 'Khmer', 'km'
-    yield t, 'Kikuyu', 'ki'
-    yield t, 'Kinyarwanda', 'rw'
-    yield t, 'Komi', 'kv'
-    yield t, 'Korean', 'ko'
-    yield t, 'Kuanyama', 'kj'
-    yield t, 'Kurdish', 'ku'
-    yield t, 'Kyrgyz', 'ky'
-    yield t, 'Lao', 'lo'
-    yield t, 'Latin', 'la'
-    yield t, 'Latvian', 'lv'
-    yield t, 'Letzeburgesch', 'lb'
-    yield t, 'Lingala', 'ln'
-    yield t, 'Lithuanian', 'lt'
-    yield t, 'Macedonian', 'mk'
-    yield t, 'Malagasy', 'mg'
-    yield t, 'Malay', 'ms'
-    yield t, 'Malayalam', 'ml'
-    yield t, 'Maltese', 'mt'
-    yield t, 'Maori', 'mi'
-    yield t, 'Marathi', 'mr'
-    yield t, 'Marshall', 'mh'
-    yield t, 'Moldavian', 'ro_MD'  # XXX poedit uses deprecated "mo"
-    yield t, 'Mongolian', 'mn'
-    yield t, 'Nauru', 'na'
-    yield t, 'Navajo', 'nv'
-    yield t, 'Ndebele, South', 'nr'
-    yield t, 'Ndonga', 'ng'
-    yield t, 'Nepali', 'ne'
-    yield t, 'Northern Sami', 'se'
-    yield t, 'Norwegian Bokmal', 'nb'
-    yield t, 'Norwegian Nynorsk', 'nn'
-    yield t, 'Occitan', 'oc'
-    yield t, 'Oriya', 'or'
-    yield t, 'Ossetian; Ossetic', 'os'
-    yield t, 'Pali', 'pi'
-    yield t, 'Panjabi', 'pa'
-    yield t, 'Pashto, Pushto', 'ps'
-    yield t, 'Persian', 'fa'
-    yield t, 'Polish', 'pl'
-    yield t, 'Portuguese', 'pt'
-    yield t, 'Quechua', 'qu'
-    yield t, 'Rhaeto-Romance', 'rm'
-    yield t, 'Romanian', 'ro'
-    yield t, 'Rundi', 'rn'
-    yield t, 'Russian', 'ru'
-    yield t, 'Samoan', 'sm'
-    yield t, 'Sangro', 'sg'
-    yield t, 'Sanskrit', 'sa'
-    yield t, 'Sardinian', 'sc'
-    yield t, 'Serbian', 'sr'
-    yield t, 'Sesotho', 'st'
-    yield t, 'Setswana', 'tn'
-    yield t, 'Shona', 'sn'
-    yield t, 'Sindhi', 'sd'
-    yield t, 'Sinhalese', 'si'
-    yield t, 'Siswati', 'ss'
-    yield t, 'Slovak', 'sk'
-    yield t, 'Slovenian', 'sl'
-    yield t, 'Somali', 'so'
-    yield t, 'Spanish', 'es'
-    yield t, 'Sundanese', 'su'
-    yield t, 'Swahili', 'sw'
-    yield t, 'Swedish', 'sv'
-    yield t, 'Tagalog', 'tl'
-    yield t, 'Tahitian', 'ty'
-    yield t, 'Tajik', 'tg'
-    yield t, 'Tamil', 'ta'
-    yield t, 'Tatar', 'tt'
-    yield t, 'Telugu', 'te'
-    yield t, 'Thai', 'th'
-    yield t, 'Tibetan', 'bo'
-    yield t, 'Tigrinya', 'ti'
-    yield t, 'Tonga', 'to'
-    yield t, 'Tsonga', 'ts'
-    yield t, 'Turkish', 'tr'
-    yield t, 'Turkmen', 'tk'
-    yield t, 'Twi', 'tw'
-    yield t, 'Ukrainian', 'uk'
-    yield t, 'Urdu', 'ur'
-    yield t, 'Uyghur', 'ug'
-    yield t, 'Uzbek', 'uz'
-    yield t, 'Vietnamese', 'vi'
-    yield t, 'Volapuk', 'vo'
-    yield t, 'Walloon', 'wa'
-    yield t, 'Welsh', 'cy'
-    yield t, 'Wolof', 'wo'
-    yield t, 'Xhosa', 'xh'
-    yield t, 'Yiddish', 'yi'
-    yield t, 'Yoruba', 'yo'
-    yield t, 'Zhuang', 'za'
-    yield t, 'Zulu', 'zu'
+        raise unittest.SkipTest('expected failure')
+    yield t, ('Abkhazian', 'ab')
+    yield t, ('Afar', 'aa')
+    yield t, ('Afrikaans', 'af')
+    yield t, ('Albanian', 'sq')
+    yield t, ('Amharic', 'am')
+    yield t, ('Arabic', 'ar')
+    yield t, ('Armenian', 'hy')
+    yield t, ('Assamese', 'as')
+    yield t, ('Avestan', 'ae')
+    yield t, ('Aymara', 'ay')
+    yield t, ('Azerbaijani', 'az')
+    yield t, ('Bashkir', 'ba')
+    yield t, ('Basque', 'eu')
+    yield t, ('Belarusian', 'be')
+    yield t, ('Bengali', 'bn')
+    yield t, ('Bislama', 'bi')
+    yield t, ('Bosnian', 'bs')
+    yield t, ('Breton', 'br')
+    yield t, ('Bulgarian', 'bg')
+    yield t, ('Burmese', 'my')
+    yield t, ('Catalan', 'ca')
+    yield t, ('Chamorro', 'ch')
+    yield t, ('Chechen', 'ce')
+    yield t, ('Chichewa; Nyanja', 'ny')
+    yield t, ('Chinese', 'zh')
+    yield t, ('Church Slavic', 'cu')
+    yield t, ('Chuvash', 'cv')
+    yield t, ('Cornish', 'kw')
+    yield t, ('Corsican', 'co')
+    yield t, ('Croatian', 'hr')
+    yield t, ('Czech', 'cs')
+    yield t, ('Danish', 'da')
+    yield t, ('Dutch', 'nl')
+    yield t, ('Dzongkha', 'dz')
+    yield t, ('English', 'en')
+    yield t, ('Esperanto', 'eo')
+    yield t, ('Estonian', 'et')
+    yield t, ('Faroese', 'fo')
+    yield t, ('Fijian', 'fj')
+    yield t, ('Finnish', 'fi')
+    yield t, ('French', 'fr')
+    yield t, ('Frisian', 'fy')
+    yield t, ('Friulian', 'fur')
+    yield t, ('Gaelic', 'gd')
+    yield t, ('Galician', 'gl')
+    yield t, ('Georgian', 'ka')
+    yield t, ('German', 'de')
+    yield t, ('Greek', 'el')
+    yield t, ('Guarani', 'gn')
+    yield t, ('Gujarati', 'gu')
+    yield t, ('Hausa', 'ha')
+    yield t, ('Hebrew', 'he')
+    yield t, ('Herero', 'hz')
+    yield t, ('Hindi', 'hi')
+    yield t, ('Hiri Motu', 'ho')
+    yield t, ('Hungarian', 'hu')
+    yield t, ('Icelandic', 'is')
+    yield t, ('Indonesian', 'id')
+    yield t, ('Interlingua', 'ia')
+    yield t, ('Interlingue', 'ie')
+    yield t, ('Inuktitut', 'iu')
+    yield t, ('Inupiaq', 'ik')
+    yield t, ('Irish', 'ga')
+    yield t, ('Italian', 'it')
+    yield t, ('Japanese', 'ja')
+    yield t, ('Javanese', 'jv')  # https://github.com/vslavik/poedit/pull/193
+    yield t, ('Kalaallisut', 'kl')
+    yield t, ('Kannada', 'kn')
+    yield t, ('Kashmiri', 'ks')
+    yield t, ('Kazakh', 'kk')
+    yield t, ('Khmer', 'km')
+    yield t, ('Kikuyu', 'ki')
+    yield t, ('Kinyarwanda', 'rw')
+    yield t, ('Komi', 'kv')
+    yield t, ('Korean', 'ko')
+    yield t, ('Kuanyama', 'kj')
+    yield t, ('Kurdish', 'ku')
+    yield t, ('Kyrgyz', 'ky')
+    yield t, ('Lao', 'lo')
+    yield t, ('Latin', 'la')
+    yield t, ('Latvian', 'lv')
+    yield t, ('Letzeburgesch', 'lb')
+    yield t, ('Lingala', 'ln')
+    yield t, ('Lithuanian', 'lt')
+    yield t, ('Macedonian', 'mk')
+    yield t, ('Malagasy', 'mg')
+    yield t, ('Malay', 'ms')
+    yield t, ('Malayalam', 'ml')
+    yield t, ('Maltese', 'mt')
+    yield t, ('Maori', 'mi')
+    yield t, ('Marathi', 'mr')
+    yield t, ('Marshall', 'mh')
+    yield t, ('Moldavian', 'ro_MD')  # XXX poedit uses deprecated "mo"
+    yield t, ('Mongolian', 'mn')
+    yield t, ('Nauru', 'na')
+    yield t, ('Navajo', 'nv')
+    yield t, ('Ndebele, South', 'nr')
+    yield t, ('Ndonga', 'ng')
+    yield t, ('Nepali', 'ne')
+    yield t, ('Northern Sami', 'se')
+    yield t, ('Norwegian Bokmal', 'nb')
+    yield t, ('Norwegian Nynorsk', 'nn')
+    yield t, ('Occitan', 'oc')
+    yield t, ('Oriya', 'or')
+    yield t, ('Ossetian; Ossetic', 'os')
+    yield t, ('Pali', 'pi')
+    yield t, ('Panjabi', 'pa')
+    yield t, ('Pashto, Pushto', 'ps')
+    yield t, ('Persian', 'fa')
+    yield t, ('Polish', 'pl')
+    yield t, ('Portuguese', 'pt')
+    yield t, ('Quechua', 'qu')
+    yield t, ('Rhaeto-Romance', 'rm')
+    yield t, ('Romanian', 'ro')
+    yield t, ('Rundi', 'rn')
+    yield t, ('Russian', 'ru')
+    yield t, ('Samoan', 'sm')
+    yield t, ('Sangro', 'sg')
+    yield t, ('Sanskrit', 'sa')
+    yield t, ('Sardinian', 'sc')
+    yield t, ('Serbian', 'sr')
+    yield t, ('Sesotho', 'st')
+    yield t, ('Setswana', 'tn')
+    yield t, ('Shona', 'sn')
+    yield t, ('Sindhi', 'sd')
+    yield t, ('Sinhalese', 'si')
+    yield t, ('Siswati', 'ss')
+    yield t, ('Slovak', 'sk')
+    yield t, ('Slovenian', 'sl')
+    yield t, ('Somali', 'so')
+    yield t, ('Spanish', 'es')
+    yield t, ('Sundanese', 'su')
+    yield t, ('Swahili', 'sw')
+    yield t, ('Swedish', 'sv')
+    yield t, ('Tagalog', 'tl')
+    yield t, ('Tahitian', 'ty')
+    yield t, ('Tajik', 'tg')
+    yield t, ('Tamil', 'ta')
+    yield t, ('Tatar', 'tt')
+    yield t, ('Telugu', 'te')
+    yield t, ('Thai', 'th')
+    yield t, ('Tibetan', 'bo')
+    yield t, ('Tigrinya', 'ti')
+    yield t, ('Tonga', 'to')
+    yield t, ('Tsonga', 'ts')
+    yield t, ('Turkish', 'tr')
+    yield t, ('Turkmen', 'tk')
+    yield t, ('Twi', 'tw')
+    yield t, ('Ukrainian', 'uk')
+    yield t, ('Urdu', 'ur')
+    yield t, ('Uyghur', 'ug')
+    yield t, ('Uzbek', 'uz')
+    yield t, ('Vietnamese', 'vi')
+    yield t, ('Volapuk', 'vo')
+    yield t, ('Walloon', 'wa')
+    yield t, ('Welsh', 'cy')
+    yield t, ('Wolof', 'wo')
+    yield t, ('Xhosa', 'xh')
+    yield t, ('Yiddish', 'yi')
+    yield t, ('Yoruba', 'yo')
+    yield t, ('Zhuang', 'za')
+    yield t, ('Zulu', 'zu')
     # TODO:
-    yield x, '(Afan) Oromo', 'om'
-    yield x, 'Bihari', 'bh'  # "bh" is marked as collective in ISO-639-2
-    yield x, 'Serbian (Latin)', 'sr_RS@latin'
-    yield x, 'Serbo-Croatian', 'sh'  # "sh" is deprecated in favor of "sr", "hr", "bs"
+    yield x, ('(Afan) Oromo', 'om')
+    yield x, ('Bihari', 'bh')  # "bh" is marked as collective in ISO-639-2
+    yield x, ('Serbian (Latin)', 'sr_RS@latin')
+    yield x, ('Serbo-Croatian', 'sh')  # "sh" is deprecated in favor of "sr", "hr", "bs"
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_misc.py
===================================================================
--- i18nspector-0.26.orig/tests/test_misc.py
+++ i18nspector-0.26/tests/test_misc.py
@@ -24,14 +24,7 @@ import stat
 import tempfile
 import time
 
-from nose.tools import (
-    assert_almost_equal,
-    assert_equal,
-    assert_is_instance,
-    assert_is_not_none,
-    assert_raises,
-    assert_true,
-)
+import pytest
 
 import lib.misc as M
 
@@ -40,11 +33,11 @@ from . import tools
 class test_unsorted:
 
     def t(self, lst, expected):
-        assert_is_instance(lst, list)
+        assert isinstance(lst, list)
         r = M.unsorted(lst)
-        assert_equal(r, expected)
+        assert r == expected
         r = M.unsorted(x for x in lst)
-        assert_equal(r, expected)
+        assert r == expected
 
     def test_0(self):
         self.t([], None)
@@ -71,7 +64,7 @@ class test_unsorted:
             while True:
                 yield 23
         r = M.unsorted(iterable())
-        assert_equal(r, (37, 23))
+        assert r == (37, 23)
 
 class test_check_sorted:
 
@@ -79,25 +72,24 @@ class test_check_sorted:
         M.check_sorted([17, 23, 37])
 
     def test_unsorted(self):
-        with assert_raises(M.DataIntegrityError) as cm:
+        with pytest.raises(M.DataIntegrityError) as cm:
             M.check_sorted([23, 37, 17])
-        assert_equal(str(cm.exception), '37 > 17')
+        assert str(cm.value) == '37 > 17'
 
 def test_sorted_vk():
     lst = ['eggs', 'spam', 'ham']
     d = dict(enumerate(lst))
-    assert_equal(
-        lst,
-        list(M.sorted_vk(d))
-    )
+    assert (
+        lst ==
+        list(M.sorted_vk(d)))
 
 class test_utc_now:
 
     def test_types(self):
         now = M.utc_now()
-        assert_is_instance(now, datetime.datetime)
-        assert_is_not_none(now.tzinfo)
-        assert_equal(now.tzinfo.utcoffset(now), datetime.timedelta(0))
+        assert isinstance(now, datetime.datetime)
+        assert now.tzinfo is not None
+        assert now.tzinfo.utcoffset(now) == datetime.timedelta(0)
 
     @tools.fork_isolation
     def test_tz_resistance(self):
@@ -108,18 +100,17 @@ class test_utc_now:
         now1 = t('Etc/GMT-4')
         now2 = t('Etc/GMT+2')
         tdelta = (now1 - now2).total_seconds()
-        assert_almost_equal(tdelta, 0, delta=0.75)
+        assert abs(tdelta - 0) <= 0.75
 
 class test_format_range:
 
     def t(self, x, y, max, expected):  # pylint: disable=redefined-builtin
-        assert_equal(
-            M.format_range(range(x, y), max=max),
-            expected
-        )
+        assert (
+            M.format_range(range(x, y), max=max) ==
+            expected)
 
     def test_max_is_lt_4(self):
-        with assert_raises(ValueError):
+        with pytest.raises(ValueError):
             self.t(5, 10, 3, None)
 
     def test_len_lt_max(self):
@@ -139,7 +130,7 @@ def test_throwaway_tempdir():
     with M.throwaway_tempdir('test'):
         d = tempfile.gettempdir()
         st = os.stat(d)
-        assert_equal(stat.S_IMODE(st.st_mode), 0o700)
-        assert_true(stat.S_ISDIR(st.st_mode))
+        assert stat.S_IMODE(st.st_mode) == 0o700
+        assert stat.S_ISDIR(st.st_mode)
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_moparser.py
===================================================================
--- i18nspector-0.26.orig/tests/test_moparser.py
+++ i18nspector-0.26/tests/test_moparser.py
@@ -20,10 +20,7 @@
 
 import random
 
-from nose.tools import (
-    assert_equal,
-    assert_raises,
-)
+import pytest
 
 import lib.moparser as M
 
@@ -38,21 +35,21 @@ def parser_for_bytes(data):
 class test_magic:
 
     def test_value(self):
-        assert_equal(M.little_endian_magic, b'\xDE\x12\x04\x95')
-        assert_equal(M.big_endian_magic, b'\x95\x04\x12\xDE')
+        assert M.little_endian_magic == b'\xDE\x12\x04\x95'
+        assert M.big_endian_magic == b'\x95\x04\x12\xDE'
 
     def test_short(self):
         for j in range(0, 3):
             data = M.little_endian_magic[:j]
-            with assert_raises(M.SyntaxError) as cm:
+            with pytest.raises(M.SyntaxError) as cm:
                 parser_for_bytes(data)
-            assert_equal(str(cm.exception), 'unexpected magic')
+            assert str(cm.value) == 'unexpected magic'
 
     def test_full(self):
         for magic in {M.little_endian_magic, M.big_endian_magic}:
-            with assert_raises(M.SyntaxError) as cm:
+            with pytest.raises(M.SyntaxError) as cm:
                 parser_for_bytes(magic)
-            assert_equal(str(cm.exception), 'truncated file')
+            assert str(cm.value) == 'truncated file'
 
     def test_random(self):
         while True:
@@ -62,8 +59,8 @@ class test_magic:
             if random_magic in {M.little_endian_magic, M.big_endian_magic}:
                 continue
             break
-        with assert_raises(M.SyntaxError) as cm:
+        with pytest.raises(M.SyntaxError) as cm:
             parser_for_bytes(random_magic)
-        assert_equal(str(cm.exception), 'unexpected magic')
+        assert str(cm.value) == 'unexpected magic'
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_polib4us.py
===================================================================
--- i18nspector-0.26.orig/tests/test_polib4us.py
+++ i18nspector-0.26/tests/test_polib4us.py
@@ -20,11 +20,6 @@
 
 import polib
 
-from nose.tools import (
-    assert_list_equal,
-    assert_true,
-)
-
 import lib.polib4us as M
 
 from . import tools
@@ -50,7 +45,7 @@ msgstr "b"
                 file.write(s)
                 file.flush()
                 po = polib.pofile(file.name)
-                assert_true(po[-1].obsolete)
+                assert po[-1].obsolete
         t()
         M.install_patches()
         t()
@@ -60,9 +55,8 @@ def test_flag_splitting():
     M.install_patches()
     e = polib.POEntry()
     e.flags = ['fuzzy,c-format']
-    assert_list_equal(
-        e.flags,
-        ['fuzzy', 'c-format']
-    )
+    assert (
+        e.flags ==
+        ['fuzzy', 'c-format'])
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_strformat_c.py
===================================================================
--- i18nspector-0.26.orig/tests/test_strformat_c.py
+++ i18nspector-0.26/tests/test_strformat_c.py
@@ -21,22 +21,25 @@
 import os
 import struct
 import sys
+import unittest
 import unittest.mock
 
-import nose
-from nose.tools import (
-    assert_equal,
-    assert_greater,
-    assert_is_instance,
-    assert_raises,
-    assert_sequence_equal,
-)
+import pytest
 
 import lib.strformat.c as M
 
+from . import tools
+
+
+# methods using the tools.collect_yielded decorator don't have a 'self'
+# since they end up being run before 'self' exists. pylint doesn't
+# understand this unusual situation
+# pylint: disable=no-method-argument
+
+
 def test_INT_MAX():
     struct.pack('=i', M.INT_MAX)
-    with assert_raises(struct.error):
+    with pytest.raises(struct.error):
         struct.pack('=i', M.INT_MAX + 1)
 
 def is_glibc():
@@ -49,121 +52,134 @@ def is_glibc():
 def test_NL_ARGMAX():
     plat = sys.platform
     if plat.startswith('linux') and is_glibc():
-        assert_equal(
-            M.NL_ARGMAX,
-            os.sysconf('SC_NL_ARGMAX')
-        )
+        assert (
+            M.NL_ARGMAX ==
+            os.sysconf('SC_NL_ARGMAX'))
     else:
-        raise nose.SkipTest('Test specific to Linux with glibc')
+        raise unittest.SkipTest('Test specific to Linux with glibc')
 
 small_NL_ARGMAX = unittest.mock.patch('lib.strformat.c.NL_ARGMAX', 42)
 # Setting NL_ARGMAX to a small number makes the *_index_out_of_range() tests
 # much faster.
 
 def test_lone_percent():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('%')
 
 def test_invalid_conversion_spec():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('%!')
 
 def test_add_argument():
     fmt = M.FormatString('%s')
-    with assert_raises(RuntimeError):
+    with pytest.raises(RuntimeError):
         fmt.add_argument(2, None)
 
 def test_text():
     fmt = M.FormatString('eggs%dbacon%dspam')
-    assert_equal(len(fmt), 5)
+    assert len(fmt) == 5
     fmt = list(fmt)
-    assert_equal(fmt[0], 'eggs')
-    assert_equal(fmt[2], 'bacon')
-    assert_equal(fmt[4], 'spam')
+    assert fmt[0] == 'eggs'
+    assert fmt[2] == 'bacon'
+    assert fmt[4] == 'spam'
 
 class test_types:
 
-    def t(self, s, tp, warn_type=None, integer=False):
+    @staticmethod
+    def t(s, tp, warn_type=None, integer=False):
         fmt = M.FormatString(s)
         [conv] = fmt
-        assert_is_instance(conv, M.Conversion)
-        assert_equal(conv.type, tp)
+        assert isinstance(conv, M.Conversion)
+        assert conv.type == tp
         if tp == 'void':
-            assert_sequence_equal(fmt.arguments, [])
+            assert fmt.arguments == []
         else:
             [[arg]] = fmt.arguments
-            assert_equal(arg.type, tp)
+            assert arg.type == tp
         if warn_type is None:
-            assert_sequence_equal(fmt.warnings, [])
+            assert fmt.warnings == []
         else:
             [warning] = fmt.warnings
-            assert_is_instance(warning, warn_type)
-        assert_equal(conv.integer, integer)
+            assert isinstance(warning, warn_type)
+        assert conv.integer == integer
 
-    def test_integer(self):
-        def t(s, tp, warn_type=None):
+    @tools.collect_yielded
+    def test_integer():
+        def t(s, tp, suffix, warn_type=None):
             integer = not suffix
-            self.t(s, tp + suffix, warn_type, integer)
+            test_types.t(s, tp + suffix, warn_type, integer)
         for c in 'din':
             suffix = ''
             if c == 'n':
                 suffix = ' *'
-            yield t, ('%hh' + c), 'signed char'
-            yield t, ('%h' + c), 'short int'
-            yield t, ('%' + c), 'int'
-            yield t, ('%l' + c), 'long int'
-            yield t, ('%ll' + c), 'long long int'
-            yield t, ('%L' + c), 'long long int', M.NonPortableConversion
-            yield t, ('%q' + c), 'long long int', M.NonPortableConversion
-            yield t, ('%j' + c), 'intmax_t'
-            yield t, ('%z' + c), 'ssize_t'
-            yield t, ('%Z' + c), 'ssize_t', M.NonPortableConversion
-            yield t, ('%t' + c), 'ptrdiff_t'
+            yield t, (('%hh' + c), 'signed char', suffix)
+            yield t, (('%h' + c), 'short int', suffix)
+            yield t, (('%' + c), 'int', suffix)
+            yield t, (('%l' + c), 'long int', suffix)
+            yield t, (('%ll' + c), 'long long int', suffix)
+            yield t, (('%L' + c), 'long long int', suffix, M.NonPortableConversion)
+            yield t, (('%q' + c), 'long long int', suffix, M.NonPortableConversion)
+            yield t, (('%j' + c), 'intmax_t', suffix)
+            yield t, (('%z' + c), 'ssize_t', suffix)
+            yield t, (('%Z' + c), 'ssize_t', suffix, M.NonPortableConversion)
+            yield t, (('%t' + c), 'ptrdiff_t', suffix)
         for c in 'ouxX':
             suffix = ''
-            yield t, ('%hh' + c), 'unsigned char'
-            yield t, ('%h' + c), 'unsigned short int'
-            yield t, ('%' + c), 'unsigned int'
-            yield t, ('%l' + c), 'unsigned long int'
-            yield t, ('%ll' + c), 'unsigned long long int'
-            yield t, ('%L' + c), 'unsigned long long int', M.NonPortableConversion
-            yield t, ('%q' + c), 'unsigned long long int', M.NonPortableConversion
-            yield t, ('%j' + c), 'uintmax_t'
-            yield t, ('%z' + c), 'size_t'
-            yield t, ('%Z' + c), 'size_t', M.NonPortableConversion
-            yield t, ('%t' + c), '[unsigned ptrdiff_t]'
+            yield t, (('%hh' + c), 'unsigned char', suffix)
+            yield t, (('%h' + c), 'unsigned short int', suffix)
+            yield t, (('%' + c), 'unsigned int', suffix)
+            yield t, (('%l' + c), 'unsigned long int', suffix)
+            yield t, (('%ll' + c), 'unsigned long long int', suffix)
+            yield t, (('%L' + c), 'unsigned long long int', suffix, M.NonPortableConversion)
+            yield t, (('%q' + c), 'unsigned long long int', suffix, M.NonPortableConversion)
+            yield t, (('%j' + c), 'uintmax_t', suffix)
+            yield t, (('%z' + c), 'size_t', suffix)
+            yield t, (('%Z' + c), 'size_t', suffix, M.NonPortableConversion)
+            yield t, (('%t' + c), '[unsigned ptrdiff_t]', suffix)
+
+    @tools.collect_yielded
+    def test_double():
+        def t(*args):
+            test_types.t(*args)
 
-    def test_double(self):
-        t = self.t
         for c in 'aefgAEFG':
-            yield t, ('%' + c), 'double'
-            yield t, ('%l' + c), 'double', M.NonPortableConversion
-            yield t, ('%L' + c), 'long double'
-
-    def test_char(self):
-        t = self.t
-        yield t, '%c', 'char'
-        yield t, '%lc', 'wint_t'
-        yield t, '%C', 'wint_t', M.NonPortableConversion
-        yield t, '%s', 'const char *'
-        yield t, '%ls', 'const wchar_t *'
-        yield t, '%S', 'const wchar_t *', M.NonPortableConversion
-
-    def test_void(self):
-        t = self.t
-        yield t, '%p', 'void *'
-        yield t, '%m', 'void'
-        yield t, '%%', 'void'
+            yield t, (('%' + c), 'double')
+            yield t, (('%l' + c), 'double', M.NonPortableConversion)
+            yield t, (('%L' + c), 'long double')
+
+    @tools.collect_yielded
+    def test_char():
+        def t(*args):
+            test_types.t(*args)
+
+        yield t, ('%c', 'char')
+        yield t, ('%lc', 'wint_t')
+        yield t, ('%C', 'wint_t', M.NonPortableConversion)
+        yield t, ('%s', 'const char *')
+        yield t, ('%ls', 'const wchar_t *')
+        yield t, ('%S', 'const wchar_t *', M.NonPortableConversion)
+
+    @tools.collect_yielded
+    def test_void():
+        def t(*args):
+            test_types.t(*args)
+
+        yield t, ('%p', 'void *')
+        yield t, ('%m', 'void')
+        yield t, ('%%', 'void')
 
-    def test_c99_macros(self):
+    @tools.collect_yielded
+    def test_c99_macros():
         # pylint: disable=undefined-loop-variable
         def _t(s, tp):
-            return self.t(s, tp, integer=True)
+            return test_types.t(s, tp, integer=True)
         def t(s, tp):
             return (
                 _t,
-                '%<{macro}>'.format(macro=s.format(c=c, n=n)),
-                ('u' if unsigned else '') + tp.format(n=n)
+                (
+                    '%<{macro}>'.format(macro=s.format(c=c, n=n)),
+                    ('u' if unsigned else '') + tp.format(n=n)
+                )
             )
         # pylint: enable=undefined-loop-variable
         for c in 'diouxX':
@@ -175,64 +191,71 @@ class test_types:
             yield t('PRI{c}MAX', 'intmax_t')
             yield t('PRI{c}PTR', 'intptr_t')
 
+_lengths = ['hh', 'h', 'l', 'll', 'q', 'j', 'z', 't', 'L']
+
 class test_invalid_length:
-    def t(self, s):
-        with assert_raises(M.LengthError):
+    @staticmethod
+    def t(s):
+        with pytest.raises(M.LengthError):
             M.FormatString(s)
 
-    _lengths = ['hh', 'h', 'l', 'll', 'q', 'j', 'z', 't', 'L']
-
-    def test_double(self):
-        t = self.t
+    @tools.collect_yielded
+    def test_double():
+        def t(*args):
+            test_invalid_length.t(*args)
         for c in 'aefgAEFG':
-            for l in self._lengths:
+            for l in _lengths:
                 if l in 'lL':
                     continue
                 yield t, ('%' + l + c)
 
-    def test_char(self):
-        t = self.t
+    @tools.collect_yielded
+    def test_char():
+        def t(*args):
+            test_invalid_length.t(*args)
         for c in 'cs':
-            for l in self._lengths:
+            for l in _lengths:
                 if l != 'l':
                     yield t, '%' + l + c
                 yield t, ('%' + l + c.upper())
 
-    def test_void(self):
-        t = self.t
+    @tools.collect_yielded
+    def test_void():
+        def t(*args):
+            test_invalid_length.t(*args)
         for c in 'pm%':
-            for l in self._lengths:
+            for l in _lengths:
                 yield t, ('%' + l + c)
 
 class test_numeration:
 
     def test_percent(self):
-        with assert_raises(M.ForbiddenArgumentIndex):
+        with pytest.raises(M.ForbiddenArgumentIndex):
             M.FormatString('%1$%')
 
     def test_errno(self):
         # FIXME?
         fmt = M.FormatString('%1$m')
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.arguments), 0)
+        assert len(fmt) == 1
+        assert len(fmt.arguments) == 0
 
     def test_swapped(self):
         fmt = M.FormatString('%2$s%1$d')
-        assert_equal(len(fmt), 2)
+        assert len(fmt) == 2
         [a1], [a2] = fmt.arguments
-        assert_equal(a1.type, 'int')
-        assert_equal(a2.type, 'const char *')
+        assert a1.type == 'int'
+        assert a2.type == 'const char *'
 
     def test_numbering_mixture(self):
         def t(s):
-            with assert_raises(M.ArgumentNumberingMixture):
+            with pytest.raises(M.ArgumentNumberingMixture):
                 M.FormatString(s)
         t('%s%2$s')
         t('%2$s%s')
 
     @small_NL_ARGMAX
     def test_index_out_of_range(self):
-        with assert_raises(M.ArgumentRangeError):
+        with pytest.raises(M.ArgumentRangeError):
             M.FormatString('%0$d')
         def fs(n):
             s = ''.join(
@@ -241,17 +264,17 @@ class test_numeration:
             )
             return M.FormatString(s)
         fmt = fs(M.NL_ARGMAX)
-        assert_equal(len(fmt), M.NL_ARGMAX)
-        assert_equal(len(fmt.arguments), M.NL_ARGMAX)
-        with assert_raises(M.ArgumentRangeError):
+        assert len(fmt) == M.NL_ARGMAX
+        assert len(fmt.arguments) == M.NL_ARGMAX
+        with pytest.raises(M.ArgumentRangeError):
             fs(M.NL_ARGMAX + 1)
 
     def test_initial_gap(self):
-        with assert_raises(M.MissingArgument):
+        with pytest.raises(M.MissingArgument):
             M.FormatString('%2$d')
 
     def test_gap(self):
-        with assert_raises(M.MissingArgument):
+        with pytest.raises(M.MissingArgument):
             M.FormatString('%3$d%1$d')
 
 class test_redundant_flag:
@@ -259,7 +282,7 @@ class test_redundant_flag:
     def t(self, s):
         fmt = M.FormatString(s)
         [exc] = fmt.warnings
-        assert_is_instance(exc, M.RedundantFlag)
+        assert isinstance(exc, M.RedundantFlag)
 
     def test_duplicate(self):
         self.t('%--17d')
@@ -274,87 +297,114 @@ class test_redundant_flag:
 
 class test_expected_flag:
 
-    def t(self, s):
+    @staticmethod
+    def t(s):
         fmt = M.FormatString(s)
-        assert_equal(len(fmt), 1)
+        assert len(fmt) == 1
 
-    def test_hash(self):
+    @tools.collect_yielded
+    def test_hash():
+        def t(*args):
+            test_expected_flag.t(*args)
         for c in 'oxXaAeEfFgG':
-            yield self.t, ('%#' + c)
+            yield t, ('%#' + c)
 
-    def test_zero(self):
+    @tools.collect_yielded
+    def test_zero():
+        def t(*args):
+            test_expected_flag.t(*args)
         for c in 'diouxXaAeEfFgG':
-            yield self.t, ('%0' + c)
+            yield t, ('%0' + c)
 
-    def test_apos(self):
+    @tools.collect_yielded
+    def test_apos():
+        def t(*args):
+            test_expected_flag.t(*args)
         for c in 'diufFgG':
-            yield self.t, ("%'" + c)
+            yield t, ("%'" + c)
 
-    def test_other(self):
+    @tools.collect_yielded
+    def test_other():
+        def t(*args):
+            test_expected_flag.t(*args)
         for flag in '- +I':
             for c in 'diouxXaAeEfFgGcCsSpm':
-                yield self.t, ('%' + flag + c)
+                yield t, ('%' + flag + c)
 
 class test_unexpected_flag:
 
-    def t(self, s):
-        with assert_raises(M.FlagError):
+    @staticmethod
+    def t(s):
+        with pytest.raises(M.FlagError):
             M.FormatString(s)
 
-    def test_hash(self):
+    @tools.collect_yielded
+    def test_hash():
+        def t(*args):
+            test_unexpected_flag.t(*args)
         for c in 'dicCsSnpm%':
-            yield self.t, ('%#' + c)
+            yield t, ('%#' + c)
 
-    def test_zero(self):
+    @tools.collect_yielded
+    def test_zero():
+        def t(*args):
+            test_unexpected_flag.t(*args)
         for c in 'cCsSnpm%':
-            yield self.t, ('%0' + c)
+            yield t, ('%0' + c)
 
-    def test_apos(self):
+    @tools.collect_yielded
+    def test_apos():
+        def t(*args):
+            test_unexpected_flag.t(*args)
         for c in 'oxXaAeEcCsSnpm%':
-            yield self.t, ("%'" + c)
+            yield t, ("%'" + c)
 
-    def test_other(self):
+    @tools.collect_yielded
+    def test_other():
+        def t(*args):
+            test_unexpected_flag.t(*args)
         for c in '%n':
             for flag in '- +I':
-                yield self.t, ('%' + flag + c)
+                yield t, ('%' + flag + c)
 
 class test_width:
 
-    def test_ok(self):
+    @tools.collect_yielded
+    def test_ok():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
+            assert len(fmt) == 1
         for c in 'diouxXaAeEfFgGcCsSp':
             yield t, ('%1' + c)
         yield t, '%1m'  # FIXME?
 
     def test_invalid(self):
         for c in '%n':
-            with assert_raises(M.WidthError):
+            with pytest.raises(M.WidthError):
                 M.FormatString('%1' + c)
 
     def test_too_large(self):
         fmt = M.FormatString('%{0}d'.format(M.INT_MAX))
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.arguments), 1)
-        with assert_raises(M.WidthRangeError):
+        assert len(fmt) == 1
+        assert len(fmt.arguments) == 1
+        with pytest.raises(M.WidthRangeError):
             M.FormatString('%{0}d'.format(M.INT_MAX + 1))
 
     def test_variable(self):
         fmt = M.FormatString('%*s')
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.arguments), 2)
+        assert len(fmt) == 1
+        assert len(fmt.arguments) == 2
         [a1], [a2] = fmt.arguments
-        assert_equal(a1.type, 'int')
-        assert_equal(a2.type, 'const char *')
+        assert a1.type == 'int'
+        assert a2.type == 'const char *'
 
     def _test_index(self, i):
         fmt = M.FormatString('%2$*{0}$s'.format(i))
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.arguments), 2)
+        assert len(fmt) == 1
+        assert len(fmt.arguments) == 2
         [a1], [a2] = fmt.arguments
-        assert_equal(a1.type, 'int')
-        assert_equal(a2.type, 'const char *')
+        assert a1.type == 'int'
+        assert a2.type == 'const char *'
 
     def test_index(self):
         self._test_index(1)
@@ -365,7 +415,7 @@ class test_width:
 
     @small_NL_ARGMAX
     def test_index_out_of_range(self):
-        with assert_raises(M.ArgumentRangeError):
+        with pytest.raises(M.ArgumentRangeError):
             M.FormatString('%1$*0$s')
         def fs(n):
             s = ''.join(
@@ -374,14 +424,14 @@ class test_width:
             ) + '%1$*{0}$s'.format(n)
             return M.FormatString(s)
         fmt = fs(M.NL_ARGMAX)
-        assert_equal(len(fmt), M.NL_ARGMAX - 1)
-        assert_equal(len(fmt.arguments), M.NL_ARGMAX)
-        with assert_raises(M.ArgumentRangeError):
+        assert len(fmt) == M.NL_ARGMAX - 1
+        assert len(fmt.arguments) == M.NL_ARGMAX
+        with pytest.raises(M.ArgumentRangeError):
             fs(M.NL_ARGMAX + 1)
 
     def test_numbering_mixture(self):
         def t(s):
-            with assert_raises(M.ArgumentNumberingMixture):
+            with pytest.raises(M.ArgumentNumberingMixture):
                 M.FormatString(s)
         t('%1$*s')
         t('%*1$s')
@@ -390,58 +440,62 @@ class test_width:
 
 class test_precision:
 
-    def test_ok(self):
+    @tools.collect_yielded
+    def test_ok():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
+            assert len(fmt) == 1
         for c in 'diouxXaAeEfFgGsS':
             yield t, ('%.1' + c)
 
-    def test_redundant_0(self):
+    @tools.collect_yielded
+    def test_redundant_0():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
+            assert len(fmt) == 1
             [warning] = fmt.warnings
-            assert_is_instance(warning, M.RedundantFlag)
+            assert isinstance(warning, M.RedundantFlag)
         for c in 'diouxX':
             yield t, ('%0.1' + c)
 
-    def test_non_redundant_0(self):
+    @tools.collect_yielded
+    def test_non_redundant_0():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
-            assert_sequence_equal(fmt.warnings, [])
+            assert len(fmt) == 1
+            assert fmt.warnings == []
         for c in 'aAeEfFgG':
             yield t, ('%0.1' + c)
 
-    def test_unexpected(self):
+    @tools.collect_yielded
+    def test_unexpected():
         def t(s):
-            with assert_raises(M.PrecisionError):
+            with pytest.raises(M.PrecisionError):
                 M.FormatString(s)
         for c in 'cCpnm%':
             yield t, ('%.1' + c)
 
     def test_too_large(self):
         fmt = M.FormatString('%.{0}f'.format(M.INT_MAX))
-        assert_equal(len(fmt), 1)
-        with assert_raises(M.PrecisionRangeError):
+        assert len(fmt) == 1
+        with pytest.raises(M.PrecisionRangeError):
             M.FormatString('%.{0}f'.format(M.INT_MAX + 1))
 
     def test_variable(self):
         fmt = M.FormatString('%.*f')
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.arguments), 2)
+        assert len(fmt) == 1
+        assert len(fmt.arguments) == 2
         [a1], [a2] = fmt.arguments
-        assert_equal(a1.type, 'int')
-        assert_equal(a2.type, 'double')
+        assert a1.type == 'int'
+        assert a2.type == 'double'
 
     def _test_index(self, i):
         fmt = M.FormatString('%2$.*{0}$f'.format(i))
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.arguments), 2)
+        assert len(fmt) == 1
+        assert len(fmt.arguments) == 2
         [a1], [a2] = fmt.arguments
-        assert_equal(a1.type, 'int')
-        assert_equal(a2.type, 'double')
+        assert a1.type == 'int'
+        assert a2.type == 'double'
 
     def test_index(self):
         self._test_index(1)
@@ -452,7 +506,7 @@ class test_precision:
 
     @small_NL_ARGMAX
     def test_index_out_of_range(self):
-        with assert_raises(M.ArgumentRangeError):
+        with pytest.raises(M.ArgumentRangeError):
             M.FormatString('%1$.*0$f')
         def fs(n):
             s = ''.join(
@@ -461,14 +515,14 @@ class test_precision:
             ) + '%1$.*{0}$f'.format(n)
             return M.FormatString(s)
         fmt = fs(M.NL_ARGMAX)
-        assert_equal(len(fmt), M.NL_ARGMAX - 1)
-        assert_equal(len(fmt.arguments), M.NL_ARGMAX)
-        with assert_raises(M.ArgumentRangeError):
+        assert len(fmt) == M.NL_ARGMAX - 1
+        assert len(fmt.arguments) == M.NL_ARGMAX
+        with pytest.raises(M.ArgumentRangeError):
             fs(M.NL_ARGMAX + 1)
 
     def test_numbering_mixture(self):
         def t(s):
-            with assert_raises(M.ArgumentNumberingMixture):
+            with pytest.raises(M.ArgumentNumberingMixture):
                 M.FormatString(s)
         t('%1$.*f')
         t('%.*1$f')
@@ -481,15 +535,15 @@ class test_type_compatibility:
         def t(s, tp):
             fmt = M.FormatString(s)
             [args] = fmt.arguments
-            assert_greater(len(args), 1)
+            assert len(args) > 1
             for arg in args:
-                assert_equal(arg.type, tp)
+                assert arg.type == tp
         t('%1$d%1$d', 'int')
         t('%1$d%1$i', 'int')
 
     def test_mismatch(self):
         def t(s):
-            with assert_raises(M.ArgumentTypeMismatch):
+            with pytest.raises(M.ArgumentTypeMismatch):
                 M.FormatString(s)
         t('%1$d%1$hd')
         t('%1$d%1$u')
@@ -498,11 +552,11 @@ class test_type_compatibility:
 @small_NL_ARGMAX
 def test_too_many_conversions():
     def t(s):
-        with assert_raises(M.ArgumentRangeError):
+        with pytest.raises(M.ArgumentRangeError):
             M.FormatString(s)
     s = M.NL_ARGMAX * '%d'
     fmt = M.FormatString(s)
-    assert_equal(len(fmt), M.NL_ARGMAX)
+    assert len(fmt) == M.NL_ARGMAX
     t(s + '%f')
     t(s + '%*f')
     t(s + '%.*f')
@@ -512,7 +566,7 @@ class test_get_last_integer_conversion:
     def test_overflow(self):
         fmt = M.FormatString('%s%d')
         for n in [-1, 0, 3]:
-            with assert_raises(IndexError):
+            with pytest.raises(IndexError):
                 fmt.get_last_integer_conversion(n=n)
 
     def t(self, s, n, tp=M.Conversion):
@@ -520,7 +574,7 @@ class test_get_last_integer_conversion:
         conv = fmt.get_last_integer_conversion(n=n)
         if tp is None:
             tp = type(tp)
-        assert_is_instance(conv, tp)
+        assert isinstance(conv, tp)
         return conv
 
     def test_okay(self):
Index: i18nspector-0.26/tests/test_strformat_perlbrace.py
===================================================================
--- i18nspector-0.26.orig/tests/test_strformat_perlbrace.py
+++ i18nspector-0.26/tests/test_strformat_perlbrace.py
@@ -18,40 +18,37 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from nose.tools import (
-    assert_equal,
-    assert_raises,
-)
+import pytest
 
 import lib.strformat.perlbrace as M
 
 def test_lone_lcb():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('{')
 
 def test_lone_rcb():
     M.FormatString('}')
 
 def test_invalid_field():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('{@}')
 
 def test_text():
     fmt = M.FormatString('bacon')
-    assert_equal(len(fmt), 1)
+    assert len(fmt) == 1
     [fld] = fmt
-    assert_equal(fld, 'bacon')
+    assert fld == 'bacon'
 
 class test_named_arguments:
 
     def test_good(self):
         fmt = M.FormatString('{spam}')
-        assert_equal(len(fmt), 1)
+        assert len(fmt) == 1
         [fld] = fmt
-        assert_equal(fld, '{spam}')
+        assert fld == '{spam}'
 
     def test_bad(self):
-        with assert_raises(M.Error):
+        with pytest.raises(M.Error):
             M.FormatString('{3ggs}')
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_strformat_pybrace.py
===================================================================
--- i18nspector-0.26.orig/tests/test_strformat_pybrace.py
+++ i18nspector-0.26/tests/test_strformat_pybrace.py
@@ -21,18 +21,13 @@
 import struct
 import unittest.mock
 
-from nose.tools import (
-    assert_equal,
-    assert_is,
-    assert_is_instance,
-    assert_raises,
-)
+import pytest
 
 import lib.strformat.pybrace as M
 
 def test_SSIZE_MAX():
     struct.pack('=i', M.SSIZE_MAX)
-    with assert_raises(struct.error):
+    with pytest.raises(struct.error):
         struct.pack('=i', M.SSIZE_MAX + 1)
 
 small_SSIZE_MAX = unittest.mock.patch('lib.strformat.pybrace.SSIZE_MAX', 42)
@@ -40,31 +35,31 @@ small_SSIZE_MAX = unittest.mock.patch('l
 # a very large number of arguments without running out of memory.
 
 def test_lone_lcb():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('{')
 
 def test_lone_rcb():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('}')
 
 def test_invalid_field():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('{@}')
 
 def test_add_argument():
     fmt = M.FormatString('{}')
-    with assert_raises(RuntimeError):
+    with pytest.raises(RuntimeError):
         fmt.add_argument(None, None)
-    with assert_raises(RuntimeError):
+    with pytest.raises(RuntimeError):
         fmt.add_argument('eggs', None)
 
 def test_text():
     fmt = M.FormatString('eggs{}bacon{}spam')
-    assert_equal(len(fmt), 5)
+    assert len(fmt) == 5
     fmt = list(fmt)
-    assert_equal(fmt[0], 'eggs')
-    assert_equal(fmt[2], 'bacon')
-    assert_equal(fmt[4], 'spam')
+    assert fmt[0] == 'eggs'
+    assert fmt[2] == 'bacon'
+    assert fmt[4] == 'spam'
 
 class test_types:
 
@@ -72,12 +67,12 @@ class test_types:
         types = frozenset(tp.__name__ for tp in types)
         fmt = M.FormatString('{:' + k + '}')
         [fld] = fmt
-        assert_is_instance(fld, M.Field)
-        assert_equal(fld.types, types)
-        assert_equal(len(fmt.argument_map), 1)
+        assert isinstance(fld, M.Field)
+        assert fld.types == types
+        assert len(fmt.argument_map) == 1
         [(key, [afld])] = fmt.argument_map.items()
-        assert_equal(key, 0)
-        assert_is(fld, afld)
+        assert key == 0
+        assert fld is afld
 
     def test_default(self):
         self.t('', int, float, str)
@@ -102,12 +97,12 @@ class test_conversion:
         types = frozenset(tp.__name__ for tp in types)
         fmt = M.FormatString('{!' + c + ':' + k + '}')
         [fld] = fmt
-        assert_is_instance(fld, M.Field)
-        assert_equal(fld.types, types)
-        assert_equal(len(fmt.argument_map), 1)
+        assert isinstance(fld, M.Field)
+        assert fld.types == types
+        assert len(fmt.argument_map) == 1
         [(key, [afld])] = fmt.argument_map.items()
-        assert_equal(key, 0)
-        assert_is(fld, afld)
+        assert key == 0
+        assert fld is afld
 
     def test_default(self):
         for c in 'sra':
@@ -120,11 +115,11 @@ class test_conversion:
     def test_numeric(self):
         for c in 'sra':
             for k in 'bcdoxXneEfFgG':
-                with assert_raises(M.FormatTypeMismatch):
+                with pytest.raises(M.FormatTypeMismatch):
                     self.t(c, k, int)
 
     def test_bad(self):
-        with assert_raises(M.ConversionError):
+        with pytest.raises(M.ConversionError):
             self.t('z', '')
 
 class test_numbered_arguments:
@@ -134,12 +129,12 @@ class test_numbered_arguments:
 
     def t(self, s, *types):
         fmt = M.FormatString(s)
-        assert_equal(len(fmt), len(types))
-        assert_equal(len(fmt.argument_map), len(types))
+        assert len(fmt) == len(types)
+        assert len(fmt.argument_map) == len(types)
         for (key, args), (xkey, xtype) in zip(sorted(fmt.argument_map.items()), enumerate(types)):
             [arg] = args
-            assert_equal(key, xkey)
-            assert_equal(arg.types, frozenset({xtype.__name__}))
+            assert key == xkey
+            assert arg.types == frozenset({xtype.__name__})
 
     def test_unnumbered(self):
         self.t('{:d}{:f}', int, float)
@@ -151,9 +146,9 @@ class test_numbered_arguments:
         self.t('{1:d}{0:f}', float, int)
 
     def test_mixed(self):
-        with assert_raises(M.ArgumentNumberingMixture):
+        with pytest.raises(M.ArgumentNumberingMixture):
             self.t('{0:d}{:f}')
-        with assert_raises(M.ArgumentNumberingMixture):
+        with pytest.raises(M.ArgumentNumberingMixture):
             self.t('{:d}{0:f}')
 
     def test_numbered_out_of_range(self):
@@ -161,7 +156,7 @@ class test_numbered_arguments:
             s = ('{' + str(i) + '}')
             M.FormatString(s)
         t(M.SSIZE_MAX)
-        with assert_raises(M.ArgumentRangeError):
+        with pytest.raises(M.ArgumentRangeError):
             t(M.SSIZE_MAX + 1)
 
     @small_SSIZE_MAX
@@ -170,7 +165,7 @@ class test_numbered_arguments:
             s = '{}' * i
             M.FormatString(s)
         t(M.SSIZE_MAX + 1)
-        with assert_raises(M.ArgumentRangeError):
+        with pytest.raises(M.ArgumentRangeError):
             t(M.SSIZE_MAX + 2)
 
 class test_named_arguments:
@@ -179,21 +174,21 @@ class test_named_arguments:
         fmt = M.FormatString('{spam}')
         [fld] = fmt
         [(aname, [afld])] = fmt.argument_map.items()
-        assert_equal(aname, 'spam')
-        assert_is(fld, afld)
+        assert aname == 'spam'
+        assert fld is afld
 
     def test_bad(self):
-        with assert_raises(M.Error):
+        with pytest.raises(M.Error):
             M.FormatString('{3ggs}')
 
 class test_format_spec:
 
     def test_bad_char(self):
-        with assert_raises(M.Error):
+        with pytest.raises(M.Error):
             M.FormatString('{:@}')
 
     def test_bad_letter(self):
-        with assert_raises(M.Error):
+        with pytest.raises(M.Error):
             M.FormatString('{:Z}')
 
     def test_comma(self):
@@ -203,7 +198,7 @@ class test_format_spec:
         for k in 'bcdoxXeEfFgG':
             t(k)
         for k in 'ns':
-            with assert_raises(M.Error):
+            with pytest.raises(M.Error):
                 t(k)
 
     def test_alt_sign(self):
@@ -213,7 +208,7 @@ class test_format_spec:
             t(c, '')
             for k in 'bcdoxXneEfFgG':
                 t(c, k)
-            with assert_raises(M.Error):
+            with pytest.raises(M.Error):
                 t(c, 's')
 
     def test_align(self):
@@ -228,7 +223,7 @@ class test_format_spec:
             t(c, '')
             for k in 'bcdoxXneEfFgG':
                 t(c, k)
-            with assert_raises(M.Error):
+            with pytest.raises(M.Error):
                 t(c, 's')
 
     def test_width(self):
@@ -239,7 +234,7 @@ class test_format_spec:
         for k in 'bcdoxXneEfFgGs\0':
             for i in 4, 37, M.SSIZE_MAX:
                 t(i, k)
-            with assert_raises(M.Error):
+            with pytest.raises(M.Error):
                 t(M.SSIZE_MAX + 1, k)
 
     def test_precision(self):
@@ -250,11 +245,11 @@ class test_format_spec:
         for k in 'neEfFgGs\0':
             for i in {4, 37, M.SSIZE_MAX}:
                 t(i, k)
-            with assert_raises(M.Error):
+            with pytest.raises(M.Error):
                 t(M.SSIZE_MAX + 1, k)
         for k in 'bcdoxX':
             for i in {4, 37, M.SSIZE_MAX, M.SSIZE_MAX + 1}:
-                with assert_raises(M.Error):
+                with pytest.raises(M.Error):
                     t(i, k)
 
     def test_type_compat(self):
@@ -262,7 +257,7 @@ class test_format_spec:
             s = '{0:' + k1 + '}{0:' + k2 + '}'
             M.FormatString(s)
         def e(k1, k2):
-            with assert_raises(M.ArgumentTypeMismatch):
+            with pytest.raises(M.ArgumentTypeMismatch):
                 t(k1, k2)
         ks = 'bcdoxXneEfFgGs'
         compat = [
@@ -291,13 +286,13 @@ class test_format_spec:
             s = '{' + str(v) + ':{' + str(f) + '}}'
             return M.FormatString(s)
         fmt = t()
-        assert_equal(len(fmt.argument_map), 2)
+        assert len(fmt.argument_map) == 2
         t(v=0, f=M.SSIZE_MAX)
-        with assert_raises(M.ArgumentRangeError):
+        with pytest.raises(M.ArgumentRangeError):
             t(v=0, f=(M.SSIZE_MAX + 1))
-        with assert_raises(M.ArgumentNumberingMixture):
+        with pytest.raises(M.ArgumentNumberingMixture):
             t(v=0)
-        with assert_raises(M.ArgumentNumberingMixture):
+        with pytest.raises(M.ArgumentNumberingMixture):
             t(f=0)
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_strformat_python.py
===================================================================
--- i18nspector-0.26.orig/tests/test_strformat_python.py
+++ i18nspector-0.26/tests/test_strformat_python.py
@@ -20,53 +20,55 @@
 
 import struct
 
-from nose.tools import (
-    assert_equal,
-    assert_greater,
-    assert_is_instance,
-    assert_raises,
-    assert_sequence_equal,
-)
+import pytest
 
 import lib.strformat.python as M
+from . import tools
+
+
+# methods using the tools.collect_yielded decorator don't have a 'self'
+# since they end up being run before 'self' exists. pylint doesn't
+# understand this unusual situation
+# pylint: disable=no-method-argument
+
 
 def test_SSIZE_MAX():
     struct.pack('=i', M.SSIZE_MAX)
-    with assert_raises(struct.error):
+    with pytest.raises(struct.error):
         struct.pack('=i', M.SSIZE_MAX + 1)
 
 def test_lone_percent():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('%')
 
 def test_invalid_conversion_spec():
-    with assert_raises(M.Error):
+    with pytest.raises(M.Error):
         M.FormatString('%!')
 
 def test_add_argument():
     fmt = M.FormatString('%s')
-    with assert_raises(RuntimeError):
+    with pytest.raises(RuntimeError):
         fmt.add_argument(None, None)
-    with assert_raises(RuntimeError):
+    with pytest.raises(RuntimeError):
         fmt.add_argument('eggs', None)
 
 def test_text():
     fmt = M.FormatString('eggs%dbacon%dspam')
-    assert_equal(len(fmt), 5)
+    assert len(fmt) == 5
     fmt = list(fmt)
-    assert_equal(fmt[0], 'eggs')
-    assert_equal(fmt[2], 'bacon')
-    assert_equal(fmt[4], 'spam')
+    assert fmt[0] == 'eggs'
+    assert fmt[2] == 'bacon'
+    assert fmt[4] == 'spam'
 
 class test_map:
 
     def t(self, key):
         s = '%(' + key + ')s'
         fmt = M.FormatString(s)
-        assert_equal(len(fmt), 1)
-        assert_sequence_equal(fmt.seq_arguments, [])
+        assert len(fmt) == 1
+        assert fmt.seq_arguments == []
         [pkey] = fmt.map_arguments.keys()
-        assert_equal(key, pkey)
+        assert key == pkey
 
     def test_simple(self):
         self.t('eggs')
@@ -75,69 +77,82 @@ class test_map:
         self.t('eggs(ham)spam')
 
     def test_unbalanced_parens(self):
-        with assert_raises(M.Error):
+        with pytest.raises(M.Error):
             self.t('eggs(ham')
 
 class test_types:
 
-    def t(self, s, tp, warn_type=None):
+    @staticmethod
+    def t( s, tp, warn_type=None):
         fmt = M.FormatString(s)
         [conv] = fmt
-        assert_is_instance(conv, M.Conversion)
-        assert_equal(conv.type, tp)
-        assert_equal(len(fmt.map_arguments), 0)
+        assert isinstance(conv, M.Conversion)
+        assert conv.type == tp
+        assert len(fmt.map_arguments) == 0
         if tp == 'None':
-            assert_sequence_equal(fmt.seq_arguments, [])
+            assert fmt.seq_arguments == []
         else:
             [arg] = fmt.seq_arguments
-            assert_equal(arg.type, tp)
+            assert arg.type == tp
         if warn_type is None:
-            assert_equal(len(fmt.warnings), 0)
+            assert len(fmt.warnings) == 0
         else:
             [warning] = fmt.warnings
-            assert_is_instance(warning, warn_type)
+            assert isinstance(warning, warn_type)
 
-    def test_integer(self):
-        t = self.t
+    @tools.collect_yielded
+    def test_integer():
+        def t(*args):
+            test_types.t(*args)
         for c in 'oxXdi':
-            yield t, '%' + c, 'int'
-        yield t, '%u', 'int', M.ObsoleteConversion
+            yield t, ('%' + c, 'int')
+        yield t, ('%u', 'int', M.ObsoleteConversion)
 
-    def test_float(self):
-        t = self.t
+    @tools.collect_yielded
+    def test_float():
+        def t(*args):
+            test_types.t(*args)
         for c in 'eEfFgG':
-            yield t, '%' + c, 'float'
-
-    def test_str(self):
-        t = self.t
-        yield t, '%c', 'chr'
-        yield t, '%s', 'str'
+            yield t, ('%' + c, 'float')
 
-    def test_repr(self):
-        t = self.t
+    @tools.collect_yielded
+    def test_str():
+        def t(*args):
+            test_types.t(*args)
+        yield t, ('%c', 'chr')
+        yield t, ('%s', 'str')
+
+    @tools.collect_yielded
+    def test_repr():
+        def t(*args):
+            test_types.t(*args)
         for c in 'ra':
-            yield t, '%' + c, 'object'
+            yield t, ('%' + c, 'object')
 
-    def test_void(self):
-        yield self.t, '%%', 'None'
+    @tools.collect_yielded
+    def test_void():
+        def t(*args):
+            test_types.t(*args)
+        yield t, ('%%', 'None')
 
+@tools.collect_yielded
 def test_length():
     def t(l):
         fmt = M.FormatString('%' + l + 'd')
         [warning] = fmt.warnings
-        assert_is_instance(warning, M.RedundantLength)
+        assert isinstance(warning, M.RedundantLength)
     for l in 'hlL':
         yield t, l
 
 class test_indexing:
 
     def test_percent(self):
-        with assert_raises(M.ForbiddenArgumentKey):
+        with pytest.raises(M.ForbiddenArgumentKey):
             M.FormatString('%(eggs)%')
 
     def test_indexing_mixture(self):
         def t(s):
-            with assert_raises(M.ArgumentIndexingMixture):
+            with pytest.raises(M.ArgumentIndexingMixture):
                 M.FormatString(s)
         t('%s%(eggs)s')
         t('%(eggs)s%s')
@@ -149,7 +164,7 @@ class test_multiple_flags:
     def t(self, s):
         fmt = M.FormatString(s)
         [exc] = fmt.warnings
-        assert_is_instance(exc, M.RedundantFlag)
+        assert isinstance(exc, M.RedundantFlag)
 
     def test_duplicate(self):
         self.t('%--17d')
@@ -160,108 +175,114 @@ class test_multiple_flags:
     def test_plus_space(self):
         self.t('%+ d')
 
+@tools.collect_yielded
 def test_single_flag():
 
     def t(s, expected):
         fmt = M.FormatString(s)
-        assert_equal(len(fmt), 1)
+        assert len(fmt) == 1
         if expected:
-            assert_sequence_equal(fmt.warnings, [])
+            assert fmt.warnings == []
         else:
             [exc] = fmt.warnings
-            assert_is_instance(exc, M.RedundantFlag)
+            assert isinstance(exc, M.RedundantFlag)
 
     for c in 'dioxXeEfFgGcrsa%':
-        yield t, ('%#' + c), (c in 'oxXeEfFgG')
+        yield t, (('%#' + c), (c in 'oxXeEfFgG'))
         for flag in '0 +':
-            yield t, ('%' + flag + c), (c in 'dioxXeEfFgG')
-        yield t, ('%-' + c), True
+            yield t, (('%' + flag + c), (c in 'dioxXeEfFgG'))
+        yield t, (('%-' + c), True)
 
 class test_width:
 
-    def test_ok(self):
+    @tools.collect_yielded
+    def test_ok():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
-            assert_sequence_equal(fmt.warnings, [])
+            assert len(fmt) == 1
+            assert fmt.warnings == []
         for c in 'dioxXeEfFgGcrsa%':
             yield t, ('%1' + c)
 
     def test_too_large(self):
         fmt = M.FormatString('%{0}d'.format(M.SSIZE_MAX))
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.seq_arguments), 1)
-        assert_equal(len(fmt.map_arguments), 0)
-        with assert_raises(M.WidthRangeError):
+        assert len(fmt) == 1
+        assert len(fmt.seq_arguments) == 1
+        assert len(fmt.map_arguments) == 0
+        with pytest.raises(M.WidthRangeError):
             M.FormatString('%{0}d'.format(M.SSIZE_MAX + 1))
 
     def test_variable(self):
         fmt = M.FormatString('%*s')
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.map_arguments), 0)
+        assert len(fmt) == 1
+        assert len(fmt.map_arguments) == 0
         [a1, a2] = fmt.seq_arguments
-        assert_equal(a1.type, 'int')
-        assert_equal(a2.type, 'str')
+        assert a1.type == 'int'
+        assert a2.type == 'str'
 
     def test_indexing_mixture(self):
         def t(s):
-            with assert_raises(M.ArgumentIndexingMixture):
+            with pytest.raises(M.ArgumentIndexingMixture):
                 M.FormatString(s)
         t('%*s%(eggs)s')
         t('%(eggs)s%*s')
 
 class test_precision:
 
-    def test_ok(self):
+    @tools.collect_yielded
+    def test_ok():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
+            assert len(fmt) == 1
         for c in 'dioxXeEfFgGrsa':
             yield t, ('%.1' + c)
 
-    def test_redundant_0(self):
+    @tools.collect_yielded
+    def test_redundant_0():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
+            assert len(fmt) == 1
             [warning] = fmt.warnings
-            assert_is_instance(warning, M.RedundantFlag)
+            assert isinstance(warning, M.RedundantFlag)
         for c in 'dioxX':
             yield t, ('%0.1' + c)
 
-    def test_non_redundant_0(self):
+    @tools.collect_yielded
+    def test_non_redundant_0():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
-            assert_sequence_equal(fmt.warnings, [])
+            assert len(fmt) == 1
+            assert fmt.warnings == []
         for c in 'eEfFgG':
             yield t, ('%0.1' + c)
 
-    def test_unexpected(self):
+    @tools.collect_yielded
+    def test_unexpected():
         def t(s):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt), 1)
+            assert len(fmt) == 1
             [warning] = fmt.warnings
-            assert_is_instance(warning, M.RedundantPrecision)
+            assert isinstance(warning, M.RedundantPrecision)
         for c in 'c%':
             yield t, ('%.1' + c)
 
     def test_too_large(self):
         fmt = M.FormatString('%.{0}f'.format(M.SSIZE_MAX))
-        assert_equal(len(fmt), 1)
-        with assert_raises(M.PrecisionRangeError):
+        assert len(fmt) == 1
+        with pytest.raises(M.PrecisionRangeError):
             M.FormatString('%.{0}f'.format(M.SSIZE_MAX + 1))
 
     def test_variable(self):
         fmt = M.FormatString('%.*f')
-        assert_equal(len(fmt), 1)
-        assert_equal(len(fmt.map_arguments), 0)
+        assert len(fmt) == 1
+        assert len(fmt.map_arguments) == 0
         [a1, a2] = fmt.seq_arguments
-        assert_equal(a1.type, 'int')
-        assert_equal(a2.type, 'float')
+        assert a1.type == 'int'
+        assert a2.type == 'float'
 
     def test_indexing_mixture(self):
         def t(s):
-            with assert_raises(M.ArgumentIndexingMixture):
+            with pytest.raises(M.ArgumentIndexingMixture):
                 M.FormatString(s)
         t('%.*f%(eggs)f')
         t('%(eggs)f%.*f')
@@ -271,26 +292,26 @@ class test_type_compatibility:
     def test_okay(self):
         def t(s, tp):
             fmt = M.FormatString(s)
-            assert_equal(len(fmt.seq_arguments), 0)
+            assert len(fmt.seq_arguments) == 0
             [args] = fmt.map_arguments.values()
-            assert_greater(len(args), 1)
+            assert len(args) > 1
             for arg in args:
-                assert_equal(arg.type, tp)
+                assert arg.type == tp
         t('%(eggs)d%(eggs)d', 'int')
         t('%(eggs)d%(eggs)i', 'int')
 
     def test_mismatch(self):
         def t(s):
-            with assert_raises(M.ArgumentTypeMismatch):
+            with pytest.raises(M.ArgumentTypeMismatch):
                 M.FormatString(s)
         t('%(eggs)d%(eggs)s')
 
 def test_seq_conversions():
     def t(s, n):
         fmt = M.FormatString(s)
-        assert_equal(len(fmt.seq_conversions), n)
+        assert len(fmt.seq_conversions) == n
         for arg in fmt.seq_conversions:
-            assert_is_instance(arg, M.Conversion)
+            assert isinstance(arg, M.Conversion)
     t('%d', 1)
     t('%d%d', 2)
     t('eggs%dham', 1)
Index: i18nspector-0.26/tests/test_tags.py
===================================================================
--- i18nspector-0.26.orig/tests/test_tags.py
+++ i18nspector-0.26/tests/test_tags.py
@@ -24,26 +24,20 @@ import inspect
 import operator
 import pkgutil
 
-from nose.tools import (
-    assert_equal,
-    assert_false,
-    assert_is_instance,
-    assert_raises,
-    assert_true,
-)
+import pytest
 
 import lib.check
 import lib.tags as M
+from . import tools
 
 class test_escape:
 
     def t(self, s, expected):
         result = M.safe_format('{}', s)
-        assert_is_instance(result, M.safestr)
-        assert_equal(
-            result,
-            expected
-        )
+        assert isinstance(result, M.safestr)
+        assert (
+            result ==
+            expected)
 
     def test_safe(self):
         s = 'fox'
@@ -75,6 +69,7 @@ def ast_to_tagnames(node):
     if ok:
         yield node.args[0].s
 
+@tools.collect_yielded
 def test_consistency():
     source_tagnames = set()
     def visit_mod(modname):
@@ -117,13 +112,13 @@ class test_enums:
         for op in operators:
             for i, x in enumerate(keys):
                 for j, y in enumerate(keys):
-                    assert_equal(op(x, y), op(i, j))
+                    assert op(x, y) == op(i, j)
                     if op is operator.eq:
-                        assert_false(op(x, j))
+                        assert not op(x, j)
                     elif op is operator.ne:
-                        assert_true(op(x, j))
+                        assert op(x, j)
                     else:
-                        with assert_raises(TypeError):
+                        with pytest.raises(TypeError):
                             op(x, j)
 
     def test_severities(self):
Index: i18nspector-0.26/tests/test_terminal.py
===================================================================
--- i18nspector-0.26.orig/tests/test_terminal.py
+++ i18nspector-0.26/tests/test_terminal.py
@@ -23,17 +23,13 @@ import os
 import pty
 import sys
 
-from nose.tools import (
-    assert_equal,
-)
-
 import lib.terminal as T
 
 from . import tools
 
 def test_strip_delay():
     def t(s, r=b''):
-        assert_equal(T._strip_delay(s), r)  # pylint: disable=protected-access
+        assert T._strip_delay(s) == r  # pylint: disable=protected-access
     t(b'$<1>')
     t(b'$<2/>')
     t(b'$<3*>')
@@ -56,7 +52,7 @@ def assert_tseq_equal(s, expected):
         # but not their subclasses. We don't want detailed comparisons,
         # because diff could contain control characters.
         pass
-    assert_equal(S(expected), S(s))
+    assert S(expected) == S(s)
 
 def test_dummy():
     t = assert_tseq_equal
Index: i18nspector-0.26/tests/test_version.py
===================================================================
--- i18nspector-0.26.orig/tests/test_version.py
+++ i18nspector-0.26/tests/test_version.py
@@ -20,10 +20,6 @@
 
 import os
 
-from nose.tools import (
-    assert_equal,
-)
-
 from lib.cli import __version__
 
 here = os.path.dirname(__file__)
@@ -34,7 +30,7 @@ def test_changelog():
     with open(path, 'rt', encoding='UTF-8') as file:
         line = file.readline()
     changelog_version = line.split()[1].strip('()')
-    assert_equal(changelog_version, __version__)
+    assert changelog_version == __version__
 
 def test_manpage():
     path = os.path.join(docdir, 'manpage.rst')
@@ -44,6 +40,6 @@ def test_manpage():
             if line.startswith(':version:'):
                 manpage_version = line.split()[-1]
                 break
-    assert_equal(manpage_version, __version__)
+    assert manpage_version == __version__
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/test_xml.py
===================================================================
--- i18nspector-0.26.orig/tests/test_xml.py
+++ i18nspector-0.26/tests/test_xml.py
@@ -21,11 +21,7 @@
 import re
 import xml.etree.ElementTree as etree
 
-from nose.tools import (
-    assert_is_none,
-    assert_is_not_none,
-    assert_raises,
-)
+import pytest
 
 import lib.xml as M
 
@@ -44,7 +40,7 @@ class test_well_formed:
 class test_malformed:
 
     def t(self, s):
-        with assert_raises(M.SyntaxError):
+        with pytest.raises(M.SyntaxError):
             M.check_fragment(s)
 
     def test_non_xml_character(self):
@@ -73,7 +69,7 @@ class test_name_re():
     def test_good(self):
         def t(s):
             match = self.regexp.match(s)
-            assert_is_not_none(match)
+            assert match is not None
         t(':')
         t('_')
         t('e')
@@ -85,7 +81,7 @@ class test_name_re():
     def test_bad(self):
         def t(s):
             match = self.regexp.match(s)
-            assert_is_none(match)
+            assert match is None
         t('')
         t('0')
         t('-')
Index: i18nspector-0.26/tests/tools.py
===================================================================
--- i18nspector-0.26.orig/tests/tools.py
+++ i18nspector-0.26/tests/tools.py
@@ -1,4 +1,5 @@
 # Copyright © 2012-2019 Jakub Wilk <jwilk@jwilk.net>
+# Copyright © 2021 Stuart Prescott <stuart@debian.org>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the “Software”), to deal
@@ -23,8 +24,9 @@ import os
 import sys
 import tempfile
 import traceback
+import unittest
 
-import nose
+import pytest
 
 temporary_file = functools.partial(
     tempfile.NamedTemporaryFile,
@@ -52,8 +54,8 @@ def fork_isolation(f):
     EXIT_SKIP_TEST = 102
 
     exit = os._exit  # pylint: disable=redefined-builtin,protected-access
-    # sys.exit() can't be used here, because nose catches all exceptions,
-    # including SystemExit
+    # sys.exit() can't be used here, because the test harness catches all
+    # exceptions, including SystemExit
 
     # pylint:disable=consider-using-sys-exit
 
@@ -66,7 +68,7 @@ def fork_isolation(f):
             os.close(readfd)
             try:
                 f(*args, **kwargs)
-            except nose.SkipTest as exc:
+            except unittest.SkipTest as exc:
                 s = str(exc).encode('UTF-8')
                 with os.fdopen(writefd, 'wb') as fp:
                     fp.write(s)
@@ -90,7 +92,7 @@ def fork_isolation(f):
             if status == (EXIT_EXCEPTION << 8):
                 raise IsolatedException() from Exception('\n\n' + msg)
             elif status == (EXIT_SKIP_TEST << 8):
-                raise nose.SkipTest(msg)
+                raise unittest.SkipTest(msg)
             elif status == 0 and msg == '':
                 pass
             else:
@@ -100,6 +102,37 @@ def fork_isolation(f):
 
     return wrapper
 
+
+def collect_yielded(collect_func):
+    # figure out if this is a function or method being wrapped
+    # as the wrapper needs a slightly different signature
+    if collect_func.__name__ != collect_func.__qualname__:
+        # method
+        @pytest.mark.parametrize("func, test_args",
+                                list(collect_func()))
+        def yield_tester(self, func, test_args):
+            # pylint: disable=unused-argument
+            # self is unused here
+            if isinstance(test_args, (tuple, list)):
+                func(*test_args)
+            elif isinstance(test_args, (str, int, float)):
+                func(test_args)
+            else:
+                raise ValueError("args must be either a tuple or a str")
+    else:
+        # function
+        @pytest.mark.parametrize("func, test_args",
+                                list(collect_func()))
+        def yield_tester(func, test_args):
+            if isinstance(test_args, (tuple, list)):
+                func(*test_args)
+            elif isinstance(test_args, (str, int, float)):
+                func(test_args)
+            else:
+                raise ValueError("args must be either a tuple or a str")
+    return yield_tester
+
+
 basedir = os.path.join(
     os.path.dirname(__file__),
     os.pardir,
Index: i18nspector-0.26/.coveragerc
===================================================================
--- i18nspector-0.26.orig/.coveragerc
+++ i18nspector-0.26/.coveragerc
@@ -1,5 +1,11 @@
 [run]
 branch = true
+omit =
+  *_boot*.py
+  */lib/python*/*
+  */dist-packages/*
+  */tests/*
+source = lib
 
 [report]
 show_missing = true
Index: i18nspector-0.26/private/update-branch-coverage
===================================================================
--- i18nspector-0.26.orig/private/update-branch-coverage
+++ i18nspector-0.26/private/update-branch-coverage
@@ -1,6 +1,6 @@
-#!/usr/bin/env python3
+#!/bin/bash
 
-# Copyright © 2014-2018 Jakub Wilk <jwilk@jwilk.net>
+# Copyright © 2021 Stuart Prescott <stuart@debian.org>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the “Software”), to deal
@@ -20,55 +20,22 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-import glob
-import io
-import os
-import sys
-
-import nose
-import nose.plugins.cover
-
-class Coverage(nose.plugins.cover.Coverage):
-
-    stream = None
-
-    def report(self, stream):
-        return super().report(self.stream)
-
-basedir = os.path.join(
-    os.path.dirname(__file__),
-    os.pardir,
-)
-
-def main():
-    os.chdir(basedir)
-    module_glob = os.path.join('tests', 'test_*.py')
-    modules = glob.glob(module_glob)
-    argv = [
-        sys.argv[0],
-        '--with-coverage',
-        '--cover-package=lib',
-        '--cover-erase',
-    ] + modules
-    path = os.path.join(
-        'tests',
-        'coverage'
-    )
-    plugin = Coverage()
-    report_stream = plugin.stream = io.StringIO()
-    print('Generated automatically by private/update-branch-coverage. '
-        'Do not edit.\n', file=report_stream)
-    ok = nose.run(argv=argv, plugins=[plugin])
-    if not ok:
-        sys.exit(1)
-    report_stream.seek(0)
-    with open(path + '.tmp', 'wt', encoding='ASCII') as file:
-        for line in report_stream:
-            line = line.rstrip()
-            print(line, file=file)
-    os.rename(path + '.tmp', path)
 
-if __name__ == '__main__':
-    main()
+set -e
+
+basedir=$(dirname "$0")
+
+cd "$basedir/.."
+
+pytest --cov lib/ --cov-branch -rsx -v tests --ignore tests/blackbox_tests
+
+(
+  set -e
+  echo 'Generated automatically by private/update-branch-coverage. Do not edit.'
+  echo
+  python3-coverage report
+) > tests/coverage.tmp
+
+mv tests/coverage.tmp tests/coverage
 
 # vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/blackbox_tests/__init__.py
===================================================================
--- i18nspector-0.26.orig/tests/blackbox_tests/__init__.py
+++ i18nspector-0.26/tests/blackbox_tests/__init__.py
@@ -1,415 +0,0 @@
-# Copyright © 2012-2017 Jakub Wilk <jwilk@jwilk.net>
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the “Software”), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-import difflib
-import inspect
-import io
-import multiprocessing as mp
-import os
-import re
-import shlex
-import signal
-import subprocess as ipc
-import sys
-import traceback
-import unittest
-
-import nose
-import nose.plugins
-
-from .. import tools
-
-here = os.path.dirname(__file__)
-
-# ----------------------------------------
-
-def this():
-    '''
-    Return function that called this function. (Hopefully!)
-    '''
-    return globals()[inspect.stack()[1][0].f_code.co_name]
-
-# ----------------------------------------
-
-_parse_etag = re.compile(r'([A-Z]): (([\w-]+).*)').match
-
-def parse_etag(contents, path):
-    match = _parse_etag(contents)
-    if match is None:
-        return
-    t = ETag(match.group(1), path, match.group(2))
-    return t
-
-def etags_from_tagstring(obj, path):
-    try:
-        docstring = obj.tagstring
-    except AttributeError:
-        return
-    for line in docstring.splitlines():
-        line = line.lstrip()
-        t = parse_etag(line, path)
-        if t is not None:
-            yield t
-
-def tagstring(s):
-    def update(x):
-        x.tagstring = s
-        return x
-    return update
-
-# ----------------------------------------
-
-class ETag():
-
-    _ellipsis = '<...>'
-    _split = re.compile('({})'.format(re.escape(_ellipsis))).split
-
-    def __init__(self, code, path, rest):
-        self._s = s = '{code}: {path}: {rest}'.format(
-            code=code,
-            path=path,
-            rest=rest,
-        )
-        self.tag = rest.split(None, 1)[0]
-        regexp = ''.join(
-            '.*' if chunk == self._ellipsis else re.escape(chunk)
-            for chunk in self._split(s)
-        )
-        self._regexp = re.compile('^{}$'.format(regexp))
-
-    def __eq__(self, other):
-        if isinstance(other, str):
-            return self._regexp.match(other)
-        else:
-            return NotImplemented
-
-    def __str__(self):
-        return self._s
-
-    def __repr__(self):
-        return repr(self._s)
-
-# ----------------------------------------
-
-def _get_signal_names():
-    data = dict(
-        (name, getattr(signal, name))
-        for name in dir(signal)
-        if re.compile('^SIG[A-Z0-9]*$').match(name)
-    )
-    try:
-        if data['SIGABRT'] == data['SIGIOT']:
-            del data['SIGIOT']
-    except KeyError:
-        pass
-    try:
-        if data['SIGCHLD'] == data['SIGCLD']:
-            del data['SIGCLD']
-    except KeyError:
-        pass
-    for name, n in data.items():
-        yield n, name
-
-_signal_names = dict(_get_signal_names())
-
-def get_signal_name(n):
-    try:
-        return _signal_names[n]
-    except KeyError:
-        return str(n)
-
-# ----------------------------------------
-
-test_file_extensions = ('.mo', '.po', '.pot', '.pop')
-# .pop is a special extension to trigger unknown-file-type
-
-class Plugin(nose.plugins.Plugin):
-
-    name = 'po-plugin'
-    enabled = True
-
-    def options(self, parser, env):
-        pass
-
-    def wantFile(self, path):
-        if path.endswith(test_file_extensions):
-            if path.startswith(os.path.join(os.path.abspath(here), '')):
-                return True
-
-    def loadTestsFromFile(self, path):
-        if self.wantFile(path):
-            yield TestCase(path)
-
-    def wantFunction(self, func):
-        if getattr(func, 'redundant', False):
-            return False
-
-class TestCase(unittest.TestCase):
-
-    def __init__(self, path):
-        super().__init__('_test')
-        self.path = os.path.relpath(path)
-
-    def _test(self):
-        _test_file(self.path, basedir=None)
-
-    def __str__(self):
-        return self.path
-
-class SubprocessError(Exception):
-    pass
-
-def queue_get(queue, process):
-    '''
-    Remove and return an item from the queue.
-    Block until the process terminates.
-    '''
-    while True:
-        try:
-            return queue.get(timeout=1)
-            # This semi-active waiting is ugly, but there doesn't seem be any
-            # obvious better way.
-        except mp.queues.Empty:
-            if process.exitcode is None:
-                continue
-            else:
-                raise
-
-def run_i18nspector(options, path):
-    commandline = os.environ.get('I18NSPECTOR_COMMANDLINE')
-    if commandline is None:
-        # We cheat here a bit, because exec(3)ing is very expensive.
-        # Let's load the needed Python modules, and use multiprocessing to
-        # “emulate” the command execution.
-        import lib.cli  # pylint: disable=import-outside-toplevel
-        assert lib.cli  # make pyflakes happy
-        prog = os.path.join(here, os.pardir, os.pardir, 'i18nspector')
-        commandline = [sys.executable, prog]
-        queue = mp.Queue()
-        child = mp.Process(
-            target=_mp_run_i18nspector,
-            args=(prog, options, path, queue)
-        )
-        child.start()
-        [stdout, stderr] = (
-            s.splitlines()
-            for s in queue_get(queue, child)
-        )
-        child.join()
-        rc = child.exitcode
-    else:
-        commandline = shlex.split(commandline)
-        commandline += options
-        commandline += [path]
-        fixed_env = dict(os.environ, PYTHONIOENCODING='UTF-8')
-        child = ipc.Popen(commandline, stdout=ipc.PIPE, stderr=ipc.PIPE, env=fixed_env)
-        stdout, stderr = (
-            s.decode('UTF-8').splitlines()
-            for s in child.communicate()
-        )
-        rc = child.poll()
-    assert isinstance(rc, int)
-    if rc == 0:
-        return stdout
-    if rc < 0:
-        message = ['command was interrupted by signal {sig}'.format(sig=get_signal_name(-rc))]  # pylint: disable=invalid-unary-operand-type
-    else:
-        message = ['command exited with status {rc}'.format(rc=rc)]
-    message += ['']
-    if stdout:
-        message += ['stdout:']
-        message += ['| ' + s for s in stdout] + ['']
-    else:
-        message += ['stdout: (empty)']
-    if stderr:
-        message += ['stderr:']
-        message += ['| ' + s for s in stderr]
-    else:
-        message += ['stderr: (empty)']
-    raise SubprocessError('\n'.join(message))
-
-def _mp_run_i18nspector(prog, options, path, queue):
-    with open(prog, 'rt', encoding='UTF-8') as file:
-        code = file.read()
-    sys.argv = [prog] + list(options) + [path]
-    orig_stdout = sys.stdout
-    orig_stderr = sys.stderr
-    code = compile(code, prog, 'exec')
-    io_stdout = io.StringIO()
-    io_stderr = io.StringIO()
-    gvars = dict(
-        __file__=prog,
-    )
-    (sys.stdout, sys.stderr) = (io_stdout, io_stderr)
-    try:
-        try:
-            exec(code, gvars)  # pylint: disable=exec-used
-        finally:
-            (sys.stdout, sys.stderr) = (orig_stdout, orig_stderr)
-            stdout = io_stdout.getvalue()
-            stderr = io_stderr.getvalue()
-    except SystemExit:
-        queue.put([stdout, stderr])
-        raise
-    except:  # pylint: disable=bare-except
-        exctp, exc, tb = sys.exc_info()
-        stderr += ''.join(
-            traceback.format_exception(exctp, exc, tb)
-        )
-        queue.put([stdout, stderr])
-        sys.exit(1)
-        raise  # hi, pydiatra!
-    else:
-        queue.put([stdout, stderr])
-        sys.exit(0)
-
-def assert_emit_tags(path, etags, *, options=()):
-    etags = list(etags)
-    stdout = run_i18nspector(options, path)
-    expected_failure = os.path.basename(path).startswith('xfail-')
-    if stdout != etags:
-        if expected_failure:
-            raise nose.SkipTest('expected failure')
-        str_etags = [str(x) for x in etags]
-        message = ['Tags differ:', '']
-        diff = list(
-            difflib.unified_diff(str_etags, stdout, n=9999)
-        )
-        message += diff[3:]
-        raise AssertionError('\n'.join(message))
-    elif expected_failure:
-        raise AssertionError('unexpected success')
-
-class TestFileSyntaxError(Exception):
-    pass
-
-def _parse_test_header_file(file, path, *, comments_only):
-    etags = []
-    options = []
-    for n, line in enumerate(file):
-        orig_line = line
-        if comments_only:
-            if n == 0 and line.startswith('#!'):
-                continue
-            if line.startswith('# '):
-                line = line[2:]
-            else:
-                break
-        if line.startswith('--'):
-            options += shlex.split(line)
-        else:
-            etag = parse_etag(line, path)
-            if etag is None:
-                if comments_only:
-                    break
-                else:
-                    raise TestFileSyntaxError(orig_line)
-            etags += [etag]
-    return etags, options
-
-def _parse_test_headers(path):
-    # <path>.tags:
-    try:
-        file = open(path + '.tags', encoding='UTF-8')
-    except FileNotFoundError:
-        pass
-    else:
-        with file:
-            return _parse_test_header_file(file, path, comments_only=False)
-    # <path>.gen:
-    try:
-        file = open(path + '.gen', encoding='UTF-8', errors='ignore')
-    except FileNotFoundError:
-        pass
-    else:
-        with file:
-            return _parse_test_header_file(file, path, comments_only=True)
-    # <path>:
-    with open(path, 'rt', encoding='UTF-8', errors='ignore') as file:
-        return _parse_test_header_file(file, path, comments_only=True)
-
-def _test_file(path, basedir=here):
-    if basedir is not None:
-        path = os.path.relpath(os.path.join(basedir, path), start=os.getcwd())
-    options = []
-    etags, options = _parse_test_headers(path)
-    assert_emit_tags(path, etags, options=options)
-
-def get_coverage_for_file(path):
-    etags, options = _parse_test_headers(path)
-    del options
-    return (t.tag for t in etags)
-
-def get_coverage_for_function(fn):
-    for etag in etags_from_tagstring(fn, ''):
-        yield etag.tag
-
-def _get_test_filenames():
-    for root, dirnames, filenames in os.walk(here):
-        del dirnames
-        for filename in filenames:
-            if not filename.endswith(test_file_extensions):
-                continue
-            yield os.path.join(root, filename)
-
-def test_file():
-    for filename in _get_test_filenames():
-        path = os.path.relpath(filename, start=here)
-        yield _test_file, path
-test_file.redundant = True  # not needed if the plugin is enabled
-
-@tagstring('''
-E: os-error No such file or directory
-''')
-def test_os_error_no_such_file():
-    with tools.temporary_directory() as tmpdir:
-        path = os.path.join(tmpdir, 'nonexistent.po')
-        expected = etags_from_tagstring(this(), path)
-        assert_emit_tags(path, expected)
-
-@tagstring('''
-E: os-error Permission denied
-''')
-def test_os_error_permission_denied():
-    if os.getuid() == 0:
-        raise nose.SkipTest('this test must not be run as root')
-    with tools.temporary_directory() as tmpdir:
-        path = os.path.join(tmpdir, 'denied.po')
-        with open(path, 'wb'):
-            pass
-        os.chmod(path, 0)
-        expected = etags_from_tagstring(this(), path)
-        assert_emit_tags(path, expected)
-
-# ----------------------------------------
-
-def get_coverage():
-    coverage = set()
-    for filename in _get_test_filenames():
-        for tag in get_coverage_for_file(filename):
-            coverage.add(tag)
-    for objname, obj in globals().items():
-        if not objname.startswith('test_'):
-            continue
-        for tag in get_coverage_for_function(obj):
-            coverage.add(tag)
-    return coverage
-
-# vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/blackbox_tests/conftest.py
===================================================================
--- /dev/null
+++ i18nspector-0.26/tests/blackbox_tests/conftest.py
@@ -0,0 +1,424 @@
+# Copyright © 2012-2021 Jakub Wilk <jwilk@jwilk.net>
+# Copyright © 2021 Stuart Prescott <stuart@debian.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the “Software”), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import difflib
+import inspect
+import io
+import multiprocessing as mp
+import os
+import re
+import shlex
+import signal
+import subprocess as ipc
+import sys
+import traceback
+import unittest
+
+import pytest
+
+
+here = os.path.dirname(__file__)
+
+
+# ----------------------------------------
+
+def this():
+    '''
+    Return function that called this function. (Hopefully!)
+    '''
+    return globals()[inspect.stack()[1][0].f_code.co_name]
+
+# ----------------------------------------
+
+_parse_etag = re.compile(r'([A-Z]): (([\w-]+).*)').match
+
+def parse_etag(contents, path):
+    match = _parse_etag(contents)
+    if match is None:
+        return
+    t = ETag(match.group(1), path, match.group(2))
+    return t
+
+def etags_from_tagstring(obj, path):
+    try:
+        docstring = obj.tagstring
+    except AttributeError:
+        return
+    for line in docstring.splitlines():
+        line = line.lstrip()
+        t = parse_etag(line, path)
+        if t is not None:
+            yield t
+
+def tagstring(s):
+    def update(x):
+        x.tagstring = s
+        return x
+    return update
+
+# ----------------------------------------
+
+class ETag():
+
+    _ellipsis = '<...>'
+    _split = re.compile('({})'.format(re.escape(_ellipsis))).split
+
+    def __init__(self, code, path, rest):
+        self._s = s = '{code}: {path}: {rest}'.format(
+            code=code,
+            path=path,
+            rest=rest,
+        )
+        self.tag = rest.split(None, 1)[0]
+        regexp = ''.join(
+            '.*' if chunk == self._ellipsis else re.escape(chunk)
+            for chunk in self._split(s)
+        )
+        self._regexp = re.compile('^{}$'.format(regexp))
+
+    def __eq__(self, other):
+        if isinstance(other, str):
+            return self._regexp.match(other)
+        else:
+            return NotImplemented
+
+    def __str__(self):
+        return self._s
+
+    def __repr__(self):
+        return repr(self._s)
+
+# ----------------------------------------
+
+def _get_signal_names():
+    data = dict(
+        (name, getattr(signal, name))
+        for name in dir(signal)
+        if re.compile('^SIG[A-Z0-9]*$').match(name)
+    )
+    try:
+        if data['SIGABRT'] == data['SIGIOT']:
+            del data['SIGIOT']
+    except KeyError:
+        pass
+    try:
+        if data['SIGCHLD'] == data['SIGCLD']:
+            del data['SIGCLD']
+    except KeyError:
+        pass
+    for name, n in data.items():
+        yield n, name
+
+_signal_names = dict(_get_signal_names())
+
+def get_signal_name(n):
+    try:
+        return _signal_names[n]
+    except KeyError:
+        return str(n)
+
+# ----------------------------------------
+
+# Here, a pytest hook function (pytest_collect_file) is used to decide that
+# the .mo/.po/.pot files in the directory are actually test items.
+# For each file that is found, a PoTestFile is created. For each PoTestFile,
+# a testable item (PoTestItem) is created; if needed, there could be be
+# more than one PoTestItem per PoTestFile, but the code currently only
+# creates a single test.
+
+test_file_extensions = ('.mo', '.po', '.pot', '.pop')
+# .pop is a special extension to trigger unknown-file-type
+
+
+def pytest_collect_file(parent, path):
+    """hook function that decides that po files are actually tests"""
+    if path.ext in test_file_extensions:
+        return PoTestFile.from_parent(parent, fspath=path)
+
+
+class PoTestFile(pytest.File):
+    """Class to yield one or more test items that are contained in the file
+
+    In this implementation, only one test is yielded.
+    """
+    # pylint: disable=abstract-method
+    # pylint gets confused about this subclassing and emits warnings about
+    # (unneeded) abstract methods not being defined.
+    # https://github.com/pytest-dev/pytest/issues/7591
+
+    def collect(self):
+        # record the po file's basename and its full filesystem path as
+        # strings; pytest will happily use Path objects but the rest of
+        # stdlib on older Python versions only wants str not Path.
+        fname = str(self.fspath)
+        name = os.path.basename(fname)
+        yield PoTestItem.from_parent(self, name=name, filename=fname)
+
+
+class PoTestItem(pytest.Item):
+    """Class that represents a single test."""
+    # pylint: disable=abstract-method
+
+    def __init__(self, name, parent, filename):
+        super().__init__(name, parent)
+        self.filename = filename
+
+    def runtest(self):
+        """run the test
+
+        The intended i18nspector error tags are extracted from the file,
+        i18nspector is run on the file and the sets of tags are asserted
+        to be identical. Exceptions may also be raised for various failure
+        modes of i18nspector or other test harness failures.
+        """
+        etags, options = self.parse_test_headers()
+        assert_emit_tags(self.filename, etags, options=options)
+
+    def repr_failure(self, excinfo, style=None):
+        """Presentation of test failures for when self.runtest() raises an exception."""
+        if isinstance(excinfo.value, PoTestFileException):
+            return "\n".join(
+                [
+                    "blackbox test failed for " + self.name,
+                    str(excinfo.value),
+                ]
+            )
+        else:
+            return "\n".join(
+                [
+                    "blackbox test error for " + self.name,
+                    str(excinfo.value),
+                ]
+            )
+
+    def reportinfo(self):
+        """ header for test failure/error/stdout reporting """
+        return self.fspath, 0, "blackbox test: " + self.name
+
+    def _parse_test_header_file(self, fh, path, *, comments_only):
+        etags = []
+        options = []
+        for n, line in enumerate(fh):
+            orig_line = line
+            if comments_only:
+                if n == 0 and line.startswith('#!'):
+                    continue
+                if line.startswith('# '):
+                    line = line[2:]
+                else:
+                    break
+            if line.startswith('--'):
+                options += shlex.split(line)
+            else:
+                etag = parse_etag(line, path)
+                if etag is None:
+                    if comments_only:
+                        break
+                    else:
+                        raise TestFileSyntaxError(orig_line)
+                etags += [etag]
+        return etags, options
+
+    def parse_test_headers(self):
+        path = self.filename
+        # <path>.tags:
+        try:
+            fh = open(path + '.tags', encoding='UTF-8')  # pylint: disable=consider-using-with
+        except FileNotFoundError:
+            pass
+        else:
+            with fh:
+                return self._parse_test_header_file(fh, path, comments_only=False)
+        # <path>.gen:
+        try:
+            fh = open(path + '.gen', encoding='UTF-8', errors='ignore')  # pylint: disable=consider-using-with
+        except FileNotFoundError:
+            pass
+        else:
+            with fh:
+                return self._parse_test_header_file(fh, path, comments_only=True)
+        # <path>:
+        with open(path, 'rt', encoding='UTF-8', errors='ignore') as fh:
+            return self._parse_test_header_file(fh, path, comments_only=True)
+
+
+class PoTestFileException(AssertionError):
+    """Custom exception for error reporting."""
+
+
+class TestFileSyntaxError(Exception):
+    pass
+
+
+def queue_get(queue, process):
+    '''
+    Remove and return an item from the queue.
+    Block until the process terminates.
+    '''
+    while True:
+        try:
+            return queue.get(timeout=1)
+            # This semi-active waiting is ugly, but there doesn't seem be any
+            # obvious better way.
+        except mp.queues.Empty:
+            if process.exitcode is None:
+                continue
+            else:
+                raise
+
+def run_i18nspector(options, path):
+    commandline = os.environ.get('I18NSPECTOR_COMMANDLINE')
+    if commandline is None:
+        # We cheat here a bit, because exec(3)ing is very expensive.
+        # Let's load the needed Python modules, and use multiprocessing to
+        # “emulate” the command execution.
+        import lib.cli  # pylint: disable=import-outside-toplevel
+        assert lib.cli  # make pyflakes happy
+        prog = os.path.join(here, os.pardir, os.pardir, 'i18nspector')
+        commandline = [sys.executable, prog]
+        queue = mp.Queue()
+        child = mp.Process(
+            target=_mp_run_i18nspector,
+            args=(prog, options, path, queue)
+        )
+        child.start()
+        [stdout, stderr] = (
+            s.splitlines()
+            for s in queue_get(queue, child)
+        )
+        child.join()
+        rc = child.exitcode
+    else:
+        commandline = shlex.split(commandline)
+        commandline += options
+        commandline += [path]
+        fixed_env = dict(os.environ, PYTHONIOENCODING='UTF-8')
+        with ipc.Popen(commandline, stdout=ipc.PIPE, stderr=ipc.PIPE, env=fixed_env) as child:
+            stdout, stderr = (
+                s.decode('UTF-8').splitlines()
+                for s in child.communicate()
+            )
+        rc = child.poll()
+    assert isinstance(rc, int)
+    if rc == 0:
+        return stdout
+    if rc < 0:
+        message = ['command was interrupted by signal {sig}'.format(sig=get_signal_name(-rc))]  # pylint: disable=invalid-unary-operand-type
+    else:
+        message = ['command exited with status {rc}'.format(rc=rc)]
+    message += ['']
+    if stdout:
+        message += ['stdout:']
+        message += ['| ' + s for s in stdout] + ['']
+    else:
+        message += ['stdout: (empty)']
+    if stderr:
+        message += ['stderr:']
+        message += ['| ' + s for s in stderr]
+    else:
+        message += ['stderr: (empty)']
+    raise PoTestFileException('\n'.join(message))
+
+def _mp_run_i18nspector(prog, options, path, queue):
+    with open(prog, 'rt', encoding='UTF-8') as file:
+        code = file.read()
+    sys.argv = [prog] + list(options) + [path]
+    orig_stdout = sys.stdout
+    orig_stderr = sys.stderr
+    code = compile(code, prog, 'exec')
+    io_stdout = io.StringIO()
+    io_stderr = io.StringIO()
+    gvars = dict(
+        __file__=prog,
+    )
+    (sys.stdout, sys.stderr) = (io_stdout, io_stderr)
+    try:
+        try:
+            exec(code, gvars)  # pylint: disable=exec-used
+        finally:
+            (sys.stdout, sys.stderr) = (orig_stdout, orig_stderr)
+            stdout = io_stdout.getvalue()
+            stderr = io_stderr.getvalue()
+    except SystemExit:
+        queue.put([stdout, stderr])
+        raise
+    except:  # pylint: disable=bare-except
+        exctp, exc, tb = sys.exc_info()
+        stderr += ''.join(
+            traceback.format_exception(exctp, exc, tb)
+        )
+        queue.put([stdout, stderr])
+        sys.exit(1)
+        raise  # hi, pydiatra!
+    else:
+        queue.put([stdout, stderr])
+        sys.exit(0)
+
+def assert_emit_tags(path, etags, *, options=()):
+    etags = list(etags)
+    stdout = run_i18nspector(options, path)
+    expected_failure = os.path.basename(path).startswith('xfail-')
+    if stdout != etags:
+        if expected_failure:
+            raise unittest.SkipTest('expected failure')
+        str_etags = [str(x) for x in etags]
+        message = ['Tags differ:', '']
+        diff = list(
+            difflib.unified_diff(str_etags, stdout, n=9999)
+        )
+        message += diff[3:]
+        raise PoTestFileException('\n'.join(message))
+    elif expected_failure:
+        raise PoTestFileException('unexpected success')
+
+
+# ----------------------------------------
+# Tag coverage recording code
+
+class PofileRecorderPlugin:
+    """pytest plugin to just record the test files that are collected"""
+    def __init__(self):
+        self.collected = []
+
+    def pytest_collection_modifyitems(self, items):
+        for item in items:
+            self.collected.append((item.nodeid, item))
+
+
+def get_coverage():
+    # ask pytest to enumerate the tests it knows of in this directory
+    directory = os.path.dirname(__file__)
+    pofile_recorder = PofileRecorderPlugin()
+    pytest.main(['--collect-only', '-p', 'no:terminal', directory], plugins=[pofile_recorder])
+
+    coverage = set()
+    for _, item in pofile_recorder.collected:
+        if isinstance(item, PoTestItem):
+            etags, _ = item.parse_test_headers()
+            for t in etags:
+                coverage.add(t.tag)
+        else:
+            for tag in etags_from_tagstring(item.function, ''):
+                coverage.add(tag.tag)
+
+    return coverage
+
+# vim:ts=4 sts=4 sw=4 et
Index: i18nspector-0.26/tests/blackbox_tests/test_errors.py
===================================================================
--- /dev/null
+++ i18nspector-0.26/tests/blackbox_tests/test_errors.py
@@ -0,0 +1,36 @@
+import inspect
+import os.path
+import unittest
+
+from .conftest import tagstring, etags_from_tagstring, assert_emit_tags
+from .. import tools
+
+
+def this():
+    '''
+    Return function that called this function. (Hopefully!)
+    '''
+    return globals()[inspect.stack()[1][0].f_code.co_name]
+
+@tagstring('''
+E: os-error No such file or directory
+''')
+def test_os_error_no_such_file():
+    with tools.temporary_directory() as tmpdir:
+        path = os.path.join(tmpdir, 'nonexistent.po')
+        expected = etags_from_tagstring(this(), path)
+        assert_emit_tags(path, expected)
+
+@tagstring('''
+E: os-error Permission denied
+''')
+def test_os_error_permission_denied():
+    if os.getuid() == 0:
+        raise unittest.SkipTest('this test must not be run as root')
+    with tools.temporary_directory() as tmpdir:
+        path = os.path.join(tmpdir, 'denied.po')
+        with open(path, 'wb'):
+            pass
+        os.chmod(path, 0)
+        expected = etags_from_tagstring(this(), path)
+        assert_emit_tags(path, expected)
Index: i18nspector-0.26/private/update-tag-coverage
===================================================================
--- i18nspector-0.26.orig/private/update-tag-coverage
+++ i18nspector-0.26/private/update-tag-coverage
@@ -21,12 +21,14 @@
 # SOFTWARE.
 
 import os
+import os.path
 import sys
 
 sys.path[0] += '/..'
 
 from lib import tags
-from tests import blackbox_tests
+from tests.blackbox_tests import conftest as blackbox_tests
+
 
 def main():
     coverage = blackbox_tests.get_coverage()
openSUSE Build Service is sponsored by