Codebase list natsort / 7517825
New upstream release. Debian Janitor 2 years ago
60 changed file(s) with 2278 addition(s) and 860 deletion(s). Raw diff Collapse all Expand all
+0
-16
.coveragerc less more
0 [report]
1 # Regexes for lines to exclude from consideration
2 exclude_lines =
3 # Have to re-enable the standard pragma
4 pragma: no cover
5
6 # Don't complain if tests don't hit defensive assertion code:
7 raise AssertionError
8 raise NotImplementedError
9 raise$
10
11 # Don't complain if non-runnable code isn't run:
12 if 0:
13 if __name__ == .__main__.:
14
15 ignore_errors = True
+0
-23
.github/ISSUE_TEMPLATE/bug_report.md less more
0 ---
1 name: Bug report
2 about: Report unexpected behavior, a crash, or incorrect results
3
4 ---
5
6 **Describe the bug**
7 A clear and concise description of what the bug is.
8
9 **Expected behavior**
10 A clear and concise description of what you expected to happen.
11
12 **Environment (please complete the following information):**
13 - Python Version: [e.g. 3.6]
14 - OS [e.g. Windows, Fedora]
15 - If the bug involves `LOCALE` or `humansorted`:
16 - Is `PyICU` installed?
17 - Do you have a locale set? If so, to what?
18
19 **To Reproduce**
20 Include a Minimum, Complete, Verifiable Example. If there is a traceback (or error message), **please** include the *entire* traceback (or error message), even if you think it is too big.
21
22 See https://stackoverflow.com/help/mcve for an explanation.
+0
-14
.github/ISSUE_TEMPLATE/feature_request.md less more
0 ---
1 name: Feature request
2 about: Suggest or request an enhancement
3
4 ---
5
6 **Describe the feature or enhancement**
7 Be as descriptive and precise as possible.
8
9 **Provide a concrete example of how the feature or enhancement will improve `natsort`**
10 Code examples are an excellent way to show how this feature or enhancement will help. To make your case stronger, show the current workaround due to the lack of the feature. What is the return-on-investment for including the feature or enhancement?
11
12 **Would you be willing to submit a Pull Request for this feature?**
13 Extra help is *always* welcome.
+0
-7
.github/ISSUE_TEMPLATE/question.md less more
0 ---
1 name: Question
2 about: Inquiry about natsort
3
4 ---
5
6 - [ ] I have read the [`natsort` documentation](https://natsort.readthedocs.io/en/master/) and the [README](https://github.com/SethMMorton/natsort#natsort), and my question is still not answered
+0
-117
.travis.yml less more
0 dist: xenial
1 language: python
2 cache:
3 - pip
4 - directories:
5 - $HOME/.pyenv_cache
6 python:
7 - 3.5
8 - 3.6
9 - 3.7
10 - 3.8
11 - 3.9
12
13 # Explicitly include other jobs/configurations not defined by the above settings
14 jobs:
15 include:
16
17 # For Python 3.8 do some extra configurations.
18 # Linux with both "icu" and "fastnumbers"
19 - python: 3.8
20 name: "Test with ICU and fastnumbers"
21 env: WITH_EXTRAS="fast,icu"
22 addons:
23 apt:
24 packages:
25 - libicu-dev
26 - language-pack-de
27 - language-pack-en
28
29 # For MacOS and Windows, only run one Python version without "icu" to test native locales
30 - language: sh
31 os: osx
32 osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4
33 name: "Test on MacOS"
34 env: TOXENV=py37
35 install:
36 - python3 -m pip install -U pip
37 - python3 -m pip install tox tox-travis codacy-coverage codecov
38 - language: sh
39 os: windows
40 name: "Test on Windows"
41 env: TOXENV=py38
42 before_install:
43 - choco install python --version=3.8.1
44 - export PATH="/c/Python38:/c/Python38/Scripts:$PATH"
45
46 # This "code quality" stage does static analysis and formatting checks.
47 # Platform- and Python-version-independent.
48 # No python version specified, will use the first listed in the python: list.
49 - stage: code quality
50 python: 3.6 # black requires >= 3.6
51 name: "Formatting"
52 install: pip install black
53 script: black --quiet --check --diff
54 - stage: code quality
55 name: "Static Analysis"
56 install: pip install flake8 flake8-import-order flake8-bugbear pep8-naming
57 script: flake8
58 - stage: code quality
59 name: "Package Validation"
60 install: pip install twine check-manifest
61 script:
62 - check-manifest --ignore ".github*,*.md,.coveragerc"
63 - python setup.py sdist
64 - twine check dist/*
65
66 # The "deploy" stage will actually upload the package to PyPI.
67 # For non-tags, we deploy to the test PyPI, for tags it's for real.
68 # Platform- and Python-version-independent.
69 # No python version specified, will use the first listed in the python: list.
70 - stage: deploy
71 name: "Deploy to PyPI (real on tagged commits, test otherwise)"
72 install: skip
73 script: skip
74 deploy:
75 - provider: pypi
76 server: https://test.pypi.org/legacy/
77 user: SethMMorton
78 password:
79 secure: "Va9uj9+6uDHMH6qcB3Z35MKDvqBDSLai0+cQN2rWjAZfhYq1H3B2TKb/cToN1dhy96t2Q7u7sXeWy9ptiJRACUOXeabL0+Ao3tFpRAgF7YBV9WUsoz9ux7waDoyMRrv1Oztbztg8sR6T3Sltz7Utd9Uf1TlYINO6D8poO7g2Cdo="
80 distributions: sdist --format=gztar bdist_wheel
81 skip_existing: true
82 on:
83 tags: false
84 repo: SethMMorton/natsort
85 branch: master
86 - provider: pypi
87 user: SethMMorton
88 password:
89 secure: "Va9uj9+6uDHMH6qcB3Z35MKDvqBDSLai0+cQN2rWjAZfhYq1H3B2TKb/cToN1dhy96t2Q7u7sXeWy9ptiJRACUOXeabL0+Ao3tFpRAgF7YBV9WUsoz9ux7waDoyMRrv1Oztbztg8sR6T3Sltz7Utd9Uf1TlYINO6D8poO7g2Cdo="
90 distributions: sdist --format=gztar bdist_wheel
91 skip_existing: true
92 on:
93 tags: true
94 repo: SethMMorton/natsort
95 branch: master
96
97 # The remainder of the code should be the same no matter the configuration/OS
98
99 install:
100 - python -m pip install -U pip
101 - python -m pip install tox tox-travis codacy-coverage codecov
102
103 script:
104 - tox
105
106 stages:
107 - code quality
108 - test
109 # Only deploy on master branch and from the main repository
110 - name: deploy
111 if: (branch = master OR tag IS present) AND repo = SethMMorton/natsort
112
113 after_success:
114 - coverage xml
115 - python-codacy-coverage -r coverage.xml
116 - codecov
00 Unreleased
11 ---
2
3 [8.0.0] - 2021-11-03
4 ---
5
6 - Re-release 7.2.0 as 8.0.0 because introduction of type hints can break CI
7 builds (issue #139)
8
9 [7.2.0] - 2021-11-02 (Yanked)
10 ---
11
12 ### Added
13 - Type hints (contributions from [@thethiny](https://github.com/thethiny) and
14 [@domdfcoding](https://github.com/domdfcoding), issues #132, #135, and #138)
15 - Explicit testing for Python 3.10
16
17 ### Removed
18 - Support for Python 3.4 and Python 3.5
19
20 [7.1.1] - 2021-01-24
21 ---
22
23 ### Changed
24 - Use GitHub Actions instead of Travis-CI (issue #125)
25 - No longer pin testing dependencies (issue #126)
26
27 ### Fixed
28 - Correct a minor typo ([@madphysicist](https://github.com/madphysicist), issue #127)
229
330 [7.1.0] - 2020-11-19
431 ---
549576 - Sorting algorithm to support floats (including exponentials) and basic version number support
550577
551578 <!---Comparison links-->
579 [8.0.0]: https://github.com/SethMMorton/natsort/compare/7.2.0...8.0.0
580 [7.2.0]: https://github.com/SethMMorton/natsort/compare/7.1.1...7.2.0
581 [7.1.1]: https://github.com/SethMMorton/natsort/compare/7.1.0...7.1.1
552582 [7.1.0]: https://github.com/SethMMorton/natsort/compare/7.0.1...7.1.0
553583 [7.0.1]: https://github.com/SethMMorton/natsort/compare/7.0.0...7.0.1
554584 [7.0.0]: https://github.com/SethMMorton/natsort/compare/6.2.0...7.0.0
+0
-46
CODE_OF_CONDUCT.md less more
0 # Contributor Covenant Code of Conduct
1
2 ## Our Pledge
3
4 In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
5
6 ## Our Standards
7
8 Examples of behavior that contributes to creating a positive environment include:
9
10 * Using welcoming and inclusive language
11 * Being respectful of differing viewpoints and experiences
12 * Gracefully accepting constructive criticism
13 * Focusing on what is best for the community
14 * Showing empathy towards other community members
15
16 Examples of unacceptable behavior by participants include:
17
18 * The use of sexualized language or imagery and unwelcome sexual attention or advances
19 * Trolling, insulting/derogatory comments, and personal or political attacks
20 * Public or private harassment
21 * Publishing others' private information, such as a physical or electronic address, without explicit permission
22 * Other conduct which could reasonably be considered inappropriate in a professional setting
23
24 ## Our Responsibilities
25
26 Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
27
28 Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
29
30 ## Scope
31
32 This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
33
34 ## Enforcement
35
36 Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at drtuba78@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
37
38 Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
39
40 ## Attribution
41
42 This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version]
43
44 [homepage]: https://www.contributor-covenant.org/
45 [version]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+0
-44
CONTRIBUTING.md less more
0 # Contributing
1
2 If you have an idea for how to improve `natsort`, please contribute! It can
3 be as simple as a bug fix or documentation update, or as complicated as a more
4 robust algorithm. Contributions that change the public API of
5 `natsort` will have to ensure that the library does not become
6 less usable after the contribution and is backwards-compatible (unless there is
7 a good reason not to be).
8
9 I do not have strong opinions on how one should contribute, so
10 I have copy/pasted some text verbatim from the
11 [Contributor's Guide](http://docs.python-requests.org/en/latest/dev/contributing/) section of
12 [Kenneth Reitz's](http://docs.python-requests.org/en/latest/dev/contributing/)
13 excellent [requests](https://github.com/kennethreitz/requests) library in
14 lieu of coming up with my own.
15
16 > ### Steps for Submitting Code
17
18 > When contributing code, you'll want to follow this checklist:
19
20 > - Fork the repository on GitHub.
21 > - Run the tests to confirm they all pass on your system.
22 If they don't, you'll need to investigate why they fail.
23 If you're unable to diagnose this yourself,
24 raise it as a bug report.
25 > - Write tests that demonstrate your bug or feature. Ensure that they fail.
26 > - Make your change.
27 > - Run the entire test suite again, confirming that all tests pass including the
28 ones you just added.
29 > - Send a GitHub Pull Request to the main repository's master branch.
30 GitHub Pull Requests are the expected method of code collaboration on this project.
31
32 > ### Documentation Contributions
33 > Documentation improvements are always welcome! The documentation files live in the
34 docs/ directory of the codebase. They're written in
35 [reStructuredText](http://docutils.sourceforge.net/rst.html), and use
36 [Sphinx](http://sphinx-doc.org/index.html)
37 to generate the full suite of documentation.
38
39 > When contributing documentation, please do your best to follow the style of the
40 documentation files. This means a soft-limit of 79 characters wide in your text
41 files and a semi-formal, yet friendly and approachable, prose style.
42
43 > When presenting Python code, use single-quoted strings ('hello' instead of "hello").
0 Copyright (c) 2012-2020 Seth M. Morton
0 Copyright (c) 2012-2021 Seth M. Morton
11
22 Permission is hereby granted, free of charge, to any person obtaining a copy of
33 this software and associated documentation files (the "Software"), to deal in
0 Metadata-Version: 2.1
1 Name: natsort
2 Version: 8.0.0
3 Summary: Simple yet flexible natural sorting in Python.
4 Home-page: https://github.com/SethMMorton/natsort
5 Author: Seth M. Morton
6 Author-email: drtuba78@gmail.com
7 License: MIT
8 Platform: UNKNOWN
9 Classifier: Development Status :: 5 - Production/Stable
10 Classifier: Intended Audience :: Developers
11 Classifier: Intended Audience :: Science/Research
12 Classifier: Intended Audience :: System Administrators
13 Classifier: Intended Audience :: Information Technology
14 Classifier: Intended Audience :: Financial and Insurance Industry
15 Classifier: Operating System :: OS Independent
16 Classifier: License :: OSI Approved :: MIT License
17 Classifier: Natural Language :: English
18 Classifier: Programming Language :: Python
19 Classifier: Programming Language :: Python :: 3
20 Classifier: Programming Language :: Python :: 3.6
21 Classifier: Programming Language :: Python :: 3.7
22 Classifier: Programming Language :: Python :: 3.8
23 Classifier: Programming Language :: Python :: 3.9
24 Classifier: Programming Language :: Python :: 3.10
25 Classifier: Topic :: Scientific/Engineering :: Information Analysis
26 Classifier: Topic :: Utilities
27 Classifier: Topic :: Text Processing
28 Requires-Python: >=3.6
29 Description-Content-Type: text/x-rst
30 Provides-Extra: fast
31 Provides-Extra: icu
32 License-File: LICENSE
33
34 natsort
35 =======
36
37 .. image:: https://img.shields.io/pypi/v/natsort.svg
38 :target: https://pypi.org/project/natsort/
39
40 .. image:: https://img.shields.io/pypi/pyversions/natsort.svg
41 :target: https://pypi.org/project/natsort/
42
43 .. image:: https://img.shields.io/pypi/l/natsort.svg
44 :target: https://github.com/SethMMorton/natsort/blob/master/LICENSE
45
46 .. image:: https://github.com/SethMMorton/natsort/workflows/Tests/badge.svg
47 :target: https://github.com/SethMMorton/natsort/actions
48
49 .. image:: https://codecov.io/gh/SethMMorton/natsort/branch/master/graph/badge.svg
50 :target: https://codecov.io/gh/SethMMorton/natsort
51
52 Simple yet flexible natural sorting in Python.
53
54 - Source Code: https://github.com/SethMMorton/natsort
55 - Downloads: https://pypi.org/project/natsort/
56 - Documentation: https://natsort.readthedocs.io/
57
58 - `Examples and Recipes <https://natsort.readthedocs.io/en/master/examples.html>`_
59 - `How Does Natsort Work? <https://natsort.readthedocs.io/en/master/howitworks.html>`_
60 - `API <https://natsort.readthedocs.io/en/master/api.html>`_
61
62 - `Quick Description`_
63 - `Quick Examples`_
64 - `FAQ`_
65 - `Requirements`_
66 - `Optional Dependencies`_
67 - `Installation`_
68 - `How to Run Tests`_
69 - `How to Build Documentation`_
70 - `Deprecation Schedule`_
71 - `History`_
72
73 **NOTE**: Please see the `Deprecation Schedule`_ section for changes in
74 ``natsort`` version 7.0.0.
75
76 Quick Description
77 -----------------
78
79 When you try to sort a list of strings that contain numbers, the normal python
80 sort algorithm sorts lexicographically, so you might not get the results that
81 you expect:
82
83 .. code-block:: pycon
84
85 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
86 >>> sorted(a)
87 ['1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '2 ft 7 in', '7 ft 6 in']
88
89 Notice that it has the order ('1', '10', '2') - this is because the list is
90 being sorted in lexicographical order, which sorts numbers like you would
91 letters (i.e. 'b', 'ba', 'c').
92
93 ``natsort`` provides a function ``natsorted`` that helps sort lists
94 "naturally" ("naturally" is rather ill-defined, but in general it means
95 sorting based on meaning and not computer code point).
96 Using ``natsorted`` is simple:
97
98 .. code-block:: pycon
99
100 >>> from natsort import natsorted
101 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
102 >>> natsorted(a)
103 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
104
105 ``natsorted`` identifies numbers anywhere in a string and sorts them
106 naturally. Below are some other things you can do with ``natsort``
107 (also see the `examples <https://natsort.readthedocs.io/en/master/examples.html>`_
108 for a quick start guide, or the
109 `api <https://natsort.readthedocs.io/en/master/api.html>`_ for complete details).
110
111 **Note**: ``natsorted`` is designed to be a drop-in replacement for the
112 built-in ``sorted`` function. Like ``sorted``, ``natsorted``
113 `does not sort in-place`. To sort a list and assign the output to the same
114 variable, you must explicitly assign the output to a variable:
115
116 .. code-block:: pycon
117
118 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
119 >>> natsorted(a)
120 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
121 >>> print(a) # 'a' was not sorted; "natsorted" simply returned a sorted list
122 ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
123 >>> a = natsorted(a) # Now 'a' will be sorted because the sorted list was assigned to 'a'
124 >>> print(a)
125 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
126
127 Please see `Generating a Reusable Sorting Key and Sorting In-Place`_ for
128 an alternate way to sort in-place naturally.
129
130 Quick Examples
131 --------------
132
133 - `Sorting Versions`_
134 - `Sort Paths Like My File Browser (e.g. Windows Explorer on Windows)`_
135 - `Sorting by Real Numbers (i.e. Signed Floats)`_
136 - `Locale-Aware Sorting (or "Human Sorting")`_
137 - `Further Customizing Natsort`_
138 - `Sorting Mixed Types`_
139 - `Handling Bytes on Python 3`_
140 - `Generating a Reusable Sorting Key and Sorting In-Place`_
141 - `Other Useful Things`_
142
143 Sorting Versions
144 ++++++++++++++++
145
146 ``natsort`` does not actually *comprehend* version numbers.
147 It just so happens that the most common versioning schemes are designed to
148 work with standard natural sorting techniques; these schemes include
149 ``MAJOR.MINOR``, ``MAJOR.MINOR.PATCH``, ``YEAR.MONTH.DAY``. If your data
150 conforms to a scheme like this, then it will work out-of-the-box with
151 ``natsorted`` (as of ``natsort`` version >= 4.0.0):
152
153 .. code-block:: pycon
154
155 >>> a = ['version-1.9', 'version-2.0', 'version-1.11', 'version-1.10']
156 >>> natsorted(a)
157 ['version-1.9', 'version-1.10', 'version-1.11', 'version-2.0']
158
159 If you need to versions that use a more complicated scheme, please see
160 `these examples <https://natsort.readthedocs.io/en/master/examples.html#rc-sorting>`_.
161
162 Sort Paths Like My File Browser (e.g. Windows Explorer on Windows)
163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
164
165 Prior to ``natsort`` version 7.1.0, it was a common request to be able to
166 sort paths like Windows Explorer. As of ``natsort`` 7.1.0, the function
167 ``os_sorted`` has been added to provide users the ability to sort
168 in the order that their file browser might sort (e.g Windows Explorer on
169 Windows, Finder on MacOS, Dolphin/Nautilus/Thunar/etc. on Linux).
170
171 .. code-block:: python
172
173 import os
174 from natsort import os_sorted
175 print(os_sorted(os.listdir()))
176 # The directory sorted like your file browser might show
177
178 Output will be different depending on the operating system you are on.
179
180 For users **not** on Windows (e.g. MacOS/Linux) it is **strongly** recommended
181 to also install `PyICU <https://pypi.org/project/PyICU>`_, which will help
182 ``natsort`` give results that match most file browsers. If this is not installed,
183 it will fall back on Python's built-in ``locale`` module and will give good
184 results for most input, but will give poor results for special characters.
185
186 Sorting by Real Numbers (i.e. Signed Floats)
187 ++++++++++++++++++++++++++++++++++++++++++++
188
189 This is useful in scientific data analysis (and was
190 the default behavior of ``natsorted`` for ``natsort``
191 version < 4.0.0). Use the ``realsorted`` function:
192
193 .. code-block:: pycon
194
195 >>> from natsort import realsorted, ns
196 >>> # Note that when interpreting as signed floats, the below numbers are
197 >>> # +5.10, -3.00, +5.30, +2.00
198 >>> a = ['position5.10.data', 'position-3.data', 'position5.3.data', 'position2.data']
199 >>> natsorted(a)
200 ['position2.data', 'position5.3.data', 'position5.10.data', 'position-3.data']
201 >>> natsorted(a, alg=ns.REAL)
202 ['position-3.data', 'position2.data', 'position5.10.data', 'position5.3.data']
203 >>> realsorted(a) # shortcut for natsorted with alg=ns.REAL
204 ['position-3.data', 'position2.data', 'position5.10.data', 'position5.3.data']
205
206 Locale-Aware Sorting (or "Human Sorting")
207 +++++++++++++++++++++++++++++++++++++++++
208
209 This is where the non-numeric characters are also ordered based on their
210 meaning, not on their ordinal value, and a locale-dependent thousands
211 separator and decimal separator is accounted for in the number.
212 This can be achieved with the ``humansorted`` function:
213
214 .. code-block:: pycon
215
216 >>> a = ['Apple', 'apple15', 'Banana', 'apple14,689', 'banana']
217 >>> natsorted(a)
218 ['Apple', 'Banana', 'apple14,689', 'apple15', 'banana']
219 >>> import locale
220 >>> locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
221 'en_US.UTF-8'
222 >>> natsorted(a, alg=ns.LOCALE)
223 ['apple15', 'apple14,689', 'Apple', 'banana', 'Banana']
224 >>> from natsort import humansorted
225 >>> humansorted(a) # shortcut for natsorted with alg=ns.LOCALE
226 ['apple15', 'apple14,689', 'Apple', 'banana', 'Banana']
227
228 You may find you need to explicitly set the locale to get this to work
229 (as shown in the example).
230 Please see `locale issues <https://natsort.readthedocs.io/en/master/locale_issues.html>`_ and the
231 `Optional Dependencies`_ section below before using the ``humansorted`` function.
232
233 Further Customizing Natsort
234 +++++++++++++++++++++++++++
235
236 If you need to combine multiple algorithm modifiers (such as ``ns.REAL``,
237 ``ns.LOCALE``, and ``ns.IGNORECASE``), you can combine the options using the
238 bitwise OR operator (``|``). For example,
239
240 .. code-block:: pycon
241
242 >>> a = ['Apple', 'apple15', 'Banana', 'apple14,689', 'banana']
243 >>> natsorted(a, alg=ns.REAL | ns.LOCALE | ns.IGNORECASE)
244 ['Apple', 'apple15', 'apple14,689', 'Banana', 'banana']
245 >>> # The ns enum provides long and short forms for each option.
246 >>> ns.LOCALE == ns.L
247 True
248 >>> # You can also customize the convenience functions, too.
249 >>> natsorted(a, alg=ns.REAL | ns.LOCALE | ns.IGNORECASE) == realsorted(a, alg=ns.L | ns.IC)
250 True
251 >>> natsorted(a, alg=ns.REAL | ns.LOCALE | ns.IGNORECASE) == humansorted(a, alg=ns.R | ns.IC)
252 True
253
254 All of the available customizations can be found in the documentation for
255 `the ns enum <https://natsort.readthedocs.io/en/master/api.html#natsort.ns>`_.
256
257 You can also add your own custom transformation functions with the ``key``
258 argument. These can be used with ``alg`` if you wish.
259
260 .. code-block:: pycon
261
262 >>> a = ['apple2.50', '2.3apple']
263 >>> natsorted(a, key=lambda x: x.replace('apple', ''), alg=ns.REAL)
264 ['2.3apple', 'apple2.50']
265
266 Sorting Mixed Types
267 +++++++++++++++++++
268
269 You can mix and match ``int``, ``float``, and ``str`` (or ``unicode``) types
270 when you sort:
271
272 .. code-block:: pycon
273
274 >>> a = ['4.5', 6, 2.0, '5', 'a']
275 >>> natsorted(a)
276 [2.0, '4.5', '5', 6, 'a']
277 >>> # On Python 2, sorted(a) would return [2.0, 6, '4.5', '5', 'a']
278 >>> # On Python 3, sorted(a) would raise an "unorderable types" TypeError
279
280 Handling Bytes on Python 3
281 ++++++++++++++++++++++++++
282
283 ``natsort`` does not officially support the `bytes` type on Python 3, but
284 convenience functions are provided that help you decode to `str` first:
285
286 .. code-block:: pycon
287
288 >>> from natsort import as_utf8
289 >>> a = [b'a', 14.0, 'b']
290 >>> # On Python 2, natsorted(a) would would work as expected.
291 >>> # On Python 3, natsorted(a) would raise a TypeError (bytes() < str())
292 >>> natsorted(a, key=as_utf8) == [14.0, b'a', 'b']
293 True
294 >>> a = [b'a56', b'a5', b'a6', b'a40']
295 >>> # On Python 2, natsorted(a) would would work as expected.
296 >>> # On Python 3, natsorted(a) would return the same results as sorted(a)
297 >>> natsorted(a, key=as_utf8) == [b'a5', b'a6', b'a40', b'a56']
298 True
299
300 Generating a Reusable Sorting Key and Sorting In-Place
301 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
302
303 Under the hood, ``natsorted`` works by generating a custom sorting
304 key using ``natsort_keygen`` and then passes that to the built-in
305 ``sorted``. You can use the ``natsort_keygen`` function yourself to
306 generate a custom sorting key to sort in-place using the ``list.sort``
307 method.
308
309 .. code-block:: pycon
310
311 >>> from natsort import natsort_keygen
312 >>> natsort_key = natsort_keygen()
313 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
314 >>> natsorted(a) == sorted(a, key=natsort_key)
315 True
316 >>> a.sort(key=natsort_key)
317 >>> a
318 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
319
320 All of the algorithm customizations mentioned in the
321 `Further Customizing Natsort`_ section can also be applied to
322 ``natsort_keygen`` through the *alg* keyword option.
323
324 Other Useful Things
325 +++++++++++++++++++
326
327 - recursively descend into lists of lists
328 - automatic unicode normalization of input data
329 - `controlling the case-sensitivity <https://natsort.readthedocs.io/en/master/examples.html#case-sort>`_
330 - `sorting file paths correctly <https://natsort.readthedocs.io/en/master/examples.html#path-sort>`_
331 - `allow custom sorting keys <https://natsort.readthedocs.io/en/master/examples.html#custom-sort>`_
332 - `accounting for units <https://natsort.readthedocs.io/en/master/examples.html#accounting-for-units-when-sorting>`_
333
334 FAQ
335 ---
336
337 How do I debug ``natsort.natsorted()``?
338 The best way to debug ``natsorted()`` is to generate a key using ``natsort_keygen()``
339 with the same options being passed to ``natsorted``. One can take a look at
340 exactly what is being done with their input using this key - it is highly
341 recommended
342 to `look at this issue describing how to debug <https://github.com/SethMMorton/natsort/issues/13#issuecomment-50422375>`_
343 for *how* to debug, and also to review the
344 `How Does Natsort Work? <https://natsort.readthedocs.io/en/master/howitworks.html>`_
345 page for *why* ``natsort`` is doing that to your data.
346
347 If you are trying to sort custom classes and running into trouble, please
348 take a look at https://github.com/SethMMorton/natsort/issues/60. In short,
349 custom classes are not likely to be sorted correctly if one relies
350 on the behavior of ``__lt__`` and the other rich comparison operators in
351 their custom class - it is better to use a ``key`` function with
352 ``natsort``, or use the ``natsort`` key as part of your rich comparison
353 operator definition.
354
355 ``natsort`` gave me results I didn't expect, and it's a terrible library!
356 Did you try to debug using the above advice? If so, and you still cannot figure out
357 the error, then please `file an issue <https://github.com/SethMMorton/natsort/issues/new>`_.
358
359 How *does* ``natsort`` work?
360 If you don't want to read `How Does Natsort Work? <https://natsort.readthedocs.io/en/master/howitworks.html>`_,
361 here is a quick primer.
362
363 ``natsort`` provides a `key function <https://docs.python.org/3/howto/sorting.html#key-functions>`_
364 that can be passed to `list.sort() <https://docs.python.org/3/library/stdtypes.html#list.sort>`_
365 or `sorted() <https://docs.python.org/3/library/functions.html#sorted>`_ in order to
366 modify the default sorting behavior. This key is generated on-demand with
367 the key generator ``natsort.natsort_keygen()``. ``natsort.natsorted()``
368 is essentially a wrapper for the following code:
369
370 .. code-block:: pycon
371
372 >>> from natsort import natsort_keygen
373 >>> natsort_key = natsort_keygen()
374 >>> sorted(['1', '10', '2'], key=natsort_key)
375 ['1', '2', '10']
376
377 Users can further customize ``natsort`` sorting behavior with the ``key``
378 and/or ``alg`` options (see details in the `Further Customizing Natsort`_
379 section).
380
381 The key generated by ``natsort_keygen`` *always* returns a ``tuple``. It
382 does so in the following way (*some details omitted for clarity*):
383
384 1. Assume the input is a string, and attempt to split it into numbers and
385 non-numbers using regular expressions. Numbers are then converted into
386 either ``int`` or ``float``.
387 2. If the above fails because the input is not a string, assume the input
388 is some other sequence (e.g. ``list`` or ``tuple``), and recursively
389 apply the key to each element of the sequence.
390 3. If the above fails because the input is not iterable, assume the input
391 is an ``int`` or ``float``, and just return the input in a ``tuple``.
392
393 Because a ``tuple`` is always returned, a ``TypeError`` should not be common
394 unless one tries to do something odd like sort an ``int`` against a ``list``.
395
396 Shell script
397 ------------
398
399 ``natsort`` comes with a shell script called ``natsort``, or can also be called
400 from the command line with ``python -m natsort``.
401
402 Requirements
403 ------------
404
405 ``natsort`` requires Python 3.6 or greater.
406
407 Optional Dependencies
408 ---------------------
409
410 fastnumbers
411 +++++++++++
412
413 The most efficient sorting can occur if you install the
414 `fastnumbers <https://pypi.org/project/fastnumbers>`_ package
415 (version >=2.0.0); it helps with the string to number conversions.
416 ``natsort`` will still run (efficiently) without the package, but if you need
417 to squeeze out that extra juice it is recommended you include this as a
418 dependency. ``natsort`` will not require (or check) that
419 `fastnumbers <https://pypi.org/project/fastnumbers>`_ is installed
420 at installation.
421
422 PyICU
423 +++++
424
425 It is recommended that you install `PyICU <https://pypi.org/project/PyICU>`_
426 if you wish to sort in a locale-dependent manner, see
427 https://natsort.readthedocs.io/en/master/locale_issues.html for an explanation why.
428
429 Installation
430 ------------
431
432 Use ``pip``!
433
434 .. code-block:: console
435
436 $ pip install natsort
437
438 If you want to install the `Optional Dependencies`_, you can use the
439 `"extras" notation <https://packaging.python.org/tutorials/installing-packages/#installing-setuptools-extras>`_
440 at installation time to install those dependencies as well - use ``fast`` for
441 `fastnumbers <https://pypi.org/project/fastnumbers>`_ and ``icu`` for
442 `PyICU <https://pypi.org/project/PyICU>`_.
443
444 .. code-block:: console
445
446 # Install both optional dependencies.
447 $ pip install natsort[fast,icu]
448 # Install just fastnumbers
449 $ pip install natsort[fast]
450
451 How to Run Tests
452 ----------------
453
454 Please note that ``natsort`` is NOT set-up to support ``python setup.py test``.
455
456 The recommended way to run tests is with `tox <https://tox.readthedocs.io/en/latest/>`_.
457 After installing ``tox``, running tests is as simple as executing the following
458 in the ``natsort`` directory:
459
460 .. code-block:: console
461
462 $ tox
463
464 ``tox`` will create virtual a virtual environment for your tests and install
465 all the needed testing requirements for you. You can specify a particular
466 python version with the ``-e`` flag, e.g. ``tox -e py36``. Static analysis
467 is done with ``tox -e flake8``. You can see all available testing environments
468 with ``tox --listenvs``.
469
470 How to Build Documentation
471 --------------------------
472
473 If you want to build the documentation for ``natsort``, it is recommended to
474 use ``tox``:
475
476 .. code-block:: console
477
478 $ tox -e docs
479
480 This will place the documentation in ``build/sphinx/html``.
481
482 Deprecation Schedule
483 --------------------
484
485 Dropped Python 3.4 and Python 3.5 Support
486 +++++++++++++++++++++++++++++++++++++++++
487
488 ``natsort`` version 8.0.0 dropped support for Python < 3.6.
489
490 Dropped Python 2.7 Support
491 ++++++++++++++++++++++++++
492
493 ``natsort`` version 7.0.0 dropped support for Python 2.7.
494
495 The version 6.X branch will remain as a "long term support" branch where bug
496 fixes are applied so that users who cannot update from Python 2.7 will not be
497 forced to use a buggy ``natsort`` version (bug fixes will need to be requested;
498 by default only the 7.X branch will be updated).
499 New features would not be added to version 6.X, only bug fixes.
500
501 Dropped Deprecated APIs
502 +++++++++++++++++++++++
503
504 In ``natsort`` version 6.0.0, the following APIs and functions were removed
505
506 - ``number_type`` keyword argument (deprecated since 3.4.0)
507 - ``signed`` keyword argument (deprecated since 3.4.0)
508 - ``exp`` keyword argument (deprecated since 3.4.0)
509 - ``as_path`` keyword argument (deprecated since 3.4.0)
510 - ``py3_safe`` keyword argument (deprecated since 3.4.0)
511 - ``ns.TYPESAFE`` (deprecated since version 5.0.0)
512 - ``ns.DIGIT`` (deprecated since version 5.0.0)
513 - ``ns.VERSION`` (deprecated since version 5.0.0)
514 - ``versorted()`` (discouraged since version 4.0.0,
515 officially deprecated since version 5.5.0)
516 - ``index_versorted()`` (discouraged since version 4.0.0,
517 officially deprecated since version 5.5.0)
518
519 In general, if you want to determine if you are using deprecated APIs you
520 can run your code with the following flag
521
522 .. code-block:: console
523
524 $ python -Wdefault::DeprecationWarning my-code.py
525
526 By default ``DeprecationWarnings`` are not shown, but this will cause them
527 to be shown. Alternatively, you can just set the environment variable
528 ``PYTHONWARNINGS`` to "default::DeprecationWarning" and then run your code.
529
530 Author
531 ------
532
533 Seth M. Morton
534
535 History
536 -------
537
538 Please visit the changelog
539 `on GitHub <https://github.com/SethMMorton/natsort/blob/master/CHANGELOG.md>`_ or
540 `in the documentation <https://natsort.readthedocs.io/en/master/changelog.html>`_.
541
542
99 .. image:: https://img.shields.io/pypi/l/natsort.svg
1010 :target: https://github.com/SethMMorton/natsort/blob/master/LICENSE
1111
12 .. image:: https://img.shields.io/travis/SethMMorton/natsort/master.svg?label=travis-ci
13 :target: https://travis-ci.com/SethMMorton/natsort
12 .. image:: https://github.com/SethMMorton/natsort/workflows/Tests/badge.svg
13 :target: https://github.com/SethMMorton/natsort/actions
1414
1515 .. image:: https://codecov.io/gh/SethMMorton/natsort/branch/master/graph/badge.svg
1616 :target: https://codecov.io/gh/SethMMorton/natsort
17
18 .. image:: https://api.codacy.com/project/badge/Grade/f2bf04b1fc5d4792bf546f6e497cf4b8
19 :target: https://www.codacy.com/app/SethMMorton/natsort
2017
2118 Simple yet flexible natural sorting in Python.
2219
150147 to also install `PyICU <https://pypi.org/project/PyICU>`_, which will help
151148 ``natsort`` give results that match most file browsers. If this is not installed,
152149 it will fall back on Python's built-in ``locale`` module and will give good
153 results for most input, but will give poor restuls for special characters.
150 results for most input, but will give poor results for special characters.
154151
155152 Sorting by Real Numbers (i.e. Signed Floats)
156153 ++++++++++++++++++++++++++++++++++++++++++++
371368 Requirements
372369 ------------
373370
374 ``natsort`` requires Python 3.5 or greater. Python 3.4 is unofficially supported,
375 meaning that support has not been removed, but it is no longer tested.
371 ``natsort`` requires Python 3.6 or greater.
376372
377373 Optional Dependencies
378374 ---------------------
437433 is done with ``tox -e flake8``. You can see all available testing environments
438434 with ``tox --listenvs``.
439435
440 If you do not wish to use ``tox``, you can install the testing dependencies with the
441 ``dev/requirements.txt`` file and then run the tests manually using
442 `pytest <https://docs.pytest.org/en/latest/>`_.
443
444 .. code-block:: console
445
446 $ pip install -r dev/requirements.txt
447 $ python -m pytest
448
449 Note that above I invoked ``python -m pytest`` instead of just ``pytest`` - this is because
450 `the former puts the CWD on sys.path <https://docs.pytest.org/en/latest/usage.html#calling-pytest-through-python-m-pytest>`_.
451
452436 How to Build Documentation
453437 --------------------------
454438
459443
460444 $ tox -e docs
461445
462 This will place the documentation in ``build/sphinx/html``. If you do not
463 which to use ``tox``, you can do the following:
464
465 .. code-block:: console
466
467 $ pip install sphinx sphinx_rtd_theme
468 $ python setup.py build_sphinx
446 This will place the documentation in ``build/sphinx/html``.
469447
470448 Deprecation Schedule
471449 --------------------
450
451 Dropped Python 3.4 and Python 3.5 Support
452 +++++++++++++++++++++++++++++++++++++++++
453
454 ``natsort`` version 8.0.0 dropped support for Python < 3.6.
472455
473456 Dropped Python 2.7 Support
474457 ++++++++++++++++++++++++++
0 natsort (8.0.0-1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Debian Janitor <janitor@jelmer.uk> Wed, 10 Nov 2021 20:02:50 -0000
5
06 natsort (7.1.0-1) unstable; urgency=medium
17
28 * Team upload.
88 - `clean.py` - This file cleans most files that are created during development.
99 Run in the project home directory.
1010 It is not really intended to be called directly, but instead through `tox -e clean`.
11 - `requirements.in` - Our direct requirements to run tests.
12 - `requirements.txt` - All pinned requirements to run tests.
11 - `generate_new_unicode_numbers.py` is used to update `natsort/unicode_numeric_hex.py`
12 when new Python versions are released.
3232 cmd = ["bump2version", *args, severity]
3333 try:
3434 if catch:
35 return subprocess.run(cmd, check=True, capture_output=True, text=True).stdout
35 return subprocess.run(
36 cmd, check=True, capture_output=True, text=True
37 ).stdout
3638 else:
3739 subprocess.run(cmd, check=True, text=True)
3840 except subprocess.CalledProcessError as e:
5658 "<!---Comparison links-->\n[{new}]: {url}/{current}...{new}".format(
5759 new=data["new_version"],
5860 current=data["current_version"],
59 url="https://github.com/SethMMorton/natsort/compare"
60 )
61 url="https://github.com/SethMMorton/natsort/compare",
62 ),
6163 )
6264 with open("CHANGELOG.md", "w") as fl:
6365 fl.write(changelog)
+0
-7
dev/requirements.in less more
0 coverage
1 pytest
2 pytest-cov
3 pytest-mock
4 hypothesis
5 # pytest-faulthandler; platform_python_implementation == 'CPython'
6 semver
+0
-23
dev/requirements.txt less more
0 #
1 # This file is autogenerated by pip-compile
2 # To update, run:
3 #
4 # pip-compile requirements.in
5 #
6 attrs==19.3.0 # via hypothesis, pytest
7 coverage==5.0.2
8 hypothesis==5.1.1
9 importlib-metadata==1.3.0 # via pluggy, pytest
10 more-itertools==8.0.2 # via pytest, zipp
11 packaging==20.0 # via pytest
12 pluggy==0.13.1 # via pytest
13 py==1.8.1 # via pytest
14 pyparsing==2.4.6 # via packaging
15 pytest-cov==2.8.1
16 pytest-mock==2.0.0
17 pytest==5.3.2
18 semver==2.9.0
19 six==1.13.0 # via packaging
20 sortedcontainers==2.1.0 # via hypothesis
21 wcwidth==0.1.8 # via pytest
22 zipp==0.6.0 # via importlib-metadata
116116 the corresponding regular expression to locate numbers will be returned.
117117
118118 .. autofunction:: numeric_regex_chooser
119
120 Help With Type Hinting
121 ++++++++++++++++++++++
122
123 If you need to explictly specify the types that natsort accepts or returns
124 in your code, the following types have been exposed for your convenience.
125
126 +--------------------------------+----------------------------------------------------------------------------------------+
127 | Type | Purpose |
128 +================================+========================================================================================+
129 |:attr:`natsort.NatsortKeyType` | Returned by :func:`natsort.natsort_keygen`, and type of :attr:`natsort.natsort_key` |
130 +--------------------------------+----------------------------------------------------------------------------------------+
131 |:attr:`natsort.OSSortKeyType` | Returned by :func:`natsort.os_sort_keygen`, and type of :attr:`natsort.os_sort_key` |
132 +--------------------------------+----------------------------------------------------------------------------------------+
133 |:attr:`natsort.KeyType` | Type of `key` argument to :func:`natsort.natsorted` and :func:`natsort.natsort_keygen` |
134 +--------------------------------+----------------------------------------------------------------------------------------+
135 |:attr:`natsort.NatsortInType` | The input type of :attr:`natsort.NatsortKeyType` |
136 +--------------------------------+----------------------------------------------------------------------------------------+
137 |:attr:`natsort.NatsortOutType` | The output type of :attr:`natsort.NatsortKeyType` |
138 +--------------------------------+----------------------------------------------------------------------------------------+
139 |:attr:`natsort.NSType` | The type of the :class:`ns` enum |
140 +--------------------------------+----------------------------------------------------------------------------------------+
2727 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
2828 # ones.
2929 extensions = [
30 'sphinx.ext.autodoc',
31 'sphinx.ext.autosummary',
32 'sphinx.ext.intersphinx',
33 'sphinx.ext.mathjax',
34 'sphinx.ext.napoleon',
35 'm2r',
30 "sphinx.ext.autodoc",
31 "sphinx.ext.autosummary",
32 "sphinx.ext.intersphinx",
33 "sphinx.ext.mathjax",
34 "sphinx.ext.napoleon",
35 "m2r2",
3636 ]
37 autodoc_typehints = "none"
3738
3839 # Add any paths that contain templates here, relative to this directory.
39 templates_path = ['_templates']
40 templates_path = ["_templates"]
4041
4142 # The suffix of source filenames.
42 source_suffix = ['.rst', '.md']
43 source_suffix = [".rst", ".md"]
4344
4445 # The encoding of source files.
4546 # source_encoding = 'utf-8-sig'
4647
4748 # The master toctree document.
48 master_doc = 'index'
49 master_doc = "index"
4950
5051 # General information about the project.
51 project = 'natsort'
52 project = "natsort"
5253 # noinspection PyShadowingBuiltins
53 copyright = '2014, Seth M. Morton'
54 copyright = "2014, Seth M. Morton"
5455
5556 # The version info for the project you're documenting, acts as replacement for
5657 # |version| and |release|, also used in various other places throughout the
5758 # built documents.
5859 #
5960 # The full version, including alpha/beta/rc tags.
60 release = '7.1.0'
61 release = "8.0.0"
6162 # The short X.Y version.
62 version = '.'.join(release.split('.')[0:2])
63 version = ".".join(release.split(".")[0:2])
6364
6465 # The language for content autogenerated by Sphinx. Refer to documentation
6566 # for a list of supported languages.
9192 # show_authors = False
9293
9394 # The name of the Pygments (syntax highlighting) style to use.
94 pygments_style = 'sphinx'
95 highlight_language = 'python'
95 pygments_style = "sphinx"
96 highlight_language = "python"
9697
9798 # A list of ignored prefixes for module index sorting.
9899 # modindex_common_prefix = []
105106
106107 # The theme to use for HTML and HTML Help pages. See the documentation for
107108 # a list of builtin themes.
108 on_rtd = os.environ.get('READTHEDOCS') == 'True'
109 on_rtd = os.environ.get("READTHEDOCS") == "True"
109110 if on_rtd:
110 html_theme = 'default'
111 html_theme = "default"
111112 else:
112113 import sphinx_rtd_theme # noqa: F401
113114
114 html_theme = 'sphinx_rtd_theme'
115 html_theme = "sphinx_rtd_theme"
115116 # html_theme = 'solar'
116117
117118 # Theme options are theme-specific and customize the look and feel of a theme
120121 # html_theme_options = {}
121122
122123 # Add any paths that contain custom themes here, relative to this directory.
123 html_theme_path = ['.']
124 html_theme_path = ["."]
124125
125126 # The name for this set of Sphinx documents. If None, it defaults to
126127 # "<project> v<release> documentation".
190191 # html_file_suffix = None
191192
192193 # Output file base name for HTML help builder.
193 htmlhelp_basename = 'natsortdoc'
194 htmlhelp_basename = "natsortdoc"
194195
195196 # -- Options for LaTeX output ---------------------------------------------
196197
197198 latex_elements = {
198199 # The paper size ('letterpaper' or 'a4paper').
199200 # 'papersize': 'letterpaper',
200
201201 # The font size ('10pt', '11pt' or '12pt').
202202 # 'pointsize': '10pt',
203
204203 # Additional stuff for the LaTeX preamble.
205204 # 'preamble': '',
206205 }
209208 # (source start file, target name, title,
210209 # author, documentclass [howto, manual, or own class]).
211210 latex_documents = [
212 ('index', 'natsort.tex', 'natsort Documentation',
213 'Seth M. Morton', 'manual'),
211 ("index", "natsort.tex", "natsort Documentation", "Seth M. Morton", "manual"),
214212 ]
215213
216214 # The name of an image file (relative to this directory) to place at the top of
238236
239237 # One entry per manual page. List of tuples
240238 # (source start file, name, description, authors, manual section).
241 man_pages = [
242 ('index', 'natsort', 'natsort Documentation',
243 ['Seth M. Morton'], 1)
244 ]
239 man_pages = [("index", "natsort", "natsort Documentation", ["Seth M. Morton"], 1)]
245240
246241 # If true, show URL addresses after external links.
247242 # man_show_urls = False
253248 # (source start file, target name, title, author,
254249 # dir menu entry, description, category)
255250 texinfo_documents = [
256 ('index', 'natsort', 'natsort Documentation',
257 'Seth M. Morton', 'natsort', 'One line description of project.',
258 'Miscellaneous'),
251 (
252 "index",
253 "natsort",
254 "natsort Documentation",
255 "Seth M. Morton",
256 "natsort",
257 "One line description of project.",
258 "Miscellaneous",
259 ),
259260 ]
260261
261262 # Documents to append as an appendix to all manuals.
272273
273274
274275 # Example configuration for intersphinx: refer to the Python standard library.
275 intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
276 intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
8080
8181 .. code-block:: pycon
8282
83 >>> from semver import parse_version_info
83 >>> from semver import VersionInfo
8484 >>> a = ['3.4.5-pre.1', '3.4.5', '3.4.5-pre.2+build.4']
85 >>> sorted(a, key=parse_version_info)
85 >>> sorted(a, key=VersionInfo.parse)
8686 ['3.4.5-pre.1', '3.4.5-pre.2+build.4', '3.4.5']
8787
8888 .. _path_sort:
2222
2323 When :func:`~natsort.natsort_keygen` is called it returns a key function that
2424 hard-codes the provided settings. This means that the key returned when
25 ``ns.LOCALE`` is used contins the settings specifed by the locale
25 ``ns.LOCALE`` is used contains the settings specifed by the locale
2626 *loaded at the time the key is generated*. If you change the locale,
2727 you should regenerate the key to account for the new locale.
2828
0 m2r @ git+https://github.com/crossnox/m2r@dev#egg=m2r
0 m2r2
33 #
44 # pip-compile
55 #
6 docutils==0.16 # via m2r
7 git+https://github.com/crossnox/m2r@dev#egg=m2r # via -r requirements.in
8 mistune==0.8.4 # via m2r
6 docutils==0.16 # via m2r2
7 m2r2==0.2.7 # via -r requirements.in
8 mistune==0.8.4 # via m2r2
00 # -*- coding: utf-8 -*-
11
22 from natsort.natsort import (
3 NatsortKeyType,
4 OSSortKeyType,
35 as_ascii,
46 as_utf8,
57 decoder,
1012 natsort_key,
1113 natsort_keygen,
1214 natsorted,
13 ns,
1415 numeric_regex_chooser,
1516 order_by_index,
1617 os_sort_key,
1819 os_sorted,
1920 realsorted,
2021 )
21 from natsort.utils import chain_functions
22 from natsort.ns_enum import NSType, ns
23 from natsort.utils import KeyType, NatsortInType, NatsortOutType, chain_functions
2224
23 __version__ = "7.1.0"
25 __version__ = "8.0.0"
2426
2527 __all__ = [
2628 "natsort_key",
4143 "os_sort_key",
4244 "os_sort_keygen",
4345 "os_sorted",
46 "NatsortKeyType",
47 "OSSortKeyType",
48 "KeyType",
49 "NatsortInType",
50 "NatsortOutType",
51 "NSType",
4452 ]
4553
4654 # Add the ns keys to this namespace for convenience.
47 globals().update(ns._asdict())
55 globals().update({name: value for name, value in ns.__members__.items()})
00 # -*- coding: utf-8 -*-
11
2 import argparse
23 import sys
4 from typing import Callable, Iterable, List, Optional, Pattern, Tuple, Union, cast
35
46 import natsort
57 from natsort.utils import regex_chooser
68
7
8 def main(*arguments):
9 Num = Union[float, int]
10 NumIter = Iterable[Num]
11 NumPair = Tuple[Num, Num]
12 NumPairIter = Iterable[NumPair]
13 NumConverter = Callable[[str], Num]
14
15
16 class TypedArgs(argparse.Namespace):
17 paths: bool
18 filter: Optional[List[NumPair]]
19 reverse_filter: Optional[List[NumPair]]
20 exclude: List[Num]
21 reverse: bool
22 number_type: str
23 nosign: bool
24 sign: bool
25 noexp: bool
26 locale: bool
27 entries: List[str]
28
29 def __init__(
30 self,
31 filter: Optional[List[NumPair]] = None,
32 reverse_filter: Optional[List[NumPair]] = None,
33 exclude: Optional[List[Num]] = None,
34 paths: bool = False,
35 reverse: bool = False,
36 ) -> None:
37 """Used by testing only"""
38 self.filter = filter
39 self.reverse_filter = reverse_filter
40 self.exclude = [] if exclude is None else exclude
41 self.paths = paths
42 self.reverse = reverse
43 self.number_type = "int"
44 self.signed = False
45 self.exp = True
46 self.locale = False
47
48
49 def main(*arguments: str) -> None:
950 """
1051 Performs a natural sort on entries given on the command-line.
1152
1657 from textwrap import dedent
1758
1859 parser = ArgumentParser(
19 description=dedent(main.__doc__), formatter_class=RawDescriptionHelpFormatter
60 description=dedent(cast(str, main.__doc__)),
61 formatter_class=RawDescriptionHelpFormatter,
2062 )
2163 parser.add_argument(
2264 "--version",
125167 help="The entries to sort. Taken from stdin if nothing is given on "
126168 "the command line.",
127169 )
128 args = parser.parse_args(arguments or None)
170 args = parser.parse_args(arguments or None, namespace=TypedArgs())
129171
130172 # Make sure the filter range is given properly. Does nothing if no filter
131173 args.filter = check_filters(args.filter)
138180 sort_and_print_entries(entries, args)
139181
140182
141 def range_check(low, high):
183 def range_check(low: Num, high: Num) -> NumPair:
142184 """
143185 Verify that that given range has a low lower than the high.
144186
163205 return low, high
164206
165207
166 def check_filters(filters):
208 def check_filters(filters: Optional[NumPairIter]) -> Optional[List[NumPair]]:
167209 """
168210 Execute range_check for every element of an iterable.
169211
191233 raise ValueError("Error in --filter: " + str(err))
192234
193235
194 def keep_entry_range(entry, lows, highs, converter, regex):
236 def keep_entry_range(
237 entry: str,
238 lows: NumIter,
239 highs: NumIter,
240 converter: NumConverter,
241 regex: Pattern[str],
242 ) -> bool:
195243 """
196244 Check if an entry falls into a desired range.
197245
223271 )
224272
225273
226 def keep_entry_value(entry, values, converter, regex):
274 def keep_entry_value(
275 entry: str, values: NumIter, converter: NumConverter, regex: Pattern[str]
276 ) -> bool:
227277 """
228278 Check if an entry does not match a given value.
229279
248298 return not any(converter(num) in values for num in regex.findall(entry))
249299
250300
251 def sort_and_print_entries(entries, args):
301 def sort_and_print_entries(entries: List[str], args: TypedArgs) -> None:
252302 """Sort the entries, applying the filters first if necessary."""
253303
254304 # Extract the proper number type.
255305 is_float = args.number_type in ("float", "real", "f", "r")
256306 signed = args.signed or args.number_type in ("real", "r")
257 alg = (
307 alg: int = (
258308 natsort.ns.FLOAT * is_float
259309 | natsort.ns.SIGNED * signed
260310 | natsort.ns.NOEXP * (not args.exp)
22 This module is intended to replicate some of the functionality
33 from the fastnumbers module in the event that module is not installed.
44 """
5 import unicodedata
6 from typing import Callable, FrozenSet, Optional, Union
57
6 # Std. lib imports.
7 import unicodedata
8
9 # Local imports.
108 from natsort.unicode_numbers import decimal_chars
119
12 NAN_INF = [
10 _NAN_INF = [
1311 "INF",
1412 "INf",
1513 "Inf",
2725 "nAN",
2826 "Nan",
2927 ]
30 NAN_INF.extend(["+" + x[:2] for x in NAN_INF] + ["-" + x[:2] for x in NAN_INF])
31 NAN_INF = frozenset(NAN_INF)
28 _NAN_INF.extend(["+" + x[:2] for x in _NAN_INF] + ["-" + x[:2] for x in _NAN_INF])
29 NAN_INF = frozenset(_NAN_INF)
3230 ASCII_NUMS = "0123456789+-"
3331 POTENTIAL_FIRST_CHAR = frozenset(decimal_chars + list(ASCII_NUMS + "."))
32
33 StrOrFloat = Union[str, float]
34 StrOrInt = Union[str, int]
3435
3536
3637 # noinspection PyIncorrectDocstring
3738 def fast_float(
38 x,
39 key=lambda x: x,
40 nan=None,
41 _uni=unicodedata.numeric,
42 _nan_inf=NAN_INF,
43 _first_char=POTENTIAL_FIRST_CHAR,
44 ):
39 x: str,
40 key: Callable[[str], StrOrFloat] = lambda x: x,
41 nan: Optional[StrOrFloat] = None,
42 _uni: Callable[[str, StrOrFloat], StrOrFloat] = unicodedata.numeric,
43 _nan_inf: FrozenSet[str] = NAN_INF,
44 _first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
45 ) -> StrOrFloat:
4546 """
4647 Convert a string to a float quickly, return input as-is if not possible.
4748
6465 """
6566 if x[0] in _first_char or x.lstrip()[:3] in _nan_inf:
6667 try:
67 x = float(x)
68 return nan if nan is not None and x != x else x
68 ret = float(x)
69 return nan if nan is not None and ret != ret else ret
6970 except ValueError:
7071 try:
7172 return _uni(x, key(x)) if len(x) == 1 else key(x)
8081
8182 # noinspection PyIncorrectDocstring
8283 def fast_int(
83 x,
84 key=lambda x: x,
85 _uni=unicodedata.digit,
86 _first_char=POTENTIAL_FIRST_CHAR,
87 ):
84 x: str,
85 key: Callable[[str], StrOrInt] = lambda x: x,
86 _uni: Callable[[str, StrOrInt], StrOrInt] = unicodedata.digit,
87 _first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
88 ) -> StrOrInt:
8889 """
8990 Convert a string to a int quickly, return input as-is if not possible.
9091
22 Interface for natsort to access fastnumbers functions without
33 having to worry if it is actually installed.
44 """
5 import re
56
6 from distutils.version import StrictVersion
7 __all__ = ["fast_float", "fast_int"]
8
9
10 def is_supported_fastnumbers(fastnumbers_version: str) -> bool:
11 match = re.match(
12 r"^(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?$",
13 fastnumbers_version,
14 flags=re.ASCII,
15 )
16
17 if not match:
18 raise ValueError(
19 "Invalid fastnumbers version number '{}'".format(fastnumbers_version)
20 )
21
22 (major, minor, patch) = match.group(1, 2, 4)
23
24 return (int(major), int(minor), int(patch)) >= (2, 0, 0)
25
726
827 # If the user has fastnumbers installed, they will get great speed
928 # benefits. If not, we use the simulated functions that come with natsort.
1231 from fastnumbers import fast_float, fast_int, __version__ as fn_ver
1332
1433 # Require >= version 2.0.0.
15 if StrictVersion(fn_ver) < StrictVersion("2.0.0"):
34 if not is_supported_fastnumbers(fn_ver):
1635 raise ImportError # pragma: no cover
1736 except ImportError:
18 from natsort.compat.fake_fastnumbers import fast_float, fast_int # noqa: F401
37 from natsort.compat.fake_fastnumbers import fast_float, fast_int # type: ignore
22 Interface for natsort to access locale functionality without
33 having to worry about if it is using PyICU or the built-in locale.
44 """
5 import sys
6 from typing import Callable, Union, cast
57
6 # Std. lib imports.
7 import sys
8 StrOrBytes = Union[str, bytes]
9 TrxfmFunc = Callable[[str], StrOrBytes]
810
911 # This string should be sorted after any other byte string because
1012 # it contains the max unicode character repeated 20 times.
1113 # You would need some odd data to come after that.
1214 null_string = ""
1315 null_string_max = chr(sys.maxunicode) * 20
16
17 # This variable could be str or bytes depending on the locale library
18 # being used, so give the type-checker this information.
19 null_string_locale: StrOrBytes
20 null_string_locale_max: StrOrBytes
1421
1522 # strxfrm can be buggy (especially on BSD-based systems),
1623 # so prefer icu if available.
2532 # You would need some odd data to come after that.
2633 null_string_locale_max = b"x7f" * 50
2734
28 def dumb_sort():
35 def dumb_sort() -> bool:
2936 return False
3037
3138 # If using icu, get the locale from the current global locale,
32 def get_icu_locale():
39 def get_icu_locale() -> str:
3340 try:
34 return icu.Locale(".".join(getlocale()))
41 return cast(str, icu.Locale(".".join(getlocale())))
3542 except TypeError: # pragma: no cover
36 return icu.Locale()
43 return cast(str, icu.Locale())
3744
38 def get_strxfrm():
39 return icu.Collator.createInstance(get_icu_locale()).getSortKey
45 def get_strxfrm() -> TrxfmFunc:
46 return cast(TrxfmFunc, icu.Collator.createInstance(get_icu_locale()).getSortKey)
4047
41 def get_thousands_sep():
48 def get_thousands_sep() -> str:
4249 sep = icu.DecimalFormatSymbols.kGroupingSeparatorSymbol
43 return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
50 return cast(str, icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep))
4451
45 def get_decimal_point():
52 def get_decimal_point() -> str:
4653 sep = icu.DecimalFormatSymbols.kDecimalSeparatorSymbol
47 return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
54 return cast(str, icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep))
4855
4956
5057 except ImportError:
5663
5764 # On some systems, locale is broken and does not sort in the expected
5865 # order. We will try to detect this and compensate.
59 def dumb_sort():
66 def dumb_sort() -> bool:
6067 return strxfrm("A") < strxfrm("a")
6168
62 def get_strxfrm():
69 def get_strxfrm() -> TrxfmFunc:
6370 return strxfrm
6471
65 def get_thousands_sep():
66 sep = locale.localeconv()["thousands_sep"]
72 def get_thousands_sep() -> str:
73 sep = cast(str, locale.localeconv()["thousands_sep"])
6774 # If this locale library is broken, some of the thousands separator
6875 # characters are incorrectly blank. Here is a lookup table of the
6976 # corrections I am aware of.
110117 else:
111118 return sep
112119
113 def get_decimal_point():
114 return locale.localeconv()["decimal_point"]
120 def get_decimal_point() -> str:
121 return cast(str, locale.localeconv()["decimal_point"])
88 import platform
99 from functools import partial
1010 from operator import itemgetter
11 from typing import (
12 Any,
13 Callable,
14 Iterable,
15 Iterator,
16 List,
17 Optional,
18 Sequence,
19 Tuple,
20 Union,
21 cast,
22 overload,
23 )
1124
1225 import natsort.compat.locale
1326 from natsort import utils
14 from natsort.ns_enum import NS_DUMB, ns
15
16
17 def decoder(encoding):
27 from natsort.ns_enum import NSType, NS_DUMB, ns
28 from natsort.utils import (
29 KeyType,
30 MaybeKeyType,
31 NatsortInType,
32 NatsortOutType,
33 StrBytesNum,
34 StrBytesPathNum,
35 )
36
37 # Common input and output types
38 Iter_ns = Iterable[NatsortInType]
39 Iter_any = Iterable[Any]
40 List_ns = List[NatsortInType]
41 List_any = List[Any]
42 List_int = List[int]
43
44 # The type that natsort_key returns
45 NatsortKeyType = Callable[[NatsortInType], NatsortOutType]
46
47 # Types for os_sorted
48 OSSortInType = Iterable[Optional[StrBytesPathNum]]
49 OSSortOutType = Tuple[Union[StrBytesNum, Tuple[StrBytesNum, ...]], ...]
50 OSSortKeyType = Callable[[Optional[StrBytesPathNum]], OSSortOutType]
51 Iter_path = Iterable[Optional[StrBytesPathNum]]
52 List_path = List[StrBytesPathNum]
53
54
55 def decoder(encoding: str) -> Callable[[NatsortInType], NatsortInType]:
1856 """
1957 Return a function that can be used to decode bytes to unicode.
2058
5593 return partial(utils.do_decoding, encoding=encoding)
5694
5795
58 def as_ascii(s):
96 def as_ascii(s: NatsortInType) -> NatsortInType:
5997 """
6098 Function to decode an input with the ASCII codec, or return as-is.
6199
78116 return utils.do_decoding(s, "ascii")
79117
80118
81 def as_utf8(s):
119 def as_utf8(s: NatsortInType) -> NatsortInType:
82120 """
83121 Function to decode an input with the UTF-8 codec, or return as-is.
84122
101139 return utils.do_decoding(s, "utf-8")
102140
103141
104 def natsort_keygen(key=None, alg=ns.DEFAULT):
142 def natsort_keygen(
143 key: MaybeKeyType = None, alg: NSType = ns.DEFAULT
144 ) -> NatsortKeyType:
105145 """
106146 Generate a key to sort strings and numbers naturally.
107147
211251 """
212252
213253
214 def natsorted(seq, key=None, reverse=False, alg=ns.DEFAULT):
254 @overload
255 def natsorted(
256 seq: Iter_ns, key: None = None, reverse: bool = False, alg: NSType = ns.DEFAULT
257 ) -> List_ns:
258 ...
259
260
261 @overload
262 def natsorted(
263 seq: Iter_any, key: KeyType, reverse: bool = False, alg: NSType = ns.DEFAULT
264 ) -> List_any:
265 ...
266
267
268 def natsorted(
269 seq: Iter_any,
270 key: MaybeKeyType = None,
271 reverse: bool = False,
272 alg: NSType = ns.DEFAULT,
273 ) -> List_any:
215274 """
216275 Sorts an iterable naturally.
217276
256315 ['num2', 'num3', 'num5']
257316
258317 """
259 key = natsort_keygen(key, alg)
260 return sorted(seq, reverse=reverse, key=key)
261
262
263 def humansorted(seq, key=None, reverse=False, alg=ns.DEFAULT):
318 return sorted(seq, reverse=reverse, key=natsort_keygen(key, alg))
319
320
321 @overload
322 def humansorted(
323 seq: Iter_ns, key: None = None, reverse: bool = False, alg: NSType = ns.DEFAULT
324 ) -> List_ns:
325 ...
326
327
328 @overload
329 def humansorted(
330 seq: Iter_any, key: KeyType, reverse: bool = False, alg: NSType = ns.DEFAULT
331 ) -> List_any:
332 ...
333
334
335 def humansorted(
336 seq: Iter_any,
337 key: MaybeKeyType = None,
338 reverse: bool = False,
339 alg: NSType = ns.DEFAULT,
340 ) -> List_any:
264341 """
265342 Convenience function to properly sort non-numeric characters.
266343
312389 return natsorted(seq, key, reverse, alg | ns.LOCALE)
313390
314391
315 def realsorted(seq, key=None, reverse=False, alg=ns.DEFAULT):
392 @overload
393 def realsorted(
394 seq: Iter_ns, key: None = None, reverse: bool = False, alg: NSType = ns.DEFAULT
395 ) -> List_ns:
396 ...
397
398
399 @overload
400 def realsorted(
401 seq: Iter_any, key: KeyType, reverse: bool = False, alg: NSType = ns.DEFAULT
402 ) -> List_any:
403 ...
404
405
406 def realsorted(
407 seq: Iter_any,
408 key: MaybeKeyType = None,
409 reverse: bool = False,
410 alg: NSType = ns.DEFAULT,
411 ) -> List_any:
316412 """
317413 Convenience function to properly sort signed floats.
318414
365461 return natsorted(seq, key, reverse, alg | ns.REAL)
366462
367463
368 def index_natsorted(seq, key=None, reverse=False, alg=ns.DEFAULT):
464 @overload
465 def index_natsorted(
466 seq: Iter_ns, key: None = None, reverse: bool = False, alg: NSType = ns.DEFAULT
467 ) -> List_int:
468 ...
469
470
471 @overload
472 def index_natsorted(
473 seq: Iter_any, key: KeyType, reverse: bool = False, alg: NSType = ns.DEFAULT
474 ) -> List_int:
475 ...
476
477
478 def index_natsorted(
479 seq: Iter_any,
480 key: MaybeKeyType = None,
481 reverse: bool = False,
482 alg: NSType = ns.DEFAULT,
483 ) -> List_int:
369484 """
370485 Determine the list of the indexes used to sort the input sequence.
371486
421536 ['baz', 'foo', 'bar']
422537
423538 """
539 newkey: KeyType
424540 if key is None:
425541 newkey = itemgetter(1)
426542 else:
427543
428 def newkey(x):
429 return key(itemgetter(1)(x))
544 def newkey(x: Any) -> NatsortInType:
545 return cast(KeyType, key)(itemgetter(1)(x))
430546
431547 # Pair the index and sequence together, then sort by element
432548 index_seq_pair = [(x, y) for x, y in enumerate(seq)]
434550 return [x for x, _ in index_seq_pair]
435551
436552
437 def index_humansorted(seq, key=None, reverse=False, alg=ns.DEFAULT):
553 @overload
554 def index_humansorted(
555 seq: Iter_ns, key: None = None, reverse: bool = False, alg: NSType = ns.DEFAULT
556 ) -> List_int:
557 ...
558
559
560 @overload
561 def index_humansorted(
562 seq: Iter_any, key: KeyType, reverse: bool = False, alg: NSType = ns.DEFAULT
563 ) -> List_int:
564 ...
565
566
567 def index_humansorted(
568 seq: Iter_any,
569 key: MaybeKeyType = None,
570 reverse: bool = False,
571 alg: NSType = ns.DEFAULT,
572 ) -> List_int:
438573 """
439574 This is a wrapper around ``index_natsorted(seq, alg=ns.LOCALE)``.
440575
483618 return index_natsorted(seq, key, reverse, alg | ns.LOCALE)
484619
485620
486 def index_realsorted(seq, key=None, reverse=False, alg=ns.DEFAULT):
621 @overload
622 def index_realsorted(
623 seq: Iter_ns, key: None = None, reverse: bool = False, alg: NSType = ns.DEFAULT
624 ) -> List_int:
625 ...
626
627
628 @overload
629 def index_realsorted(
630 seq: Iter_any, key: KeyType, reverse: bool = False, alg: NSType = ns.DEFAULT
631 ) -> List_int:
632 ...
633
634
635 def index_realsorted(
636 seq: Iter_any,
637 key: MaybeKeyType = None,
638 reverse: bool = False,
639 alg: NSType = ns.DEFAULT,
640 ) -> List_int:
487641 """
488642 This is a wrapper around ``index_natsorted(seq, alg=ns.REAL)``.
489643
529683
530684
531685 # noinspection PyShadowingBuiltins,PyUnresolvedReferences
532 def order_by_index(seq, index, iter=False):
686 def order_by_index(
687 seq: Sequence[Any], index: Iterable[int], iter: bool = False
688 ) -> Iter_any:
533689 """
534690 Order a given sequence by an index sequence.
535691
588744 return (seq[i] for i in index) if iter else [seq[i] for i in index]
589745
590746
591 def numeric_regex_chooser(alg):
747 def numeric_regex_chooser(alg: NSType) -> str:
592748 """
593749 Select an appropriate regex for the type of number of interest.
594750
607763 return utils.regex_chooser(alg).pattern[1:-1]
608764
609765
610 def _split_apply(v, key=None):
766 def _split_apply(v: Any, key: MaybeKeyType = None) -> Iterator[str]:
611767 if key is not None:
612768 v = key(v)
613769 return utils.path_splitter(str(v))
616772 # Choose the implementation based on the host OS
617773 if platform.system() == "Windows":
618774
619 from ctypes import wintypes, windll
775 from ctypes import wintypes, windll # type: ignore
620776 from functools import cmp_to_key
621777
622778 _windows_sort_cmp = windll.Shlwapi.StrCmpLogicalW
624780 _windows_sort_cmp.restype = wintypes.INT
625781 _winsort_key = cmp_to_key(_windows_sort_cmp)
626782
627 def os_sort_keygen(key=None):
628 return lambda x: tuple(map(_winsort_key, _split_apply(x, key)))
783 def os_sort_keygen(key: MaybeKeyType = None) -> OSSortKeyType:
784 return cast(
785 OSSortKeyType, lambda x: tuple(map(_winsort_key, _split_apply(x, key)))
786 )
629787
630788
631789 else:
644802
645803 except ImportError:
646804 # No ICU installed
647 def os_sort_keygen(key=None):
648 return natsort_keygen(
649 key=key, alg=ns.LOCALE | ns.PATH | ns.IGNORECASE
805 def os_sort_keygen(key: MaybeKeyType = None) -> OSSortKeyType:
806 return cast(
807 OSSortKeyType,
808 natsort_keygen(key=key, alg=ns.LOCALE | ns.PATH | ns.IGNORECASE),
650809 )
651810
652811 else:
653812 # ICU installed
654 def os_sort_keygen(key=None):
813 def os_sort_keygen(key: MaybeKeyType = None) -> OSSortKeyType:
655814 loc = natsort.compat.locale.get_icu_locale()
656815 collator = icu.Collator.createInstance(loc)
657816 collator.setAttribute(
698857 """
699858
700859
701 def os_sorted(seq, key=None, reverse=False):
860 @overload
861 def os_sorted(seq: Iter_path, key: None = None, reverse: bool = False) -> List_path:
862 ...
863
864
865 @overload
866 def os_sorted(seq: Iter_any, key: KeyType, reverse: bool = False) -> List_any:
867 ...
868
869
870 def os_sorted(
871 seq: Iter_any, key: MaybeKeyType = None, reverse: bool = False
872 ) -> List_any:
702873 """
703874 Sort elements in the same order as your operating system's file browser
704875
33 what algorithm natsort uses.
44 """
55
6 import collections
7
8 # The below are the base ns options. The values will be stored as powers
9 # of two so bitmasks can be used to extract the user's requested options.
10 enum_options = [
11 "FLOAT",
12 "SIGNED",
13 "NOEXP",
14 "PATH",
15 "LOCALEALPHA",
16 "LOCALENUM",
17 "IGNORECASE",
18 "LOWERCASEFIRST",
19 "GROUPLETTERS",
20 "UNGROUPLETTERS",
21 "NANLAST",
22 "COMPATIBILITYNORMALIZE",
23 "NUMAFTER",
24 ]
25
26 # Following were previously options but are now defaults.
27 enum_do_nothing = ["DEFAULT", "INT", "UNSIGNED"]
28
29 # The following are bitwise-OR combinations of other fields.
30 enum_combos = [("REAL", ("FLOAT", "SIGNED")), ("LOCALE", ("LOCALEALPHA", "LOCALENUM"))]
31
32 # The following are aliases for other fields.
33 enum_aliases = [
34 ("I", "INT"),
35 ("U", "UNSIGNED"),
36 ("F", "FLOAT"),
37 ("S", "SIGNED"),
38 ("R", "REAL"),
39 ("N", "NOEXP"),
40 ("P", "PATH"),
41 ("LA", "LOCALEALPHA"),
42 ("LN", "LOCALENUM"),
43 ("L", "LOCALE"),
44 ("IC", "IGNORECASE"),
45 ("LF", "LOWERCASEFIRST"),
46 ("G", "GROUPLETTERS"),
47 ("UG", "UNGROUPLETTERS"),
48 ("C", "UNGROUPLETTERS"),
49 ("CAPITALFIRST", "UNGROUPLETTERS"),
50 ("NL", "NANLAST"),
51 ("CN", "COMPATIBILITYNORMALIZE"),
52 ("NA", "NUMAFTER"),
53 ]
54
55 # Construct the list of bitwise distinct enums with their fields.
56 enum_fields = collections.OrderedDict(
57 (name, 1 << i) for i, name in enumerate(enum_options)
58 )
59 enum_fields.update((name, 0) for name in enum_do_nothing)
60
61 for name, combo in enum_combos:
62 combined_value = enum_fields[combo[0]]
63 for combo_name in combo[1:]:
64 combined_value |= enum_fields[combo_name]
65 enum_fields[name] = combined_value
66
67 enum_fields.update(
68 (alias, enum_fields[name]) for alias, name in enum_aliases
69 )
6 import enum
7 import itertools
8 import typing
709
7110
72 # Subclass the namedtuple to improve the docstring.
73 # noinspection PyUnresolvedReferences
74 class _NSEnum(collections.namedtuple("_NSEnum", enum_fields.keys())):
11 _counter = itertools.count(0)
12
13
14 class ns(enum.IntEnum): # noqa: N801
7515 """
7616 Enum to control the `natsort` algorithm.
7717
187127
188128 """
189129
130 # The below are the base ns options. The values will be stored as powers
131 # of two so bitmasks can be used to extract the user's requested options.
132 FLOAT = F = 1 << next(_counter)
133 SIGNED = S = 1 << next(_counter)
134 NOEXP = N = 1 << next(_counter)
135 PATH = P = 1 << next(_counter)
136 LOCALEALPHA = LA = 1 << next(_counter)
137 LOCALENUM = LN = 1 << next(_counter)
138 IGNORECASE = IC = 1 << next(_counter)
139 LOWERCASEFIRST = LF = 1 << next(_counter)
140 GROUPLETTERS = G = 1 << next(_counter)
141 UNGROUPLETTERS = CAPITALFIRST = C = UG = 1 << next(_counter)
142 NANLAST = NL = 1 << next(_counter)
143 COMPATIBILITYNORMALIZE = CN = 1 << next(_counter)
144 NUMAFTER = NA = 1 << next(_counter)
190145
191 # Here is where the instance of the ns enum that will be exported is created.
192 # It is a poor-man's singleton.
193 ns = _NSEnum(*enum_fields.values())
146 # Following were previously options but are now defaults.
147 DEFAULT = 0
148 INT = I = 0 # noqa: E741
149 UNSIGNED = U = 0
150
151 # The following are bitwise-OR combinations of other fields.
152 REAL = R = FLOAT | SIGNED
153 LOCALE = L = LOCALEALPHA | LOCALENUM
154
194155
195156 # The below is private for internal use only.
196157 NS_DUMB = 1 << 31
158
159 # An integer can be used in place of the ns enum so make the
160 # type to use for this enum a union of it and an inteter.
161 NSType = typing.Union[ns, int]
(New empty file)
3737 and thus has a slightly improved performance at runtime.
3838
3939 """
40
4140 import re
4241 from functools import partial, reduce
4342 from itertools import chain as ichain
4443 from operator import methodcaller
4544 from pathlib import PurePath
45 from typing import (
46 Any,
47 Callable,
48 Dict,
49 Iterable,
50 Iterator,
51 List,
52 Match,
53 Optional,
54 Pattern,
55 Tuple,
56 Union,
57 cast,
58 overload,
59 )
4660 from unicodedata import normalize
4761
4862 from natsort.compat.fastnumbers import fast_float, fast_int
49 from natsort.compat.locale import get_decimal_point, get_strxfrm, get_thousands_sep
50 from natsort.ns_enum import NS_DUMB, ns
63 from natsort.compat.locale import (
64 StrOrBytes,
65 get_decimal_point,
66 get_strxfrm,
67 get_thousands_sep,
68 )
69 from natsort.ns_enum import NSType, NS_DUMB, ns
5170 from natsort.unicode_numbers import digits_no_decimals, numeric_no_decimals
5271
72 #
73 # Pre-define a slew of aggregate types which makes the type hinting below easier
74 #
75 StrToStr = Callable[[str], str]
76 AnyCall = Callable[[Any], Any]
77
78 # For the bytes transform factory
79 BytesTuple = Tuple[bytes]
80 NestedBytesTuple = Tuple[Tuple[bytes]]
81 BytesTransform = Union[BytesTuple, NestedBytesTuple]
82 BytesTransformer = Callable[[bytes], BytesTransform]
83
84 # For the number transform factory
85 NumType = Union[float, int]
86 MaybeNumType = Optional[NumType]
87 NumTuple = Tuple[StrOrBytes, NumType]
88 NestedNumTuple = Tuple[NumTuple]
89 StrNumTuple = Tuple[Tuple[str], NumTuple]
90 NestedStrNumTuple = Tuple[StrNumTuple]
91 MaybeNumTransform = Union[NumTuple, NestedNumTuple, StrNumTuple, NestedStrNumTuple]
92 MaybeNumTransformer = Callable[[MaybeNumType], MaybeNumTransform]
93
94 # For the string component transform factory
95 StrBytesNum = Union[str, bytes, float, int]
96 StrTransformer = Callable[[str], StrBytesNum]
97
98 # For the final data transform factory
99 TwoBlankTuple = Tuple[Tuple[()], Tuple[()]]
100 TupleOfAny = Tuple[Any, ...]
101 TupleOfStrAnyPair = Tuple[Tuple[str], TupleOfAny]
102 FinalTransform = Union[TwoBlankTuple, TupleOfAny, TupleOfStrAnyPair]
103 FinalTransformer = Callable[[Iterable[Any], str], FinalTransform]
104
105 # For the string parsing factory
106 StrSplitter = Callable[[str], Iterable[str]]
107 StrParser = Callable[[str], FinalTransform]
108
109 # For the path splitter
110 PathArg = Union[str, PurePath]
111 MatchFn = Callable[[str], Optional[Match]]
112
113 # For the path parsing factory
114 PathSplitter = Callable[[PathArg], Tuple[FinalTransform, ...]]
115
116 # For the natsort key
117 StrBytesPathNum = Union[str, bytes, float, int, PurePath]
118 NatsortInType = Union[
119 Optional[StrBytesPathNum], Iterable[Union[Optional[StrBytesPathNum], Iterable[Any]]]
120 ]
121 NatsortOutType = Tuple[
122 Union[StrBytesNum, Tuple[Union[StrBytesNum, Tuple[Any, ...]], ...]], ...
123 ]
124 KeyType = Callable[[Any], NatsortInType]
125 MaybeKeyType = Optional[KeyType]
126
53127
54128 class NumericalRegularExpressions:
55129 """
61135 """
62136
63137 # All unicode numeric characters (minus the decimal characters).
64 numeric = numeric_no_decimals
138 numeric: str = numeric_no_decimals
65139 # All unicode digit characters (minus the decimal characters).
66 digits = digits_no_decimals
140 digits: str = digits_no_decimals
67141 # Regular expression to match exponential component of a float.
68 exp = r"(?:[eE][-+]?\d+)?"
142 exp: str = r"(?:[eE][-+]?\d+)?"
69143 # Regular expression to match a floating point number.
70 float_num = r"(?:\d+\.?\d*|\.\d+)"
144 float_num: str = r"(?:\d+\.?\d*|\.\d+)"
71145
72146 @classmethod
73 def _construct_regex(cls, fmt):
147 def _construct_regex(cls, fmt: str) -> Pattern[str]:
74148 """Given a format string, construct the regex with class attributes."""
75149 return re.compile(fmt.format(**vars(cls)), flags=re.U)
76150
77151 @classmethod
78 def int_sign(cls):
152 def int_sign(cls) -> Pattern[str]:
79153 """Regular expression to match a signed int."""
80154 return cls._construct_regex(r"([-+]?\d+|[{digits}])")
81155
82156 @classmethod
83 def int_nosign(cls):
157 def int_nosign(cls) -> Pattern[str]:
84158 """Regular expression to match an unsigned int."""
85159 return cls._construct_regex(r"(\d+|[{digits}])")
86160
87161 @classmethod
88 def float_sign_exp(cls):
162 def float_sign_exp(cls) -> Pattern[str]:
89163 """Regular expression to match a signed float with exponent."""
90164 return cls._construct_regex(r"([-+]?{float_num}{exp}|[{numeric}])")
91165
92166 @classmethod
93 def float_nosign_exp(cls):
167 def float_nosign_exp(cls) -> Pattern[str]:
94168 """Regular expression to match an unsigned float with exponent."""
95169 return cls._construct_regex(r"({float_num}{exp}|[{numeric}])")
96170
97171 @classmethod
98 def float_sign_noexp(cls):
172 def float_sign_noexp(cls) -> Pattern[str]:
99173 """Regular expression to match a signed float without exponent."""
100174 return cls._construct_regex(r"([-+]?{float_num}|[{numeric}])")
101175
102176 @classmethod
103 def float_nosign_noexp(cls):
177 def float_nosign_noexp(cls) -> Pattern[str]:
104178 """Regular expression to match an unsigned float without exponent."""
105179 return cls._construct_regex(r"({float_num}|[{numeric}])")
106180
107181
108 def regex_chooser(alg):
182 def regex_chooser(alg: NSType) -> Pattern[str]:
109183 """
110184 Select an appropriate regex for the type of number of interest.
111185
135209 }[alg]
136210
137211
138 def _no_op(x):
212 def _no_op(x: Any) -> Any:
139213 """A function that does nothing and returns the input as-is."""
140214 return x
141215
142216
143 def _normalize_input_factory(alg):
217 def _normalize_input_factory(alg: NSType) -> StrToStr:
144218 """
145219 Create a function that will normalize unicode input data.
146220
160234 return partial(normalize, normalization_form)
161235
162236
163 def natsort_key(val, key, string_func, bytes_func, num_func):
237 @overload
238 def natsort_key(
239 val: NatsortInType,
240 key: None,
241 string_func: Union[StrParser, PathSplitter],
242 bytes_func: BytesTransformer,
243 num_func: MaybeNumTransformer,
244 ) -> NatsortOutType:
245 ...
246
247
248 @overload
249 def natsort_key(
250 val: Any,
251 key: KeyType,
252 string_func: Union[StrParser, PathSplitter],
253 bytes_func: BytesTransformer,
254 num_func: MaybeNumTransformer,
255 ) -> NatsortOutType:
256 ...
257
258
259 def natsort_key(
260 val: Union[NatsortInType, Any],
261 key: MaybeKeyType,
262 string_func: Union[StrParser, PathSplitter],
263 bytes_func: BytesTransformer,
264 num_func: MaybeNumTransformer,
265 ) -> NatsortOutType:
164266 """
165267 Key to sort strings and numbers naturally.
166268
169271
170272 Parameters
171273 ----------
172 val : str | unicode | bytes | int | float | iterable
274 val : str | bytes | int | float | iterable
173275 key : callable | None
174276 A key to apply to the *val* before any other operations are performed.
175277 string_func : callable
209311
210312 # Assume the input are strings, which is the most common case
211313 try:
212 return string_func(val)
314 return string_func(cast(str, val))
213315 except (TypeError, AttributeError):
214316
215317 # If bytes type, use the bytes_func
216318 if type(val) in (bytes,):
217 return bytes_func(val)
319 return bytes_func(cast(bytes, val))
218320
219321 # Otherwise, assume it is an iterable that must be parsed recursively.
220322 # Do not apply the key recursively.
221323 try:
222324 return tuple(
223 natsort_key(x, None, string_func, bytes_func, num_func) for x in val
325 natsort_key(x, None, string_func, bytes_func, num_func)
326 for x in cast(Iterable[Any], val)
224327 )
225328
226329 # If that failed, it must be a number.
227330 except TypeError:
228 return num_func(val)
229
230
231 def parse_bytes_factory(alg):
331 return num_func(cast(NumType, val))
332
333
334 def parse_bytes_factory(alg: NSType) -> BytesTransformer:
232335 """
233336 Create a function that will format a *bytes* object into a tuple.
234337
261364 return lambda x: (x,)
262365
263366
264 def parse_number_or_none_factory(alg, sep, pre_sep):
367 def parse_number_or_none_factory(
368 alg: NSType, sep: StrOrBytes, pre_sep: str
369 ) -> MaybeNumTransformer:
265370 """
266371 Create a function that will format a number (or None) into a tuple.
267372
292397 """
293398 nan_replace = float("+inf") if alg & ns.NANLAST else float("-inf")
294399
295 def func(val, _nan_replace=nan_replace, _sep=sep):
400 def func(
401 val: MaybeNumType, _nan_replace: float = nan_replace, _sep: StrOrBytes = sep
402 ) -> NumTuple:
296403 """Given a number, place it in a tuple with a leading null string."""
297404 return _sep, (_nan_replace if val != val or val is None else val)
298405
308415
309416
310417 def parse_string_factory(
311 alg, sep, splitter, input_transform, component_transform, final_transform
312 ):
418 alg: NSType,
419 sep: StrOrBytes,
420 splitter: StrSplitter,
421 input_transform: StrToStr,
422 component_transform: StrTransformer,
423 final_transform: FinalTransformer,
424 ) -> StrParser:
313425 """
314426 Create a function that will split and format a *str* into a tuple.
315427
360472 original_func = input_transform if orig_after_xfrm else _no_op
361473 normalize_input = _normalize_input_factory(alg)
362474
363 def func(x):
475 def func(x: str) -> FinalTransform:
364476 # Apply string input transformation function and return to x.
365477 # Original function is usually a no-op, but some algorithms require it
366478 # to also be the transformation function.
367 x = normalize_input(x)
368 x, original = input_transform(x), original_func(x)
369 x = splitter(x) # Split string into components.
370 x = filter(None, x) # Remove empty strings.
371 x = map(component_transform, x) # Apply transform on components.
372 x = sep_inserter(x, sep) # Insert '' between numbers.
373 return final_transform(x, original) # Apply the final transform.
479 a = normalize_input(x)
480 b, original = input_transform(a), original_func(a)
481 c = splitter(b) # Split string into components.
482 d = filter(None, c) # Remove empty strings.
483 e = map(component_transform, d) # Apply transform on components.
484 f = sep_inserter(e, sep) # Insert '' between numbers.
485 return final_transform(f, original) # Apply the final transform.
374486
375487 return func
376488
377489
378 def parse_path_factory(str_split):
490 def parse_path_factory(str_split: StrParser) -> PathSplitter:
379491 """
380492 Create a function that will properly split and format a path.
381493
402514 return lambda x: tuple(map(str_split, path_splitter(x)))
403515
404516
405 def sep_inserter(iterable, sep):
406 """
407 Insert '' between numbers in an iterable.
408
409 Parameters
410 ----------
411 iterable
517 def sep_inserter(iterator: Iterator[Any], sep: StrOrBytes) -> Iterator[Any]:
518 """
519 Insert '' between numbers in an iterator.
520
521 Parameters
522 ----------
523 iterator
412524 sep : str
413525 The string character to be inserted between adjacent numeric objects.
414526
415527 Yields
416528 ------
417 The values of *iterable* in order, with *sep* inserted where adjacent
529 The values of *iterator* in order, with *sep* inserted where adjacent
418530 elements are numeric. If the first element in the input is numeric
419531 then *sep* will be the first value yielded.
420532
421533 """
422534 try:
423 # Get the first element. A StopIteration indicates an empty iterable.
535 # Get the first element. A StopIteration indicates an empty iterator.
424536 # Since we are controlling the types of the input, 'type' is used
425537 # instead of 'isinstance' for the small speed advantage it offers.
426538 types = (int, float)
427 first = next(iterable)
539 first = next(iterator)
428540 if type(first) in types:
429541 yield sep
430542 yield first
431543
432544 # Now, check if pair of elements are both numbers. If so, add ''.
433 second = next(iterable)
545 second = next(iterator)
434546 if type(first) in types and type(second) in types:
435547 yield sep
436548 yield second
437549
438550 # Now repeat in a loop.
439 for x in iterable:
551 for x in iterator:
440552 first, second = second, x
441553 if type(first) in types and type(second) in types:
442554 yield sep
447559 return
448560
449561
450 def input_string_transform_factory(alg):
562 def input_string_transform_factory(alg: NSType) -> StrToStr:
451563 """
452564 Create a function to transform a string.
453565
472584 dumb = alg & NS_DUMB
473585
474586 # Build the chain of functions to execute in order.
475 function_chain = []
587 function_chain: List[StrToStr] = []
476588 if (dumb and not lowfirst) or (lowfirst and not dumb):
477589 function_chain.append(methodcaller("swapcase"))
478590
501613 strip_thousands = strip_thousands.format(
502614 thou=re.escape(get_thousands_sep()), nodecimal=nodecimal
503615 )
504 strip_thousands = re.compile(strip_thousands, flags=re.VERBOSE)
505 function_chain.append(partial(strip_thousands.sub, ""))
616 strip_thousands_re = re.compile(strip_thousands, flags=re.VERBOSE)
617 function_chain.append(partial(strip_thousands_re.sub, ""))
506618
507619 # Create a regular expression that will change the decimal point to
508620 # a period if not already a period.
510622 if alg & ns.FLOAT and decimal != ".":
511623 switch_decimal = r"(?<=[0-9]){decimal}|{decimal}(?=[0-9])"
512624 switch_decimal = switch_decimal.format(decimal=re.escape(decimal))
513 switch_decimal = re.compile(switch_decimal)
514 function_chain.append(partial(switch_decimal.sub, "."))
625 switch_decimal_re = re.compile(switch_decimal)
626 function_chain.append(partial(switch_decimal_re.sub, "."))
515627
516628 # Return the chained functions.
517629 return chain_functions(function_chain)
518630
519631
520 def string_component_transform_factory(alg):
632 def string_component_transform_factory(alg: NSType) -> StrTransformer:
521633 """
522634 Create a function to either transform a string or convert to a number.
523635
544656 nan_val = float("+inf") if alg & ns.NANLAST else float("-inf")
545657
546658 # Build the chain of functions to execute in order.
547 func_chain = []
659 func_chain: List[Callable[[str], StrOrBytes]] = []
548660 if group_letters:
549661 func_chain.append(groupletters)
550662 if use_locale:
551663 func_chain.append(get_strxfrm())
664
665 # Return the correct chained functions.
666 kwargs: Dict[str, Union[float, Callable[[str], StrOrBytes]]]
552667 kwargs = {"key": chain_functions(func_chain)} if func_chain else {}
553
554 # Return the correct chained functions.
555668 if alg & ns.FLOAT:
556669 # noinspection PyTypeChecker
557670 kwargs["nan"] = nan_val
558 return partial(fast_float, **kwargs)
671 return cast(Callable[[str], StrOrBytes], partial(fast_float, **kwargs))
559672 else:
560 return partial(fast_int, **kwargs)
561
562
563 def final_data_transform_factory(alg, sep, pre_sep):
673 return cast(Callable[[str], StrOrBytes], partial(fast_int, **kwargs))
674
675
676 def final_data_transform_factory(
677 alg: NSType, sep: StrOrBytes, pre_sep: str
678 ) -> FinalTransformer:
564679 """
565680 Create a function to transform a tuple.
566681
588703 """
589704 if alg & ns.UNGROUPLETTERS and alg & ns.LOCALEALPHA:
590705 swap = alg & NS_DUMB and alg & ns.LOWERCASEFIRST
591 transform = methodcaller("swapcase") if swap else _no_op
592
593 def func(split_val, val, _transform=transform, _sep=sep, _pre_sep=pre_sep):
706 transform = cast(StrToStr, methodcaller("swapcase")) if swap else _no_op
707
708 def func(
709 split_val: Iterable[NatsortInType],
710 val: str,
711 _transform: StrToStr = transform,
712 _sep: StrOrBytes = sep,
713 _pre_sep: str = pre_sep,
714 ) -> FinalTransform:
594715 """
595716 Return a tuple with the first character of the first element
596717 of the return value as the first element, and the return value
605726 else:
606727 return (_transform(val[0]),), split_val
607728
608 return func
609729 else:
610 return lambda split_val, val: tuple(split_val)
611
612
613 lower_function = methodcaller("casefold")
730
731 def func(
732 split_val: Iterable[NatsortInType],
733 val: str,
734 _transform: StrToStr = _no_op,
735 _sep: StrOrBytes = sep,
736 _pre_sep: str = pre_sep,
737 ) -> FinalTransform:
738 return tuple(split_val)
739
740 return func
741
742
743 lower_function: StrToStr = cast(StrToStr, methodcaller("casefold"))
614744
615745
616746 # noinspection PyIncorrectDocstring
617 def groupletters(x, _low=lower_function):
747 def groupletters(x: str, _low: StrToStr = lower_function) -> str:
618748 """
619749 Double all characters, making doubled letters lowercase.
620750
636766 return "".join(ichain.from_iterable((_low(y), y) for y in x))
637767
638768
639 def chain_functions(functions):
769 def chain_functions(functions: Iterable[AnyCall]) -> AnyCall:
640770 """
641771 Chain a list of single-argument functions together and return.
642772
673803 return partial(reduce, lambda res, f: f(res), functions)
674804
675805
676 def do_decoding(s, encoding):
806 @overload
807 def do_decoding(s: bytes, encoding: str) -> str:
808 ...
809
810
811 @overload
812 def do_decoding(s: NatsortInType, encoding: str) -> NatsortInType:
813 ...
814
815
816 def do_decoding(s: NatsortInType, encoding: str) -> NatsortInType:
677817 """
678818 Helper to decode a *bytes* object, or return the object as-is.
679819
691831
692832 """
693833 try:
694 return s.decode(encoding)
834 return cast(bytes, s).decode(encoding)
695835 except (AttributeError, TypeError):
696836 return s
697837
698838
699839 # noinspection PyIncorrectDocstring
700 def path_splitter(s, _d_match=re.compile(r"\.\d").match):
840 def path_splitter(
841 s: PathArg, _d_match: MatchFn = re.compile(r"\.\d").match
842 ) -> Iterator[str]:
701843 """
702844 Split a string into its path components.
703845
0 Metadata-Version: 2.1
1 Name: natsort
2 Version: 8.0.0
3 Summary: Simple yet flexible natural sorting in Python.
4 Home-page: https://github.com/SethMMorton/natsort
5 Author: Seth M. Morton
6 Author-email: drtuba78@gmail.com
7 License: MIT
8 Platform: UNKNOWN
9 Classifier: Development Status :: 5 - Production/Stable
10 Classifier: Intended Audience :: Developers
11 Classifier: Intended Audience :: Science/Research
12 Classifier: Intended Audience :: System Administrators
13 Classifier: Intended Audience :: Information Technology
14 Classifier: Intended Audience :: Financial and Insurance Industry
15 Classifier: Operating System :: OS Independent
16 Classifier: License :: OSI Approved :: MIT License
17 Classifier: Natural Language :: English
18 Classifier: Programming Language :: Python
19 Classifier: Programming Language :: Python :: 3
20 Classifier: Programming Language :: Python :: 3.6
21 Classifier: Programming Language :: Python :: 3.7
22 Classifier: Programming Language :: Python :: 3.8
23 Classifier: Programming Language :: Python :: 3.9
24 Classifier: Programming Language :: Python :: 3.10
25 Classifier: Topic :: Scientific/Engineering :: Information Analysis
26 Classifier: Topic :: Utilities
27 Classifier: Topic :: Text Processing
28 Requires-Python: >=3.6
29 Description-Content-Type: text/x-rst
30 Provides-Extra: fast
31 Provides-Extra: icu
32 License-File: LICENSE
33
34 natsort
35 =======
36
37 .. image:: https://img.shields.io/pypi/v/natsort.svg
38 :target: https://pypi.org/project/natsort/
39
40 .. image:: https://img.shields.io/pypi/pyversions/natsort.svg
41 :target: https://pypi.org/project/natsort/
42
43 .. image:: https://img.shields.io/pypi/l/natsort.svg
44 :target: https://github.com/SethMMorton/natsort/blob/master/LICENSE
45
46 .. image:: https://github.com/SethMMorton/natsort/workflows/Tests/badge.svg
47 :target: https://github.com/SethMMorton/natsort/actions
48
49 .. image:: https://codecov.io/gh/SethMMorton/natsort/branch/master/graph/badge.svg
50 :target: https://codecov.io/gh/SethMMorton/natsort
51
52 Simple yet flexible natural sorting in Python.
53
54 - Source Code: https://github.com/SethMMorton/natsort
55 - Downloads: https://pypi.org/project/natsort/
56 - Documentation: https://natsort.readthedocs.io/
57
58 - `Examples and Recipes <https://natsort.readthedocs.io/en/master/examples.html>`_
59 - `How Does Natsort Work? <https://natsort.readthedocs.io/en/master/howitworks.html>`_
60 - `API <https://natsort.readthedocs.io/en/master/api.html>`_
61
62 - `Quick Description`_
63 - `Quick Examples`_
64 - `FAQ`_
65 - `Requirements`_
66 - `Optional Dependencies`_
67 - `Installation`_
68 - `How to Run Tests`_
69 - `How to Build Documentation`_
70 - `Deprecation Schedule`_
71 - `History`_
72
73 **NOTE**: Please see the `Deprecation Schedule`_ section for changes in
74 ``natsort`` version 7.0.0.
75
76 Quick Description
77 -----------------
78
79 When you try to sort a list of strings that contain numbers, the normal python
80 sort algorithm sorts lexicographically, so you might not get the results that
81 you expect:
82
83 .. code-block:: pycon
84
85 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
86 >>> sorted(a)
87 ['1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '2 ft 7 in', '7 ft 6 in']
88
89 Notice that it has the order ('1', '10', '2') - this is because the list is
90 being sorted in lexicographical order, which sorts numbers like you would
91 letters (i.e. 'b', 'ba', 'c').
92
93 ``natsort`` provides a function ``natsorted`` that helps sort lists
94 "naturally" ("naturally" is rather ill-defined, but in general it means
95 sorting based on meaning and not computer code point).
96 Using ``natsorted`` is simple:
97
98 .. code-block:: pycon
99
100 >>> from natsort import natsorted
101 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
102 >>> natsorted(a)
103 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
104
105 ``natsorted`` identifies numbers anywhere in a string and sorts them
106 naturally. Below are some other things you can do with ``natsort``
107 (also see the `examples <https://natsort.readthedocs.io/en/master/examples.html>`_
108 for a quick start guide, or the
109 `api <https://natsort.readthedocs.io/en/master/api.html>`_ for complete details).
110
111 **Note**: ``natsorted`` is designed to be a drop-in replacement for the
112 built-in ``sorted`` function. Like ``sorted``, ``natsorted``
113 `does not sort in-place`. To sort a list and assign the output to the same
114 variable, you must explicitly assign the output to a variable:
115
116 .. code-block:: pycon
117
118 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
119 >>> natsorted(a)
120 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
121 >>> print(a) # 'a' was not sorted; "natsorted" simply returned a sorted list
122 ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
123 >>> a = natsorted(a) # Now 'a' will be sorted because the sorted list was assigned to 'a'
124 >>> print(a)
125 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
126
127 Please see `Generating a Reusable Sorting Key and Sorting In-Place`_ for
128 an alternate way to sort in-place naturally.
129
130 Quick Examples
131 --------------
132
133 - `Sorting Versions`_
134 - `Sort Paths Like My File Browser (e.g. Windows Explorer on Windows)`_
135 - `Sorting by Real Numbers (i.e. Signed Floats)`_
136 - `Locale-Aware Sorting (or "Human Sorting")`_
137 - `Further Customizing Natsort`_
138 - `Sorting Mixed Types`_
139 - `Handling Bytes on Python 3`_
140 - `Generating a Reusable Sorting Key and Sorting In-Place`_
141 - `Other Useful Things`_
142
143 Sorting Versions
144 ++++++++++++++++
145
146 ``natsort`` does not actually *comprehend* version numbers.
147 It just so happens that the most common versioning schemes are designed to
148 work with standard natural sorting techniques; these schemes include
149 ``MAJOR.MINOR``, ``MAJOR.MINOR.PATCH``, ``YEAR.MONTH.DAY``. If your data
150 conforms to a scheme like this, then it will work out-of-the-box with
151 ``natsorted`` (as of ``natsort`` version >= 4.0.0):
152
153 .. code-block:: pycon
154
155 >>> a = ['version-1.9', 'version-2.0', 'version-1.11', 'version-1.10']
156 >>> natsorted(a)
157 ['version-1.9', 'version-1.10', 'version-1.11', 'version-2.0']
158
159 If you need to versions that use a more complicated scheme, please see
160 `these examples <https://natsort.readthedocs.io/en/master/examples.html#rc-sorting>`_.
161
162 Sort Paths Like My File Browser (e.g. Windows Explorer on Windows)
163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
164
165 Prior to ``natsort`` version 7.1.0, it was a common request to be able to
166 sort paths like Windows Explorer. As of ``natsort`` 7.1.0, the function
167 ``os_sorted`` has been added to provide users the ability to sort
168 in the order that their file browser might sort (e.g Windows Explorer on
169 Windows, Finder on MacOS, Dolphin/Nautilus/Thunar/etc. on Linux).
170
171 .. code-block:: python
172
173 import os
174 from natsort import os_sorted
175 print(os_sorted(os.listdir()))
176 # The directory sorted like your file browser might show
177
178 Output will be different depending on the operating system you are on.
179
180 For users **not** on Windows (e.g. MacOS/Linux) it is **strongly** recommended
181 to also install `PyICU <https://pypi.org/project/PyICU>`_, which will help
182 ``natsort`` give results that match most file browsers. If this is not installed,
183 it will fall back on Python's built-in ``locale`` module and will give good
184 results for most input, but will give poor results for special characters.
185
186 Sorting by Real Numbers (i.e. Signed Floats)
187 ++++++++++++++++++++++++++++++++++++++++++++
188
189 This is useful in scientific data analysis (and was
190 the default behavior of ``natsorted`` for ``natsort``
191 version < 4.0.0). Use the ``realsorted`` function:
192
193 .. code-block:: pycon
194
195 >>> from natsort import realsorted, ns
196 >>> # Note that when interpreting as signed floats, the below numbers are
197 >>> # +5.10, -3.00, +5.30, +2.00
198 >>> a = ['position5.10.data', 'position-3.data', 'position5.3.data', 'position2.data']
199 >>> natsorted(a)
200 ['position2.data', 'position5.3.data', 'position5.10.data', 'position-3.data']
201 >>> natsorted(a, alg=ns.REAL)
202 ['position-3.data', 'position2.data', 'position5.10.data', 'position5.3.data']
203 >>> realsorted(a) # shortcut for natsorted with alg=ns.REAL
204 ['position-3.data', 'position2.data', 'position5.10.data', 'position5.3.data']
205
206 Locale-Aware Sorting (or "Human Sorting")
207 +++++++++++++++++++++++++++++++++++++++++
208
209 This is where the non-numeric characters are also ordered based on their
210 meaning, not on their ordinal value, and a locale-dependent thousands
211 separator and decimal separator is accounted for in the number.
212 This can be achieved with the ``humansorted`` function:
213
214 .. code-block:: pycon
215
216 >>> a = ['Apple', 'apple15', 'Banana', 'apple14,689', 'banana']
217 >>> natsorted(a)
218 ['Apple', 'Banana', 'apple14,689', 'apple15', 'banana']
219 >>> import locale
220 >>> locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
221 'en_US.UTF-8'
222 >>> natsorted(a, alg=ns.LOCALE)
223 ['apple15', 'apple14,689', 'Apple', 'banana', 'Banana']
224 >>> from natsort import humansorted
225 >>> humansorted(a) # shortcut for natsorted with alg=ns.LOCALE
226 ['apple15', 'apple14,689', 'Apple', 'banana', 'Banana']
227
228 You may find you need to explicitly set the locale to get this to work
229 (as shown in the example).
230 Please see `locale issues <https://natsort.readthedocs.io/en/master/locale_issues.html>`_ and the
231 `Optional Dependencies`_ section below before using the ``humansorted`` function.
232
233 Further Customizing Natsort
234 +++++++++++++++++++++++++++
235
236 If you need to combine multiple algorithm modifiers (such as ``ns.REAL``,
237 ``ns.LOCALE``, and ``ns.IGNORECASE``), you can combine the options using the
238 bitwise OR operator (``|``). For example,
239
240 .. code-block:: pycon
241
242 >>> a = ['Apple', 'apple15', 'Banana', 'apple14,689', 'banana']
243 >>> natsorted(a, alg=ns.REAL | ns.LOCALE | ns.IGNORECASE)
244 ['Apple', 'apple15', 'apple14,689', 'Banana', 'banana']
245 >>> # The ns enum provides long and short forms for each option.
246 >>> ns.LOCALE == ns.L
247 True
248 >>> # You can also customize the convenience functions, too.
249 >>> natsorted(a, alg=ns.REAL | ns.LOCALE | ns.IGNORECASE) == realsorted(a, alg=ns.L | ns.IC)
250 True
251 >>> natsorted(a, alg=ns.REAL | ns.LOCALE | ns.IGNORECASE) == humansorted(a, alg=ns.R | ns.IC)
252 True
253
254 All of the available customizations can be found in the documentation for
255 `the ns enum <https://natsort.readthedocs.io/en/master/api.html#natsort.ns>`_.
256
257 You can also add your own custom transformation functions with the ``key``
258 argument. These can be used with ``alg`` if you wish.
259
260 .. code-block:: pycon
261
262 >>> a = ['apple2.50', '2.3apple']
263 >>> natsorted(a, key=lambda x: x.replace('apple', ''), alg=ns.REAL)
264 ['2.3apple', 'apple2.50']
265
266 Sorting Mixed Types
267 +++++++++++++++++++
268
269 You can mix and match ``int``, ``float``, and ``str`` (or ``unicode``) types
270 when you sort:
271
272 .. code-block:: pycon
273
274 >>> a = ['4.5', 6, 2.0, '5', 'a']
275 >>> natsorted(a)
276 [2.0, '4.5', '5', 6, 'a']
277 >>> # On Python 2, sorted(a) would return [2.0, 6, '4.5', '5', 'a']
278 >>> # On Python 3, sorted(a) would raise an "unorderable types" TypeError
279
280 Handling Bytes on Python 3
281 ++++++++++++++++++++++++++
282
283 ``natsort`` does not officially support the `bytes` type on Python 3, but
284 convenience functions are provided that help you decode to `str` first:
285
286 .. code-block:: pycon
287
288 >>> from natsort import as_utf8
289 >>> a = [b'a', 14.0, 'b']
290 >>> # On Python 2, natsorted(a) would would work as expected.
291 >>> # On Python 3, natsorted(a) would raise a TypeError (bytes() < str())
292 >>> natsorted(a, key=as_utf8) == [14.0, b'a', 'b']
293 True
294 >>> a = [b'a56', b'a5', b'a6', b'a40']
295 >>> # On Python 2, natsorted(a) would would work as expected.
296 >>> # On Python 3, natsorted(a) would return the same results as sorted(a)
297 >>> natsorted(a, key=as_utf8) == [b'a5', b'a6', b'a40', b'a56']
298 True
299
300 Generating a Reusable Sorting Key and Sorting In-Place
301 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
302
303 Under the hood, ``natsorted`` works by generating a custom sorting
304 key using ``natsort_keygen`` and then passes that to the built-in
305 ``sorted``. You can use the ``natsort_keygen`` function yourself to
306 generate a custom sorting key to sort in-place using the ``list.sort``
307 method.
308
309 .. code-block:: pycon
310
311 >>> from natsort import natsort_keygen
312 >>> natsort_key = natsort_keygen()
313 >>> a = ['2 ft 7 in', '1 ft 5 in', '10 ft 2 in', '2 ft 11 in', '7 ft 6 in']
314 >>> natsorted(a) == sorted(a, key=natsort_key)
315 True
316 >>> a.sort(key=natsort_key)
317 >>> a
318 ['1 ft 5 in', '2 ft 7 in', '2 ft 11 in', '7 ft 6 in', '10 ft 2 in']
319
320 All of the algorithm customizations mentioned in the
321 `Further Customizing Natsort`_ section can also be applied to
322 ``natsort_keygen`` through the *alg* keyword option.
323
324 Other Useful Things
325 +++++++++++++++++++
326
327 - recursively descend into lists of lists
328 - automatic unicode normalization of input data
329 - `controlling the case-sensitivity <https://natsort.readthedocs.io/en/master/examples.html#case-sort>`_
330 - `sorting file paths correctly <https://natsort.readthedocs.io/en/master/examples.html#path-sort>`_
331 - `allow custom sorting keys <https://natsort.readthedocs.io/en/master/examples.html#custom-sort>`_
332 - `accounting for units <https://natsort.readthedocs.io/en/master/examples.html#accounting-for-units-when-sorting>`_
333
334 FAQ
335 ---
336
337 How do I debug ``natsort.natsorted()``?
338 The best way to debug ``natsorted()`` is to generate a key using ``natsort_keygen()``
339 with the same options being passed to ``natsorted``. One can take a look at
340 exactly what is being done with their input using this key - it is highly
341 recommended
342 to `look at this issue describing how to debug <https://github.com/SethMMorton/natsort/issues/13#issuecomment-50422375>`_
343 for *how* to debug, and also to review the
344 `How Does Natsort Work? <https://natsort.readthedocs.io/en/master/howitworks.html>`_
345 page for *why* ``natsort`` is doing that to your data.
346
347 If you are trying to sort custom classes and running into trouble, please
348 take a look at https://github.com/SethMMorton/natsort/issues/60. In short,
349 custom classes are not likely to be sorted correctly if one relies
350 on the behavior of ``__lt__`` and the other rich comparison operators in
351 their custom class - it is better to use a ``key`` function with
352 ``natsort``, or use the ``natsort`` key as part of your rich comparison
353 operator definition.
354
355 ``natsort`` gave me results I didn't expect, and it's a terrible library!
356 Did you try to debug using the above advice? If so, and you still cannot figure out
357 the error, then please `file an issue <https://github.com/SethMMorton/natsort/issues/new>`_.
358
359 How *does* ``natsort`` work?
360 If you don't want to read `How Does Natsort Work? <https://natsort.readthedocs.io/en/master/howitworks.html>`_,
361 here is a quick primer.
362
363 ``natsort`` provides a `key function <https://docs.python.org/3/howto/sorting.html#key-functions>`_
364 that can be passed to `list.sort() <https://docs.python.org/3/library/stdtypes.html#list.sort>`_
365 or `sorted() <https://docs.python.org/3/library/functions.html#sorted>`_ in order to
366 modify the default sorting behavior. This key is generated on-demand with
367 the key generator ``natsort.natsort_keygen()``. ``natsort.natsorted()``
368 is essentially a wrapper for the following code:
369
370 .. code-block:: pycon
371
372 >>> from natsort import natsort_keygen
373 >>> natsort_key = natsort_keygen()
374 >>> sorted(['1', '10', '2'], key=natsort_key)
375 ['1', '2', '10']
376
377 Users can further customize ``natsort`` sorting behavior with the ``key``
378 and/or ``alg`` options (see details in the `Further Customizing Natsort`_
379 section).
380
381 The key generated by ``natsort_keygen`` *always* returns a ``tuple``. It
382 does so in the following way (*some details omitted for clarity*):
383
384 1. Assume the input is a string, and attempt to split it into numbers and
385 non-numbers using regular expressions. Numbers are then converted into
386 either ``int`` or ``float``.
387 2. If the above fails because the input is not a string, assume the input
388 is some other sequence (e.g. ``list`` or ``tuple``), and recursively
389 apply the key to each element of the sequence.
390 3. If the above fails because the input is not iterable, assume the input
391 is an ``int`` or ``float``, and just return the input in a ``tuple``.
392
393 Because a ``tuple`` is always returned, a ``TypeError`` should not be common
394 unless one tries to do something odd like sort an ``int`` against a ``list``.
395
396 Shell script
397 ------------
398
399 ``natsort`` comes with a shell script called ``natsort``, or can also be called
400 from the command line with ``python -m natsort``.
401
402 Requirements
403 ------------
404
405 ``natsort`` requires Python 3.6 or greater.
406
407 Optional Dependencies
408 ---------------------
409
410 fastnumbers
411 +++++++++++
412
413 The most efficient sorting can occur if you install the
414 `fastnumbers <https://pypi.org/project/fastnumbers>`_ package
415 (version >=2.0.0); it helps with the string to number conversions.
416 ``natsort`` will still run (efficiently) without the package, but if you need
417 to squeeze out that extra juice it is recommended you include this as a
418 dependency. ``natsort`` will not require (or check) that
419 `fastnumbers <https://pypi.org/project/fastnumbers>`_ is installed
420 at installation.
421
422 PyICU
423 +++++
424
425 It is recommended that you install `PyICU <https://pypi.org/project/PyICU>`_
426 if you wish to sort in a locale-dependent manner, see
427 https://natsort.readthedocs.io/en/master/locale_issues.html for an explanation why.
428
429 Installation
430 ------------
431
432 Use ``pip``!
433
434 .. code-block:: console
435
436 $ pip install natsort
437
438 If you want to install the `Optional Dependencies`_, you can use the
439 `"extras" notation <https://packaging.python.org/tutorials/installing-packages/#installing-setuptools-extras>`_
440 at installation time to install those dependencies as well - use ``fast`` for
441 `fastnumbers <https://pypi.org/project/fastnumbers>`_ and ``icu`` for
442 `PyICU <https://pypi.org/project/PyICU>`_.
443
444 .. code-block:: console
445
446 # Install both optional dependencies.
447 $ pip install natsort[fast,icu]
448 # Install just fastnumbers
449 $ pip install natsort[fast]
450
451 How to Run Tests
452 ----------------
453
454 Please note that ``natsort`` is NOT set-up to support ``python setup.py test``.
455
456 The recommended way to run tests is with `tox <https://tox.readthedocs.io/en/latest/>`_.
457 After installing ``tox``, running tests is as simple as executing the following
458 in the ``natsort`` directory:
459
460 .. code-block:: console
461
462 $ tox
463
464 ``tox`` will create virtual a virtual environment for your tests and install
465 all the needed testing requirements for you. You can specify a particular
466 python version with the ``-e`` flag, e.g. ``tox -e py36``. Static analysis
467 is done with ``tox -e flake8``. You can see all available testing environments
468 with ``tox --listenvs``.
469
470 How to Build Documentation
471 --------------------------
472
473 If you want to build the documentation for ``natsort``, it is recommended to
474 use ``tox``:
475
476 .. code-block:: console
477
478 $ tox -e docs
479
480 This will place the documentation in ``build/sphinx/html``.
481
482 Deprecation Schedule
483 --------------------
484
485 Dropped Python 3.4 and Python 3.5 Support
486 +++++++++++++++++++++++++++++++++++++++++
487
488 ``natsort`` version 8.0.0 dropped support for Python < 3.6.
489
490 Dropped Python 2.7 Support
491 ++++++++++++++++++++++++++
492
493 ``natsort`` version 7.0.0 dropped support for Python 2.7.
494
495 The version 6.X branch will remain as a "long term support" branch where bug
496 fixes are applied so that users who cannot update from Python 2.7 will not be
497 forced to use a buggy ``natsort`` version (bug fixes will need to be requested;
498 by default only the 7.X branch will be updated).
499 New features would not be added to version 6.X, only bug fixes.
500
501 Dropped Deprecated APIs
502 +++++++++++++++++++++++
503
504 In ``natsort`` version 6.0.0, the following APIs and functions were removed
505
506 - ``number_type`` keyword argument (deprecated since 3.4.0)
507 - ``signed`` keyword argument (deprecated since 3.4.0)
508 - ``exp`` keyword argument (deprecated since 3.4.0)
509 - ``as_path`` keyword argument (deprecated since 3.4.0)
510 - ``py3_safe`` keyword argument (deprecated since 3.4.0)
511 - ``ns.TYPESAFE`` (deprecated since version 5.0.0)
512 - ``ns.DIGIT`` (deprecated since version 5.0.0)
513 - ``ns.VERSION`` (deprecated since version 5.0.0)
514 - ``versorted()`` (discouraged since version 4.0.0,
515 officially deprecated since version 5.5.0)
516 - ``index_versorted()`` (discouraged since version 4.0.0,
517 officially deprecated since version 5.5.0)
518
519 In general, if you want to determine if you are using deprecated APIs you
520 can run your code with the following flag
521
522 .. code-block:: console
523
524 $ python -Wdefault::DeprecationWarning my-code.py
525
526 By default ``DeprecationWarnings`` are not shown, but this will cause them
527 to be shown. Alternatively, you can just set the environment variable
528 ``PYTHONWARNINGS`` to "default::DeprecationWarning" and then run your code.
529
530 Author
531 ------
532
533 Seth M. Morton
534
535 History
536 -------
537
538 Please visit the changelog
539 `on GitHub <https://github.com/SethMMorton/natsort/blob/master/CHANGELOG.md>`_ or
540 `in the documentation <https://natsort.readthedocs.io/en/master/changelog.html>`_.
541
542
0 CHANGELOG.md
1 LICENSE
2 MANIFEST.in
3 README.rst
4 RELEASING.md
5 setup.cfg
6 setup.py
7 tox.ini
8 dev/README.md
9 dev/bump.py
10 dev/clean.py
11 dev/generate_new_unicode_numbers.py
12 docs/api.rst
13 docs/changelog.rst
14 docs/conf.py
15 docs/examples.rst
16 docs/howitworks.rst
17 docs/index.rst
18 docs/locale_issues.rst
19 docs/requirements.in
20 docs/requirements.txt
21 docs/shell.rst
22 docs/special_cases_everywhere.jpg
23 natsort/__init__.py
24 natsort/__main__.py
25 natsort/natsort.py
26 natsort/ns_enum.py
27 natsort/py.typed
28 natsort/unicode_numbers.py
29 natsort/unicode_numeric_hex.py
30 natsort/utils.py
31 natsort.egg-info/PKG-INFO
32 natsort.egg-info/SOURCES.txt
33 natsort.egg-info/dependency_links.txt
34 natsort.egg-info/entry_points.txt
35 natsort.egg-info/not-zip-safe
36 natsort.egg-info/requires.txt
37 natsort.egg-info/top_level.txt
38 natsort/compat/__init__.py
39 natsort/compat/fake_fastnumbers.py
40 natsort/compat/fastnumbers.py
41 natsort/compat/locale.py
42 tests/conftest.py
43 tests/profile_natsorted.py
44 tests/test_fake_fastnumbers.py
45 tests/test_final_data_transform_factory.py
46 tests/test_input_string_transform_factory.py
47 tests/test_main.py
48 tests/test_natsort_key.py
49 tests/test_natsort_keygen.py
50 tests/test_natsorted.py
51 tests/test_natsorted_convenience.py
52 tests/test_ns_enum.py
53 tests/test_os_sorted.py
54 tests/test_parse_bytes_function.py
55 tests/test_parse_number_function.py
56 tests/test_parse_string_function.py
57 tests/test_regex.py
58 tests/test_string_component_transform_factory.py
59 tests/test_unicode_numbers.py
60 tests/test_utils.py
0 [console_scripts]
1 natsort = natsort.__main__:main
2
0
1 [fast]
2 fastnumbers>=2.0.0
3
4 [icu]
5 PyICU>=1.0.0
00 [bumpversion]
1 current_version = 7.1.0
1 current_version = 8.0.0
22 commit = True
33 tag = True
44 tag_name = {new_version}
2424 Natural Language :: English
2525 Programming Language :: Python
2626 Programming Language :: Python :: 3
27 Programming Language :: Python :: 3.4
28 Programming Language :: Python :: 3.5
2927 Programming Language :: Python :: 3.6
3028 Programming Language :: Python :: 3.7
3129 Programming Language :: Python :: 3.8
3230 Programming Language :: Python :: 3.9
31 Programming Language :: Python :: 3.10
3332 Topic :: Scientific/Engineering :: Information Analysis
3433 Topic :: Utilities
3534 Topic :: Text Processing
6362 docs,
6463 .venv
6564
65 [mypy]
66
67 [mypy-icu]
68 ignore_missing_imports = True
69
70 [egg_info]
71 tag_build =
72 tag_date = 0
73
00 #! /usr/bin/env python
11
22 from setuptools import find_packages, setup
3
34 setup(
4 name='natsort',
5 version='7.1.0',
5 name="natsort",
6 version="8.0.0",
67 packages=find_packages(),
7 entry_points={'console_scripts': ['natsort = natsort.__main__:main']},
8 python_requires=">=3.4",
9 extras_require={
10 'fast': ["fastnumbers >= 2.0.0"],
11 'icu': ["PyICU >= 1.0.0"]
12 }
8 entry_points={"console_scripts": ["natsort = natsort.__main__:main"]},
9 python_requires=">=3.6",
10 extras_require={"fast": ["fastnumbers >= 2.0.0"], "icu": ["PyICU >= 1.0.0"]},
11 package_data={"": ["py.typed"]},
12 zip_safe=False,
1313 )
22 """
33
44 import locale
5 from typing import Iterator
56
67 import hypothesis
78 import pytest
1112 # For some reason it thinks that the text/binary generation is too
1213 # slow then causes the tests to fail.
1314 hypothesis.settings.register_profile(
14 "slow-tests",
15 suppress_health_check=[hypothesis.HealthCheck.too_slow],
15 "slow-tests", suppress_health_check=[hypothesis.HealthCheck.too_slow]
1616 )
1717
1818
19 def load_locale(x):
19 def load_locale(x: str) -> None:
2020 """Convenience to load a locale, trying ISO8859-1 first."""
2121 try:
2222 locale.setlocale(locale.LC_ALL, str("{}.ISO8859-1".format(x)))
2525
2626
2727 @pytest.fixture()
28 def with_locale_en_us():
28 def with_locale_en_us() -> Iterator[None]:
2929 """Convenience to load the en_US locale - reset when complete."""
3030 orig = locale.getlocale()
31 yield load_locale("en_US")
31 load_locale("en_US")
32 yield
3233 locale.setlocale(locale.LC_ALL, orig)
3334
3435
3536 @pytest.fixture()
36 def with_locale_de_de():
37 def with_locale_de_de() -> Iterator[None]:
3738 """
3839 Convenience to load the de_DE locale - reset when complete - skip if missing.
3940 """
66 import cProfile
77 import locale
88 import sys
9 from typing import List, Union
910
1011 try:
1112 from natsort import ns, natsort_keygen
1213 except ImportError:
1314 sys.path.insert(0, ".")
1415 from natsort import ns, natsort_keygen
16
17 from natsort.natsort import NatsortKeyType
1518
1619 locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
1720
3134 locale_key = natsort_keygen(alg=ns.LOCALE)
3235
3336
34 def prof_time_to_generate():
37 def prof_time_to_generate() -> None:
3538 print("*** Generate Plain Key ***")
3639 for _ in range(100000):
3740 natsort_keygen()
4043 cProfile.run("prof_time_to_generate()", sort="time")
4144
4245
43 def prof_parsing(a, msg, key=basic_key):
46 def prof_parsing(
47 a: Union[str, int, bytes, List[str]], msg: str, key: NatsortKeyType = basic_key
48 ) -> None:
4449 print(msg)
4550 for _ in range(100000):
4651 key(a)
44
55 import unicodedata
66 from math import isnan
7 from typing import Union, cast
78
89 from hypothesis import given
910 from hypothesis.strategies import floats, integers, text
1011 from natsort.compat.fake_fastnumbers import fast_float, fast_int
1112
1213
13 def is_float(x):
14 def is_float(x: str) -> bool:
1415 try:
1516 float(x)
1617 except ValueError:
2425 return True
2526
2627
27 def not_a_float(x):
28 def not_a_float(x: str) -> bool:
2829 return not is_float(x)
2930
3031
31 def is_int(x):
32 def is_int(x: Union[str, float]) -> bool:
3233 try:
33 return x.is_integer()
34 return cast(float, x).is_integer()
3435 except AttributeError:
3536 try:
3637 int(x)
3738 except ValueError:
3839 try:
39 unicodedata.digit(x)
40 unicodedata.digit(cast(str, x))
4041 except (ValueError, TypeError):
4142 return False
4243 else:
4546 return True
4647
4748
48 def not_an_int(x):
49 def not_an_int(x: Union[str, float]) -> bool:
4950 return not is_int(x)
5051
5152
5354 # and a test that uses the hypothesis module.
5455
5556
56 def test_fast_float_returns_nan_alternate_if_nan_option_is_given():
57 def test_fast_float_returns_nan_alternate_if_nan_option_is_given() -> None:
5758 assert fast_float("nan", nan=7) == 7
5859
5960
60 def test_fast_float_converts_float_string_to_float_example():
61 def test_fast_float_converts_float_string_to_float_example() -> None:
6162 assert fast_float("45.8") == 45.8
6263 assert fast_float("-45") == -45.0
6364 assert fast_float("45.8e-2", key=len) == 45.8e-2
64 assert isnan(fast_float("nan"))
65 assert isnan(fast_float("+nan"))
66 assert isnan(fast_float("-NaN"))
65 assert isnan(cast(float, fast_float("nan")))
66 assert isnan(cast(float, fast_float("+nan")))
67 assert isnan(cast(float, fast_float("-NaN")))
6768 assert fast_float("۱۲.۱۲") == 12.12
6869 assert fast_float("-۱۲.۱۲") == -12.12
6970
7071
7172 @given(floats(allow_nan=False))
72 def test_fast_float_converts_float_string_to_float(x):
73 def test_fast_float_converts_float_string_to_float(x: float) -> None:
7374 assert fast_float(repr(x)) == x
7475
7576
76 def test_fast_float_leaves_string_as_is_example():
77 def test_fast_float_leaves_string_as_is_example() -> None:
7778 assert fast_float("invalid") == "invalid"
7879
7980
8081 @given(text().filter(not_a_float).filter(bool))
81 def test_fast_float_leaves_string_as_is(x):
82 def test_fast_float_leaves_string_as_is(x: str) -> None:
8283 assert fast_float(x) == x
8384
8485
85 def test_fast_float_with_key_applies_to_string_example():
86 def test_fast_float_with_key_applies_to_string_example() -> None:
8687 assert fast_float("invalid", key=len) == len("invalid")
8788
8889
8990 @given(text().filter(not_a_float).filter(bool))
90 def test_fast_float_with_key_applies_to_string(x):
91 def test_fast_float_with_key_applies_to_string(x: str) -> None:
9192 assert fast_float(x, key=len) == len(x)
9293
9394
94 def test_fast_int_leaves_float_string_as_is_example():
95 def test_fast_int_leaves_float_string_as_is_example() -> None:
9596 assert fast_int("45.8") == "45.8"
9697 assert fast_int("nan") == "nan"
9798 assert fast_int("inf") == "inf"
9899
99100
100101 @given(floats().filter(not_an_int))
101 def test_fast_int_leaves_float_string_as_is(x):
102 def test_fast_int_leaves_float_string_as_is(x: float) -> None:
102103 assert fast_int(repr(x)) == repr(x)
103104
104105
105 def test_fast_int_converts_int_string_to_int_example():
106 def test_fast_int_converts_int_string_to_int_example() -> None:
106107 assert fast_int("-45") == -45
107108 assert fast_int("+45") == 45
108109 assert fast_int("۱۲") == 12
110111
111112
112113 @given(integers())
113 def test_fast_int_converts_int_string_to_int(x):
114 def test_fast_int_converts_int_string_to_int(x: int) -> None:
114115 assert fast_int(repr(x)) == x
115116
116117
117 def test_fast_int_leaves_string_as_is_example():
118 def test_fast_int_leaves_string_as_is_example() -> None:
118119 assert fast_int("invalid") == "invalid"
119120
120121
121122 @given(text().filter(not_an_int).filter(bool))
122 def test_fast_int_leaves_string_as_is(x):
123 def test_fast_int_leaves_string_as_is(x: str) -> None:
123124 assert fast_int(x) == x
124125
125126
126 def test_fast_int_with_key_applies_to_string_example():
127 def test_fast_int_with_key_applies_to_string_example() -> None:
127128 assert fast_int("invalid", key=len) == len("invalid")
128129
129130
130131 @given(text().filter(not_an_int).filter(bool))
131 def test_fast_int_with_key_applies_to_string(x):
132 def test_fast_int_with_key_applies_to_string(x: str) -> None:
132133 assert fast_int(x, key=len) == len(x)
00 # -*- coding: utf-8 -*-
11 """These test the utils.py functions."""
2 from typing import Callable, Union
23
34 import pytest
45 from hypothesis import example, given
56 from hypothesis.strategies import floats, integers, text
6 from natsort.ns_enum import NS_DUMB, ns
7 from natsort.ns_enum import NSType, NS_DUMB, ns
78 from natsort.utils import final_data_transform_factory
89
910
1011 @pytest.mark.parametrize("alg", [ns.DEFAULT, ns.UNGROUPLETTERS, ns.LOCALE])
1112 @given(x=text(), y=floats(allow_nan=False, allow_infinity=False) | integers())
1213 @pytest.mark.usefixtures("with_locale_en_us")
13 def test_final_data_transform_factory_default(x, y, alg):
14 def test_final_data_transform_factory_default(
15 x: str, y: Union[int, float], alg: NSType
16 ) -> None:
1417 final_data_transform_func = final_data_transform_factory(alg, "", "::")
1518 value = (x, y)
1619 original_value = "".join(map(str, value))
2528 (ns.LOCALE | ns.UNGROUPLETTERS | NS_DUMB, lambda x: x),
2629 (ns.LOCALE | ns.UNGROUPLETTERS | ns.LOWERCASEFIRST, lambda x: x),
2730 (
28 ns.LOCALE | ns.UNGROUPLETTERS | NS_DUMB | ns.LOWERCASEFIRST,
29 lambda x: x.swapcase(),
31 ns.LOCALE | ns.UNGROUPLETTERS | NS_DUMB | ns.LOWERCASEFIRST,
32 lambda x: x.swapcase(),
3033 ),
3134 ],
3235 )
3336 @given(x=text(), y=floats(allow_nan=False, allow_infinity=False) | integers())
3437 @example(x="İ", y=0)
3538 @pytest.mark.usefixtures("with_locale_en_us")
36 def test_final_data_transform_factory_ungroup_and_locale(x, y, alg, func):
39 def test_final_data_transform_factory_ungroup_and_locale(
40 x: str, y: Union[int, float], alg: NSType, func: Callable[[str], str]
41 ) -> None:
3742 final_data_transform_func = final_data_transform_factory(alg, "", "::")
3843 value = (x, y)
3944 original_value = "".join(map(str, value))
4550 assert result == expected
4651
4752
48 def test_final_data_transform_factory_ungroup_and_locale_empty_tuple():
53 def test_final_data_transform_factory_ungroup_and_locale_empty_tuple() -> None:
4954 final_data_transform_func = final_data_transform_factory(ns.UG | ns.L, "", "::")
5055 assert final_data_transform_func((), "") == ((), ())
00 # -*- coding: utf-8 -*-
11 """These test the utils.py functions."""
2 from typing import Callable
23
34 import pytest
45 from hypothesis import example, given
56 from hypothesis.strategies import integers, text
6 from natsort.ns_enum import NS_DUMB, ns
7 from natsort.ns_enum import NSType, NS_DUMB, ns
78 from natsort.utils import input_string_transform_factory
89
910
10 def thousands_separated_int(n):
11 def thousands_separated_int(n: str) -> str:
1112 """Insert thousands separators in an int."""
1213 new_int = ""
1314 for i, y in enumerate(reversed(n), 1):
1920
2021
2122 @given(text())
22 def test_input_string_transform_factory_is_no_op_for_no_alg_options(x):
23 def test_input_string_transform_factory_is_no_op_for_no_alg_options(x: str) -> None:
2324 input_string_transform_func = input_string_transform_factory(ns.DEFAULT)
2425 assert input_string_transform_func(x) is x
2526
3536 ],
3637 )
3738 @given(x=text())
38 def test_input_string_transform_factory(x, alg, example_func):
39 def test_input_string_transform_factory(
40 x: str, alg: NSType, example_func: Callable[[str], str]
41 ) -> None:
3942 input_string_transform_func = input_string_transform_factory(alg)
4043 assert input_string_transform_func(x) == example_func(x)
4144
4346 @example(12543642642534980) # 12,543,642,642,534,980 => 12543642642534980
4447 @given(x=integers(min_value=1000))
4548 @pytest.mark.usefixtures("with_locale_en_us")
46 def test_input_string_transform_factory_cleans_thousands(x):
49 def test_input_string_transform_factory_cleans_thousands(x: int) -> None:
4750 int_str = str(x).rstrip("lL")
4851 thousands_int_str = thousands_separated_int(int_str)
4952 assert thousands_int_str.replace(",", "") != thousands_int_str
6871 ],
6972 )
7073 @pytest.mark.usefixtures("with_locale_en_us")
71 def test_input_string_transform_factory_handles_us_locale(x, expected):
74 def test_input_string_transform_factory_handles_us_locale(
75 x: str, expected: str
76 ) -> None:
7277 input_string_transform_func = input_string_transform_factory(ns.LOCALE)
7378 assert input_string_transform_func(x) == expected
7479
8287 ],
8388 )
8489 @pytest.mark.usefixtures("with_locale_de_de")
85 def test_input_string_transform_factory_handles_de_locale(x, expected):
90 def test_input_string_transform_factory_handles_de_locale(
91 x: str, expected: str
92 ) -> None:
8693 input_string_transform_func = input_string_transform_factory(ns.LOCALE)
8794 assert input_string_transform_func(x) == expected
8895
96103 ],
97104 )
98105 @pytest.mark.usefixtures("with_locale_de_de")
99 def test_input_string_transform_factory_handles_german_locale(alg, expected):
106 def test_input_string_transform_factory_handles_german_locale(
107 alg: NSType, expected: str
108 ) -> None:
100109 input_string_transform_func = input_string_transform_factory(alg)
101110 assert input_string_transform_func("1543,753") == expected
102111
103112
104113 @pytest.mark.usefixtures("with_locale_de_de")
105 def test_input_string_transform_factory_does_nothing_with_non_num_input():
114 def test_input_string_transform_factory_does_nothing_with_non_num_input() -> None:
106115 input_string_transform_func = input_string_transform_factory(ns.LOCALE | ns.FLOAT)
107116 expected = "154s,t53"
108117 assert input_string_transform_func("154s,t53") == expected
44
55 import re
66 import sys
7 from typing import Any, List, Union
78
89 import pytest
910 from hypothesis import given
10 from hypothesis.strategies import data, floats, integers, lists
11 from hypothesis.strategies import DataObject, data, floats, integers, lists
1112 from natsort.__main__ import (
13 TypedArgs,
1214 check_filters,
1315 keep_entry_range,
1416 keep_entry_value,
1618 range_check,
1719 sort_and_print_entries,
1820 )
19
20
21 def test_main_passes_default_arguments_with_no_command_line_options(mocker):
21 from pytest_mock import MockerFixture
22
23
24 def test_main_passes_default_arguments_with_no_command_line_options(
25 mocker: MockerFixture,
26 ) -> None:
2227 p = mocker.patch("natsort.__main__.sort_and_print_entries")
2328 main("num-2", "num-6", "num-1")
2429 args = p.call_args[0][1]
2530 assert not args.paths
2631 assert args.filter is None
2732 assert args.reverse_filter is None
28 assert args.exclude is None
33 assert args.exclude == []
2934 assert not args.reverse
3035 assert args.number_type == "int"
3136 assert not args.signed
3338 assert not args.locale
3439
3540
36 def test_main_passes_arguments_with_all_command_line_options(mocker):
41 def test_main_passes_arguments_with_all_command_line_options(
42 mocker: MockerFixture,
43 ) -> None:
3744 arguments = ["--paths", "--reverse", "--locale"]
3845 arguments.extend(["--filter", "4", "10"])
3946 arguments.extend(["--reverse-filter", "100", "110"])
5663 assert args.locale
5764
5865
59 class Args:
60 """A dummy class to simulate the argparse Namespace object"""
61
62 def __init__(self, filt, reverse_filter, exclude, as_path, reverse):
63 self.filter = filt
64 self.reverse_filter = reverse_filter
65 self.exclude = exclude
66 self.reverse = reverse
67 self.number_type = "float"
68 self.signed = True
69 self.exp = True
70 self.paths = as_path
71 self.locale = 0
72
73
7466 mock_print = "__builtin__.print" if sys.version[0] == "2" else "builtins.print"
7567
7668 entries = [
134126 ([None, None, False, True, True], reversed([2, 3, 1, 0, 5, 6, 4])),
135127 ],
136128 )
137 def test_sort_and_print_entries(options, order, mocker):
129 def test_sort_and_print_entries(
130 options: List[Any], order: List[int], mocker: MockerFixture
131 ) -> None:
138132 p = mocker.patch(mock_print)
139 sort_and_print_entries(entries, Args(*options))
133 sort_and_print_entries(entries, TypedArgs(*options))
140134 e = [mocker.call(entries[i]) for i in order]
141135 p.assert_has_calls(e)
142136
145139 # and a test that uses the hypothesis module.
146140
147141
148 def test_range_check_returns_range_as_is_but_with_floats_example():
142 def test_range_check_returns_range_as_is_but_with_floats_example() -> None:
149143 assert range_check(10, 11) == (10.0, 11.0)
150144 assert range_check(6.4, 30) == (6.4, 30.0)
151145
152146
153 @given(x=floats(allow_nan=False, min_value=-1E8, max_value=1E8) | integers(), d=data())
154 def test_range_check_returns_range_as_is_if_first_is_less_than_second(x, d):
147 @given(x=floats(allow_nan=False, min_value=-1e8, max_value=1e8) | integers(), d=data())
148 def test_range_check_returns_range_as_is_if_first_is_less_than_second(
149 x: Union[int, float], d: DataObject
150 ) -> None:
155151 # Pull data such that the first is less than the second.
156152 if isinstance(x, float):
157 y = d.draw(floats(min_value=x + 1.0, max_value=1E9, allow_nan=False))
153 y = d.draw(floats(min_value=x + 1.0, max_value=1e9, allow_nan=False))
158154 else:
159155 y = d.draw(integers(min_value=x + 1))
160156 assert range_check(x, y) == (x, y)
161157
162158
163 def test_range_check_raises_value_error_if_second_is_less_than_first_example():
159 def test_range_check_raises_value_error_if_second_is_less_than_first_example() -> None:
164160 with pytest.raises(ValueError, match="low >= high"):
165161 range_check(7, 2)
166162
167163
168164 @given(x=floats(allow_nan=False), d=data())
169 def test_range_check_raises_value_error_if_second_is_less_than_first(x, d):
165 def test_range_check_raises_value_error_if_second_is_less_than_first(
166 x: float, d: DataObject
167 ) -> None:
170168 # Pull data such that the first is greater than or equal to the second.
171169 y = d.draw(floats(max_value=x, allow_nan=False))
172170 with pytest.raises(ValueError, match="low >= high"):
173171 range_check(x, y)
174172
175173
176 def test_check_filters_returns_none_if_filter_evaluates_to_false():
174 def test_check_filters_returns_none_if_filter_evaluates_to_false() -> None:
177175 assert check_filters(()) is None
178 assert check_filters(False) is None
179 assert check_filters(None) is None
180
181
182 def test_check_filters_returns_input_as_is_if_filter_is_valid_example():
176
177
178 def test_check_filters_returns_input_as_is_if_filter_is_valid_example() -> None:
183179 assert check_filters([(6, 7)]) == [(6, 7)]
184180 assert check_filters([(6, 7), (2, 8)]) == [(6, 7), (2, 8)]
185181
186182
187183 @given(x=lists(integers(), min_size=1), d=data())
188 def test_check_filters_returns_input_as_is_if_filter_is_valid(x, d):
184 def test_check_filters_returns_input_as_is_if_filter_is_valid(
185 x: List[int], d: DataObject
186 ) -> None:
189187 # ensure y is element-wise greater than x
190188 y = [d.draw(integers(min_value=val + 1)) for val in x]
191189 assert check_filters(list(zip(x, y))) == [(i, j) for i, j in zip(x, y)]
192190
193191
194 def test_check_filters_raises_value_error_if_filter_is_invalid_example():
192 def test_check_filters_raises_value_error_if_filter_is_invalid_example() -> None:
195193 with pytest.raises(ValueError, match="Error in --filter: low >= high"):
196194 check_filters([(7, 2)])
197195
198196
199197 @given(x=lists(integers(), min_size=1), d=data())
200 def test_check_filters_raises_value_error_if_filter_is_invalid(x, d):
198 def test_check_filters_raises_value_error_if_filter_is_invalid(
199 x: List[int], d: DataObject
200 ) -> None:
201201 # ensure y is element-wise less than or equal to x
202202 y = [d.draw(integers(max_value=val)) for val in x]
203203 with pytest.raises(ValueError, match="Error in --filter: low >= high"):
211211 # 3. No portion is between the bounds => False.
212212 [([0], [100], True), ([1, 88], [20, 90], True), ([1], [20], False)],
213213 )
214 def test_keep_entry_range(lows, highs, truth):
214 def test_keep_entry_range(lows: List[int], highs: List[int], truth: bool) -> None:
215215 assert keep_entry_range("a56b23c89", lows, highs, int, re.compile(r"\d+")) is truth
216216
217217
218218 # 1. Values not in entry => True. 2. Values in entry => False.
219219 @pytest.mark.parametrize("values, truth", [([100, 45], True), ([23], False)])
220 def test_keep_entry_value(values, truth):
220 def test_keep_entry_value(values: List[int], truth: bool) -> None:
221221 assert keep_entry_value("a56b23c89", values, int, re.compile(r"\d+")) is truth
00 # -*- coding: utf-8 -*-
11 """These test the utils.py functions."""
2 from typing import Any, List, NoReturn, Tuple, Union, cast
23
34 from hypothesis import given
45 from hypothesis.strategies import binary, floats, integers, lists, text
56 from natsort.utils import natsort_key
67
78
8 def str_func(x):
9 def str_func(x: Any) -> Tuple[str]:
910 if isinstance(x, str):
10 return x
11 return (x,)
1112 else:
1213 raise TypeError("Not a str!")
1314
1415
15 def fail(_):
16 def fail(_: Any) -> NoReturn:
1617 raise AssertionError("This should never be reached!")
1718
1819
1920 @given(floats(allow_nan=False) | integers())
20 def test_natsort_key_with_numeric_input_takes_number_path(x):
21 assert natsort_key(x, None, str_func, fail, lambda y: y) is x
21 def test_natsort_key_with_numeric_input_takes_number_path(x: Union[float, int]) -> None:
22 assert natsort_key(x, None, str_func, fail, lambda y: ("", y))[1] is x
2223
2324
2425 @given(binary().filter(bool))
25 def test_natsort_key_with_bytes_input_takes_bytes_path(x):
26 assert natsort_key(x, None, str_func, lambda y: y, fail) is x
26 def test_natsort_key_with_bytes_input_takes_bytes_path(x: bytes) -> None:
27 assert natsort_key(x, None, str_func, lambda y: (y,), fail)[0] is x
2728
2829
2930 @given(text())
30 def test_natsort_key_with_text_input_takes_string_path(x):
31 assert natsort_key(x, None, str_func, fail, fail) is x
31 def test_natsort_key_with_text_input_takes_string_path(x: str) -> None:
32 assert natsort_key(x, None, str_func, fail, fail)[0] is x
3233
3334
3435 @given(lists(elements=text(), min_size=1, max_size=10))
35 def test_natsort_key_with_nested_input_takes_nested_path(x):
36 assert natsort_key(x, None, str_func, fail, fail) == tuple(x)
36 def test_natsort_key_with_nested_input_takes_nested_path(x: List[str]) -> None:
37 assert natsort_key(x, None, str_func, fail, fail) == tuple((y,) for y in x)
3738
3839
3940 @given(text())
40 def test_natsort_key_with_key_argument_applies_key_before_processing(x):
41 assert natsort_key(x, len, str_func, fail, lambda y: y) == len(x)
41 def test_natsort_key_with_key_argument_applies_key_before_processing(x: str) -> None:
42 assert natsort_key(x, len, str_func, fail, lambda y: ("", cast(int, y)))[1] == len(
43 x
44 )
44 """
55
66 import os
7 from typing import List, Tuple, Union
78
89 import pytest
910 from natsort import natsort_key, natsort_keygen, natsorted, ns
1011 from natsort.compat.locale import get_strxfrm, null_string_locale
12 from natsort.ns_enum import NSType
13 from natsort.utils import BytesTransform, FinalTransform
14 from pytest_mock import MockerFixture
1115
1216
1317 @pytest.fixture
14 def arbitrary_input():
18 def arbitrary_input() -> List[Union[str, float]]:
1519 return ["6A-5.034e+1", "/Folder (1)/Foo", 56.7]
1620
1721
1822 @pytest.fixture
19 def bytes_input():
23 def bytes_input() -> bytes:
2024 return b"6A-5.034e+1"
2125
2226
23 def test_natsort_keygen_demonstration():
27 def test_natsort_keygen_demonstration() -> None:
2428 original_list = ["a50", "a51.", "a50.31", "a50.4", "a5.034e1", "a50.300"]
2529 copy_of_list = original_list[:]
2630 original_list.sort(key=natsort_keygen(alg=ns.F))
2832 assert original_list == natsorted(copy_of_list, alg=ns.F)
2933
3034
31 def test_natsort_key_public():
35 def test_natsort_key_public() -> None:
3236 assert natsort_key("a-5.034e2") == ("a-", 5, ".", 34, "e", 2)
3337
3438
35 def test_natsort_keygen_with_invalid_alg_input_raises_value_error():
39 def test_natsort_keygen_with_invalid_alg_input_raises_value_error() -> None:
3640 # Invalid arguments give the correct response
3741 with pytest.raises(ValueError, match="'alg' argument"):
38 natsort_keygen(None, "1")
42 natsort_keygen(None, "1") # type: ignore
3943
4044
4145 @pytest.mark.parametrize(
4246 "alg, expected",
4347 [(ns.DEFAULT, ("a-", 5, ".", 34, "e", 1)), (ns.FLOAT | ns.SIGNED, ("a", -50.34))],
4448 )
45 def test_natsort_keygen_returns_natsort_key_that_parses_input(alg, expected):
49 def test_natsort_keygen_returns_natsort_key_that_parses_input(
50 alg: NSType, expected: Tuple[Union[str, int, float], ...]
51 ) -> None:
4652 ns_key = natsort_keygen(alg=alg)
4753 assert ns_key("a-5.034e1") == expected
4854
7783 ),
7884 ],
7985 )
80 def test_natsort_keygen_handles_arbitrary_input(arbitrary_input, alg, expected):
86 def test_natsort_keygen_handles_arbitrary_input(
87 arbitrary_input: List[Union[str, float]], alg: NSType, expected: FinalTransform
88 ) -> None:
8189 ns_key = natsort_keygen(alg=alg)
8290 assert ns_key(arbitrary_input) == expected
8391
92100 (ns.PATH | ns.GROUPLETTERS, ((b"6A-5.034e+1",),)),
93101 ],
94102 )
95 def test_natsort_keygen_handles_bytes_input(bytes_input, alg, expected):
103 def test_natsort_keygen_handles_bytes_input(
104 bytes_input: bytes, alg: NSType, expected: BytesTransform
105 ) -> None:
96106 ns_key = natsort_keygen(alg=alg)
97107 assert ns_key(bytes_input) == expected
98108
130140 ],
131141 )
132142 @pytest.mark.usefixtures("with_locale_en_us")
133 def test_natsort_keygen_with_locale(mocker, arbitrary_input, alg, expected, is_dumb):
143 def test_natsort_keygen_with_locale(
144 mocker: MockerFixture,
145 arbitrary_input: List[Union[str, float]],
146 alg: NSType,
147 expected: FinalTransform,
148 is_dumb: bool,
149 ) -> None:
134150 # First, apply the correct strxfrm function to the string values.
135151 strxfrm = get_strxfrm()
136 expected = [list(sub) for sub in expected]
152 expected_tmp = [list(sub) for sub in expected]
137153 try:
138154 for i in (2, 4, 6):
139 expected[0][i] = strxfrm(expected[0][i])
155 expected_tmp[0][i] = strxfrm(expected_tmp[0][i])
140156 for i in (0, 2):
141 expected[1][i] = strxfrm(expected[1][i])
142 expected = tuple(tuple(sub) for sub in expected)
157 expected_tmp[1][i] = strxfrm(expected_tmp[1][i])
158 expected = tuple(tuple(sub) for sub in expected_tmp)
143159 except IndexError: # ns.LOCALE | ns.CAPITALFIRST
144 expected = [[list(subsub) for subsub in sub] for sub in expected]
160 expected_tmp = [[list(subsub) for subsub in sub] for sub in expected_tmp]
145161 for i in (2, 4, 6):
146 expected[0][1][i] = strxfrm(expected[0][1][i])
162 expected_tmp[0][1][i] = strxfrm(expected_tmp[0][1][i])
147163 for i in (0, 2):
148 expected[1][1][i] = strxfrm(expected[1][1][i])
149 expected = tuple(tuple(tuple(subsub) for subsub in sub) for sub in expected)
164 expected_tmp[1][1][i] = strxfrm(expected_tmp[1][1][i])
165 expected = tuple(tuple(tuple(subsub) for subsub in sub) for sub in expected_tmp)
150166
151167 mocker.patch("natsort.compat.locale.dumb_sort", return_value=is_dumb)
152168 ns_key = natsort_keygen(alg=alg)
158174 [(ns.LOCALE, False), (ns.LOCALE, True), (ns.LOCALE | ns.CAPITALFIRST, False)],
159175 )
160176 @pytest.mark.usefixtures("with_locale_en_us")
161 def test_natsort_keygen_with_locale_bytes(mocker, bytes_input, alg, is_dumb):
177 def test_natsort_keygen_with_locale_bytes(
178 mocker: MockerFixture, bytes_input: bytes, alg: NSType, is_dumb: bool
179 ) -> None:
162180 expected = (b"6A-5.034e+1",)
163181 mocker.patch("natsort.compat.locale.dumb_sort", return_value=is_dumb)
164182 ns_key = natsort_keygen(alg=alg)
44 """
55
66 from operator import itemgetter
7 from typing import List, Tuple, Union
78
89 import pytest
910 from natsort import as_utf8, natsorted, ns
11 from natsort.ns_enum import NSType
1012 from pytest import raises
1113
1214
1315 @pytest.fixture
14 def float_list():
16 def float_list() -> List[str]:
1517 return ["a50", "a51.", "a50.31", "a-50", "a50.4", "a5.034e1", "a50.300"]
1618
1719
1820 @pytest.fixture
19 def fruit_list():
21 def fruit_list() -> List[str]:
2022 return ["Apple", "corn", "Corn", "Banana", "apple", "banana"]
2123
2224
2325 @pytest.fixture
24 def mixed_list():
26 def mixed_list() -> List[Union[str, int, float]]:
2527 return ["Ä", "0", "ä", 3, "b", 1.5, "2", "Z"]
2628
2729
28 def test_natsorted_numbers_in_ascending_order():
30 def test_natsorted_numbers_in_ascending_order() -> None:
2931 given = ["a2", "a5", "a9", "a1", "a4", "a10", "a6"]
3032 expected = ["a1", "a2", "a4", "a5", "a6", "a9", "a10"]
3133 assert natsorted(given) == expected
3234
3335
34 def test_natsorted_can_sort_as_signed_floats_with_exponents(float_list):
36 def test_natsorted_can_sort_as_signed_floats_with_exponents(
37 float_list: List[str],
38 ) -> None:
3539 expected = ["a-50", "a50", "a50.300", "a50.31", "a5.034e1", "a50.4", "a51."]
3640 assert natsorted(float_list, alg=ns.REAL) == expected
3741
4145 "alg",
4246 [ns.NOEXP | ns.FLOAT | ns.UNSIGNED, ns.NOEXP | ns.FLOAT],
4347 )
44 def test_natsorted_can_sort_as_unsigned_and_ignore_exponents(float_list, alg):
48 def test_natsorted_can_sort_as_unsigned_and_ignore_exponents(
49 float_list: List[str], alg: NSType
50 ) -> None:
4551 expected = ["a5.034e1", "a50", "a50.300", "a50.31", "a50.4", "a51.", "a-50"]
4652 assert natsorted(float_list, alg=alg) == expected
4753
4854
4955 # DEFAULT and INT are all equivalent.
5056 @pytest.mark.parametrize("alg", [ns.DEFAULT, ns.INT])
51 def test_natsorted_can_sort_as_unsigned_ints_which_is_default(float_list, alg):
57 def test_natsorted_can_sort_as_unsigned_ints_which_is_default(
58 float_list: List[str], alg: NSType
59 ) -> None:
5260 expected = ["a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51.", "a-50"]
5361 assert natsorted(float_list, alg=alg) == expected
5462
5563
56 def test_natsorted_can_sort_as_signed_ints(float_list):
64 def test_natsorted_can_sort_as_signed_ints(float_list: List[str]) -> None:
5765 expected = ["a-50", "a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51."]
5866 assert natsorted(float_list, alg=ns.SIGNED) == expected
5967
6270 "alg, expected",
6371 [(ns.UNSIGNED, ["a7", "a+2", "a-5"]), (ns.SIGNED, ["a-5", "a+2", "a7"])],
6472 )
65 def test_natsorted_can_sort_with_or_without_accounting_for_sign(alg, expected):
73 def test_natsorted_can_sort_with_or_without_accounting_for_sign(
74 alg: NSType, expected: List[str]
75 ) -> None:
6676 given = ["a-5", "a7", "a+2"]
6777 assert natsorted(given, alg=alg) == expected
6878
6979
70 def test_natsorted_can_sort_as_version_numbers():
80 def test_natsorted_can_sort_as_version_numbers() -> None:
7181 given = ["1.9.9a", "1.11", "1.9.9b", "1.11.4", "1.10.1"]
7282 expected = ["1.9.9a", "1.9.9b", "1.10.1", "1.11", "1.11.4"]
7383 assert natsorted(given) == expected
8090 (ns.NUMAFTER, ["Ä", "Z", "ä", "b", "0", 1.5, "2", 3]),
8191 ],
8292 )
83 def test_natsorted_handles_mixed_types(mixed_list, alg, expected):
93 def test_natsorted_handles_mixed_types(
94 mixed_list: List[Union[str, int, float]],
95 alg: NSType,
96 expected: List[Union[str, int, float]],
97 ) -> None:
8498 assert natsorted(mixed_list, alg=alg) == expected
8599
86100
87101 @pytest.mark.parametrize(
88102 "alg, expected, slc",
89103 [
90 (ns.DEFAULT, [float("nan"), 5, "25", 1E40], slice(1, None)),
91 (ns.NANLAST, [5, "25", 1E40, float("nan")], slice(None, 3)),
92 ],
93 )
94 def test_natsorted_handles_nan(alg, expected, slc):
95 given = ["25", 5, float("nan"), 1E40]
104 (ns.DEFAULT, [float("nan"), 5, "25", 1e40], slice(1, None)),
105 (ns.NANLAST, [5, "25", 1e40, float("nan")], slice(None, 3)),
106 ],
107 )
108 def test_natsorted_handles_nan(
109 alg: NSType, expected: List[Union[str, float, int]], slc: slice
110 ) -> None:
111 given: List[Union[str, float, int]] = ["25", 5, float("nan"), 1e40]
96112 # The slice is because NaN != NaN
97113 # noinspection PyUnresolvedReferences
98114 assert natsorted(given, alg=alg)[slc] == expected[slc]
99115
100116
101 def test_natsorted_with_mixed_bytes_and_str_input_raises_type_error():
117 def test_natsorted_with_mixed_bytes_and_str_input_raises_type_error() -> None:
102118 with raises(TypeError, match="bytes"):
103119 natsorted(["ä", b"b"])
104120
106122 assert natsorted(["ä", b"b"], key=as_utf8) == ["ä", b"b"]
107123
108124
109 def test_natsorted_raises_type_error_for_non_iterable_input():
125 def test_natsorted_raises_type_error_for_non_iterable_input() -> None:
110126 with raises(TypeError, match="'int' object is not iterable"):
111 natsorted(100)
112
113
114 def test_natsorted_recurses_into_nested_lists():
127 natsorted(100) # type: ignore
128
129
130 def test_natsorted_recurses_into_nested_lists() -> None:
115131 given = [["a1", "a5"], ["a1", "a40"], ["a10", "a1"], ["a2", "a5"]]
116132 expected = [["a1", "a5"], ["a1", "a40"], ["a2", "a5"], ["a10", "a1"]]
117133 assert natsorted(given) == expected
118134
119135
120 def test_natsorted_applies_key_to_each_list_element_before_sorting_list():
136 def test_natsorted_applies_key_to_each_list_element_before_sorting_list() -> None:
121137 given = [("a", "num3"), ("b", "num5"), ("c", "num2")]
122138 expected = [("c", "num2"), ("a", "num3"), ("b", "num5")]
123139 assert natsorted(given, key=itemgetter(1)) == expected
124140
125141
126 def test_natsorted_returns_list_in_reversed_order_with_reverse_option(float_list):
142 def test_natsorted_returns_list_in_reversed_order_with_reverse_option(
143 float_list: List[str],
144 ) -> None:
127145 expected = natsorted(float_list)[::-1]
128146 assert natsorted(float_list, reverse=True) == expected
129147
130148
131 def test_natsorted_handles_filesystem_paths():
149 def test_natsorted_handles_filesystem_paths() -> None:
132150 given = [
133151 "/p/Folder (10)/file.tar.gz",
134152 "/p/Folder (1)/file (1).tar.gz",
156174 assert natsorted(given, alg=ns.FLOAT | ns.PATH) == expected_correct
157175
158176
159 def test_natsorted_handles_numbers_and_filesystem_paths_simultaneously():
177 def test_natsorted_handles_numbers_and_filesystem_paths_simultaneously() -> None:
160178 # You can sort paths and numbers, not that you'd want to
161 given = ["/Folder (9)/file.exe", 43]
162 expected = [43, "/Folder (9)/file.exe"]
179 given: List[Union[str, int]] = ["/Folder (9)/file.exe", 43]
180 expected: List[Union[str, int]] = [43, "/Folder (9)/file.exe"]
163181 assert natsorted(given, alg=ns.PATH) == expected
164182
165183
173191 (ns.G | ns.LF, ["apple", "Apple", "banana", "Banana", "corn", "Corn"]),
174192 ],
175193 )
176 def test_natsorted_supports_case_handling(alg, expected, fruit_list):
194 def test_natsorted_supports_case_handling(
195 alg: NSType, expected: List[str], fruit_list: List[str]
196 ) -> None:
177197 assert natsorted(fruit_list, alg=alg) == expected
178198
179199
185205 (ns.IGNORECASE, [("a3", "a1"), ("A5", "a6")]),
186206 ],
187207 )
188 def test_natsorted_supports_nested_case_handling(alg, expected):
208 def test_natsorted_supports_nested_case_handling(
209 alg: NSType, expected: List[Tuple[str, str]]
210 ) -> None:
189211 given = [("A5", "a6"), ("a3", "a1")]
190212 assert natsorted(given, alg=alg) == expected
191213
200222 ],
201223 )
202224 @pytest.mark.usefixtures("with_locale_en_us")
203 def test_natsorted_can_sort_using_locale(fruit_list, alg, expected):
225 def test_natsorted_can_sort_using_locale(
226 fruit_list: List[str], alg: NSType, expected: List[str]
227 ) -> None:
204228 assert natsorted(fruit_list, alg=ns.LOCALE | alg) == expected
205229
206230
207231 @pytest.mark.usefixtures("with_locale_en_us")
208 def test_natsorted_can_sort_locale_specific_numbers_en():
232 def test_natsorted_can_sort_locale_specific_numbers_en() -> None:
209233 given = ["c", "a5,467.86", "ä", "b", "a5367.86", "a5,6", "a5,50"]
210234 expected = ["a5,6", "a5,50", "a5367.86", "a5,467.86", "ä", "b", "c"]
211235 assert natsorted(given, alg=ns.LOCALE | ns.F) == expected
212236
213237
214238 @pytest.mark.usefixtures("with_locale_de_de")
215 def test_natsorted_can_sort_locale_specific_numbers_de():
239 def test_natsorted_can_sort_locale_specific_numbers_de() -> None:
216240 given = ["c", "a5.467,86", "ä", "b", "a5367.86", "a5,6", "a5,50"]
217241 expected = ["a5,50", "a5,6", "a5367.86", "a5.467,86", "ä", "b", "c"]
218242 assert natsorted(given, alg=ns.LOCALE | ns.F) == expected
219243
220244
221245 @pytest.mark.usefixtures("with_locale_de_de")
222 def test_natsorted_locale_bug_regression_test_109():
246 def test_natsorted_locale_bug_regression_test_109() -> None:
223247 # https://github.com/SethMMorton/natsort/issues/109
224248 given = ["462166", "461761"]
225249 expected = ["461761", "462166"]
241265 ],
242266 )
243267 @pytest.mark.usefixtures("with_locale_en_us")
244 def test_natsorted_handles_mixed_types_with_locale(mixed_list, alg, expected):
268 def test_natsorted_handles_mixed_types_with_locale(
269 mixed_list: List[Union[str, int, float]],
270 alg: NSType,
271 expected: List[Union[str, int, float]],
272 ) -> None:
245273 assert natsorted(mixed_list, alg=ns.LOCALE | alg) == expected
246274
247275
252280 (ns.NUMAFTER, ["Banana", "apple", "corn", "~~~~~~", "73", "5039"]),
253281 ],
254282 )
255 def test_natsorted_sorts_an_odd_collection_of_strings(alg, expected):
283 def test_natsorted_sorts_an_odd_collection_of_strings(
284 alg: NSType, expected: List[str]
285 ) -> None:
256286 given = ["apple", "Banana", "73", "5039", "corn", "~~~~~~"]
257287 assert natsorted(given, alg=alg) == expected
258288
259289
260 def test_natsorted_sorts_mixed_ascii_and_non_ascii_numbers():
290 def test_natsorted_sorts_mixed_ascii_and_non_ascii_numbers() -> None:
261291 given = [
262292 "1st street",
263293 "10th street",
44 """
55
66 from operator import itemgetter
7 from typing import List
78
89 import pytest
910 from natsort import (
2223
2324
2425 @pytest.fixture
25 def version_list():
26 def version_list() -> List[str]:
2627 return ["1.9.9a", "1.11", "1.9.9b", "1.11.4", "1.10.1"]
2728
2829
2930 @pytest.fixture
30 def float_list():
31 def float_list() -> List[str]:
3132 return ["a50", "a51.", "a50.31", "a-50", "a50.4", "a5.034e1", "a50.300"]
3233
3334
3435 @pytest.fixture
35 def fruit_list():
36 def fruit_list() -> List[str]:
3637 return ["Apple", "corn", "Corn", "Banana", "apple", "banana"]
3738
3839
39 def test_decoder_returns_function_that_can_decode_bytes_but_return_non_bytes_as_is():
40 def test_decoder_returns_function_that_decodes_bytes_but_returns_other_as_is() -> None:
4041 func = decoder("latin1")
4142 str_obj = "bytes"
4243 int_obj = 14
4344 assert func(b"bytes") == str_obj
4445 assert func(int_obj) is int_obj # returns as-is, same object ID
45 assert (
46 func(str_obj) is str_obj
47 ) # same object returned b/c only bytes has decode
46 assert func(str_obj) is str_obj # same object returned b/c only bytes has decode
4847
4948
50 def test_as_ascii_converts_bytes_to_ascii():
49 def test_as_ascii_converts_bytes_to_ascii() -> None:
5150 assert decoder("ascii")(b"bytes") == as_ascii(b"bytes")
5251
5352
54 def test_as_utf8_converts_bytes_to_utf8():
53 def test_as_utf8_converts_bytes_to_utf8() -> None:
5554 assert decoder("utf8")(b"bytes") == as_utf8(b"bytes")
5655
5756
58 def test_realsorted_is_identical_to_natsorted_with_real_alg(float_list):
57 def test_realsorted_is_identical_to_natsorted_with_real_alg(
58 float_list: List[str],
59 ) -> None:
5960 assert realsorted(float_list) == natsorted(float_list, alg=ns.REAL)
6061
6162
6263 @pytest.mark.usefixtures("with_locale_en_us")
63 def test_humansorted_is_identical_to_natsorted_with_locale_alg(fruit_list):
64 def test_humansorted_is_identical_to_natsorted_with_locale_alg(
65 fruit_list: List[str],
66 ) -> None:
6467 assert humansorted(fruit_list) == natsorted(fruit_list, alg=ns.LOCALE)
6568
6669
67 def test_index_natsorted_returns_integer_list_of_sort_order_for_input_list():
70 def test_index_natsorted_returns_integer_list_of_sort_order_for_input_list() -> None:
6871 given = ["num3", "num5", "num2"]
6972 other = ["foo", "bar", "baz"]
7073 index = index_natsorted(given)
7376 assert [other[i] for i in index] == ["baz", "foo", "bar"]
7477
7578
76 def test_index_natsorted_reverse():
79 def test_index_natsorted_reverse() -> None:
7780 given = ["num3", "num5", "num2"]
7881 assert index_natsorted(given, reverse=True) == index_natsorted(given)[::-1]
7982
8083
81 def test_index_natsorted_applies_key_function_before_sorting():
84 def test_index_natsorted_applies_key_function_before_sorting() -> None:
8285 given = [("a", "num3"), ("b", "num5"), ("c", "num2")]
8386 expected = [2, 0, 1]
8487 assert index_natsorted(given, key=itemgetter(1)) == expected
8588
8689
87 def test_index_realsorted_is_identical_to_index_natsorted_with_real_alg(float_list):
90 def test_index_realsorted_is_identical_to_index_natsorted_with_real_alg(
91 float_list: List[str],
92 ) -> None:
8893 assert index_realsorted(float_list) == index_natsorted(float_list, alg=ns.REAL)
8994
9095
9196 @pytest.mark.usefixtures("with_locale_en_us")
92 def test_index_humansorted_is_identical_to_index_natsorted_with_locale_alg(fruit_list):
97 def test_index_humansorted_is_identical_to_index_natsorted_with_locale_alg(
98 fruit_list: List[str],
99 ) -> None:
93100 assert index_humansorted(fruit_list) == index_natsorted(fruit_list, alg=ns.LOCALE)
94101
95102
96 def test_order_by_index_sorts_list_according_to_order_of_integer_list():
103 def test_order_by_index_sorts_list_according_to_order_of_integer_list() -> None:
97104 given = ["num3", "num5", "num2"]
98105 index = [2, 0, 1]
99106 expected = [given[i] for i in index]
101108 assert order_by_index(given, index) == expected
102109
103110
104 def test_order_by_index_returns_generator_with_iter_true():
111 def test_order_by_index_returns_generator_with_iter_true() -> None:
105112 given = ["num3", "num5", "num2"]
106113 index = [2, 0, 1]
107114 assert order_by_index(given, index, True) != [given[i] for i in index]
0 import pytest
01 from natsort import ns
12
23
3 def test_ns_enum():
4 enum_name_values = [
4 @pytest.mark.parametrize(
5 "given, expected",
6 [
57 ("FLOAT", 0x0001),
68 ("SIGNED", 0x0002),
79 ("NOEXP", 0x0004),
3941 ("NL", 0x0400),
4042 ("CN", 0x0800),
4143 ("NA", 0x1000),
42 ]
43 assert list(ns._asdict().items()) == enum_name_values
44 ],
45 )
46 def test_ns_enum(given: str, expected: int) -> None:
47 assert ns[given] == expected
22 Testing for the OS sorting
33 """
44 import platform
5 from typing import cast
56
67 import natsort
78 import pytest
1415 has_icu = True
1516
1617
17 def test_os_sorted_compound():
18 def test_os_sorted_compound() -> None:
1819 given = [
1920 "/p/Folder (10)/file.tar.gz",
2021 "/p/Folder (1)/file (1).tar.gz",
3536 assert result == expected
3637
3738
38 def test_os_sorted_misc_no_fail():
39 def test_os_sorted_misc_no_fail() -> None:
3940 natsort.os_sorted([9, 4.3, None, float("nan")])
4041
4142
42 def test_os_sorted_key():
43 def test_os_sorted_key() -> None:
4344 given = ["foo0", "foo2", "goo1"]
4445 expected = ["foo0", "goo1", "foo2"]
45 result = natsort.os_sorted(given, key=lambda x: x.replace("g", "f"))
46 result = natsort.os_sorted(given, key=lambda x: cast(str, x).replace("g", "f"))
4647 assert result == expected
4748
4849
198199
199200
200201 @pytest.mark.usefixtures("with_locale_en_us")
201 def test_os_sorted_corpus():
202 def test_os_sorted_corpus() -> None:
202203 result = natsort.os_sorted(given)
203204 print(result)
204205 assert result == expected
33 import pytest
44 from hypothesis import given
55 from hypothesis.strategies import binary
6 from natsort.ns_enum import ns
7 from natsort.utils import parse_bytes_factory
6 from natsort.ns_enum import NSType, ns
7 from natsort.utils import BytesTransformer, parse_bytes_factory
88
99
1010 @pytest.mark.parametrize(
1818 ],
1919 )
2020 @given(x=binary())
21 def test_parse_bytest_factory_makes_function_that_returns_tuple(x, alg, example_func):
21 def test_parse_bytest_factory_makes_function_that_returns_tuple(
22 x: bytes, alg: NSType, example_func: BytesTransformer
23 ) -> None:
2224 parse_bytes_func = parse_bytes_factory(alg)
2325 assert parse_bytes_func(x) == example_func(x)
00 # -*- coding: utf-8 -*-
11 """These test the utils.py functions."""
2
3 from typing import Optional, Tuple, Union
24
35 import pytest
46 from hypothesis import given
57 from hypothesis.strategies import floats, integers
6 from natsort.ns_enum import ns
7 from natsort.utils import parse_number_or_none_factory
8 from natsort.ns_enum import NSType, ns
9 from natsort.utils import MaybeNumTransformer, parse_number_or_none_factory
810
911
1012 @pytest.mark.usefixtures("with_locale_en_us")
1820 ],
1921 )
2022 @given(x=floats(allow_nan=False) | integers())
21 def test_parse_number_factory_makes_function_that_returns_tuple(x, alg, example_func):
23 def test_parse_number_factory_makes_function_that_returns_tuple(
24 x: Union[float, int], alg: NSType, example_func: MaybeNumTransformer
25 ) -> None:
2226 parse_number_func = parse_number_or_none_factory(alg, "", "xx")
2327 assert parse_number_func(x) == example_func(x)
2428
3337 (ns.NANLAST, None, ("", float("+inf"))), # NANLAST makes it +infinity
3438 ],
3539 )
36 def test_parse_number_factory_treats_nan_and_none_special(alg, x, result):
40 def test_parse_number_factory_treats_nan_and_none_special(
41 alg: NSType, x: Optional[Union[float, int]], result: Tuple[str, Union[float, int]]
42 ) -> None:
3743 parse_number_func = parse_number_or_none_factory(alg, "", "xx")
3844 assert parse_number_func(x) == result
11 """These test the utils.py functions."""
22
33 import unicodedata
4 from typing import Any, Callable, Iterable, List, Tuple, Union
45
56 import pytest
67 from hypothesis import given
78 from hypothesis.strategies import floats, integers, lists, text
89 from natsort.compat.fastnumbers import fast_float
9 from natsort.ns_enum import NS_DUMB, ns
10 from natsort.utils import NumericalRegularExpressions as NumRegex
10 from natsort.ns_enum import NSType, NS_DUMB, ns
11 from natsort.utils import (
12 FinalTransform,
13 NumericalRegularExpressions as NumRegex,
14 StrParser,
15 )
1116 from natsort.utils import parse_string_factory
1217
1318
14 class CustomTuple(tuple):
19 class CustomTuple(Tuple[Any, ...]):
1520 """Used to ensure what is given during testing is what is returned."""
1621
17 original = None
22 original: Any = None
1823
1924
20 def input_transform(x):
25 def input_transform(x: Any) -> Any:
2126 """Make uppercase."""
2227 try:
2328 return x.upper()
2530 return x
2631
2732
28 def final_transform(x, original):
33 def final_transform(x: Iterable[Any], original: str) -> FinalTransform:
2934 """Make the input a CustomTuple."""
3035 t = CustomTuple(x)
3136 t.original = original
3237 return t
3338
3439
35 @pytest.fixture
36 def parse_string_func(request):
40 def parse_string_func_factory(alg: NSType) -> StrParser:
3741 """A parse_string_factory result with sample arguments."""
3842 sep = ""
3943 return parse_string_factory(
40 request.param, # algorirhm
44 alg,
4145 sep,
4246 NumRegex.int_nosign().split,
4347 input_transform,
4650 )
4751
4852
49 @pytest.mark.parametrize("parse_string_func", [ns.DEFAULT], indirect=True)
5053 @given(x=floats() | integers())
51 def test_parse_string_factory_raises_type_error_if_given_number(x, parse_string_func):
54 def test_parse_string_factory_raises_type_error_if_given_number(
55 x: Union[int, float]
56 ) -> None:
57 parse_string_func = parse_string_func_factory(ns.DEFAULT)
5258 with pytest.raises(TypeError):
53 assert parse_string_func(x)
59 assert parse_string_func(x) # type: ignore
5460
5561
5662 # noinspection PyCallingNonCallable
5763 @pytest.mark.parametrize(
58 "parse_string_func, orig_func",
64 "alg, orig_func",
5965 [
6066 (ns.DEFAULT, lambda x: x.upper()),
6167 (ns.LOCALE, lambda x: x.upper()),
6268 (ns.LOCALE | NS_DUMB, lambda x: x), # This changes the "original" handling.
6369 ],
64 indirect=["parse_string_func"],
6570 )
6671 @given(
6772 x=lists(
6974 )
7075 )
7176 @pytest.mark.usefixtures("with_locale_en_us")
72 def test_parse_string_factory_invariance(x, parse_string_func, orig_func):
77 def test_parse_string_factory_invariance(
78 x: List[Union[float, str, int]], alg: NSType, orig_func: Callable[[str], str]
79 ) -> None:
80 parse_string_func = parse_string_func_factory(alg)
7381 # parse_string_factory is the high-level combination of several dedicated
7482 # functions involved in splitting and manipulating a string. The details of
7583 # what those functions do is not relevant to testing parse_string_factory.
00 # -*- coding: utf-8 -*-
11 """These test the splitting regular expressions."""
22
3 from typing import List, Pattern
4
35 import pytest
46 from natsort import ns, numeric_regex_chooser
7 from natsort.ns_enum import NSType
58 from natsort.utils import NumericalRegularExpressions as NumRegex
69
710
8083 f_s: ["", "12", "", "①", "", "②", "", "Ⅰ", "", "Ⅱ", "", "⅓", ""],
8184 f_ue: ["", "12", "", "①", "", "②", "", "Ⅰ", "", "Ⅱ", "", "⅓", ""],
8285 f_se: ["", "12", "", "①", "", "②", "", "Ⅰ", "", "Ⅱ", "", "⅓", ""],
83 }
86 },
8487 }
8588
8689
9497
9598
9699 @pytest.mark.parametrize("x, expected, regex", regex_params, ids=labels)
97 def test_regex_splits_correctly(x, expected, regex):
100 def test_regex_splits_correctly(
101 x: str, expected: List[str], regex: Pattern[str]
102 ) -> None:
98103 # noinspection PyUnresolvedReferences
99104 assert regex.split(x) == expected
100105
114119 (ns.FLOAT | ns.UNSIGNED | ns.NOEXP, NumRegex.float_nosign_noexp()),
115120 ],
116121 )
117 def test_regex_chooser(given, expected):
122 def test_regex_chooser(given: NSType, expected: Pattern[str]) -> None:
118123 assert numeric_regex_chooser(given) == expected.pattern[1:-1] # remove parens
11 """These test the utils.py functions."""
22
33 from functools import partial
4 from typing import Any, Callable, FrozenSet, Union
45
56 import pytest
67 from hypothesis import example, given
78 from hypothesis.strategies import floats, integers, text
89 from natsort.compat.fastnumbers import fast_float, fast_int
910 from natsort.compat.locale import get_strxfrm
10 from natsort.ns_enum import NS_DUMB, ns
11 from natsort.ns_enum import NSType, NS_DUMB, ns
1112 from natsort.utils import groupletters, string_component_transform_factory
1213
1314 # There are some unicode values that are known failures with the builtin locale
1415 # library on BSD systems that has nothing to do with natsort (a ValueError is
1516 # raised by strxfrm). Let's filter them out.
1617 try:
17 bad_uni_chars = frozenset(
18 chr(x) for x in range(0X10fefd, 0X10ffff + 1)
19 )
18 bad_uni_chars = frozenset(chr(x) for x in range(0x10FEFD, 0x10FFFF + 1))
2019 except ValueError:
2120 # Narrow unicode build... no worries.
2221 bad_uni_chars = frozenset()
2322
2423
25 def no_bad_uni_chars(x, _bad_chars=bad_uni_chars):
24 def no_bad_uni_chars(x: str, _bad_chars: FrozenSet[str] = bad_uni_chars) -> bool:
2625 """Ensure text does not contain bad unicode characters"""
2726 return not any(y in _bad_chars for y in x)
2827
2928
30 def no_null(x):
29 def no_null(x: str) -> bool:
3130 """Ensure text does not contain a null character."""
3231 return "\0" not in x
3332
6665 | text().filter(bool).filter(no_bad_uni_chars).filter(no_null)
6766 )
6867 @pytest.mark.usefixtures("with_locale_en_us")
69 def test_string_component_transform_factory(x, alg, example_func):
68 def test_string_component_transform_factory(
69 x: Union[str, float, int], alg: NSType, example_func: Callable[[str], Any]
70 ) -> None:
7071 string_component_transform_func = string_component_transform_factory(alg)
7172 try:
7273 assert string_component_transform_func(str(x)) == example_func(str(x))
1313 digits_no_decimals,
1414 numeric,
1515 numeric_chars,
16 numeric_hex,
1716 numeric_no_decimals,
1817 )
18 from natsort.unicode_numeric_hex import numeric_hex
1919
2020
21 def test_numeric_chars_contains_only_valid_unicode_numeric_characters():
21 def test_numeric_chars_contains_only_valid_unicode_numeric_characters() -> None:
2222 for a in numeric_chars:
2323 assert unicodedata.numeric(a, None) is not None
2424
2525
26 def test_digit_chars_contains_only_valid_unicode_digit_characters():
26 def test_digit_chars_contains_only_valid_unicode_digit_characters() -> None:
2727 for a in digit_chars:
2828 assert unicodedata.digit(a, None) is not None
2929
3030
31 def test_decimal_chars_contains_only_valid_unicode_decimal_characters():
31 def test_decimal_chars_contains_only_valid_unicode_decimal_characters() -> None:
3232 for a in decimal_chars:
3333 assert unicodedata.decimal(a, None) is not None
3434
3535
36 def test_numeric_chars_contains_all_valid_unicode_numeric_and_digit_characters():
36 def test_numeric_chars_contains_all_valid_unicode_numeric_and_digit_characters() -> None:
3737 set_numeric_chars = set(numeric_chars)
3838 set_digit_chars = set(digit_chars)
3939 set_decimal_chars = set(decimal_chars)
4545 assert set_numeric_chars.issuperset(numeric_no_decimals)
4646
4747
48 def test_missing_unicode_number_in_collection():
48 def test_missing_unicode_number_in_collection() -> None:
4949 ok = True
5050 set_numeric_hex = set(numeric_hex)
5151 for i in range(0x110000):
7070 )
7171
7272
73 def test_combined_string_contains_all_characters_in_list():
73 def test_combined_string_contains_all_characters_in_list() -> None:
7474 assert numeric == "".join(numeric_chars)
7575 assert digits == "".join(digit_chars)
7676 assert decimals == "".join(decimal_chars)
55 import string
66 from itertools import chain
77 from operator import neg as op_neg
8 from typing import List, Pattern, Union
89
910 import pytest
1011 from hypothesis import given
1112 from hypothesis.strategies import integers, lists, sampled_from, text
1213 from natsort import utils
13 from natsort.ns_enum import ns
14 from natsort.ns_enum import NSType, ns
1415
1516
16 def test_do_decoding_decodes_bytes_string_to_unicode():
17 def test_do_decoding_decodes_bytes_string_to_unicode() -> None:
1718 assert type(utils.do_decoding(b"bytes", "ascii")) is str
1819 assert utils.do_decoding(b"bytes", "ascii") == "bytes"
1920 assert utils.do_decoding(b"bytes", "ascii") == b"bytes".decode("ascii")
3233 (ns.F | ns.S | ns.N, utils.NumericalRegularExpressions.float_sign_noexp()),
3334 ],
3435 )
35 def test_regex_chooser_returns_correct_regular_expression_object(alg, expected):
36 def test_regex_chooser_returns_correct_regular_expression_object(
37 alg: NSType, expected: Pattern[str]
38 ) -> None:
3639 assert utils.regex_chooser(alg).pattern == expected.pattern
3740
3841
6770 (ns.REAL, ns.FLOAT | ns.SIGNED),
6871 ],
6972 )
70 def test_ns_enum_values_and_aliases(alg, value_or_alias):
73 def test_ns_enum_values_and_aliases(alg: NSType, value_or_alias: NSType) -> None:
7174 assert alg == value_or_alias
7275
7376
74 def test_chain_functions_is_a_no_op_if_no_functions_are_given():
77 def test_chain_functions_is_a_no_op_if_no_functions_are_given() -> None:
7578 x = 2345
7679 assert utils.chain_functions([])(x) is x
7780
7881
79 def test_chain_functions_does_one_function_if_one_function_is_given():
82 def test_chain_functions_does_one_function_if_one_function_is_given() -> None:
8083 x = "2345"
8184 assert utils.chain_functions([len])(x) == 4
8285
8386
84 def test_chain_functions_combines_functions_in_given_order():
87 def test_chain_functions_combines_functions_in_given_order() -> None:
8588 x = 2345
8689 assert utils.chain_functions([str, len, op_neg])(x) == -len(str(x))
8790
9093 # and a test that uses the hypothesis module.
9194
9295
93 def test_groupletters_returns_letters_with_lowercase_transform_of_letter_example():
96 def test_groupletters_gives_letters_with_lowercase_letter_transform_example() -> None:
9497 assert utils.groupletters("HELLO") == "hHeElLlLoO"
9598 assert utils.groupletters("hello") == "hheelllloo"
9699
97100
98101 @given(text().filter(bool))
99 def test_groupletters_returns_letters_with_lowercase_transform_of_letter(x):
102 def test_groupletters_gives_letters_with_lowercase_letter_transform(
103 x: str,
104 ) -> None:
100105 assert utils.groupletters(x) == "".join(
101106 chain.from_iterable([y.casefold(), y] for y in x)
102107 )
103108
104109
105 def test_sep_inserter_does_nothing_if_no_numbers_example():
110 def test_sep_inserter_does_nothing_if_no_numbers_example() -> None:
106111 assert list(utils.sep_inserter(iter(["a", "b", "c"]), "")) == ["a", "b", "c"]
107112 assert list(utils.sep_inserter(iter(["a"]), "")) == ["a"]
108113
109114
110 def test_sep_inserter_does_nothing_if_only_one_number_example():
115 def test_sep_inserter_does_nothing_if_only_one_number_example() -> None:
111116 assert list(utils.sep_inserter(iter(["a", 5]), "")) == ["a", 5]
112117
113118
114 def test_sep_inserter_inserts_separator_string_between_two_numbers_example():
119 def test_sep_inserter_inserts_separator_string_between_two_numbers_example() -> None:
115120 assert list(utils.sep_inserter(iter([5, 9]), "")) == ["", 5, "", 9]
116121
117122
118123 @given(lists(elements=text().filter(bool) | integers(), min_size=3))
119 def test_sep_inserter_inserts_separator_between_two_numbers(x):
124 def test_sep_inserter_inserts_separator_between_two_numbers(
125 x: List[Union[str, int]]
126 ) -> None:
120127 # Rather than just replicating the results in a different algorithm,
121128 # validate that the "shape" of the output is as expected.
122129 result = list(utils.sep_inserter(iter(x), ""))
126133 assert isinstance(result[i + 1], int)
127134
128135
129 def test_path_splitter_splits_path_string_by_separator_example():
136 def test_path_splitter_splits_path_string_by_sep_example() -> None:
130137 given = "/this/is/a/path"
131138 expected = (os.sep, "this", "is", "a", "path")
132139 assert tuple(utils.path_splitter(given)) == tuple(expected)
133 given = pathlib.Path(given)
134 assert tuple(utils.path_splitter(given)) == tuple(expected)
140 assert tuple(utils.path_splitter(pathlib.Path(given))) == tuple(expected)
135141
136142
137143 @given(lists(sampled_from(string.ascii_letters), min_size=2).filter(all))
138 def test_path_splitter_splits_path_string_by_separator(x):
144 def test_path_splitter_splits_path_string_by_sep(x: List[str]) -> None:
139145 z = str(pathlib.Path(*x))
140146 assert tuple(utils.path_splitter(z)) == tuple(pathlib.Path(z).parts)
141147
142148
143 def test_path_splitter_splits_path_string_by_separator_and_removes_extension_example():
149 def test_path_splitter_splits_path_string_by_sep_and_removes_extension_example() -> None:
144150 given = "/this/is/a/path/file.x1.10.tar.gz"
145151 expected = (os.sep, "this", "is", "a", "path", "file.x1.10", ".tar", ".gz")
146152 assert tuple(utils.path_splitter(given)) == tuple(expected)
147153
148154
149155 @given(lists(sampled_from(string.ascii_letters), min_size=3).filter(all))
150 def test_path_splitter_splits_path_string_by_separator_and_removes_extension(x):
156 def test_path_splitter_splits_path_string_by_sep_and_removes_extension(
157 x: List[str],
158 ) -> None:
151159 z = str(pathlib.Path(*x[:-2])) + "." + x[-1]
152160 y = tuple(pathlib.Path(z).parts)
153161 assert tuple(utils.path_splitter(z)) == y[:-1] + (
44
55 [tox]
66 envlist =
7 flake8, py35, py36, py37, py38, py39
7 flake8, mypy, py36, py37, py38, py39, py310
88 # Other valid evironments are:
99 # docs
1010 # release
1919 passenv =
2020 WITH_EXTRAS
2121 deps =
22 -r dev/requirements.txt
22 coverage
23 pytest
24 pytest-cov
25 pytest-mock
26 hypothesis
27 semver
2328 extras =
2429 {env:WITH_EXTRAS:}
2530 commands =
4651 twine check dist/*
4752 skip_install = true
4853
54 # Type checking
55 [testenv:mypy]
56 deps =
57 mypy
58 hypothesis
59 pytest
60 pytest-mock
61 fastnumbers
62 commands =
63 mypy --strict natsort tests
64 skip_install = true
65
4966 # Build documentation.
5067 # sphinx and sphinx_rtd_theme not in docs/requirements.txt because they
5168 # will already be installed on readthedocs.
5269 [testenv:docs]
5370 deps =
54 sphinx < 3.3.0
71 sphinx
5572 sphinx_rtd_theme
5673 -r docs/requirements.txt
5774 commands =
8299 deps =
83100 commands = {envpython} dev/clean.py
84101 skip_install = true
102
103 # Get GitHub actions to run the correct tox environment
104 [gh-actions]
105 python =
106 3.5: py35
107 3.6: py36
108 3.7: py37
109 3.8: py38
110 3.9: py39
111 3.10: py310