diff --git a/HISTORY.rst b/HISTORY.rst index 1e3a76e..3d9e1f8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,7 +6,8 @@ History ?.?.? (????-??-??) ++++++++++++++++++ -* Added support for Python 3.9, 3.10 and 3.11. +* Dropped support for Python 2.7, 3.5, 3.6 and 3.7. +* Added support for Python 3.9 and 3.10. 0.7.3 (2020-06-27) ++++++++++++++++++ diff --git a/README.rst b/README.rst index ea1a3ab..38532d9 100644 --- a/README.rst +++ b/README.rst @@ -65,8 +65,7 @@ For more info, see the `documentation `_. Installation ------------ -``fuzzysearch`` supports Python versions 2.7 and 3.5+, as well as PyPy 2.7 and -3.6. +``fuzzysearch`` supports Python versions 3.8+, as well as PyPy 3.9 and 3.10. .. code:: diff --git a/setup.py b/setup.py index f594107..d236e6c 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import with_statement - import os import sys from setuptools import setup, Extension -if sys.version_info < (3, 8): - from distutils.command.build_ext import build_ext - from distutils.errors import ( - CCompilerError, - DistutilsExecError as ExecError, - DistutilsPlatformError as PlatformError, - ) -else: - from setuptools.command.build_ext import build_ext - from setuptools.errors import CCompilerError, ExecError, PlatformError +from setuptools.command.build_ext import build_ext +from setuptools.errors import CCompilerError, ExecError, PlatformError # --noexts: don't try building the C extensions if '--noexts' in sys.argv[1:]: @@ -34,7 +24,7 @@ def readfile(file_path): history = readfile('HISTORY.rst').replace('.. :changelog:', '') -# Fail safe compilation based on markupsafe's, which in turn was shamelessly +# Fail-safe compilation based on markupsafe's, which in turn was shamelessly # stolen from the simplejson setup.py file. Original author: Bob Ippolito is_jython = 'java' in sys.platform @@ -135,12 +125,7 @@ def run_setup(with_binary=True): 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', diff --git a/src/fuzzysearch/common.py b/src/fuzzysearch/common.py index 00cd236..1da984b 100644 --- a/src/fuzzysearch/common.py +++ b/src/fuzzysearch/common.py @@ -1,7 +1,5 @@ from functools import wraps -from fuzzysearch.compat import int_types - from attr import attrs, attrib @@ -22,11 +20,11 @@ class Match(object): if __debug__: def __attrs_post_init__(self): - if not (isinstance(self.start, int_types) and self.start >= 0): + if not (isinstance(self.start, int) and self.start >= 0): raise ValueError('start must be a non-negative integer') - if not (isinstance(self.end, int_types) and self.end >= self.start): + if not (isinstance(self.end, int) and self.end >= self.start): raise ValueError('end must be an integer no smaller than start') - if not (isinstance(self.dist, int_types) and self.dist >= 0): + if not (isinstance(self.dist, int) and self.dist >= 0): print(self.dist) raise ValueError('dist must be a non-negative integer') if self.matched is None: diff --git a/src/fuzzysearch/compat.py b/src/fuzzysearch/compat.py deleted file mode 100644 index 72f7b5d..0000000 --- a/src/fuzzysearch/compat.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys - - -__all__ = [ - 'int_types', - 'izip', - 'text_type', - 'xrange', -] - - -if sys.version_info < (3,): - PY2 = True - PY3 = False - int_types = (int, long) - from itertools import izip - text_type = unicode - xrange = xrange -else: - PY2 = False - PY3 = True - int_types = (int,) - izip = zip - text_type = str - xrange = range diff --git a/src/fuzzysearch/generic_search.py b/src/fuzzysearch/generic_search.py index 838e965..390c43a 100644 --- a/src/fuzzysearch/generic_search.py +++ b/src/fuzzysearch/generic_search.py @@ -5,7 +5,6 @@ from fuzzysearch.common import FuzzySearchBase, Match, \ consolidate_overlapping_matches -from fuzzysearch.compat import xrange from fuzzysearch.search_exact import search_exact @@ -139,7 +138,7 @@ def make_match(start, end, dist): yield make_match(cand.start, index + 1, cand.l_dist + 1) # try skipping subsequence chars - for n_skipped in xrange(1, min(max_deletions - cand.n_dels, max_l_dist - cand.l_dist) + 1): + for n_skipped in range(1, min(max_deletions - cand.n_dels, max_l_dist - cand.l_dist) + 1): # if skipping n_dels sub-sequence chars reaches the end # of the sub-sequence, yield a match if cand.subseq_index + n_skipped == subseq_len: @@ -220,7 +219,7 @@ def find_near_matches_generic_ngrams(subsequence, sequence, search_params): if ngram_len == 0: raise ValueError('the subsequence length must be greater than max_l_dist') - for ngram_start in xrange(0, subseq_len - ngram_len + 1, ngram_len): + for ngram_start in range(0, subseq_len - ngram_len + 1, ngram_len): ngram_end = ngram_start + ngram_len start_index = max(0, ngram_start - max_l_dist) end_index = min(seq_len, seq_len - subseq_len + ngram_end + max_l_dist) diff --git a/src/fuzzysearch/levenshtein.py b/src/fuzzysearch/levenshtein.py index e9132fe..210b009 100644 --- a/src/fuzzysearch/levenshtein.py +++ b/src/fuzzysearch/levenshtein.py @@ -2,7 +2,6 @@ from fuzzysearch.common import FuzzySearchBase, Match, \ consolidate_overlapping_matches -from fuzzysearch.compat import xrange from fuzzysearch.levenshtein_ngram import find_near_matches_levenshtein_ngrams from fuzzysearch.search_exact import search_exact @@ -61,7 +60,7 @@ def make_match(start, end, dist): return Match(start, end, dist, matched=sequence[start:end]) if max_l_dist >= subseq_len: - for index in xrange(len(sequence) + 1): + for index in range(len(sequence) + 1): yield make_match(index, index, subseq_len) return @@ -112,7 +111,7 @@ def make_match(start, end, dist): )) # try skipping subsequence chars - for n_skipped in xrange(1, max_l_dist - cand.dist + 1): + for n_skipped in range(1, max_l_dist - cand.dist + 1): # if skipping n_skipped sub-sequence chars reaches the end # of the sub-sequence, yield a match if cand.subseq_index + n_skipped == subseq_len: diff --git a/src/fuzzysearch/levenshtein_ngram.py b/src/fuzzysearch/levenshtein_ngram.py index 729922a..edbd1ed 100644 --- a/src/fuzzysearch/levenshtein_ngram.py +++ b/src/fuzzysearch/levenshtein_ngram.py @@ -1,5 +1,4 @@ from fuzzysearch.common import Match -from fuzzysearch.compat import xrange from fuzzysearch.search_exact import search_exact @@ -168,7 +167,7 @@ def find_near_matches_levenshtein_ngrams(subsequence, sequence, max_l_dist): def make_match(start, end, dist): return Match(start, end, dist, matched=sequence[start:end]) - for ngram_start in xrange(0, subseq_len - ngram_len + 1, ngram_len): + for ngram_start in range(0, subseq_len - ngram_len + 1, ngram_len): ngram_end = ngram_start + ngram_len subseq_before_reversed = subsequence[:ngram_start][::-1] subseq_after = subsequence[ngram_end:] diff --git a/src/fuzzysearch/search_exact.py b/src/fuzzysearch/search_exact.py index a5adee5..594c3e6 100644 --- a/src/fuzzysearch/search_exact.py +++ b/src/fuzzysearch/search_exact.py @@ -1,7 +1,6 @@ from functools import wraps from fuzzysearch.common import FuzzySearchBase, Match -from fuzzysearch.compat import text_type, xrange __all__ = [ @@ -11,7 +10,7 @@ CLASSES_WITH_INDEX = (list, tuple) -CLASSES_WITH_FIND = (bytes, bytearray, text_type) +CLASSES_WITH_FIND = (bytes, bytearray, str) try: from Bio.Seq import Seq @@ -41,7 +40,7 @@ def find_in_index_range(start_index): start_index = first_index + 1 except ValueError: return -1 - for subseq_index in xrange(1, len(subsequence)): + for subseq_index in range(1, len(subsequence)): if sequence[first_index + subseq_index] != subsequence[subseq_index]: break else: diff --git a/tests/compat.py b/tests/compat.py index 2d86af3..05b7f7f 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,40 +1,8 @@ """Compatibility support of testing tools for different Python versions""" -# The required modules are installed as necessary for different Python -# versions by `tox`. See `tox.ini` for details. -import sys __all__ = [ 'b', - 'mock', - 'u', - 'unittest', ] -# The `unittest2` module is a backport of the new unittest features introduced -# in Python versions 3.2 and 2.7. Use it in older versions of Python. -# It is also used here in versions 2.7, 3.2 and 3.3 to make features added -# in version 3.4 available (specifically, TestCase.subTest). -if sys.version_info < (3, 4): - import unittest2 as unittest -else: - import unittest - -# The `mock` module was added to the stdlib as `unittest.mock` in Python -# version 3.3. -if sys.version_info < (3, 3): - import mock -else: - import unittest.mock as mock - -if sys.version_info < (3,): - def b(x): - return x - - def u(x): - return unicode(x.replace(r'\\', r'\\\\'), 'unicode_escape') -else: - def b(x): - return x.encode('latin-1') - - def u(x): - return x +def b(x): + return x.encode('latin-1') diff --git a/tests/test_common.py b/tests/test_common.py index 392b483..c7801f7 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,6 +1,8 @@ +import unittest + from fuzzysearch.common import Match, group_matches, GroupOfMatches, \ count_differences_with_maximum -from tests.compat import b, unittest +from tests.compat import b class TestGroupOfMatches(unittest.TestCase): diff --git a/tests/test_find_near_matches.py b/tests/test_find_near_matches.py index f7c70ae..c718051 100644 --- a/tests/test_find_near_matches.py +++ b/tests/test_find_near_matches.py @@ -1,4 +1,6 @@ -from tests.compat import unittest, mock +import unittest +import unittest.mock + from tests.test_search_exact import TestSearchExactBase from tests.test_substitutions_only import TestSubstitionsOnlyBase from tests.test_levenshtein import TestFindNearMatchesLevenshteinBase @@ -35,7 +37,7 @@ def patch_concrete_search_classes(self): self.mock_find_near_matches_generic = \ MockSearchClassFailsUnlessDefined() - patcher = mock.patch.multiple( + patcher = unittest.mock.patch.multiple( 'fuzzysearch', ExactSearch=self.mock_search_exact, LevenshteinSearch= @@ -130,23 +132,23 @@ def test_levenshtein(self): # find_near_matches_levenshtein self.patch_concrete_search_classes() self.mock_find_near_matches_levenshtein.return_value = \ - [mock.sentinel.SENTINEL] + [unittest.mock.sentinel.SENTINEL] self.assertEqual( find_near_matches('a', 'a', 1, 1, 1, 1), - [mock.sentinel.SENTINEL], + [unittest.mock.sentinel.SENTINEL], ) self.assertEqual(self.mock_find_near_matches_levenshtein.call_count, 1) self.assertEqual( find_near_matches('a', 'a', 2, 2, 2, 2), - [mock.sentinel.SENTINEL], + [unittest.mock.sentinel.SENTINEL], ) self.assertEqual(self.mock_find_near_matches_levenshtein.call_count, 2) self.assertEqual( find_near_matches('a', 'a', 5, 3, 7, 2), - [mock.sentinel.SENTINEL], + [unittest.mock.sentinel.SENTINEL], ) self.assertEqual(self.mock_find_near_matches_levenshtein.call_count, 3) diff --git a/tests/test_find_near_matches_in_file.py b/tests/test_find_near_matches_in_file.py index d00a778..f7dc031 100644 --- a/tests/test_find_near_matches_in_file.py +++ b/tests/test_find_near_matches_in_file.py @@ -3,14 +3,15 @@ import os import re import tempfile +import unittest +import unittest.mock import attr from fuzzysearch import find_near_matches_in_file from fuzzysearch.common import Match -from fuzzysearch.compat import text_type, PY2 -from tests.compat import b, u, mock, unittest +from tests.compat import b import tests.test_find_near_matches @@ -32,17 +33,14 @@ def test_file_bytes(f): [Match(3, 9, 1, b('PATERN'))]) def test_file_unicode(f): - self.assertEqual(find_near_matches_in_file(u(needle), f, max_l_dist=1), - [Match(3, 9, 1, u('PATERN'))]) + self.assertEqual(find_near_matches_in_file(needle, f, max_l_dist=1), + [Match(3, 9, 1, 'PATERN')]) with open(filename, 'rb') as f: test_file_bytes(f) with open(filename, 'r') as f: - if PY2: - test_file_bytes(f) - else: - test_file_unicode(f) + test_file_unicode(f) with codecs.open(filename, 'rb') as f: test_file_bytes(f) @@ -57,8 +55,8 @@ def test_file_unicode(f): test_file_unicode(f) def test_unicode_encodings(self): - needle = u('PATTERN') - haystack = u('---PATERN---') + needle = 'PATTERN' + haystack = '---PATERN---' for encoding in ['ascii', 'latin-1', 'latin1', 'utf-8', 'utf-16']: with self.subTest(encoding=encoding): @@ -69,7 +67,7 @@ def test_unicode_encodings(self): with io.open(filename, 'r', encoding=encoding) as f: self.assertEqual( find_near_matches_in_file(needle, f, max_l_dist=1), - [Match(3, 9, 1, u('PATERN'))], + [Match(3, 9, 1, 'PATERN')], ) def test_subsequence_split_between_chunks(self): @@ -122,13 +120,13 @@ def test_subsequence_split_between_chunks(self): ) with open(filename, 'r') as f: - _needle = needle if PY2 else needle.decode('utf-8') + _needle = needle.decode('utf-8') self.assertEqual( find_near_matches_in_file(_needle, f, max_l_dist=max_l_dist, _chunk_size=chunk_size), [attr.evolve(match, start=match.start + chunk_size + delta, end=match.end + chunk_size + delta, - matched=haystack_match if PY2 else haystack.decode('utf-8')) + matched=haystack.decode('utf-8')) for match in expected_matches] ) @@ -138,7 +136,7 @@ def test_subsequence_split_between_chunks(self): [attr.evolve(match, start=match.start + chunk_size + delta, end=match.end + chunk_size + delta, - matched=haystack_match if PY2 else haystack.decode('utf-8')) + matched=haystack.decode('utf-8')) for match in expected_matches] ) @@ -149,7 +147,7 @@ def test_subsequence_split_between_chunks(self): [attr.evolve(match, start=match.start + chunk_size + delta, end=match.end + chunk_size + delta, - matched=haystack_match if PY2 else haystack.decode('utf-8')) + matched=haystack.decode('utf-8')) for match in expected_matches] ) @@ -186,7 +184,7 @@ def find_near_matches_dropin(subsequence, sequence, *args, **kwargs): self.skipTest('skipping BioPython Seq tests with find_near_matches_in_file') tempfilepath = tempfile.mktemp() - if isinstance(sequence, text_type): + if isinstance(sequence, str): f = io.open(tempfilepath, 'w+', encoding='utf-8') else: f = open(tempfilepath, 'w+b') @@ -198,7 +196,7 @@ def find_near_matches_dropin(subsequence, sequence, *args, **kwargs): f.close() os.remove(tempfilepath) - patcher = mock.patch( + patcher = unittest.mock.patch( 'tests.test_find_near_matches.find_near_matches', find_near_matches_dropin) self.addCleanup(patcher.stop) diff --git a/tests/test_generic_search.py b/tests/test_generic_search.py index 1839930..f16e52e 100644 --- a/tests/test_generic_search.py +++ b/tests/test_generic_search.py @@ -1,4 +1,6 @@ -from tests.compat import b, unittest +import unittest + +from tests.compat import b from tests.test_levenshtein import TestFindNearMatchesLevenshteinBase from fuzzysearch.common import Match, get_best_match_in_group, group_matches, LevenshteinSearchParams, consolidate_overlapping_matches from tests.test_substitutions_only import TestSubstitionsOnlyBase, \ diff --git a/tests/test_generic_search_cython.py b/tests/test_generic_search_cython.py index a77707d..279b81c 100644 --- a/tests/test_generic_search_cython.py +++ b/tests/test_generic_search_cython.py @@ -1,4 +1,6 @@ -from tests.compat import b, unittest +import unittest + +from tests.compat import b from tests.utils import skip_if_arguments_arent_byteslike from tests.test_levenshtein import TestFindNearMatchesLevenshteinBase from fuzzysearch.common import Match, get_best_match_in_group, group_matches, LevenshteinSearchParams diff --git a/tests/test_levenshtein.py b/tests/test_levenshtein.py index 2483f46..bbe37a4 100644 --- a/tests/test_levenshtein.py +++ b/tests/test_levenshtein.py @@ -1,8 +1,7 @@ import re +import unittest -from tests.compat import unittest - -from fuzzysearch.common import Match, get_best_match_in_group, group_matches, consolidate_overlapping_matches +from fuzzysearch.common import Match, consolidate_overlapping_matches from fuzzysearch.levenshtein import find_near_matches_levenshtein, \ find_near_matches_levenshtein_linear_programming as fnm_levenshtein_lp from fuzzysearch.levenshtein_ngram import \ diff --git a/tests/test_memmem.py b/tests/test_memmem.py index ddb0f01..4d6a97a 100644 --- a/tests/test_memmem.py +++ b/tests/test_memmem.py @@ -1,4 +1,6 @@ -from tests.compat import b, unittest +import unittest + +from tests.compat import b class TestMemmemBase(object): diff --git a/tests/test_no_deletions.py b/tests/test_no_deletions.py index 906f43c..096c3a5 100644 --- a/tests/test_no_deletions.py +++ b/tests/test_no_deletions.py @@ -1,4 +1,4 @@ -from tests.compat import unittest, mock +import unittest from fuzzysearch.common import Match, LevenshteinSearchParams from fuzzysearch.no_deletions import _expand, \ diff --git a/tests/test_search_exact.py b/tests/test_search_exact.py index 1d1ec71..f212ad3 100644 --- a/tests/test_search_exact.py +++ b/tests/test_search_exact.py @@ -1,6 +1,7 @@ -from fuzzysearch.compat import text_type +import unittest + from fuzzysearch.search_exact import search_exact -from tests.compat import b, u, unittest +from tests.compat import b class TestSearchExactBase(object): @@ -103,7 +104,7 @@ def search(self, subsequence, sequence, start_index=0, end_index=None): @classmethod def get_supported_sequence_types(cls): - types_to_test = [b, u, list, tuple] + types_to_test = [b, str, list, tuple] try: from Bio.Seq import Seq @@ -119,7 +120,7 @@ def get_supported_sequence_types(cls): return types_to_test def test_unicode_subsequence(self): - self.assertEqual(self.search(u('\u03A3\u0393'), u('\u03A0\u03A3\u0393\u0394')), [1]) + self.assertEqual(self.search('\u03A3\u0393', '\u03A0\u03A3\u0393\u0394'), [1]) try: @@ -129,12 +130,12 @@ def test_unicode_subsequence(self): else: class TestSearchExactByteslike(TestSearchExactBase, unittest.TestCase): def search(self, subsequence, sequence, start_index=0, end_index=None): - if isinstance(subsequence, text_type): + if isinstance(subsequence, str): try: subsequence = subsequence.encode('ascii') except UnicodeEncodeError: raise self.skipTest("skipping test with non-ascii-encodable string for byteslike function") - if isinstance(sequence, text_type): + if isinstance(sequence, str): try: sequence = sequence.encode('ascii') except UnicodeEncodeError: diff --git a/tests/test_substitutions_only.py b/tests/test_substitutions_only.py index 142804e..068ee2d 100644 --- a/tests/test_substitutions_only.py +++ b/tests/test_substitutions_only.py @@ -1,3 +1,5 @@ +import unittest + from fuzzysearch.common import group_matches, Match, get_best_match_in_group, \ count_differences_with_maximum, consolidate_overlapping_matches from fuzzysearch.substitutions_only import \ @@ -8,7 +10,7 @@ find_near_matches_substitutions_ngrams as fnm_subs_ngrams, \ has_near_match_substitutions_ngrams as hnm_subs_ngrams -from tests.compat import b, u, unittest +from tests.compat import b from tests.utils import skip_if_arguments_arent_byteslike @@ -232,8 +234,8 @@ def test_missing_at_beginning(self): ) def test_unicode_substring(self): - pattern = u('\u03A3\u0393') - text = u('\u03A0\u03A3\u0393\u0394') + pattern = '\u03A3\u0393' + text = '\u03A0\u03A3\u0393\u0394' self.expectedOutcomes( self.search(pattern, text, max_subs=0), [Match(1, 3, 0, matched=text[1:3])] diff --git a/tests/utils.py b/tests/utils.py index cdeed54..44513be 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,15 +1,13 @@ from functools import wraps -from fuzzysearch.compat import text_type - def skip_if_arguments_arent_byteslike(test_method): @wraps(test_method) def new_method(self, *args, **kwargs): subsequence, sequence = args[:2] if ( - isinstance(subsequence, text_type) or - isinstance(sequence, text_type) + isinstance(subsequence, str) or + isinstance(sequence, str) ): raise self.skipTest( "skipping test with unicode data for byteslike function") diff --git a/tox.ini b/tox.ini index c5e5866..099582d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py27,py35,py36,py37,py38,py39,py310,pypy,pypy3}-{with,without}_coverage +envlist = {py38,py39,py310,pypy39,pypy310}-{with,without}_coverage [testenv] install_command = @@ -13,10 +13,7 @@ install_command = deps = ; use specific versions of testing tools with which this is known to work with_coverage: coverage>=5,<6 - {py27,pypy}: unittest2==1.1.0 - {py27,pypy}: mock==1.3.0 - {py27,py35}: biopython<=1.76 - {py36,py37,py38,py39,py310,py311,pypy3}: biopython + {py38,py39,py310,py311,pypy39,pypy310}: biopython allowlist_externals = mv commands = @@ -25,25 +22,11 @@ commands = ; * if running with coverage, merge the coverage run results from both runs ; * for Python 2.6 use the unit2 script since -m unittest2 doesn't work ; (but when running with coverage, coverage run -m unittest2 works) - {py27,pypy}-without_coverage: {envbindir}/unit2 discover -v tests -t {toxinidir} - {py27,pypy}-with_coverage: {envbindir}/coverage run --source=fuzzysearch -m unittest2 discover tests - {py35,py36,py37,py38,py39,py310,pypy3}-without_coverage: {envpython} -m unittest discover -v tests - {py35,py36,py37,py38,py39,py310,pypy3}-with_coverage: {envbindir}/coverage run --source=fuzzysearch -m unittest discover tests + {py38,py39,py310,pypy39,pypy310}-without_coverage: {envpython} -m unittest discover -v tests + {py38,py39,py310,pypy39,pypy310}-with_coverage: {envbindir}/coverage run --source=fuzzysearch -m unittest discover tests with_coverage: mv .coverage .coverage.with_extensions {envpython} -c 'import os; [os.remove(os.path.join(d, fn)) for (d, dns, fns) in os.walk(os.path.join(r"{envsitepackagesdir}", "fuzzysearch")) for fn in fns if fn.endswith((".so", ".pyd"))]' - {py27,pypy}-without_coverage: {envbindir}/unit2 discover -v tests -t {toxinidir} - {py27,pypy}-with_coverage: {envbindir}/coverage run --source=fuzzysearch -m unittest2 discover tests - {py35,py36,py37,py38,py39,py310,pypy3}-without_coverage: {envpython} -m unittest discover -v tests - {py35,py36,py37,py38,py39,py310,pypy3}-with_coverage: {envbindir}/coverage run --source=fuzzysearch -m unittest discover tests + {py38,py39,py310,pypy39,pypy310}-without_coverage: {envpython} -m unittest discover -v tests + {py38,py39,py310,pypy39,pypy310}-with_coverage: {envbindir}/coverage run --source=fuzzysearch -m unittest discover tests with_coverage: mv .coverage .coverage.no_extensions with_coverage: {envbindir}/coverage combine -basepython = - py27: python2.7 - py35: python3.5 - py36: python3.6 - py37: python3.7 - py38: python3.8 - py39: python3.9 - py310: python3.10 - pypy: pypy - pypy3: pypy3