diff options
author | Seth Morton <seth.m.morton@gmail.com> | 2018-02-10 12:17:49 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-10 12:17:49 -0800 |
commit | c56cf6d9d7ef1a4781756852d5bb2b98d5d09f58 (patch) | |
tree | 7db80224e02dbb38bdcca516a0081ec5f2ab8bbb | |
parent | 1b3b26828dd751d8f60cb8b41cd66ed19ba26cf9 (diff) | |
parent | 002cb4cc72982b38f5a38fde7fd3e528f2b64932 (diff) | |
download | natsort-c56cf6d9d7ef1a4781756852d5bb2b98d5d09f58.tar.gz |
Merge pull request #47 from rinslow/master
Add a natcmp() for comparing in Python 2.
-rw-r--r-- | natsort/__init__.py | 11 | ||||
-rw-r--r-- | natsort/compat/py23.py | 7 | ||||
-rw-r--r-- | natsort/natsort.py | 60 | ||||
-rw-r--r-- | test_natsort/test_natsort_cmp.py | 108 | ||||
-rw-r--r-- | test_natsort/test_utils.py | 9 | ||||
-rw-r--r-- | testing-requirements.txt | 1 |
6 files changed, 192 insertions, 4 deletions
diff --git a/natsort/__init__.py b/natsort/__init__.py index 62280a5..45ede6e 100644 --- a/natsort/__init__.py +++ b/natsort/__init__.py @@ -7,6 +7,11 @@ from __future__ import ( ) # Local imports. +import sys + +from natsort.utils import chain_functions +from natsort._version import __version__ + from natsort.natsort import ( natsort_key, natsort_keygen, @@ -24,8 +29,9 @@ from natsort.natsort import ( as_utf8, ns, ) -from natsort.utils import chain_functions -from natsort._version import __version__ + +if float(sys.version[:3]) < 3: + from natsort.natsort import natcmp __all__ = [ 'natsort_key', @@ -40,6 +46,7 @@ __all__ = [ 'index_realsorted', 'order_by_index', 'decoder', + 'natcmp', 'as_ascii', 'as_utf8', 'ns', diff --git a/natsort/compat/py23.py b/natsort/compat/py23.py index 8155798..fa56b06 100644 --- a/natsort/compat/py23.py +++ b/natsort/compat/py23.py @@ -30,6 +30,13 @@ py23_basestring = str if sys.version[0] == '3' else basestring # unichr function py23_unichr = chr if sys.version[0] == '3' else unichr + +def _py23_cmp(a, b): + return (a > b) - (a < b) + + +py23_cmp = _py23_cmp if sys.version[0] == '3' else cmp + # zip as an iterator if sys.version[0] == '3': py23_zip = zip diff --git a/natsort/natsort.py b/natsort/natsort.py index 15358e7..85c1637 100644 --- a/natsort/natsort.py +++ b/natsort/natsort.py @@ -23,12 +23,14 @@ from functools import partial from warnings import warn # Local imports. +import sys + import natsort.compat.locale from natsort.ns_enum import ns from natsort.compat.py23 import ( u_format, py23_str, -) + py23_cmp) from natsort.utils import ( _natsort_key, _args_to_enum, @@ -674,3 +676,59 @@ def order_by_index(seq, index, iter=False): """ return (seq[i] for i in index) if iter else [seq[i] for i in index] + +if float(sys.version[:3]) < 3: + # pylint: disable=unused-variable + class natcmp(object): + """ + Compare two objects using a key and an algorithm. + + Parameters + ---------- + x : object + First object to compare. + + y : object + Second object to compare. + + alg : ns enum, optional + This option is used to control which algorithm `natsort` + uses when sorting. For details into these options, please see + the :class:`ns` class documentation. The default is `ns.INT`. + + Returns + ------- + out: int + 0 if x and y are equal, 1 if x > y, -1 if y > x. + + See Also + -------- + natsort_keygen : Generates a key that makes natural sorting possible. + + Examples + -------- + Use `natcmp` just like the builtin `cmp`:: + + >>> one = 1 + >>> two = 2 + >>> natcmp(one, two) + -1 + """ + cached_keys = {} + + def __new__(cls, x, y, alg=0, *args, **kwargs): + try: + alg = _args_to_enum(**kwargs) | alg + except TypeError: + msg = ("natsort_keygen: 'alg' argument must be " + "from the enum 'ns'") + raise ValueError(msg + ', got {0}'.format(py23_str(alg))) + + # Add the _DUMB option if the locale library is broken. + if alg & ns.LOCALEALPHA and natsort.compat.locale.dumb_sort(): + alg |= ns._DUMB + + if alg not in cls.cached_keys: + cls.cached_keys[alg] = natsort_keygen(alg=alg) + + return py23_cmp(cls.cached_keys[alg](x), cls.cached_keys[alg](y)) diff --git a/test_natsort/test_natsort_cmp.py b/test_natsort/test_natsort_cmp.py new file mode 100644 index 0000000..a434d13 --- /dev/null +++ b/test_natsort/test_natsort_cmp.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-variable +"""These test the natcmp() function. + +Note that these tests are only relevant for Python version < 3. +""" +import sys +from functools import partial +from compat.mock import patch + +import pytest +from hypothesis import given +from hypothesis.strategies import floats, integers, lists + +from natsort import ns + +from natsort.compat.py23 import py23_cmp + +PY_VERSION = float(sys.version[:3]) + +if PY_VERSION < 3: + from natsort import natcmp + + +class Comparable(object): + """Stub class for testing natcmp functionality.""" + def __init__(self, value): + self.value = value + + def __cmp__(self, other): + return natcmp(self.value, other.value) + + +@pytest.mark.skipif(PY_VERSION >= 3.0, reason='cmp() deprecated in Python 3') +def test__classes_can_be_compared(): + one = Comparable("1") + two = Comparable("2") + another_two = Comparable("2") + ten = Comparable("10") + + assert ten > two == another_two > one + + +@pytest.mark.skipif(PY_VERSION >= 3.0, reason='cmp() deprecated in Python 3') +def test__keys_are_being_cached(): + natcmp.cached_keys = {} + assert len(natcmp.cached_keys) == 0 + natcmp(0, 0) + assert len(natcmp.cached_keys) == 1 + natcmp(0, 0) + assert len(natcmp.cached_keys) == 1 + natcmp(0, 0, alg=ns.L) + assert len(natcmp.cached_keys) == 2 + + natcmp(0, 0, alg=ns.L) + assert len(natcmp.cached_keys) == 2 + + with patch('natsort.compat.locale.dumb_sort', return_value=True): + natcmp(0, 0, alg=ns.L) + + assert len(natcmp.cached_keys) == 3 + + with patch('natsort.compat.locale.dumb_sort', return_value=True): + natcmp(0, 0, alg=ns.L) + + assert len(natcmp.cached_keys) == 3 + + +@pytest.mark.skipif(PY_VERSION >= 3.0, reason='cmp() deprecated in Python 3') +def test__illegal_algorithm_raises_error(): + try: + natcmp(0, 0, alg="Just random stuff") + assert False + + except ValueError: + assert True + + except Exception: + assert False + + +@pytest.mark.skipif(PY_VERSION >= 3.0, reason='cmp() deprecated in Python 3') +def test__classes_can_utilize_max_or_min(): + comparables = [Comparable(i) for i in range(10)] + + assert max(comparables) == comparables[-1] + assert min(comparables) == comparables[0] + + +@pytest.mark.skipif(PY_VERSION >= 3.0, reason='cmp() deprecated in Python 3') +@given(integers(), integers()) +def test__natcmp_works_the_same_for_integers_as_cmp(x, y): + assert py23_cmp(x, y) == natcmp(x, y) + + +@pytest.mark.skipif(PY_VERSION >= 3.0, reason='cmp() deprecated in Python 3') +@given(floats(allow_nan=False), floats(allow_nan=False)) +def test__natcmp_works_the_same_for_floats_as_cmp(x, y): + assert py23_cmp(x, y) == natcmp(x, y) + + +@pytest.mark.skipif(PY_VERSION >= 3.0, reason='cmp() deprecated in Python 3') +@given(lists(elements=integers())) +def test_sort_strings_with_numbers(a_list): + strings = [str(var) for var in a_list] + natcmp_sorted = sorted(strings, cmp=partial(natcmp, alg=ns.REAL)) + + assert sorted(a_list) == [int(var) for var in natcmp_sorted] diff --git a/test_natsort/test_utils.py b/test_natsort/test_utils.py index 8ae8686..cfa06b6 100644 --- a/test_natsort/test_utils.py +++ b/test_natsort/test_utils.py @@ -23,7 +23,7 @@ from natsort.utils import ( _groupletters, chain_functions, ) -from natsort.compat.py23 import py23_str +from natsort.compat.py23 import py23_str, py23_cmp from natsort.compat.locale import null_string from slow_splitters import ( sep_inserter, @@ -227,3 +227,10 @@ def test_path_splitter_splits_path_string_by_separator_and_removes_extension(x): z = py23_str(pathlib.Path(*x[:-2])) + '.' + x[-1] y = tuple(pathlib.Path(z).parts) assert tuple(_path_splitter(z)) == y[:-1] + (pathlib.Path(z).stem, pathlib.Path(z).suffix) + + +@given(integers()) +def test_py23_cmp(x): + assert py23_cmp(x, x) == 0 + assert py23_cmp(x, x + 1) < 0 + assert py23_cmp(x, x - 1) > 0 diff --git a/testing-requirements.txt b/testing-requirements.txt index 9dd1624..3aa42a9 100644 --- a/testing-requirements.txt +++ b/testing-requirements.txt @@ -10,3 +10,4 @@ pytest-cov; python_version > '2.6' pytest-flakes; python_version > '2.6' pytest-pep8; python_version > '2.6' hypothesis>=3.8.0; python_version > '2.6' +astroid==1.5.3; python_version > '2.6'
\ No newline at end of file |