summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSeth Morton <seth.m.morton@gmail.com>2018-02-10 12:17:49 -0800
committerGitHub <noreply@github.com>2018-02-10 12:17:49 -0800
commitc56cf6d9d7ef1a4781756852d5bb2b98d5d09f58 (patch)
tree7db80224e02dbb38bdcca516a0081ec5f2ab8bbb
parent1b3b26828dd751d8f60cb8b41cd66ed19ba26cf9 (diff)
parent002cb4cc72982b38f5a38fde7fd3e528f2b64932 (diff)
downloadnatsort-c56cf6d9d7ef1a4781756852d5bb2b98d5d09f58.tar.gz
Merge pull request #47 from rinslow/master
Add a natcmp() for comparing in Python 2.
-rw-r--r--natsort/__init__.py11
-rw-r--r--natsort/compat/py23.py7
-rw-r--r--natsort/natsort.py60
-rw-r--r--test_natsort/test_natsort_cmp.py108
-rw-r--r--test_natsort/test_utils.py9
-rw-r--r--testing-requirements.txt1
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