summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSeth M Morton <seth.m.morton@gmail.com>2015-03-26 20:06:55 -0700
committerSeth M Morton <seth.m.morton@gmail.com>2015-03-26 20:06:55 -0700
commit929ab6944e0f73bf2285178040d8f5a2250aa242 (patch)
tree3bdee25755c611e126aeab1b711b65ab93f10e75
parent3bc8ae9116d1de40904dc510c2a90cfd65b5ebf7 (diff)
parentcb5e8747a3660a2c26b49200651193d8c246821f (diff)
downloadnatsort-3.5.3.tar.gz
natsort release version 3.5.3.3.5.3
- Fixed bug where --reverse-filter option in shell script was not getting checked for correctness. - Documentation updates to better describe locale bug, and illustrate upcoming default behavior change. - Internal improvements, including making test suite more granular.
-rw-r--r--.travis.yml19
-rw-r--r--LICENSE2
-rw-r--r--README.rst70
-rw-r--r--docs/source/changelog.rst9
-rw-r--r--docs/source/examples.rst12
-rw-r--r--docs/source/intro.rst13
-rw-r--r--natsort/__main__.py6
-rw-r--r--natsort/_version.py2
-rw-r--r--natsort/locale_help.py13
-rw-r--r--natsort/natsort.py22
-rw-r--r--natsort/ns_enum.py46
-rw-r--r--natsort/utils.py42
-rw-r--r--setup.cfg3
-rw-r--r--setup.py9
-rw-r--r--test_natsort/test_fake_fastnumbers.py17
-rw-r--r--test_natsort/test_locale_help.py36
-rw-r--r--test_natsort/test_main.py436
-rw-r--r--test_natsort/test_natsort.py233
-rw-r--r--test_natsort/test_utils.py312
19 files changed, 708 insertions, 594 deletions
diff --git a/.travis.yml b/.travis.yml
index c3ad819..1d064bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,29 +16,14 @@ install:
- if [[ $WITH_OPTIONS == true ]]; then sudo apt-get install libicu-dev; fi
- if [[ $WITH_OPTIONS == true ]]; then pip install fastnumbers; fi
- if [[ $WITH_OPTIONS == true ]]; then pip install PyICU; fi
-- if [[ $WITH_OPTIONS == true && 1 -eq $(echo "$TRAVIS_PYTHON_VERSION < 3.4" | bc) ]]; then pip install pathlib; fi
+- if [[ $WITH_OPTIONS == true && 1 -eq $(echo "$TRAVIS_PYTHON_VERSION < 3.4" | bc -l) ]]; then pip install pathlib; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install argparse; fi
+- if [[ $(echo "$TRAVIS_PYTHON_VERSION < 3.3" | bc -l) ]]; then pip install mock; fi
- pip install pytest-cov pytest-flakes pytest-pep8
- pip install coveralls
script:
- python -m pytest --cov natsort --flakes --pep8
- python -m pytest --doctest-modules natsort
- python -m pytest README.rst docs/source/intro.rst docs/source/examples.rst
-- python -m pytest test_natsort/stress_natsort.py
after_success:
coveralls
-# before_deploy:
-# - pip install Sphinx numpydoc
-# - python setup.py build_sphinx
-# deploy:
-# provider: pypi
-# user: SethMMorton
-# password:
-# secure: OaYQtVh4mGT0ozN7Ar2lSm2IEVMKIyvOESGPGLwVyVxPqp6oC101MovJ7041bZdjMzirMs54EJwtEGQpKFmDBGcKgbjPiYId5Nqb/yDhLC/ojgarbLoFJvUKV6dWJePyY7EOycrqcMdiDabdG80Bw4zziQExbmIOdUiscsAVVmA=
-# on:
-# tags: true
-# all_branches: true
-# repo: SethMMorton/natsort
-# python: 2.7
-# distributions: "sdist bdist_wheel"
-# docs_dir: build/sphinx/html
diff --git a/LICENSE b/LICENSE
index 86a72b3..4c73ac0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2012-2014 Seth M. Morton
+Copyright (c) 2012-2015 Seth M. Morton
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
diff --git a/README.rst b/README.rst
index 31f9166..1822adc 100644
--- a/README.rst
+++ b/README.rst
@@ -13,6 +13,9 @@ Natural sorting for python.
- Downloads: https://pypi.python.org/pypi/natsort
- Documentation: http://pythonhosted.org//natsort/
+Please see `Deprecation Notices`_ for an `important` backwards incompatibility notice
+for ``natsort`` version 4.0.0.
+
Quick Description
-----------------
@@ -74,8 +77,9 @@ ordinal value; this can be achieved with the ``humansorted`` function:
You may find you need to explicitly set the locale to get this to work
(as shown in the example).
Please see the `following caveat <http://pythonhosted.org//natsort/examples.html#bug-note>`_
-and the "Optional Dependencies" section
-below before using the ``humansorted`` function.
+and the `Optional Dependencies`_ section
+below before using the ``humansorted`` function, *especially* if you are on a
+BSD-based system (like Mac OS X).
You can mix and match ``int``, ``float``, and ``str`` (or ``unicode``) types
when you sort:
@@ -94,7 +98,7 @@ The natsort algorithm does other fancy things like
- control the case-sensitivity
- sort file paths correctly
- allow custom sorting keys
- - exposes a natsort_key generator to pass to list.sort
+ - exposes a natsort_key generator to pass to ``list.sort``
Please see the package documentation for more details, including
`examples and recipes <http://pythonhosted.org//natsort/examples.html>`_.
@@ -103,8 +107,7 @@ Shell script
------------
``natsort`` comes with a shell script called ``natsort``, or can also be called
-from the command line with ``python -m natsort``. The command line script is
-only installed onto your ``PATH`` if you don't install via a wheel.
+from the command line with ``python -m natsort``.
Requirements
------------
@@ -113,6 +116,8 @@ Requirements
(this includes python 3.x). To run version 2.6, 3.0, or 3.1 the
`argparse <https://pypi.python.org/pypi/argparse>`_ module is required.
+.. _optional:
+
Optional Dependencies
---------------------
@@ -130,16 +135,28 @@ at installation.
PyICU
'''''
-On some systems, Python's ``locale`` library can be buggy (I have found this to be
-the case on Mac OS X), so ``natsort`` will use
+On BSD-based systems (this includes Mac OS X), the underlying ``locale`` library
+can be buggy (please see http://bugs.python.org/issue23195), so ``natsort`` will use
`PyICU <https://pypi.python.org/pypi/PyICU>`_ under the hood if it is installed
-on your computer; this will give more reliable results. ``natsort`` will not
-require (or check) that `PyICU <https://pypi.python.org/pypi/PyICU>`_ is installed
-at installation.
+on your computer; this will give more reliable cross-platform results.
+``natsort`` will not require (or check) that
+`PyICU <https://pypi.python.org/pypi/PyICU>`_ is installed at installation
+since in Linux-based systems and Windows systems ``locale`` should work just fine.
+Please visit https://github.com/SethMMorton/natsort/issues/21 for more details and
+how to install on Mac OS X.
+
+.. _deprecate:
Deprecation Notices
-------------------
+ - The default sorting algorithm for ``natsort`` will change in version 4.0.0
+ from signed floats (with exponents) to unsigned integers. The motivation
+ for this change is that it will cause ``natsort`` to return results that
+ pass the "least astonishment" test for the most common use case, which is
+ sorting version numbers. If you currently rely on the default behavior
+ to be signed floats, it is recommend that you add ``alg=ns.F`` to your
+ ``natsort`` calls.
- In ``natsort`` version 4.0.0, the ``number_type``, ``signed``, ``exp``,
``as_path``, and ``py3_safe`` options will be removed from the (documented)
API, in favor of the ``alg`` option and ``ns`` enum. They will remain as
@@ -147,12 +164,6 @@ Deprecation Notices
- In ``natsort`` version 4.0.0, the ``natsort_key`` function will be removed
from the public API. All future development should use ``natsort_keygen``
in preparation for this.
- - In ``natsort`` version 3.1.0, the shell script changed how it interpreted
- input; previously, all input was assumed to be a filepath, but as of 3.1.0
- input is just treated as a string. For most cases the results are the same.
-
- - As of ``natsort`` version 3.4.0, a ``--path`` option has been added to
- force the shell script to interpret the input as filepaths.
Author
------
@@ -165,6 +176,15 @@ History
These are the last three entries of the changelog. See the package documentation
for the complete `changelog <http://pythonhosted.org//natsort/changelog.html>`_.
+03-26-2015 v. 3.5.3
+'''''''''''''''''''
+
+ - Fixed bug where ``--reverse-filter`` option in shell script was not
+ getting checked for correctness.
+ - Documentation updates to better describe locale bug, and illustrate
+ upcoming default behavior change.
+ - Internal improvements, including making test suite more granular.
+
01-13-2015 v. 3.5.2
'''''''''''''''''''
@@ -179,21 +199,3 @@ for the complete `changelog <http://pythonhosted.org//natsort/changelog.html>`_.
- Refactored modules so that only the public API was in natsort.py and
ns_enum.py.
- Refactored all import statements to be absolute, not relative.
-
-09-02-2014 v. 3.5.0
-'''''''''''''''''''
-
- - Added the 'alg' argument to the 'natsort' functions. This argument
- accepts an enum that is used to indicate the options the user wishes
- to use. The 'number_type', 'signed', 'exp', 'as_path', and 'py3_safe'
- options are being deprecated and will become (undocumented)
- keyword-only options in natsort version 4.0.0.
- - The user can now modify how 'natsort' handles the case of non-numeric
- characters.
- - The user can now instruct 'natsort' to use locale-aware sorting, which
- allows 'natsort' to perform true "human sorting".
-
- - The `humansorted` convenience function has been included to make this
- easier.
-
- - Updated shell script with locale functionality.
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index ea05e70..a57532c 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -3,6 +3,15 @@
Changelog
---------
+03-26-2015 v. 3.5.3
+'''''''''''''''''''
+
+ - Fixed bug where ``--reverse-filter`` option in shell script was not
+ getting checked for correctness.
+ - Documentation updates to better describe locale bug, and illustrate
+ upcoming default behavior change.
+ - Internal improvements, including making test suite more granular.
+
01-13-2015 v. 3.5.2
'''''''''''''''''''
diff --git a/docs/source/examples.rst b/docs/source/examples.rst
index b0dfe27..1f795e4 100644
--- a/docs/source/examples.rst
+++ b/docs/source/examples.rst
@@ -112,14 +112,16 @@ you should not need to do this.
.. _bug_note:
-A Note For Bugs With Locale-Aware Sorting
-+++++++++++++++++++++++++++++++++++++++++
+Known Bugs When Using Locale-Aware Sorting On BSD-Based OSs (Including Mac OS X)
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
If you find that ``ns.LOCALE`` (or :func:`~humansorted`) does not give
the results you expect, before filing a bug report please try to first install
-`PyICU <https://pypi.python.org/pypi/PyICU>`_. There are some known bugs
-with the `locale` module from the standard library that are solved when
-using `PyICU <https://pypi.python.org/pypi/PyICU>`_.
+`PyICU <https://pypi.python.org/pypi/PyICU>`_; this *especially* applies
+to users on BSD-based systems (like Mac OS X). There are some known bugs
+with the ``locale`` module from the standard library that are solved when
+using `PyICU <https://pypi.python.org/pypi/PyICU>`_; you can read about
+them here: http://bugs.python.org/issue23195.
Controlling Case When Sorting
-----------------------------
diff --git a/docs/source/intro.rst b/docs/source/intro.rst
index ace8355..94fec70 100644
--- a/docs/source/intro.rst
+++ b/docs/source/intro.rst
@@ -139,12 +139,15 @@ without the package, but if you need to squeeze out that extra juice it is
recommended you include this as a dependency. ``natsort`` will not require (or
check) that `fastnumbers <https://pypi.python.org/pypi/fastnumbers>`_ is installed.
-On some systems, Python's ``locale`` library can be buggy (I have found this to be
-the case on Mac OS X), so ``natsort`` will use
+On BSD-based systems (this includes Mac OS X), the underlying ``locale`` library
+can be buggy (please see http://bugs.python.org/issue23195), so ``natsort`` will use
`PyICU <https://pypi.python.org/pypi/PyICU>`_ under the hood if it is installed
-on your computer; this will give more reliable results. ``natsort`` will not
-require (or check) that `PyICU <https://pypi.python.org/pypi/PyICU>`_ is installed
-at installation.
+on your computer; this will give more reliable cross-platform results.
+``natsort`` will not require (or check) that
+`PyICU <https://pypi.python.org/pypi/PyICU>`_ is installed at installation
+since in Linux-based systems and Windows systems ``locale`` should work just fine.
+Please visit https://github.com/SethMMorton/natsort/issues/21 for more details and
+how to install on Mac OS X.
:mod:`natsort` comes with a shell script called :mod:`natsort`, or can also be called
from the command line with ``python -m natsort``. The command line script is
diff --git a/natsort/__main__.py b/natsort/__main__.py
index 5368d12..85edba3 100644
--- a/natsort/__main__.py
+++ b/natsort/__main__.py
@@ -67,9 +67,8 @@ def main():
'effects the --number-type=float.')
parser.add_argument(
'--locale', '-l', action='store_true', default=False,
- help='Causes natsort to use locale-aware sorting. On some systems, '
- 'the underlying C library is broken, so if you get results that '
- 'you do not expect please install PyICU and try again.')
+ help='Causes natsort to use locale-aware sorting. You will get the '
+ 'best results if you install PyICU.')
parser.add_argument(
'entries', nargs='*', default=sys.stdin,
help='The entries to sort. Taken from stdin if nothing is given on '
@@ -78,6 +77,7 @@ def main():
# Make sure the filter range is given properly. Does nothing if no filter
args.filter = check_filter(args.filter)
+ args.reverse_filter = check_filter(args.reverse_filter)
# Remove trailing whitespace from all the entries
entries = [e.strip() for e in args.entries]
diff --git a/natsort/_version.py b/natsort/_version.py
index a1bc7de..0047fef 100644
--- a/natsort/_version.py
+++ b/natsort/_version.py
@@ -2,4 +2,4 @@
from __future__ import (print_function, division,
unicode_literals, absolute_import)
-__version__ = '3.5.2'
+__version__ = '3.5.3'
diff --git a/natsort/locale_help.py b/natsort/locale_help.py
index 32bc116..8a5cf4f 100644
--- a/natsort/locale_help.py
+++ b/natsort/locale_help.py
@@ -52,7 +52,8 @@ elif sys.version[:3] == '2.6':
return K
# Make the strxfrm function from strcoll on Python2
-# It can be buggy, so prefer PyICU if available.
+# It can be buggy (especially on BSD-based systems),
+# so prefer PyICU if available.
try:
import PyICU
from locale import getlocale
@@ -77,8 +78,10 @@ except ImportError:
from locale import strxfrm
use_pyicu = False
-# This little lambda doubles all characters, making letters lowercase.
-groupletters = lambda x: ''.join(chain(*py23_zip(x.lower(), x)))
+
+def groupletters(x):
+ """Double all characters, making doubled letters lowercase."""
+ return ''.join(chain(*py23_zip(x.lower(), x)))
def grouper(val, func):
@@ -97,8 +100,8 @@ def locale_convert(val, func, group):
"""\
Attempt to convert a string to a number, first converting
the decimal place character if needed. Then, if the conversion
- was not possible, run it through strxfrm to make the sorting
- as requested, possibly grouping first.
+ was not possible (i.e. it is not a number), run it through
+ strxfrm to make the work sorting as requested, possibly grouping first.
"""
# Format the number so that the conversion function can interpret it.
diff --git a/natsort/natsort.py b/natsort/natsort.py
index 3407982..b136f91 100644
--- a/natsort/natsort.py
+++ b/natsort/natsort.py
@@ -220,7 +220,7 @@ def natsort_keygen(key=None, number_type=float, signed=None, exp=None,
Examples
--------
- `natsort_keygen` is a convenient waynto create a custom key
+ `natsort_keygen` is a convenient way to create a custom key
to sort lists in-place (for example). Calling with no objects
will return a plain `natsort_key` instance::
@@ -402,15 +402,14 @@ def humansorted(seq, key=None, reverse=False, alg=0):
in a locale-aware fashion (a.k.a "human sorting"). This is a
wrapper around ``natsorted(seq, alg=ns.LOCALE)``.
- .. warning:: On some systems, the underlying C library that
- Python's locale module uses is broken. On these
- systems it is recommended that you install
- `PyICU <https://pypi.python.org/pypi/PyICU>`_.
- Please validate that this function works as
- expected on your target system, and if not you
- should add
+ .. warning:: On BSD-based systems (like Mac OS X), the underlying
+ C library that Python's locale module uses is broken.
+ On these systems it is recommended that you install
`PyICU <https://pypi.python.org/pypi/PyICU>`_
- as a dependency.
+ if you wish to use ``humansorted``. If you are on
+ one of systems and get unexpected results, please try
+ using `PyICU <https://pypi.python.org/pypi/PyICU>`_
+ before filing a bug report to ``natsort``.
Parameters
----------
@@ -559,7 +558,8 @@ def index_natsorted(seq, key=None, number_type=float, signed=None, exp=None,
if key is None:
newkey = itemgetter(1)
else:
- newkey = lambda x: key(itemgetter(1)(x))
+ def newkey(x):
+ return key(itemgetter(1)(x))
# Pair the index and sequence together, then sort by element
index_seq_pair = [[x, y] for x, y in enumerate(seq)]
try:
@@ -754,7 +754,7 @@ def order_by_index(seq, index, iter=False):
Examples
--------
- `order_by_index` is a comvenience function that helps you apply
+ `order_by_index` is a convenience function that helps you apply
the result of `index_natsorted` or `index_versorted`::
>>> a = ['num3', 'num5', 'num2']
diff --git a/natsort/ns_enum.py b/natsort/ns_enum.py
index cab8a64..6f042f9 100644
--- a/natsort/ns_enum.py
+++ b/natsort/ns_enum.py
@@ -16,16 +16,14 @@ class ns(object):
Each option has a shortened 1- or 2-letter form.
- .. warning:: On some systems, the underlying C library that
- Python's locale module uses is broken. On these
- systems it is recommended that you install
+ .. warning:: On BSD-based systems (like Mac OS X), the underlying
+ C library that Python's locale module uses is broken.
+ On these systems it is recommended that you install
`PyICU <https://pypi.python.org/pypi/PyICU>`_
- if you wish to use `LOCALE`.
- Please validate that `LOCALE` works as
- expected on your target system, and if not you
- should add
- `PyICU <https://pypi.python.org/pypi/PyICU>`_
- as a dependency.
+ if you wish to use ``LOCALE``. If you are on one of
+ systems and get unexpected results, please try using
+ `PyICU <https://pypi.python.org/pypi/PyICU>`_ before
+ filing a bug report to ``natsort``.
Attributes
----------
@@ -110,19 +108,19 @@ class ns(object):
# Sort algorithm "enum" values.
-_nsdict = {'FLOAT': 0, 'F': 0,
- 'INT': 1, 'I': 1,
- 'UNSIGNED': 2, 'U': 2,
- 'VERSION': 3, 'V': 3, # Shortcut for INT | UNSIGNED
- 'DIGIT': 3, 'D': 3, # Shortcut for INT | UNSIGNED
- 'NOEXP': 4, 'N': 4,
- 'PATH': 8, 'P': 8,
- 'LOCALE': 16, 'L': 16,
- 'IGNORECASE': 32, 'IC': 32,
- 'LOWERCASEFIRST': 64, 'LF': 64,
- 'GROUPLETTERS': 128, 'G': 128,
- 'TYPESAFE': 1024, 'T': 1024,
- }
-# Populate the ns class with the _nsdict values.
-for x, y in _nsdict.items():
+_ns = {'FLOAT': 0, 'F': 0,
+ 'INT': 1, 'I': 1,
+ 'UNSIGNED': 2, 'U': 2,
+ 'VERSION': 3, 'V': 3, # Shortcut for INT | UNSIGNED
+ 'DIGIT': 3, 'D': 3, # Shortcut for INT | UNSIGNED
+ 'NOEXP': 4, 'N': 4,
+ 'PATH': 8, 'P': 8,
+ 'LOCALE': 16, 'L': 16,
+ 'IGNORECASE': 32, 'IC': 32,
+ 'LOWERCASEFIRST': 64, 'LF': 64,
+ 'GROUPLETTERS': 128, 'G': 128,
+ 'TYPESAFE': 1024, 'T': 1024,
+ }
+# Populate the ns class with the _ns values.
+for x, y in _ns.items():
setattr(ns, x, y)
diff --git a/natsort/utils.py b/natsort/utils.py
index 4b580bd..0bd8309 100644
--- a/natsort/utils.py
+++ b/natsort/utils.py
@@ -19,7 +19,7 @@ from locale import localeconv
# Local imports.
from natsort.locale_help import locale_convert, grouper
from natsort.py23compat import py23_str, py23_zip
-from natsort.ns_enum import ns, _nsdict
+from natsort.ns_enum import ns, _ns
# If the user has fastnumbers installed, they will get great speed
# benefits. If not, we simulate the functions here.
@@ -85,33 +85,33 @@ def _args_to_enum(number_type, signed, exp, as_path, py3_safe):
msg = "The 'number_type' argument is deprecated as of 3.5.0, "
msg += "please use 'alg=ns.FLOAT', 'alg=ns.INT', or 'alg=ns.VERSION'"
warn(msg, DeprecationWarning)
- alg |= (_nsdict['INT'] * bool(number_type in (int, None)))
- alg |= (_nsdict['UNSIGNED'] * (number_type is None))
+ alg |= (_ns['INT'] * bool(number_type in (int, None)))
+ alg |= (_ns['UNSIGNED'] * (number_type is None))
if signed is not None:
msg = "The 'signed' argument is deprecated as of 3.5.0, "
msg += "please use 'alg=ns.UNSIGNED'."
warn(msg, DeprecationWarning)
- alg |= (_nsdict['UNSIGNED'] * (not signed))
+ alg |= (_ns['UNSIGNED'] * (not signed))
if exp is not None:
msg = "The 'exp' argument is deprecated as of 3.5.0, "
msg += "please use 'alg=ns.NOEXP'."
warn(msg, DeprecationWarning)
- alg |= (_nsdict['NOEXP'] * (not exp))
+ alg |= (_ns['NOEXP'] * (not exp))
if as_path is not None:
msg = "The 'as_path' argument is deprecated as of 3.5.0, "
msg += "please use 'alg=ns.PATH'."
warn(msg, DeprecationWarning)
- alg |= (_nsdict['PATH'] * as_path)
+ alg |= (_ns['PATH'] * as_path)
if py3_safe is not None:
msg = "The 'py3_safe' argument is deprecated as of 3.5.0, "
msg += "please use 'alg=ns.TYPESAFE'."
warn(msg, DeprecationWarning)
- alg |= (_nsdict['TYPESAFE'] * py3_safe)
+ alg |= (_ns['TYPESAFE'] * py3_safe)
return alg
-def _input_parser(s, regex, numconv, py3_safe, use_locale, group_letters):
- """Helper to parse the string input into numbers and strings."""
+def _number_extracter(s, regex, numconv, py3_safe, use_locale, group_letters):
+ """Helper to separate the string input into numbers and strings."""
# Split the input string by numbers.
# If the input is not a string, TypeError is raised.
@@ -228,7 +228,7 @@ def _natsort_key(val, key, alg):
# Convert the arguments to the proper input tuple
try:
- use_locale = alg & _nsdict['LOCALE']
+ use_locale = alg & _ns['LOCALE']
inp_options = (alg & _NUMBER_ALGORITHMS,
localeconv()['decimal_point'] if use_locale else '.')
except TypeError:
@@ -253,7 +253,7 @@ def _natsort_key(val, key, alg):
# If this is a path, convert it.
# An AttrubuteError is raised if not a string.
split_as_path = False
- if alg & _nsdict['PATH']:
+ if alg & _ns['PATH']:
try:
val = _path_splitter(val)
except AttributeError:
@@ -266,26 +266,26 @@ def _natsort_key(val, key, alg):
# Assume the input are strings, which is the most common case.
# Apply the string modification if needed.
try:
- if alg & _nsdict['LOWERCASEFIRST']:
+ if alg & _ns['LOWERCASEFIRST']:
val = val.swapcase()
- if alg & _nsdict['IGNORECASE']:
+ if alg & _ns['IGNORECASE']:
val = val.lower()
- return tuple(_input_parser(val,
- regex,
- num_function,
- alg & _nsdict['TYPESAFE'],
- use_locale,
- alg & _nsdict['GROUPLETTERS']))
+ return tuple(_number_extracter(val,
+ regex,
+ num_function,
+ alg & _ns['TYPESAFE'],
+ use_locale,
+ alg & _ns['GROUPLETTERS']))
except (TypeError, AttributeError):
# If not strings, assume it is an iterable that must
# be parsed recursively. Do not apply the key recursively.
# If this string was split as a path, turn off 'PATH'.
try:
- was_path = alg & _nsdict['PATH']
+ was_path = alg & _ns['PATH']
newalg = alg & _ALL_BUT_PATH
newalg |= (was_path * (not split_as_path))
return tuple([_natsort_key(x, None, newalg) for x in val])
# If there is still an error, it must be a number.
# Return as-is, with a leading empty string.
except TypeError:
- return (('', val,),) if alg & _nsdict['PATH'] else ('', val,)
+ return (('', val,),) if alg & _ns['PATH'] else ('', val,)
diff --git a/setup.cfg b/setup.cfg
index d44c5d7..8498b36 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -15,4 +15,7 @@ flakes-ignore =
pep8ignore =
test_natsort/test_natsort.py E501 E241 E221
test_natsort/test_utils.py E501 E241 E221
+ test_natsort/test_locale_help.py E501 E241 E221
+ test_natsort/test_main.py E501 E241 E221
+ test_natsort/profile_natsorted.py ALL
docs/source/conf.py ALL
diff --git a/setup.py b/setup.py
index 948f2a4..a78bd14 100644
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,6 @@ with open(VERSIONFILE, "rt") as fl:
s = "Unable to locate version string in {0}"
raise RuntimeError(s.format(VERSIONFILE))
-
# Read in the documentation for the long_description
DESCRIPTION = 'Sort lists naturally'
try:
@@ -53,10 +52,13 @@ try:
except IOError:
LONG_DESCRIPTION = DESCRIPTION
-
# The argparse module was introduced in python 2.7 or python 3.2
REQUIRES = 'argparse' if sys.version[:3] in ('2.6', '3.0', '3.1') else ''
+# Testing needs pytest, and mock if less than python 3.3
+TESTS_REQUIRE = ['pytest', 'pytest-pep8', 'pytest-flakes', 'pytest-cov']
+if sys.version[0] == 2 or (sys.version[3] == '3' and int(sys.version[2]) < 3):
+ TESTS_REQUIRE.append('mock')
# The setup parameters
setup(
@@ -69,8 +71,7 @@ setup(
install_requires=REQUIRES,
packages=['natsort'],
entry_points={'console_scripts': ['natsort = natsort.__main__:main']},
- tests_require=['pytest', 'pytest-pep8',
- 'pytest-flakes', 'pytest-cov'],
+ tests_require=TESTS_REQUIRE,
cmdclass={'test': PyTest},
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
diff --git a/test_natsort/test_fake_fastnumbers.py b/test_natsort/test_fake_fastnumbers.py
index 29ba9af..5aedadb 100644
--- a/test_natsort/test_fake_fastnumbers.py
+++ b/test_natsort/test_fake_fastnumbers.py
@@ -5,22 +5,31 @@ Test the fake fastnumbers module.
from natsort.fake_fastnumbers import fast_float, fast_int, isreal
-def test_fast_float():
+def test_fast_float_converts_float_string_to_float():
assert fast_float('45.8') == 45.8
assert fast_float('-45') == -45.0
assert fast_float('45.8e-2') == 45.8e-2
+
+
+def test_fast_float_leaves_string_as_is():
assert fast_float('invalid') == 'invalid'
-def test_fast_int():
+def test_fast_int_leaves_float_string_as_is():
assert fast_int('45.8') == '45.8'
+
+
+def test_fast_int_converts_int_string_to_int():
assert fast_int('-45') == -45
assert fast_int('+45') == 45
+
+
+def test_fast_int_leaves_string_as_is():
assert fast_int('invalid') == 'invalid'
-def test_isreal():
- assert not isreal('45.8')
+def test_isreal_returns_True_for_real_numbers_False_for_strings():
assert isreal(-45)
assert isreal(45.8e-2)
+ assert not isreal('45.8')
assert not isreal('invalid')
diff --git a/test_natsort/test_locale_help.py b/test_natsort/test_locale_help.py
index c654fdd..5d69408 100644
--- a/test_natsort/test_locale_help.py
+++ b/test_natsort/test_locale_help.py
@@ -9,17 +9,27 @@ from natsort.locale_help import grouper, locale_convert, use_pyicu
if use_pyicu:
from natsort.locale_help import get_pyicu_transform
from locale import getlocale
+ strxfrm = get_pyicu_transform(getlocale())
else:
from natsort.locale_help import strxfrm
-def test_grouper():
+def test_grouper_returns_letters_with_lowercase_transform_of_letter():
assert grouper('HELLO', fast_float) == 'hHeElLlLoO'
assert grouper('hello', fast_float) == 'hheelllloo'
+
+
+def test_grouper_returns_float_string_as_float():
assert grouper('45.8e-2', fast_float) == 45.8e-2
-def test_locale_convert():
+def test_locale_convert_transforms_float_string_to_float():
+ locale.setlocale(locale.LC_NUMERIC, 'en_US.UTF-8')
+ assert locale_convert('45.8', fast_float, False) == 45.8
+ locale.setlocale(locale.LC_NUMERIC, str(''))
+
+
+def test_locale_convert_transforms_nonfloat_string_to_strxfrm_string():
locale.setlocale(locale.LC_NUMERIC, 'en_US.UTF-8')
if use_pyicu:
from natsort.locale_help import get_pyicu_transform
@@ -27,18 +37,26 @@ def test_locale_convert():
strxfrm = get_pyicu_transform(getlocale())
else:
from natsort.locale_help import strxfrm
- assert locale_convert('45.8', fast_float, False) == 45.8
assert locale_convert('45,8', fast_float, False) == strxfrm('45,8')
assert locale_convert('hello', fast_float, False) == strxfrm('hello')
+ locale.setlocale(locale.LC_NUMERIC, str(''))
+
+
+def test_locale_convert_with_groupletters_transforms_nonfloat_string_to_strxfrm_string_with_grouped_letters():
+ locale.setlocale(locale.LC_NUMERIC, 'en_US.UTF-8')
+ if use_pyicu:
+ from natsort.locale_help import get_pyicu_transform
+ from locale import getlocale
+ strxfrm = get_pyicu_transform(getlocale())
+ else:
+ from natsort.locale_help import strxfrm
assert locale_convert('hello', fast_float, True) == strxfrm('hheelllloo')
assert locale_convert('45,8', fast_float, True) == strxfrm('4455,,88')
+ locale.setlocale(locale.LC_NUMERIC, str(''))
+
+def test_locale_convert_transforms_float_string_to_float_with_de_locale():
locale.setlocale(locale.LC_NUMERIC, 'de_DE.UTF-8')
- if use_pyicu:
- strxfrm = get_pyicu_transform(getlocale())
assert locale_convert('45.8', fast_float, False) == 45.8
assert locale_convert('45,8', fast_float, False) == 45.8
- assert locale_convert('hello', fast_float, False) == strxfrm('hello')
- assert locale_convert('hello', fast_float, True) == strxfrm('hheelllloo')
-
- locale.setlocale(locale.LC_NUMERIC, '')
+ locale.setlocale(locale.LC_NUMERIC, str(''))
diff --git a/test_natsort/test_main.py b/test_natsort/test_main.py
index 2323d59..0416c89 100644
--- a/test_natsort/test_main.py
+++ b/test_natsort/test_main.py
@@ -2,299 +2,199 @@
"""\
Test the natsort command-line tool functions.
"""
+from __future__ import print_function
import re
import sys
from pytest import raises
+try:
+ from unittest.mock import patch, call
+except ImportError:
+ from mock import patch, call
from natsort.__main__ import main, range_check, check_filter
from natsort.__main__ import keep_entry_range, exclude_entry
from natsort.__main__ import sort_and_print_entries
-def test_main(capsys):
-
- # Simple sorting
- sys.argv[1:] = ['num-2', 'num-6', 'num-1']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-num-6
-num-2
-num-1
-"""
-
- # Reverse order
- sys.argv[1:] = ['-r', 'num-2', 'num-6', 'num-1']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-num-1
-num-2
-num-6
-"""
-
- # Neglect '-' or '+'
- sys.argv[1:] = ['--nosign', 'num-2', 'num-6', 'num-1']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-num-1
-num-2
-num-6
-"""
-
- # Sort as digits
- sys.argv[1:] = ['-t', 'digit', 'num-2', 'num-6', 'num-1']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-num-1
-num-2
-num-6
-"""
-
- # Sort as versions (synonym for digits)
- sys.argv[1:] = ['-t', 'version', 'num-2', 'num-6', 'num-1']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-num-1
-num-2
-num-6
-"""
-
- # Exclude the number -1 and 6. Only -1 is present.
- sys.argv[1:] = ['-t', 'int', '-e', '-1', '-e', '6',
- 'num-2', 'num-6', 'num-1']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-num-6
-num-2
-"""
-
- # Exclude the number 1 and 6.
- # Both are present because we use digits/versions.
- sys.argv[1:] = ['-t', 'ver', '-e', '1', '-e', '6',
- 'num-2', 'num-6', 'num-1']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-num-2
-"""
-
- # Floats work too.
- sys.argv[1:] = ['a1.0e3', 'a5.3', 'a453.6']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-a5.3
-a453.6
-a1.0e3
-"""
-
- # Only include in the range of 1-10.
- sys.argv[1:] = ['-f', '1', '10', 'a1.0e3', 'a5.3', 'a453.6']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-a5.3
-"""
-
- # Don't include in the range of 1-10.
- sys.argv[1:] = ['-F', '1', '10', 'a1.0e3', 'a5.3', 'a453.6']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-a453.6
-a1.0e3
-"""
-
- # Include two ranges.
- sys.argv[1:] = ['-f', '1', '10', '-f', '400', '500',
- 'a1.0e3', 'a5.3', 'a453.6']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-a5.3
-a453.6
-"""
-
- # Don't account for exponential notation.
- sys.argv[1:] = ['--noexp', 'a1.0e3', 'a5.3', 'a453.6']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-a1.0e3
-a5.3
-a453.6
-"""
-
- # To sort complicated filenames you need --paths
- sys.argv[1:] = ['/Folder (1)/', '/Folder/', '/Folder (10)/']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-/Folder (1)/
-/Folder (10)/
-/Folder/
-"""
- sys.argv[1:] = ['--paths', '/Folder (1)/', '/Folder/', '/Folder (10)/']
- main()
- out, __ = capsys.readouterr()
- assert out == """\
-/Folder/
-/Folder (1)/
-/Folder (10)/
-"""
-
-
-def test_range_check():
-
- # Floats are always returned
+def test_main_passes_default_arguments_with_no_command_line_options():
+ with patch('natsort.__main__.sort_and_print_entries') as p:
+ sys.argv[1:] = ['num-2', 'num-6', 'num-1']
+ main()
+ args = p.call_args[0][1]
+ assert not args.paths
+ assert args.filter is None
+ assert args.reverse_filter is None
+ assert args.exclude is None
+ assert not args.reverse
+ assert args.number_type == 'float'
+ assert args.signed
+ assert args.exp
+ assert not args.locale
+
+
+def test_main_passes_arguments_with_all_command_line_options():
+ with patch('natsort.__main__.sort_and_print_entries') as p:
+ sys.argv[1:] = ['--paths', '--reverse', '--locale',
+ '--filter', '4', '10',
+ '--reverse-filter', '100', '110',
+ '--number-type', 'int',
+ '--nosign', '--noexp',
+ '--exclude', '34', '--exclude', '35',
+ 'num-2', 'num-6', 'num-1']
+ main()
+ args = p.call_args[0][1]
+ assert args.paths
+ assert args.filter == [(4.0, 10.0)]
+ assert args.reverse_filter == [(100.0, 110.0)]
+ assert args.exclude == [34, 35]
+ assert args.reverse
+ assert args.number_type == 'int'
+ assert not args.signed
+ assert not args.exp
+ assert args.locale
+
+
+def test_range_check_returns_range_as_is_but_with_floats():
assert range_check(10, 11) == (10.0, 11.0)
assert range_check(6.4, 30) == (6.4, 30.0)
- # Invalid ranges give a ValueErro
+
+def test_range_check_raises_ValueError_if_range_is_invalid():
with raises(ValueError) as err:
range_check(7, 2)
assert str(err.value) == 'low >= high'
-def test_check_filter():
-
- # No filter gives 'None'
+def test_check_filter_returns_None_if_filter_evaluates_to_False():
assert check_filter(()) is None
assert check_filter(False) is None
assert check_filter(None) is None
- # The check filter always returns floats
+
+def test_check_filter_converts_filter_numbers_to_floats_if_filter_is_valid():
assert check_filter([(6, 7)]) == [(6.0, 7.0)]
assert check_filter([(6, 7), (2, 8)]) == [(6.0, 7.0), (2.0, 8.0)]
- # Invalid ranges get a ValueError
+
+def test_check_filter_raises_ValueError_if_filter_is_invalid():
with raises(ValueError) as err:
check_filter([(7, 2)])
assert str(err.value) == 'Error in --filter: low >= high'
-def test_keep_entry_range():
-
- regex = re.compile(r'\d+')
- assert keep_entry_range('a56b23c89', [0], [100], int, regex)
- assert keep_entry_range('a56b23c89', [1, 88], [20, 90], int, regex)
- assert not keep_entry_range('a56b23c89', [1], [20], int, regex)
-
-
-def test_exclude_entry():
-
- # Check if the exclude value is present in the input string
- regex = re.compile(r'\d+')
- assert exclude_entry('a56b23c89', [100, 45], int, regex)
- assert not exclude_entry('a56b23c89', [23], int, regex)
-
-
-def test_sort_and_print_entries(capsys):
-
- class Args:
- """A dummy class to simulate the argparse Namespace object"""
- def __init__(self, filter, reverse_filter, exclude, as_path, reverse):
- self.filter = filter
- self.reverse_filter = reverse_filter
- self.exclude = exclude
- self.reverse = reverse
- self.number_type = 'float'
- self.signed = True
- self.exp = True
- self.paths = as_path
- self.locale = 0
-
- entries = ['tmp/a57/path2',
- 'tmp/a23/path1',
- 'tmp/a1/path1',
- 'tmp/a1 (1)/path1',
- 'tmp/a130/path1',
- 'tmp/a64/path1',
- 'tmp/a64/path2']
-
- # Just sort the paths
- sort_and_print_entries(entries, Args(None, None, False, False, False))
- out, __ = capsys.readouterr()
- assert out == """\
-tmp/a1 (1)/path1
-tmp/a1/path1
-tmp/a23/path1
-tmp/a57/path2
-tmp/a64/path1
-tmp/a64/path2
-tmp/a130/path1
-"""
-
- # You would use --paths to make them sort
- # as paths when the OS makes duplicates
- sort_and_print_entries(entries, Args(None, None, False, True, False))
- out, __ = capsys.readouterr()
- assert out == """\
-tmp/a1/path1
-tmp/a1 (1)/path1
-tmp/a23/path1
-tmp/a57/path2
-tmp/a64/path1
-tmp/a64/path2
-tmp/a130/path1
-"""
-
- # Sort the paths with numbers between 20-100
- sort_and_print_entries(entries, Args([(20, 100)], None, False,
- False, False))
- out, __ = capsys.readouterr()
- assert out == """\
-tmp/a23/path1
-tmp/a57/path2
-tmp/a64/path1
-tmp/a64/path2
-"""
-
- # Sort the paths without numbers between 20-100
- sort_and_print_entries(entries, Args(None, [(20, 100)], False,
- True, False))
- out, __ = capsys.readouterr()
- assert out == """\
-tmp/a1/path1
-tmp/a1 (1)/path1
-tmp/a130/path1
-"""
-
- # Sort the paths, excluding 23 and 130
- sort_and_print_entries(entries, Args(None, None, [23, 130], True, False))
- out, __ = capsys.readouterr()
- assert out == """\
-tmp/a1/path1
-tmp/a1 (1)/path1
-tmp/a57/path2
-tmp/a64/path1
-tmp/a64/path2
-"""
-
- # Sort the paths, excluding 2
- sort_and_print_entries(entries, Args(None, None, [2], False, False))
- out, __ = capsys.readouterr()
- assert out == """\
-tmp/a1 (1)/path1
-tmp/a1/path1
-tmp/a23/path1
-tmp/a64/path1
-tmp/a130/path1
-"""
-
- # Sort in reverse order
- sort_and_print_entries(entries, Args(None, None, False, True, True))
- out, __ = capsys.readouterr()
- assert out == """\
-tmp/a130/path1
-tmp/a64/path2
-tmp/a64/path1
-tmp/a57/path2
-tmp/a23/path1
-tmp/a1 (1)/path1
-tmp/a1/path1
-"""
+def test_keep_entry_range_returns_True_if_any_portion_of_input_is_between_the_range_bounds():
+ assert keep_entry_range('a56b23c89', [0], [100], int, re.compile(r'\d+'))
+
+
+def test_keep_entry_range_returns_True_if_any_portion_of_input_is_between_any_range_bounds():
+ assert keep_entry_range('a56b23c89', [1, 88], [20, 90], int, re.compile(r'\d+'))
+
+
+def test_keep_entry_range_returns_False_if_no_portion_of_input_is_between_the_range_bounds():
+ assert not keep_entry_range('a56b23c89', [1], [20], int, re.compile(r'\d+'))
+
+
+def test_exclude_entry_returns_True_if_exlcude_parameters_are_not_in_input():
+ assert exclude_entry('a56b23c89', [100, 45], int, re.compile(r'\d+'))
+
+
+def test_exclude_entry_returns_False_if_exlcude_parameters_are_in_input():
+ assert not exclude_entry('a56b23c89', [23], int, re.compile(r'\d+'))
+
+
+class Args:
+ """A dummy class to simulate the argparse Namespace object"""
+ def __init__(self, filter, reverse_filter, exclude, as_path, reverse):
+ self.filter = filter
+ self.reverse_filter = reverse_filter
+ self.exclude = exclude
+ self.reverse = reverse
+ self.number_type = 'float'
+ self.signed = True
+ self.exp = True
+ self.paths = as_path
+ self.locale = 0
+
+entries = ['tmp/a57/path2',
+ 'tmp/a23/path1',
+ 'tmp/a1/path1',
+ 'tmp/a1 (1)/path1',
+ 'tmp/a130/path1',
+ 'tmp/a64/path1',
+ 'tmp/a64/path2']
+
+mock_print = '__builtin__.print' if sys.version[0] == '2' else 'builtins.print'
+
+
+def test_sort_and_print_entries_uses_default_algorithm_with_all_options_false():
+ with patch(mock_print) as p:
+ # tmp/a1 (1)/path1
+ # tmp/a1/path1
+ # tmp/a23/path1
+ # tmp/a57/path2
+ # tmp/a64/path1
+ # tmp/a64/path2
+ # tmp/a130/path1
+ sort_and_print_entries(entries, Args(None, None, False, False, False))
+ e = [call(entries[i]) for i in [3, 2, 1, 0, 5, 6, 4]]
+ p.assert_has_calls(e)
+
+
+def test_sort_and_print_entries_uses_PATH_algorithm_with_path_option_true_to_properly_sort_OS_generated_path_names():
+ with patch(mock_print) as p:
+ # tmp/a1/path1
+ # tmp/a1 (1)/path1
+ # tmp/a23/path1
+ # tmp/a57/path2
+ # tmp/a64/path1
+ # tmp/a64/path2
+ # tmp/a130/path1
+ sort_and_print_entries(entries, Args(None, None, False, True, False))
+ e = [call(entries[i]) for i in [2, 3, 1, 0, 5, 6, 4]]
+ p.assert_has_calls(e)
+
+
+def test_sort_and_print_entries_keeps_only_paths_between_of_20_to_100_with_filter_option():
+ with patch(mock_print) as p:
+ # tmp/a23/path1
+ # tmp/a57/path2
+ # tmp/a64/path1
+ # tmp/a64/path2
+ sort_and_print_entries(entries, Args([(20, 100)], None, False, False, False))
+ e = [call(entries[i]) for i in [1, 0, 5, 6]]
+ p.assert_has_calls(e)
+
+
+def test_sort_and_print_entries_excludes_paths_between_of_20_to_100_with_reverse_filter_option():
+ with patch(mock_print) as p:
+ # tmp/a1/path1
+ # tmp/a1 (1)/path1
+ # tmp/a130/path1
+ sort_and_print_entries(entries, Args(None, [(20, 100)], False, True, False))
+ e = [call(entries[i]) for i in [2, 3, 4]]
+ p.assert_has_calls(e)
+
+
+def test_sort_and_print_entries_excludes_paths_23_or_130_with_exclude_option_list():
+ with patch(mock_print) as p:
+ # tmp/a1/path1
+ # tmp/a1 (1)/path1
+ # tmp/a57/path2
+ # tmp/a64/path1
+ # tmp/a64/path2
+ sort_and_print_entries(entries, Args(None, None, [23, 130], True, False))
+ e = [call(entries[i]) for i in [2, 3, 0, 5, 6]]
+ p.assert_has_calls(e)
+
+
+def test_sort_and_print_entries_reverses_order_with_reverse_option():
+ with patch(mock_print) as p:
+ # tmp/a130/path1
+ # tmp/a64/path2
+ # tmp/a64/path1
+ # tmp/a57/path2
+ # tmp/a23/path1
+ # tmp/a1 (1)/path1
+ # tmp/a1/path1
+ sort_and_print_entries(entries, Args(None, None, False, True, True))
+ e = [call(entries[i]) for i in reversed([2, 3, 1, 0, 5, 6, 4])]
+ p.assert_has_calls(e)
diff --git a/test_natsort/test_natsort.py b/test_natsort/test_natsort.py
index d89730c..cb81663 100644
--- a/test_natsort/test_natsort.py
+++ b/test_natsort/test_natsort.py
@@ -3,7 +3,7 @@
Here are a collection of examples of how this module can be used.
See the README or the natsort homepage for more details.
"""
-from __future__ import unicode_literals
+from __future__ import unicode_literals, print_function
import warnings
import locale
from operator import itemgetter
@@ -13,8 +13,7 @@ from natsort import humansorted, index_humansorted, natsort_keygen, order_by_ind
from natsort.utils import _natsort_key
-def test_natsort_key_public():
-
+def test_natsort_key_public_raises_DeprecationWarning_when_called():
# Identical to _natsort_key
# But it raises a deprecation warning
with warnings.catch_warnings(record=True) as w:
@@ -22,9 +21,6 @@ def test_natsort_key_public():
assert natsort_key('a-5.034e2') == _natsort_key('a-5.034e2', key=None, alg=ns.F)
assert len(w) == 1
assert "natsort_key is deprecated as of 3.4.0, please use natsort_keygen" in str(w[-1].message)
- assert natsort_key('a-5.034e2', number_type=float, signed=False, exp=False) == _natsort_key('a-5.034e2', key=None, alg=ns.F | ns.U | ns.N)
- assert natsort_key('a-5.034e2', alg=ns.F | ns.U | ns.N) == _natsort_key('a-5.034e2', key=None, alg=ns.F | ns.U | ns.N)
-
# It is called for each element in a list when sorting
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
@@ -33,54 +29,74 @@ def test_natsort_key_public():
assert len(w) == 7
-def test_natsort_keygen():
+def test_natsort_keygen_returns_natsort_key_with_alg_option():
+ a = 'a-5.034e1'
+ assert natsort_keygen()(a) == _natsort_key(a, None, ns.F)
+ assert natsort_keygen(alg=ns.I | ns.U)(a) == _natsort_key(a, None, ns.I | ns.U)
+
- # Creates equivalent natsort keys
+def test_natsort_keygen_with_key_returns_same_result_as_nested_lambda_with_bare_natsort_key():
a = 'a-5.034e1'
- assert natsort_keygen()(a) == _natsort_key(a, key=None, alg=ns.F)
- assert natsort_keygen(alg=ns.UNSIGNED)(a) == _natsort_key(a, key=None, alg=ns.U)
- assert natsort_keygen(alg=ns.NOEXP)(a) == _natsort_key(a, key=None, alg=ns.N)
- assert natsort_keygen(alg=ns.U | ns.N)(a) == _natsort_key(a, key=None, alg=ns.U | ns.N)
- assert natsort_keygen(alg=ns.INT)(a) == _natsort_key(a, key=None, alg=ns.INT)
- assert natsort_keygen(alg=ns.I | ns.U)(a) == _natsort_key(a, key=None, alg=ns.I | ns.U)
- assert natsort_keygen(alg=ns.VERSION)(a) == _natsort_key(a, key=None, alg=ns.V)
- assert natsort_keygen(alg=ns.PATH)(a) == _natsort_key(a, key=None, alg=ns.PATH)
-
- # Custom keys are more straightforward with keygen
f1 = natsort_keygen(key=lambda x: x.upper())
- f2 = lambda x: _natsort_key(x, key=lambda y: y.upper(), alg=ns.F)
+
+ def f2(x):
+ return _natsort_key(x, lambda y: y.upper(), ns.F)
assert f1(a) == f2(a)
- # It also makes sorting lists in-place easier (no lambdas!)
+
+def test_natsort_keygen_returns_key_that_can_be_used_to_sort_list_in_place_with_same_result_as_natsorted():
a = ['a50', 'a51.', 'a50.31', 'a50.4', 'a5.034e1', 'a50.300']
b = a[:]
a.sort(key=natsort_keygen(alg=ns.I))
assert a == natsorted(b, alg=ns.I)
-def test_natsorted():
-
- # Basic usage
+def test_natsorted_returns_strings_with_numbers_in_ascending_order():
a = ['a2', 'a5', 'a9', 'a1', 'a4', 'a10', 'a6']
assert natsorted(a) == ['a1', 'a2', 'a4', 'a5', 'a6', 'a9', 'a10']
- # Number types
+
+def test_natsorted_returns_list_of_numbers_sorted_as_signed_floats_with_exponents():
+ a = ['a50', 'a51.', 'a50.31', 'a50.4', 'a5.034e1', 'a50.300']
+ assert natsorted(a) == ['a50', 'a50.300', 'a50.31', 'a5.034e1', 'a50.4', 'a51.']
+
+
+def test_natsorted_returns_list_of_numbers_sorted_as_signed_floats_without_exponents_with_NOEXP_option():
a = ['a50', 'a51.', 'a50.31', 'a50.4', 'a5.034e1', 'a50.300']
- assert natsorted(a) == ['a50', 'a50.300', 'a50.31', 'a5.034e1', 'a50.4', 'a51.']
assert natsorted(a, alg=ns.NOEXP | ns.FLOAT) == ['a5.034e1', 'a50', 'a50.300', 'a50.31', 'a50.4', 'a51.']
- assert natsorted(a, alg=ns.INT) == ['a5.034e1', 'a50', 'a50.4', 'a50.31', 'a50.300', 'a51.']
- assert natsorted(a, alg=ns.DIGIT) == ['a5.034e1', 'a50', 'a50.4', 'a50.31', 'a50.300', 'a51.']
- # Signed option
+
+def test_natsorted_returns_list_of_numbers_sorted_as_signed_ints_with_INT_option():
+ a = ['a50', 'a51.', 'a50.31', 'a50.4', 'a5.034e1', 'a50.300']
+ assert natsorted(a, alg=ns.INT) == ['a5.034e1', 'a50', 'a50.4', 'a50.31', 'a50.300', 'a51.']
+
+
+def test_natsorted_returns_list_of_numbers_sorted_as_unsigned_ints_with_DIGIT_option():
+ a = ['a50', 'a51.', 'a50.31', 'a50.4', 'a5.034e1', 'a50.300']
+ assert natsorted(a, alg=ns.DIGIT) == ['a5.034e1', 'a50', 'a50.4', 'a50.31', 'a50.300', 'a51.']
+
+
+def test_natsorted_returns_list_of_numbers_sorted_without_accounting_for_sign_with_UNSIGNED_option():
a = ['a-5', 'a7', 'a+2']
- assert natsorted(a) == ['a-5', 'a+2', 'a7']
assert natsorted(a, alg=ns.UNSIGNED) == ['a7', 'a+2', 'a-5']
- # Number type == None
+
+def test_natsorted_returns_list_of_numbers_sorted_accounting_for_sign_without_UNSIGNED_option():
+ a = ['a-5', 'a7', 'a+2']
+ assert natsorted(a) == ['a-5', 'a+2', 'a7']
+
+
+def test_natsorted_returns_list_of_version_numbers_improperly_sorted_without_VERSION_option():
a = ['1.9.9a', '1.11', '1.9.9b', '1.11.4', '1.10.1']
- assert natsorted(a) == ['1.10.1', '1.11', '1.11.4', '1.9.9a', '1.9.9b']
- assert natsorted(a, alg=ns.DIGIT) == ['1.9.9a', '1.9.9b', '1.10.1', '1.11', '1.11.4']
+ assert natsorted(a) == ['1.10.1', '1.11', '1.11.4', '1.9.9a', '1.9.9b']
+
+def test_natsorted_returns_sorted_list_of_version_numbers_with_VERSION_option():
+ a = ['1.9.9a', '1.11', '1.9.9b', '1.11.4', '1.10.1']
+ assert natsorted(a, alg=ns.VERSION) == ['1.9.9a', '1.9.9b', '1.10.1', '1.11', '1.11.4']
+
+
+def test_natsorted_returns_sorted_list_with_mixed_type_input_and_does_not_raise_TypeError_on_Python3():
# You can mix types with natsorted. This can get around the new
# 'unorderable types' issue with Python 3.
a = [6, 4.5, '7', '2.5', 'a']
@@ -88,28 +104,29 @@ def test_natsorted():
a = [46, '5a5b2', 'af5', '5a5-4']
assert natsorted(a) == ['5a5-4', '5a5b2', 46, 'af5']
- # You still can't sort non-iterables
+
+def test_natsorted_raises_ValueError_for_non_iterable_input():
with raises(TypeError) as err:
natsorted(100)
assert str(err.value) == "'int' object is not iterable"
- # natsort will recursively descend into lists of lists so you can
- # sort by the sublist contents.
+
+def test_natsorted_recursivley_applies_key_to_nested_lists_to_return_sorted_nested_list():
data = [['a1', 'a5'], ['a1', 'a40'], ['a10', 'a1'], ['a2', 'a5']]
- assert natsorted(data) == [['a1', 'a5'], ['a1', 'a40'],
- ['a2', 'a5'], ['a10', 'a1']]
+ assert natsorted(data) == [['a1', 'a5'], ['a1', 'a40'], ['a2', 'a5'], ['a10', 'a1']]
- # You can pass a key to do non-standard sorting rules
+
+def test_natsorted_applies_key_to_each_list_element_before_sorting_list():
b = [('a', 'num3'), ('b', 'num5'), ('c', 'num2')]
- c = [('c', 'num2'), ('a', 'num3'), ('b', 'num5')]
- assert natsorted(b, key=itemgetter(1)) == c
+ assert natsorted(b, key=itemgetter(1)) == [('c', 'num2'), ('a', 'num3'), ('b', 'num5')]
+
- # Reversing the order is allowed
+def test_natsorted_returns_list_in_reversed_order_with_reverse_option():
a = ['a50', 'a51.', 'a50.31', 'a50.4', 'a5.034e1', 'a50.300']
- b = ['a50', 'a50.300', 'a50.31', 'a5.034e1', 'a50.4', 'a51.']
- assert natsorted(a, reverse=True) == b[::-1]
+ assert natsorted(a, reverse=True) == natsorted(a)[::-1]
+
- # Sorting paths just got easier
+def test_natsorted_sorts_OS_generated_paths_incorrectly_without_PATH_option():
a = ['/p/Folder (10)/file.tar.gz',
'/p/Folder/file.tar.gz',
'/p/Folder (1)/file (1).tar.gz',
@@ -118,133 +135,151 @@ def test_natsorted():
'/p/Folder (1)/file.tar.gz',
'/p/Folder (10)/file.tar.gz',
'/p/Folder/file.tar.gz']
+
+
+def test_natsorted_sorts_OS_generated_paths_correctly_with_PATH_option():
+ a = ['/p/Folder (10)/file.tar.gz',
+ '/p/Folder/file.tar.gz',
+ '/p/Folder (1)/file (1).tar.gz',
+ '/p/Folder (1)/file.tar.gz']
assert natsorted(a, alg=ns.PATH) == ['/p/Folder/file.tar.gz',
'/p/Folder (1)/file.tar.gz',
'/p/Folder (1)/file (1).tar.gz',
'/p/Folder (10)/file.tar.gz']
+
+def test_natsorted_can_handle_sorting_paths_and_numbers_with_PATH():
# You can sort paths and numbers, not that you'd want to
a = ['/Folder (9)/file.exe', 43]
assert natsorted(a, alg=ns.PATH) == [43, '/Folder (9)/file.exe']
- # You can modify how case is interpreted in your sorting.
+
+def test_natsorted_returns_results_in_ASCII_order_with_no_case_options():
a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
assert natsorted(a) == ['Apple', 'Banana', 'Corn', 'apple', 'banana', 'corn']
+
+
+def test_natsorted_returns_results_sorted_by_lowercase_ASCII_order_with_IGNORECASE():
+ a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
assert natsorted(a, alg=ns.IGNORECASE) == ['Apple', 'apple', 'Banana', 'banana', 'corn', 'Corn']
+
+
+def test_natsorted_returns_results_in_ASCII_order_but_with_lowercase_letters_first_with_LOWERCASEFIRST():
+ a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
assert natsorted(a, alg=ns.LOWERCASEFIRST) == ['apple', 'banana', 'corn', 'Apple', 'Banana', 'Corn']
+
+
+def test_natsorted_returns_results_with_uppercase_and_lowercase_letters_grouped_together_with_GROUPLETTERS():
+ a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
assert natsorted(a, alg=ns.GROUPLETTERS) == ['Apple', 'apple', 'Banana', 'banana', 'Corn', 'corn']
+
+
+def test_natsorted_returns_results_in_natural_order_with_GROUPLETTERS_and_LOWERCASEFIRST():
+ a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
assert natsorted(a, alg=ns.G | ns.LF) == ['apple', 'Apple', 'banana', 'Banana', 'corn', 'Corn']
+
+def test_natsorted_places_uppercase_letters_before_lowercase_letters_for_nested_input():
b = [('A5', 'a6'), ('a3', 'a1')]
assert natsorted(b) == [('A5', 'a6'), ('a3', 'a1')]
+
+
+def test_natsorted_with_LOWERCASEFIRST_places_lowercase_letters_before_uppercase_letters_for_nested_input():
+ b = [('A5', 'a6'), ('a3', 'a1')]
assert natsorted(b, alg=ns.LOWERCASEFIRST) == [('a3', 'a1'), ('A5', 'a6')]
+
+
+def test_natsorted_with_IGNORECASE_sorts_without_regard_to_case_for_nested_input():
+ b = [('A5', 'a6'), ('a3', 'a1')]
assert natsorted(b, alg=ns.IGNORECASE) == [('a3', 'a1'), ('A5', 'a6')]
- # You can also do locale-aware sorting
+
+def test_natsorted_with_LOCALE_returns_results_sorted_by_lowercase_first_and_grouped_letters():
+ a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
locale.setlocale(locale.LC_ALL, str('en_US.UTF-8'))
assert natsorted(a, alg=ns.LOCALE) == ['apple', 'Apple', 'banana', 'Banana', 'corn', 'Corn']
+ locale.setlocale(locale.LC_ALL, str(''))
+
+
+def test_natsorted_with_LOCALE_and_en_setting_returns_results_sorted_by_en_language():
+ locale.setlocale(locale.LC_ALL, str('en_US.UTF-8'))
a = ['c', 'ä', 'b', 'a5,6', 'a5,50']
assert natsorted(a, alg=ns.LOCALE) == ['a5,6', 'a5,50', 'ä', 'b', 'c']
+ locale.setlocale(locale.LC_ALL, str(''))
+
+def test_natsorted_with_LOCALE_and_de_setting_returns_results_sorted_by_de_language():
locale.setlocale(locale.LC_ALL, str('de_DE.UTF-8'))
+ a = ['c', 'ä', 'b', 'a5,6', 'a5,50']
assert natsorted(a, alg=ns.LOCALE) == ['a5,50', 'a5,6', 'ä', 'b', 'c']
locale.setlocale(locale.LC_ALL, str(''))
-def test_versorted():
-
+def test_versorted_returns_results_identical_to_natsorted_with_VERSION():
a = ['1.9.9a', '1.11', '1.9.9b', '1.11.4', '1.10.1']
assert versorted(a) == natsorted(a, alg=ns.VERSION)
- assert versorted(a, reverse=True) == versorted(a)[::-1]
- a = [('a', '1.9.9a'), ('a', '1.11'), ('a', '1.9.9b'),
- ('a', '1.11.4'), ('a', '1.10.1')]
- assert versorted(a) == [('a', '1.9.9a'), ('a', '1.9.9b'), ('a', '1.10.1'),
- ('a', '1.11'), ('a', '1.11.4')]
-
- # Sorting paths just got easier
- a = ['/p/Folder (10)/file1.1.0.tar.gz',
- '/p/Folder/file1.1.0.tar.gz',
- '/p/Folder (1)/file1.1.0 (1).tar.gz',
- '/p/Folder (1)/file1.1.0.tar.gz']
- assert versorted(a) == ['/p/Folder (1)/file1.1.0 (1).tar.gz',
- '/p/Folder (1)/file1.1.0.tar.gz',
- '/p/Folder (10)/file1.1.0.tar.gz',
- '/p/Folder/file1.1.0.tar.gz']
- assert versorted(a, alg=ns.PATH) == ['/p/Folder/file1.1.0.tar.gz',
- '/p/Folder (1)/file1.1.0.tar.gz',
- '/p/Folder (1)/file1.1.0 (1).tar.gz',
- '/p/Folder (10)/file1.1.0.tar.gz']
-
-
-def test_humansorted():
+
+def test_humansorted_returns_results_identical_to_natsorted_with_LOCALE():
a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
- assert humansorted(a) == ['apple', 'Apple', 'banana', 'Banana', 'corn', 'Corn']
assert humansorted(a) == natsorted(a, alg=ns.LOCALE)
- assert humansorted(a, reverse=True) == humansorted(a)[::-1]
-
-def test_index_natsorted():
- # Return the indexes of how the iterable would be sorted.
+def test_index_natsorted_returns_integer_list_of_sort_order_for_input_list():
a = ['num3', 'num5', 'num2']
b = ['foo', 'bar', 'baz']
index = index_natsorted(a)
assert index == [2, 0, 1]
assert [a[i] for i in index] == ['num2', 'num3', 'num5']
assert [b[i] for i in index] == ['baz', 'foo', 'bar']
+
+
+def test_index_natsorted_returns_reversed_integer_list_of_sort_order_for_input_list_with_reverse_option():
+ a = ['num3', 'num5', 'num2']
assert index_natsorted(a, reverse=True) == [1, 0, 2]
- # It accepts a key argument.
+
+def test_index_natsorted_applies_key_function_before_sorting():
c = [('a', 'num3'), ('b', 'num5'), ('c', 'num2')]
assert index_natsorted(c, key=itemgetter(1)) == [2, 0, 1]
- # It can avoid "unorderable types" on Python 3
+
+def test_index_natsorted_handles_unorderable_types_error_on_Python3():
a = [46, '5a5b2', 'af5', '5a5-4']
assert index_natsorted(a) == [3, 1, 0, 2]
- # It can sort lists of lists.
+
+def test_index_natsorted_returns_integer_list_of_nested_input_list():
data = [['a1', 'a5'], ['a1', 'a40'], ['a10', 'a1'], ['a2', 'a5']]
assert index_natsorted(data) == [0, 1, 3, 2]
- # It can sort paths too
+
+def test_index_natsorted_returns_integer_list_in_proper_order_for_input_paths_with_PATH():
a = ['/p/Folder (10)/',
'/p/Folder/',
'/p/Folder (1)/']
assert index_natsorted(a, alg=ns.PATH) == [1, 2, 0]
-def test_index_versorted():
-
+def test_index_versorted_returns_results_identical_to_index_natsorted_with_VERSION():
a = ['1.9.9a', '1.11', '1.9.9b', '1.11.4', '1.10.1']
assert index_versorted(a) == index_natsorted(a, alg=ns.VERSION)
- assert index_versorted(a, reverse=True) == index_versorted(a)[::-1]
- a = [('a', '1.9.9a'), ('a', '1.11'), ('a', '1.9.9b'),
- ('a', '1.11.4'), ('a', '1.10.1')]
- assert index_versorted(a) == [0, 2, 4, 1, 3]
-
- # It can sort paths too
- a = ['/p/Folder (10)/file1.1.0.tar.gz',
- '/p/Folder/file1.1.0.tar.gz',
- '/p/Folder (1)/file1.1.0 (1).tar.gz',
- '/p/Folder (1)/file1.1.0.tar.gz']
- assert index_versorted(a, alg=ns.PATH) == [1, 3, 2, 0]
-
-def test_index_humansorted():
+def test_index_humansorted_returns_results_identical_to_index_natsorted_with_LOCALE():
a = ['Apple', 'corn', 'Corn', 'Banana', 'apple', 'banana']
- assert index_humansorted(a) == [4, 0, 5, 3, 1, 2]
assert index_humansorted(a) == index_natsorted(a, alg=ns.LOCALE)
- assert index_humansorted(a, reverse=True) == index_humansorted(a)[::-1]
-def test_order_by_index():
-
- # Return the indexes of how the iterable would be sorted.
+def test_order_by_index_sorts_list_according_to_order_of_integer_list():
a = ['num3', 'num5', 'num2']
index = [2, 0, 1]
assert order_by_index(a, index) == ['num2', 'num3', 'num5']
assert order_by_index(a, index) == [a[i] for i in index]
+
+
+def test_order_by_index_returns_generator_with_iter_True():
+ a = ['num3', 'num5', 'num2']
+ index = [2, 0, 1]
assert order_by_index(a, index, True) != [a[i] for i in index]
assert list(order_by_index(a, index, True)) == [a[i] for i in index]
diff --git a/test_natsort/test_utils.py b/test_natsort/test_utils.py
index c5e3172..8d61c1a 100644
--- a/test_natsort/test_utils.py
+++ b/test_natsort/test_utils.py
@@ -5,7 +5,7 @@ import locale
from operator import itemgetter
from pytest import raises
from natsort.ns_enum import ns
-from natsort.utils import _input_parser, _py3_safe, _natsort_key, _args_to_enum
+from natsort.utils import _number_extracter, _py3_safe, _natsort_key, _args_to_enum
from natsort.utils import _float_sign_exp_re, _float_nosign_exp_re, _float_sign_noexp_re
from natsort.utils import _float_nosign_noexp_re, _int_nosign_re, _int_sign_re
from natsort.locale_help import use_pyicu
@@ -23,57 +23,121 @@ else:
has_pathlib = True
-def test_args_to_enum():
-
+def test_args_to_enum_converts_signed_exp_float_to_ns_F():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(float, True, True, False, False) == ns.F
+
+
+def test_args_to_enum_converts_signed_noexp_float_to_ns_FN():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(float, True, False, False, False) == ns.F | ns.N
+
+
+def test_args_to_enum_converts_unsigned_exp_float_to_ns_FU():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(float, False, True, False, False) == ns.F | ns.U
+
+
+def test_args_to_enum_converts_unsigned_unexp_float_to_ns_FNU():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(float, False, False, False, False) == ns.F | ns.U | ns.N
+
+
+def test_args_to_enum_converts_signed_exp_float_and_path_and_py3safe_to_ns_FPT():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(float, True, True, True, True) == ns.F | ns.P | ns.T
+
+
+def test_args_to_enum_converts_singed_int_and_path_to_ns_IP():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(int, True, True, True, False) == ns.I | ns.P
+
+
+def test_args_to_enum_converts_unsigned_int_and_py3safe_to_ns_IUT():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(int, False, True, False, True) == ns.I | ns.U | ns.T
+
+
+def test_args_to_enum_converts_None_to_ns_IU():
+ # number_type, signed, exp, as_path, py3_safe
assert _args_to_enum(None, True, True, False, False) == ns.I | ns.U
+# fttt = (fast_float, True, True, True)
+# fttf = (fast_float, True, True, False)
+ftft = (fast_float, True, False, True)
+ftff = (fast_float, True, False, False)
+# fftt = (fast_float, False, True, True)
+ffft = (fast_float, False, False, True)
+# fftf = (fast_float, False, True, False)
+ffff = (fast_float, False, False, False)
+ittt = (fast_int, True, True, True)
+ittf = (fast_int, True, True, False)
+itft = (fast_int, True, False, True)
+itff = (fast_int, True, False, False)
+# iftt = (fast_int, False, True, True)
+ifft = (fast_int, False, False, True)
+# iftf = (fast_int, False, True, False)
+ifff = (fast_int, False, False, False)
+
+
+def test_number_extracter_raises_TypeError_if_given_a_number():
+ with raises(TypeError):
+ assert _number_extracter(50.0, _float_sign_exp_re, *ffff)
+
+
+def test_number_extracter_includes_plus_sign_and_exponent_in_float_definition_for_signed_exp_floats():
+ assert _number_extracter('a5+5.034e-1', _float_sign_exp_re, *ffff) == ['a', 5.0, 0.5034]
+
+
+def test_number_extracter_excludes_plus_sign_in_float_definition_but_includes_exponent_for_unsigned_exp_floats():
+ assert _number_extracter('a5+5.034e-1', _float_nosign_exp_re, *ffff) == ['a', 5.0, '+', 0.5034]
+
+
+def test_number_extracter_includes_plus_and_minus_sign_in_float_definition_but_excludes_exponent_for_signed_noexp_floats():
+ assert _number_extracter('a5+5.034e-1', _float_sign_noexp_re, *ffff) == ['a', 5.0, 5.034, 'e', -1.0]
+
+
+def test_number_extracter_excludes_plus_sign_and_exponent_in_float_definition_for_unsigned_noexp_floats():
+ assert _number_extracter('a5+5.034e-1', _float_nosign_noexp_re, *ffff) == ['a', 5.0, '+', 5.034, 'e-', 1.0]
-def test_input_parser():
-
- # fttt = (fast_float, True, True, True)
- # fttf = (fast_float, True, True, False)
- ftft = (fast_float, True, False, True)
- ftff = (fast_float, True, False, False)
- # fftt = (fast_float, False, True, True)
- # ffft = (fast_float, False, False, True)
- # fftf = (fast_float, False, True, False)
- ffff = (fast_float, False, False, False)
- ittt = (fast_int, True, True, True)
- ittf = (fast_int, True, True, False)
- itft = (fast_int, True, False, True)
- itff = (fast_int, True, False, False)
- # iftt = (fast_int, False, True, True)
- # ifft = (fast_int, False, False, True)
- # iftf = (fast_int, False, True, False)
- ifff = (fast_int, False, False, False)
-
- assert _input_parser('a5+5.034e-1', _float_sign_exp_re, *ffff) == ['a', 5.0, 0.5034]
- assert _input_parser('a5+5.034e-1', _float_nosign_exp_re, *ffff) == ['a', 5.0, '+', 0.5034]
- assert _input_parser('a5+5.034e-1', _float_sign_noexp_re, *ffff) == ['a', 5.0, 5.034, 'e', -1.0]
- assert _input_parser('a5+5.034e-1', _float_nosign_noexp_re, *ffff) == ['a', 5.0, '+', 5.034, 'e-', 1.0]
- assert _input_parser('a5+5.034e-1', _int_nosign_re, *ifff) == ['a', 5, '+', 5, '.', 34, 'e-', 1]
- assert _input_parser('a5+5.034e-1', _int_sign_re, *ifff) == ['a', 5, 5, '.', 34, 'e', -1]
-
- assert _input_parser('a5+5.034e-1', _float_sign_exp_re, *ftff) == ['a', 5.0, '', 0.5034]
- assert _input_parser('a5+5.034e-1', _float_nosign_exp_re, *ftff) == ['a', 5.0, '+', 0.5034]
- assert _input_parser('a5+5.034e-1', _float_sign_noexp_re, *ftff) == ['a', 5.0, '', 5.034, 'e', -1.0]
- assert _input_parser('a5+5.034e-1', _float_nosign_noexp_re, *ftff) == ['a', 5.0, '+', 5.034, 'e-', 1.0]
- assert _input_parser('a5+5.034e-1', _int_nosign_re, *itff) == ['a', 5, '+', 5, '.', 34, 'e-', 1]
- assert _input_parser('a5+5.034e-1', _int_sign_re, *itff) == ['a', 5, '', 5, '.', 34, 'e', -1]
-
- assert _input_parser('6a5+5.034e-1', _float_sign_exp_re, *ffff) == ['', 6.0, 'a', 5.0, 0.5034]
- assert _input_parser('6a5+5.034e-1', _float_sign_exp_re, *ftff) == ['', 6.0, 'a', 5.0, '', 0.5034]
-
- assert _input_parser('A5+5.034E-1', _float_sign_exp_re, *ftft) == ['aA', 5.0, '', 0.5034]
- assert _input_parser('A5+5.034E-1', _int_nosign_re, *itft) == ['aA', 5, '++', 5, '..', 34, 'eE--', 1]
+def test_number_extracter_excludes_plus_and_minus_sign_in_int_definition_for_unsigned_ints():
+ assert _number_extracter('a5+5.034e-1', _int_nosign_re, *ifff) == ['a', 5, '+', 5, '.', 34, 'e-', 1]
+
+
+def test_number_extracter_includes_plus_and_minus_sign_in_int_definition_for_signed_ints():
+ assert _number_extracter('a5+5.034e-1', _int_sign_re, *ifff) == ['a', 5, 5, '.', 34, 'e', -1]
+
+
+def test_number_extracter_inserts_empty_string_between_floats_for_py3safe_option():
+ assert _number_extracter('a5+5.034e-1', _float_sign_exp_re, *ftff) == ['a', 5.0, '', 0.5034]
+
+
+def test_number_extracter_inserts_empty_string_between_ints_for_py3safe_option():
+ assert _number_extracter('a5+5.034e-1', _int_sign_re, *itff) == ['a', 5, '', 5, '.', 34, 'e', -1]
+
+
+def test_number_extracter_inserts_no_empty_string_py3safe_option_because_no_numbers_are_adjascent():
+ assert _number_extracter('a5+5.034e-1', _float_nosign_exp_re, *ftff) == ['a', 5.0, '+', 0.5034]
+
+
+def test_number_extracter_adds_leading_empty_string_if_input_begins_with_a_number():
+ assert _number_extracter('6a5+5.034e-1', _float_sign_exp_re, *ffff) == ['', 6.0, 'a', 5.0, 0.5034]
+
+
+def test_number_extracter_adds_leading_empty_string_if_input_begins_with_a_number_and_empty_string_between_numbers_for_py3safe():
+ assert _number_extracter('6a5+5.034e-1', _float_sign_exp_re, *ftff) == ['', 6.0, 'a', 5.0, '', 0.5034]
+
+
+def test_number_extracter_doubles_letters_with_lowercase_version_with_groupletters_for_float():
+ assert _number_extracter('A5+5.034E-1', _float_sign_exp_re, *ffft) == ['aA', 5.0, 0.5034]
+
+
+def test_number_extracter_doubles_letters_with_lowercase_version_with_groupletters_for_int():
+ assert _number_extracter('A5+5.034E-1', _int_nosign_re, *ifft) == ['aA', 5, '++', 5, '..', 34, 'eE--', 1]
+
+
+def test_number_extracter_extracts_numbers_and_strxfrms_strings_with_use_locale():
locale.setlocale(locale.LC_NUMERIC, str('en_US.UTF-8'))
if use_pyicu:
from natsort.locale_help import get_pyicu_transform
@@ -81,75 +145,157 @@ def test_input_parser():
strxfrm = get_pyicu_transform(getlocale())
else:
from natsort.locale_help import strxfrm
- assert _input_parser('A5+5.034E-1', _int_nosign_re, *ittf) == [strxfrm('A'), 5, strxfrm('+'), 5, strxfrm('.'), 34, strxfrm('E-'), 1]
- assert _input_parser('A5+5.034E-1', _int_nosign_re, *ittt) == [strxfrm('aA'), 5, strxfrm('++'), 5, strxfrm('..'), 34, strxfrm('eE--'), 1]
+ assert _number_extracter('A5+5.034E-1', _int_nosign_re, *ittf) == [strxfrm('A'), 5, strxfrm('+'), 5, strxfrm('.'), 34, strxfrm('E-'), 1]
locale.setlocale(locale.LC_NUMERIC, str(''))
-def test_py3_safe():
+def test_number_extracter_extracts_numbers_and_strxfrms_letter_doubled_strings_with_use_locale_and_groupletters():
+ locale.setlocale(locale.LC_NUMERIC, str('en_US.UTF-8'))
+ if use_pyicu:
+ from natsort.locale_help import get_pyicu_transform
+ from locale import getlocale
+ strxfrm = get_pyicu_transform(getlocale())
+ else:
+ from natsort.locale_help import strxfrm
+ assert _number_extracter('A5+5.034E-1', _int_nosign_re, *ittt) == [strxfrm('aA'), 5, strxfrm('++'), 5, strxfrm('..'), 34, strxfrm('eE--'), 1]
+ locale.setlocale(locale.LC_NUMERIC, str(''))
+
+def test_py3_safe_does_nothing_if_no_numbers():
assert _py3_safe(['a', 'b', 'c']) == ['a', 'b', 'c']
assert _py3_safe(['a']) == ['a']
+
+
+def test_py3_safe_does_nothing_if_only_one_number():
assert _py3_safe(['a', 5]) == ['a', 5]
+
+
+def test_py3_safe_inserts_empty_string_between_two_numbers():
assert _py3_safe([5, 9]) == [5, '', 9]
-def test_natsort_key_private():
+def test__natsort_key_with_float_splits_input_into_string_and_signed_float_with_exponent():
+ assert ns.F == ns.FLOAT
+ assert _natsort_key('a-5.034e2', None, ns.F) == ('a', -503.4)
+
+
+def test__natsort_key_with_float_and_noexp_splits_input_into_string_and_signed_float_without_exponent():
+ assert _natsort_key('a-5.034e2', None, ns.FLOAT | ns.NOEXP) == ('a', -5.034, 'e', 2.0)
+ # Default is to split on floats.
+ assert _natsort_key('a-5.034e2', None, ns.NOEXP) == ('a', -5.034, 'e', 2.0)
+
+
+def test__natsort_key_with_float_and_unsigned_splits_input_into_string_and_unsigned_float():
+ assert _natsort_key('a-5.034e2', None, ns.UNSIGNED) == ('a-', 503.4)
+
- # The below illustrates how the key works, and how the different options affect sorting.
- assert _natsort_key('a-5.034e2', key=None, alg=ns.F) == ('a', -503.4)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.FLOAT) == ('a', -503.4)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.FLOAT | ns.NOEXP) == ('a', -5.034, 'e', 2.0)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.NOEXP) == ('a', -5.034, 'e', 2.0)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.UNSIGNED) == ('a-', 503.4)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.UNSIGNED | ns.NOEXP) == ('a-', 5.034, 'e', 2.0)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.INT) == ('a', -5, '.', 34, 'e', 2)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.INT | ns.NOEXP) == ('a', -5, '.', 34, 'e', 2)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.INT | ns.UNSIGNED) == ('a-', 5, '.', 34, 'e', 2)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.VERSION) == _natsort_key('a-5.034e2', key=None, alg=ns.INT | ns.UNSIGNED)
- assert _natsort_key('a-5.034e2', key=None, alg=ns.DIGIT) == _natsort_key('a-5.034e2', key=None, alg=ns.VERSION)
- assert _natsort_key('a-5.034e2', key=lambda x: x.upper(), alg=ns.F) == ('A', -503.4)
+def test__natsort_key_with_float_and_unsigned_and_noexp_splits_input_into_string_and_unsigned_float_without_exponent():
+ assert _natsort_key('a-5.034e2', None, ns.UNSIGNED | ns.NOEXP) == ('a-', 5.034, 'e', 2.0)
+
+def test__natsort_key_with_int_splits_input_into_string_and_signed_int():
+ assert _natsort_key('a-5.034e2', None, ns.INT) == ('a', -5, '.', 34, 'e', 2)
+ # NOEXP is ignored for integers
+ assert _natsort_key('a-5.034e2', None, ns.INT | ns.NOEXP) == ('a', -5, '.', 34, 'e', 2)
+
+
+def test__natsort_key_with_int_splits_and_unsigned_input_into_string_and_unsigned_int():
+ assert _natsort_key('a-5.034e2', None, ns.INT | ns.UNSIGNED) == ('a-', 5, '.', 34, 'e', 2)
+
+
+def test__natsort_key_with_version_or_digit_matches_usigned_int():
+ assert _natsort_key('a-5.034e2', None, ns.VERSION) == _natsort_key('a-5.034e2', None, ns.INT | ns.UNSIGNED)
+ assert _natsort_key('a-5.034e2', None, ns.DIGIT) == _natsort_key('a-5.034e2', None, ns.VERSION)
+
+
+def test__natsort_key_with_key_applies_key_function_before_splitting():
+ assert _natsort_key('a-5.034e2', lambda x: x.upper(), ns.F) == ('A', -503.4)
+
+
+def test__natsort_key_with_tuple_input_returns_nested_tuples():
# Iterables are parsed recursively so you can sort lists of lists.
- assert _natsort_key(('a1', 'a-5.034e2'), key=None, alg=ns.F) == (('a', 1.0), ('a', -503.4))
- assert _natsort_key(('a1', 'a-5.034e2'), key=None, alg=ns.V) == (('a', 1), ('a-', 5, '.', 34, 'e', 2))
+ assert _natsort_key(('a1', 'a-5.034e2'), None, ns.V) == (('a', 1), ('a-', 5, '.', 34, 'e', 2))
+
+
+def test__natsort_key_with_tuple_input_but_itemgetter_key_returns_split_second_element():
# A key is applied before recursion, but not in the recursive calls.
- assert _natsort_key(('a1', 'a-5.034e2'), key=itemgetter(1), alg=ns.F) == ('a', -503.4)
+ assert _natsort_key(('a1', 'a-5.034e2'), itemgetter(1), ns.F) == ('a', -503.4)
+
+def test__natsort_key_with_input_containing_leading_numbers_returns_leading_empty_strings():
# Strings that lead with a number get an empty string at the front of the tuple.
# This is designed to get around the "unorderable types" issue.
- assert _natsort_key(('15a', '6'), key=None, alg=ns.F) == (('', 15.0, 'a'), ('', 6.0))
- assert _natsort_key(10, key=None, alg=ns.F) == ('', 10)
+ assert _natsort_key(('15a', '6'), None, ns.F) == (('', 15.0, 'a'), ('', 6.0))
- # Turn on as_path to split a file path into components
- assert _natsort_key('/p/Folder (10)/file34.5nm (2).tar.gz', key=None, alg=ns.PATH) == (('/',), ('p', ), ('Folder (', 10.0, ')',), ('file', 34.5, 'nm (', 2.0, ')'), ('.tar',), ('.gz',))
- assert _natsort_key('../Folder (10)/file (2).tar.gz', key=None, alg=ns.PATH) == (('..', ), ('Folder (', 10.0, ')',), ('file (', 2.0, ')'), ('.tar',), ('.gz',))
- assert _natsort_key('Folder (10)/file.f34.5nm (2).tar.gz', key=None, alg=ns.PATH) == (('Folder (', 10.0, ')',), ('file.f', 34.5, 'nm (', 2.0, ')'), ('.tar',), ('.gz',))
+
+def test__natsort_key_with_numeric_input_returns_number_with_leading_empty_string():
+ assert _natsort_key(10, None, ns.F) == ('', 10)
+
+
+def test__natsort_key_with_absolute_path_intput_and_PATH_returns_nested_tuple_where_each_element_is_path_component_with_leading_root_and_split_extensions():
+ # Turn on PATH to split a file path into components
+ assert _natsort_key('/p/Folder (10)/file34.5nm (2).tar.gz', None, ns.PATH) == (('/',), ('p', ), ('Folder (', 10.0, ')',), ('file', 34.5, 'nm (', 2.0, ')'), ('.tar',), ('.gz',))
+
+
+def test__natsort_key_with_relative_path_intput_and_PATH_returns_nested_tuple_where_each_element_is_path_component_with_leading_relative_parent_and_split_extensions():
+ assert _natsort_key('../Folder (10)/file (2).tar.gz', None, ns.PATH) == (('..', ), ('Folder (', 10.0, ')',), ('file (', 2.0, ')'), ('.tar',), ('.gz',))
+
+
+def test__natsort_key_with_relative_path_intput_and_PATH_returns_nested_tuple_where_each_element_is_path_component_and_split_extensions():
+ assert _natsort_key('Folder (10)/file.f34.5nm (2).tar.gz', None, ns.PATH) == (('Folder (', 10.0, ')',), ('file.f', 34.5, 'nm (', 2.0, ')'), ('.tar',), ('.gz',))
+
+
+def test__natsort_key_with_pathlib_intput_and_PATH_returns_nested_tuples():
# Converts pathlib PurePath (and subclass) objects to string before sorting
if has_pathlib:
- assert _natsort_key(pathlib.Path('../Folder (10)/file (2).tar.gz'), key=None, alg=ns.PATH) == (('..', ), ('Folder (', 10.0, ')',), ('file (', 2.0, ')'), ('.tar',), ('.gz',))
+ assert _natsort_key(pathlib.Path('../Folder (10)/file (2).tar.gz'), None, ns.PATH) == (('..', ), ('Folder (', 10.0, ')',), ('file (', 2.0, ')'), ('.tar',), ('.gz',))
+
+def test__natsort_key_with_numeric_input_and_PATH_returns_number_in_nested_tuple():
# It gracefully handles as_path for numeric input by putting an extra tuple around it
# so it will sort against the other as_path results.
- assert _natsort_key(10, key=None, alg=ns.PATH) == (('', 10),)
- # as_path also handles recursion well.
- assert _natsort_key(('/Folder', '/Folder (1)'), key=None, alg=ns.PATH) == ((('/',), ('Folder',)), (('/',), ('Folder (', 1.0, ')')))
+ assert _natsort_key(10, None, ns.PATH) == (('', 10),)
+
+
+def test__natsort_key_with_tuple_of_paths_and_PATH_returns_triply_nested_tuple():
+ # PATH also handles recursion well.
+ assert _natsort_key(('/Folder', '/Folder (1)'), None, ns.PATH) == ((('/',), ('Folder',)), (('/',), ('Folder (', 1.0, ')')))
- # Turn on py3_safe to put a '' between adjacent numbers
- assert _natsort_key('43h7+3', key=None, alg=ns.TYPESAFE) == ('', 43.0, 'h', 7.0, '', 3.0)
+def test__natsort_key_with_TYPESAFE_inserts_spaces_between_numbers():
+ # Turn on TYPESAFE to put a '' between adjacent numbers
+ assert _natsort_key('43h7+3', None, ns.TYPESAFE) == ('', 43.0, 'h', 7.0, '', 3.0)
+
+
+def test__natsort_key_with_invalid_alg_input_raises_ValueError():
# Invalid arguments give the correct response
with raises(ValueError) as err:
- _natsort_key('a', key=None, alg='1')
+ _natsort_key('a', None, '1')
assert str(err.value) == "_natsort_key: 'alg' argument must be from the enum 'ns', got 1"
+
+def test__natsort_key_without_string_modifiers_leaves_text_as_is():
# Changing the sort order of strings
- assert _natsort_key('Apple56', key=None, alg=ns.F) == ('Apple', 56.0)
- assert _natsort_key('Apple56', key=None, alg=ns.IGNORECASE) == ('apple', 56.0)
- assert _natsort_key('Apple56', key=None, alg=ns.LOWERCASEFIRST) == ('aPPLE', 56.0)
- assert _natsort_key('Apple56', key=None, alg=ns.GROUPLETTERS) == ('aAppppllee', 56.0)
- assert _natsort_key('Apple56', key=None, alg=ns.G | ns.LF) == ('aapPpPlLeE', 56.0)
+ assert _natsort_key('Apple56', None, ns.F) == ('Apple', 56.0)
+
+
+def test__natsort_key_with_IGNORECASE_lowercases_text():
+ assert _natsort_key('Apple56', None, ns.IGNORECASE) == ('apple', 56.0)
+
+
+def test__natsort_key_with_LOWERCASEFIRST_inverts_text_case():
+ assert _natsort_key('Apple56', None, ns.LOWERCASEFIRST) == ('aPPLE', 56.0)
+
+
+def test__natsort_key_with_GROUPLETTERS_doubles_text_with_lowercase_letter_first():
+ assert _natsort_key('Apple56', None, ns.GROUPLETTERS) == ('aAppppllee', 56.0)
+
+
+def test__natsort_key_with_GROUPLETTERS_and_LOWERCASEFIRST_inverts_text_first_then_doubles_letters_with_lowercase_letter_first():
+ assert _natsort_key('Apple56', None, ns.G | ns.LF) == ('aapPpPlLeE', 56.0)
+
+def test__natsort_key_with_LOCALE_transforms_floats_according_to_the_current_locale_and_strxfrms_strings():
# Locale aware sorting
locale.setlocale(locale.LC_NUMERIC, str('en_US.UTF-8'))
if use_pyicu:
@@ -158,12 +304,12 @@ def test_natsort_key_private():
strxfrm = get_pyicu_transform(getlocale())
else:
from natsort.locale_help import strxfrm
- assert _natsort_key('Apple56.5', key=None, alg=ns.LOCALE) == (strxfrm('Apple'), 56.5)
- assert _natsort_key('Apple56,5', key=None, alg=ns.LOCALE) == (strxfrm('Apple'), 56.0, strxfrm(','), 5.0)
+ assert _natsort_key('Apple56.5', None, ns.LOCALE) == (strxfrm('Apple'), 56.5)
+ assert _natsort_key('Apple56,5', None, ns.LOCALE) == (strxfrm('Apple'), 56.0, strxfrm(','), 5.0)
locale.setlocale(locale.LC_NUMERIC, str('de_DE.UTF-8'))
if use_pyicu:
strxfrm = get_pyicu_transform(getlocale())
- assert _natsort_key('Apple56.5', key=None, alg=ns.LOCALE) == (strxfrm('Apple'), 56.5)
- assert _natsort_key('Apple56,5', key=None, alg=ns.LOCALE) == (strxfrm('Apple'), 56.5)
+ assert _natsort_key('Apple56.5', None, ns.LOCALE) == (strxfrm('Apple'), 56.5)
+ assert _natsort_key('Apple56,5', None, ns.LOCALE) == (strxfrm('Apple'), 56.5)
locale.setlocale(locale.LC_NUMERIC, str(''))