diff options
author | Seth M Morton <seth.m.morton@gmail.com> | 2015-03-26 20:06:55 -0700 |
---|---|---|
committer | Seth M Morton <seth.m.morton@gmail.com> | 2015-03-26 20:06:55 -0700 |
commit | 929ab6944e0f73bf2285178040d8f5a2250aa242 (patch) | |
tree | 3bdee25755c611e126aeab1b711b65ab93f10e75 | |
parent | 3bc8ae9116d1de40904dc510c2a90cfd65b5ebf7 (diff) | |
parent | cb5e8747a3660a2c26b49200651193d8c246821f (diff) | |
download | natsort-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.yml | 19 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | README.rst | 70 | ||||
-rw-r--r-- | docs/source/changelog.rst | 9 | ||||
-rw-r--r-- | docs/source/examples.rst | 12 | ||||
-rw-r--r-- | docs/source/intro.rst | 13 | ||||
-rw-r--r-- | natsort/__main__.py | 6 | ||||
-rw-r--r-- | natsort/_version.py | 2 | ||||
-rw-r--r-- | natsort/locale_help.py | 13 | ||||
-rw-r--r-- | natsort/natsort.py | 22 | ||||
-rw-r--r-- | natsort/ns_enum.py | 46 | ||||
-rw-r--r-- | natsort/utils.py | 42 | ||||
-rw-r--r-- | setup.cfg | 3 | ||||
-rw-r--r-- | setup.py | 9 | ||||
-rw-r--r-- | test_natsort/test_fake_fastnumbers.py | 17 | ||||
-rw-r--r-- | test_natsort/test_locale_help.py | 36 | ||||
-rw-r--r-- | test_natsort/test_main.py | 436 | ||||
-rw-r--r-- | test_natsort/test_natsort.py | 233 | ||||
-rw-r--r-- | test_natsort/test_utils.py | 312 |
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 @@ -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 @@ -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,) @@ -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 @@ -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('')) |