From 0ba5a3e9ed0607bfdf33745e0edebb11767aa1792272c4a5fa0dfbef5161b52d Mon Sep 17 00:00:00 2001 From: Kyrill Detinov Date: Mon, 22 Nov 2021 23:05:17 +0000 Subject: [PATCH] Accepting request 932847 from home:StevenK:branches:M17N - Add patch switch-to-pytest.patch: * Use pytest to run tests, rather than nose. OBS-URL: https://build.opensuse.org/request/show/932847 OBS-URL: https://build.opensuse.org/package/show/M17N/i18nspector?expand=0&rev=59 --- i18nspector.changes | 6 + i18nspector.spec | 9 +- switch-to-pytest.patch | 4672 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 4684 insertions(+), 3 deletions(-) create mode 100644 switch-to-pytest.patch diff --git a/i18nspector.changes b/i18nspector.changes index a3fed11..0d76123 100644 --- a/i18nspector.changes +++ b/i18nspector.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Nov 22 05:39:58 UTC 2021 - Steve Kowalik + +- Add patch switch-to-pytest.patch: + * Use pytest to run tests, rather than nose. + ------------------------------------------------------------------- Sun Nov 29 16:15:12 UTC 2020 - Kyrill Detinov diff --git a/i18nspector.spec b/i18nspector.spec index d538a4b..c62a2df 100644 --- a/i18nspector.spec +++ b/i18nspector.spec @@ -1,7 +1,7 @@ # # spec file for package i18nspector # -# Copyright (c) 2020 SUSE LLC +# Copyright (c) 2021 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -26,11 +26,13 @@ URL: https://jwilk.net/software/i18nspector Source0: https://github.com/jwilk/i18nspector/releases/download/%{version}/%{name}-%{version}.tar.gz Source1: https://github.com/jwilk/i18nspector/releases/download/%{version}/%{name}-%{version}.tar.gz.asc Source2: %{name}.keyring +# PATCH-FIX-UPSTREAM Adapted from gh#jwilk/i18nspector/pull/8 +Patch0: switch-to-pytest.patch BuildRequires: python3-devel >= 3.4 # Requires for tests. BuildRequires: python3-curses -BuildRequires: python3-nose BuildRequires: python3-polib +BuildRequires: python3-pytest BuildRequires: python3-rply # Requires: python3-polib @@ -51,6 +53,7 @@ headers, incorrect language codes and improper plural forms. %prep %setup -q +%autopatch -p1 %build @@ -62,7 +65,7 @@ cd %{buildroot}%{_datadir}/%{name}/ %py3_compile . %check -%make_build test +pytest -v %files %license doc/LICENSE diff --git a/switch-to-pytest.patch b/switch-to-pytest.patch new file mode 100644 index 0000000..135c4d0 --- /dev/null +++ b/switch-to-pytest.patch @@ -0,0 +1,4672 @@ +From b84035ee74cc646ddbabbb99d565e50973fe7b1d Mon Sep 17 00:00:00 2001 +From: Stuart Prescott +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 ++# Copyright © 2021 Stuart Prescott + # + # 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), '') ++ assert repr(lng) == '' + + 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 ++# Copyright © 2021 Stuart Prescott + # + # 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 ++# Copyright © 2021 Stuart Prescott + # + # 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 +-# +-# 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): +- # .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) +- # .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) +- # : +- 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 ++# Copyright © 2021 Stuart Prescott ++# ++# 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 ++ # .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) ++ # .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) ++ # : ++ 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()