New upstream release.
Debian Janitor
2 years ago
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 | --- | |
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 | --- | |
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 | --- | |
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 | 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 |
0 | 0 | Unreleased |
1 | 1 | --- |
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) | |
2 | 29 | |
3 | 30 | [7.1.0] - 2020-11-19 |
4 | 31 | --- |
549 | 576 | - Sorting algorithm to support floats (including exponentials) and basic version number support |
550 | 577 | |
551 | 578 | <!---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 | |
552 | 582 | [7.1.0]: https://github.com/SethMMorton/natsort/compare/7.0.1...7.1.0 |
553 | 583 | [7.0.1]: https://github.com/SethMMorton/natsort/compare/7.0.0...7.0.1 |
554 | 584 | [7.0.0]: https://github.com/SethMMorton/natsort/compare/6.2.0...7.0.0 |
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 | # 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 | |
1 | 1 | |
2 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy of |
3 | 3 | 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 |
9 | 9 | .. image:: https://img.shields.io/pypi/l/natsort.svg |
10 | 10 | :target: https://github.com/SethMMorton/natsort/blob/master/LICENSE |
11 | 11 | |
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 | |
14 | 14 | |
15 | 15 | .. image:: https://codecov.io/gh/SethMMorton/natsort/branch/master/graph/badge.svg |
16 | 16 | :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 | |
20 | 17 | |
21 | 18 | Simple yet flexible natural sorting in Python. |
22 | 19 | |
150 | 147 | to also install `PyICU <https://pypi.org/project/PyICU>`_, which will help |
151 | 148 | ``natsort`` give results that match most file browsers. If this is not installed, |
152 | 149 | 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. | |
154 | 151 | |
155 | 152 | Sorting by Real Numbers (i.e. Signed Floats) |
156 | 153 | ++++++++++++++++++++++++++++++++++++++++++++ |
371 | 368 | Requirements |
372 | 369 | ------------ |
373 | 370 | |
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. | |
376 | 372 | |
377 | 373 | Optional Dependencies |
378 | 374 | --------------------- |
437 | 433 | is done with ``tox -e flake8``. You can see all available testing environments |
438 | 434 | with ``tox --listenvs``. |
439 | 435 | |
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 | ||
452 | 436 | How to Build Documentation |
453 | 437 | -------------------------- |
454 | 438 | |
459 | 443 | |
460 | 444 | $ tox -e docs |
461 | 445 | |
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``. | |
469 | 447 | |
470 | 448 | Deprecation Schedule |
471 | 449 | -------------------- |
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. | |
472 | 455 | |
473 | 456 | Dropped Python 2.7 Support |
474 | 457 | ++++++++++++++++++++++++++ |
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 | ||
0 | 6 | natsort (7.1.0-1) unstable; urgency=medium |
1 | 7 | |
2 | 8 | * Team upload. |
8 | 8 | - `clean.py` - This file cleans most files that are created during development. |
9 | 9 | Run in the project home directory. |
10 | 10 | 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. |
32 | 32 | cmd = ["bump2version", *args, severity] |
33 | 33 | try: |
34 | 34 | 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 | |
36 | 38 | else: |
37 | 39 | subprocess.run(cmd, check=True, text=True) |
38 | 40 | except subprocess.CalledProcessError as e: |
56 | 58 | "<!---Comparison links-->\n[{new}]: {url}/{current}...{new}".format( |
57 | 59 | new=data["new_version"], |
58 | 60 | current=data["current_version"], |
59 | url="https://github.com/SethMMorton/natsort/compare" | |
60 | ) | |
61 | url="https://github.com/SethMMorton/natsort/compare", | |
62 | ), | |
61 | 63 | ) |
62 | 64 | with open("CHANGELOG.md", "w") as fl: |
63 | 65 | fl.write(changelog) |
0 | coverage | |
1 | pytest | |
2 | pytest-cov | |
3 | pytest-mock | |
4 | hypothesis | |
5 | # pytest-faulthandler; platform_python_implementation == 'CPython' | |
6 | semver |
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 |
116 | 116 | the corresponding regular expression to locate numbers will be returned. |
117 | 117 | |
118 | 118 | .. 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 | +--------------------------------+----------------------------------------------------------------------------------------+ |
27 | 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom |
28 | 28 | # ones. |
29 | 29 | 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", | |
36 | 36 | ] |
37 | autodoc_typehints = "none" | |
37 | 38 | |
38 | 39 | # Add any paths that contain templates here, relative to this directory. |
39 | templates_path = ['_templates'] | |
40 | templates_path = ["_templates"] | |
40 | 41 | |
41 | 42 | # The suffix of source filenames. |
42 | source_suffix = ['.rst', '.md'] | |
43 | source_suffix = [".rst", ".md"] | |
43 | 44 | |
44 | 45 | # The encoding of source files. |
45 | 46 | # source_encoding = 'utf-8-sig' |
46 | 47 | |
47 | 48 | # The master toctree document. |
48 | master_doc = 'index' | |
49 | master_doc = "index" | |
49 | 50 | |
50 | 51 | # General information about the project. |
51 | project = 'natsort' | |
52 | project = "natsort" | |
52 | 53 | # noinspection PyShadowingBuiltins |
53 | copyright = '2014, Seth M. Morton' | |
54 | copyright = "2014, Seth M. Morton" | |
54 | 55 | |
55 | 56 | # The version info for the project you're documenting, acts as replacement for |
56 | 57 | # |version| and |release|, also used in various other places throughout the |
57 | 58 | # built documents. |
58 | 59 | # |
59 | 60 | # The full version, including alpha/beta/rc tags. |
60 | release = '7.1.0' | |
61 | release = "8.0.0" | |
61 | 62 | # The short X.Y version. |
62 | version = '.'.join(release.split('.')[0:2]) | |
63 | version = ".".join(release.split(".")[0:2]) | |
63 | 64 | |
64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation |
65 | 66 | # for a list of supported languages. |
91 | 92 | # show_authors = False |
92 | 93 | |
93 | 94 | # 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" | |
96 | 97 | |
97 | 98 | # A list of ignored prefixes for module index sorting. |
98 | 99 | # modindex_common_prefix = [] |
105 | 106 | |
106 | 107 | # The theme to use for HTML and HTML Help pages. See the documentation for |
107 | 108 | # a list of builtin themes. |
108 | on_rtd = os.environ.get('READTHEDOCS') == 'True' | |
109 | on_rtd = os.environ.get("READTHEDOCS") == "True" | |
109 | 110 | if on_rtd: |
110 | html_theme = 'default' | |
111 | html_theme = "default" | |
111 | 112 | else: |
112 | 113 | import sphinx_rtd_theme # noqa: F401 |
113 | 114 | |
114 | html_theme = 'sphinx_rtd_theme' | |
115 | html_theme = "sphinx_rtd_theme" | |
115 | 116 | # html_theme = 'solar' |
116 | 117 | |
117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme |
120 | 121 | # html_theme_options = {} |
121 | 122 | |
122 | 123 | # Add any paths that contain custom themes here, relative to this directory. |
123 | html_theme_path = ['.'] | |
124 | html_theme_path = ["."] | |
124 | 125 | |
125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to |
126 | 127 | # "<project> v<release> documentation". |
190 | 191 | # html_file_suffix = None |
191 | 192 | |
192 | 193 | # Output file base name for HTML help builder. |
193 | htmlhelp_basename = 'natsortdoc' | |
194 | htmlhelp_basename = "natsortdoc" | |
194 | 195 | |
195 | 196 | # -- Options for LaTeX output --------------------------------------------- |
196 | 197 | |
197 | 198 | latex_elements = { |
198 | 199 | # The paper size ('letterpaper' or 'a4paper'). |
199 | 200 | # 'papersize': 'letterpaper', |
200 | ||
201 | 201 | # The font size ('10pt', '11pt' or '12pt'). |
202 | 202 | # 'pointsize': '10pt', |
203 | ||
204 | 203 | # Additional stuff for the LaTeX preamble. |
205 | 204 | # 'preamble': '', |
206 | 205 | } |
209 | 208 | # (source start file, target name, title, |
210 | 209 | # author, documentclass [howto, manual, or own class]). |
211 | 210 | latex_documents = [ |
212 | ('index', 'natsort.tex', 'natsort Documentation', | |
213 | 'Seth M. Morton', 'manual'), | |
211 | ("index", "natsort.tex", "natsort Documentation", "Seth M. Morton", "manual"), | |
214 | 212 | ] |
215 | 213 | |
216 | 214 | # The name of an image file (relative to this directory) to place at the top of |
238 | 236 | |
239 | 237 | # One entry per manual page. List of tuples |
240 | 238 | # (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)] | |
245 | 240 | |
246 | 241 | # If true, show URL addresses after external links. |
247 | 242 | # man_show_urls = False |
253 | 248 | # (source start file, target name, title, author, |
254 | 249 | # dir menu entry, description, category) |
255 | 250 | 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 | ), | |
259 | 260 | ] |
260 | 261 | |
261 | 262 | # Documents to append as an appendix to all manuals. |
272 | 273 | |
273 | 274 | |
274 | 275 | # 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)} |
80 | 80 | |
81 | 81 | .. code-block:: pycon |
82 | 82 | |
83 | >>> from semver import parse_version_info | |
83 | >>> from semver import VersionInfo | |
84 | 84 | >>> 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) | |
86 | 86 | ['3.4.5-pre.1', '3.4.5-pre.2+build.4', '3.4.5'] |
87 | 87 | |
88 | 88 | .. _path_sort: |
22 | 22 | |
23 | 23 | When :func:`~natsort.natsort_keygen` is called it returns a key function that |
24 | 24 | 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 | |
26 | 26 | *loaded at the time the key is generated*. If you change the locale, |
27 | 27 | you should regenerate the key to account for the new locale. |
28 | 28 |
3 | 3 | # |
4 | 4 | # pip-compile |
5 | 5 | # |
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 |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | |
2 | 2 | from natsort.natsort import ( |
3 | NatsortKeyType, | |
4 | OSSortKeyType, | |
3 | 5 | as_ascii, |
4 | 6 | as_utf8, |
5 | 7 | decoder, |
10 | 12 | natsort_key, |
11 | 13 | natsort_keygen, |
12 | 14 | natsorted, |
13 | ns, | |
14 | 15 | numeric_regex_chooser, |
15 | 16 | order_by_index, |
16 | 17 | os_sort_key, |
18 | 19 | os_sorted, |
19 | 20 | realsorted, |
20 | 21 | ) |
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 | |
22 | 24 | |
23 | __version__ = "7.1.0" | |
25 | __version__ = "8.0.0" | |
24 | 26 | |
25 | 27 | __all__ = [ |
26 | 28 | "natsort_key", |
41 | 43 | "os_sort_key", |
42 | 44 | "os_sort_keygen", |
43 | 45 | "os_sorted", |
46 | "NatsortKeyType", | |
47 | "OSSortKeyType", | |
48 | "KeyType", | |
49 | "NatsortInType", | |
50 | "NatsortOutType", | |
51 | "NSType", | |
44 | 52 | ] |
45 | 53 | |
46 | 54 | # 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()}) |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | |
2 | import argparse | |
2 | 3 | import sys |
4 | from typing import Callable, Iterable, List, Optional, Pattern, Tuple, Union, cast | |
3 | 5 | |
4 | 6 | import natsort |
5 | 7 | from natsort.utils import regex_chooser |
6 | 8 | |
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: | |
9 | 50 | """ |
10 | 51 | Performs a natural sort on entries given on the command-line. |
11 | 52 | |
16 | 57 | from textwrap import dedent |
17 | 58 | |
18 | 59 | parser = ArgumentParser( |
19 | description=dedent(main.__doc__), formatter_class=RawDescriptionHelpFormatter | |
60 | description=dedent(cast(str, main.__doc__)), | |
61 | formatter_class=RawDescriptionHelpFormatter, | |
20 | 62 | ) |
21 | 63 | parser.add_argument( |
22 | 64 | "--version", |
125 | 167 | help="The entries to sort. Taken from stdin if nothing is given on " |
126 | 168 | "the command line.", |
127 | 169 | ) |
128 | args = parser.parse_args(arguments or None) | |
170 | args = parser.parse_args(arguments or None, namespace=TypedArgs()) | |
129 | 171 | |
130 | 172 | # Make sure the filter range is given properly. Does nothing if no filter |
131 | 173 | args.filter = check_filters(args.filter) |
138 | 180 | sort_and_print_entries(entries, args) |
139 | 181 | |
140 | 182 | |
141 | def range_check(low, high): | |
183 | def range_check(low: Num, high: Num) -> NumPair: | |
142 | 184 | """ |
143 | 185 | Verify that that given range has a low lower than the high. |
144 | 186 | |
163 | 205 | return low, high |
164 | 206 | |
165 | 207 | |
166 | def check_filters(filters): | |
208 | def check_filters(filters: Optional[NumPairIter]) -> Optional[List[NumPair]]: | |
167 | 209 | """ |
168 | 210 | Execute range_check for every element of an iterable. |
169 | 211 | |
191 | 233 | raise ValueError("Error in --filter: " + str(err)) |
192 | 234 | |
193 | 235 | |
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: | |
195 | 243 | """ |
196 | 244 | Check if an entry falls into a desired range. |
197 | 245 | |
223 | 271 | ) |
224 | 272 | |
225 | 273 | |
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: | |
227 | 277 | """ |
228 | 278 | Check if an entry does not match a given value. |
229 | 279 | |
248 | 298 | return not any(converter(num) in values for num in regex.findall(entry)) |
249 | 299 | |
250 | 300 | |
251 | def sort_and_print_entries(entries, args): | |
301 | def sort_and_print_entries(entries: List[str], args: TypedArgs) -> None: | |
252 | 302 | """Sort the entries, applying the filters first if necessary.""" |
253 | 303 | |
254 | 304 | # Extract the proper number type. |
255 | 305 | is_float = args.number_type in ("float", "real", "f", "r") |
256 | 306 | signed = args.signed or args.number_type in ("real", "r") |
257 | alg = ( | |
307 | alg: int = ( | |
258 | 308 | natsort.ns.FLOAT * is_float |
259 | 309 | | natsort.ns.SIGNED * signed |
260 | 310 | | natsort.ns.NOEXP * (not args.exp) |
2 | 2 | This module is intended to replicate some of the functionality |
3 | 3 | from the fastnumbers module in the event that module is not installed. |
4 | 4 | """ |
5 | import unicodedata | |
6 | from typing import Callable, FrozenSet, Optional, Union | |
5 | 7 | |
6 | # Std. lib imports. | |
7 | import unicodedata | |
8 | ||
9 | # Local imports. | |
10 | 8 | from natsort.unicode_numbers import decimal_chars |
11 | 9 | |
12 | NAN_INF = [ | |
10 | _NAN_INF = [ | |
13 | 11 | "INF", |
14 | 12 | "INf", |
15 | 13 | "Inf", |
27 | 25 | "nAN", |
28 | 26 | "Nan", |
29 | 27 | ] |
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) | |
32 | 30 | ASCII_NUMS = "0123456789+-" |
33 | 31 | POTENTIAL_FIRST_CHAR = frozenset(decimal_chars + list(ASCII_NUMS + ".")) |
32 | ||
33 | StrOrFloat = Union[str, float] | |
34 | StrOrInt = Union[str, int] | |
34 | 35 | |
35 | 36 | |
36 | 37 | # noinspection PyIncorrectDocstring |
37 | 38 | 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: | |
45 | 46 | """ |
46 | 47 | Convert a string to a float quickly, return input as-is if not possible. |
47 | 48 | |
64 | 65 | """ |
65 | 66 | if x[0] in _first_char or x.lstrip()[:3] in _nan_inf: |
66 | 67 | 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 | |
69 | 70 | except ValueError: |
70 | 71 | try: |
71 | 72 | return _uni(x, key(x)) if len(x) == 1 else key(x) |
80 | 81 | |
81 | 82 | # noinspection PyIncorrectDocstring |
82 | 83 | 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: | |
88 | 89 | """ |
89 | 90 | Convert a string to a int quickly, return input as-is if not possible. |
90 | 91 |
2 | 2 | Interface for natsort to access fastnumbers functions without |
3 | 3 | having to worry if it is actually installed. |
4 | 4 | """ |
5 | import re | |
5 | 6 | |
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 | ||
7 | 26 | |
8 | 27 | # If the user has fastnumbers installed, they will get great speed |
9 | 28 | # benefits. If not, we use the simulated functions that come with natsort. |
12 | 31 | from fastnumbers import fast_float, fast_int, __version__ as fn_ver |
13 | 32 | |
14 | 33 | # Require >= version 2.0.0. |
15 | if StrictVersion(fn_ver) < StrictVersion("2.0.0"): | |
34 | if not is_supported_fastnumbers(fn_ver): | |
16 | 35 | raise ImportError # pragma: no cover |
17 | 36 | 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 |
2 | 2 | Interface for natsort to access locale functionality without |
3 | 3 | having to worry about if it is using PyICU or the built-in locale. |
4 | 4 | """ |
5 | import sys | |
6 | from typing import Callable, Union, cast | |
5 | 7 | |
6 | # Std. lib imports. | |
7 | import sys | |
8 | StrOrBytes = Union[str, bytes] | |
9 | TrxfmFunc = Callable[[str], StrOrBytes] | |
8 | 10 | |
9 | 11 | # This string should be sorted after any other byte string because |
10 | 12 | # it contains the max unicode character repeated 20 times. |
11 | 13 | # You would need some odd data to come after that. |
12 | 14 | null_string = "" |
13 | 15 | 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 | |
14 | 21 | |
15 | 22 | # strxfrm can be buggy (especially on BSD-based systems), |
16 | 23 | # so prefer icu if available. |
25 | 32 | # You would need some odd data to come after that. |
26 | 33 | null_string_locale_max = b"x7f" * 50 |
27 | 34 | |
28 | def dumb_sort(): | |
35 | def dumb_sort() -> bool: | |
29 | 36 | return False |
30 | 37 | |
31 | 38 | # If using icu, get the locale from the current global locale, |
32 | def get_icu_locale(): | |
39 | def get_icu_locale() -> str: | |
33 | 40 | try: |
34 | return icu.Locale(".".join(getlocale())) | |
41 | return cast(str, icu.Locale(".".join(getlocale()))) | |
35 | 42 | except TypeError: # pragma: no cover |
36 | return icu.Locale() | |
43 | return cast(str, icu.Locale()) | |
37 | 44 | |
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) | |
40 | 47 | |
41 | def get_thousands_sep(): | |
48 | def get_thousands_sep() -> str: | |
42 | 49 | sep = icu.DecimalFormatSymbols.kGroupingSeparatorSymbol |
43 | return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep) | |
50 | return cast(str, icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)) | |
44 | 51 | |
45 | def get_decimal_point(): | |
52 | def get_decimal_point() -> str: | |
46 | 53 | sep = icu.DecimalFormatSymbols.kDecimalSeparatorSymbol |
47 | return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep) | |
54 | return cast(str, icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)) | |
48 | 55 | |
49 | 56 | |
50 | 57 | except ImportError: |
56 | 63 | |
57 | 64 | # On some systems, locale is broken and does not sort in the expected |
58 | 65 | # order. We will try to detect this and compensate. |
59 | def dumb_sort(): | |
66 | def dumb_sort() -> bool: | |
60 | 67 | return strxfrm("A") < strxfrm("a") |
61 | 68 | |
62 | def get_strxfrm(): | |
69 | def get_strxfrm() -> TrxfmFunc: | |
63 | 70 | return strxfrm |
64 | 71 | |
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"]) | |
67 | 74 | # If this locale library is broken, some of the thousands separator |
68 | 75 | # characters are incorrectly blank. Here is a lookup table of the |
69 | 76 | # corrections I am aware of. |
110 | 117 | else: |
111 | 118 | return sep |
112 | 119 | |
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"]) |
8 | 8 | import platform |
9 | 9 | from functools import partial |
10 | 10 | 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 | ) | |
11 | 24 | |
12 | 25 | import natsort.compat.locale |
13 | 26 | 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]: | |
18 | 56 | """ |
19 | 57 | Return a function that can be used to decode bytes to unicode. |
20 | 58 | |
55 | 93 | return partial(utils.do_decoding, encoding=encoding) |
56 | 94 | |
57 | 95 | |
58 | def as_ascii(s): | |
96 | def as_ascii(s: NatsortInType) -> NatsortInType: | |
59 | 97 | """ |
60 | 98 | Function to decode an input with the ASCII codec, or return as-is. |
61 | 99 | |
78 | 116 | return utils.do_decoding(s, "ascii") |
79 | 117 | |
80 | 118 | |
81 | def as_utf8(s): | |
119 | def as_utf8(s: NatsortInType) -> NatsortInType: | |
82 | 120 | """ |
83 | 121 | Function to decode an input with the UTF-8 codec, or return as-is. |
84 | 122 | |
101 | 139 | return utils.do_decoding(s, "utf-8") |
102 | 140 | |
103 | 141 | |
104 | def natsort_keygen(key=None, alg=ns.DEFAULT): | |
142 | def natsort_keygen( | |
143 | key: MaybeKeyType = None, alg: NSType = ns.DEFAULT | |
144 | ) -> NatsortKeyType: | |
105 | 145 | """ |
106 | 146 | Generate a key to sort strings and numbers naturally. |
107 | 147 | |
211 | 251 | """ |
212 | 252 | |
213 | 253 | |
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: | |
215 | 274 | """ |
216 | 275 | Sorts an iterable naturally. |
217 | 276 | |
256 | 315 | ['num2', 'num3', 'num5'] |
257 | 316 | |
258 | 317 | """ |
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: | |
264 | 341 | """ |
265 | 342 | Convenience function to properly sort non-numeric characters. |
266 | 343 | |
312 | 389 | return natsorted(seq, key, reverse, alg | ns.LOCALE) |
313 | 390 | |
314 | 391 | |
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: | |
316 | 412 | """ |
317 | 413 | Convenience function to properly sort signed floats. |
318 | 414 | |
365 | 461 | return natsorted(seq, key, reverse, alg | ns.REAL) |
366 | 462 | |
367 | 463 | |
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: | |
369 | 484 | """ |
370 | 485 | Determine the list of the indexes used to sort the input sequence. |
371 | 486 | |
421 | 536 | ['baz', 'foo', 'bar'] |
422 | 537 | |
423 | 538 | """ |
539 | newkey: KeyType | |
424 | 540 | if key is None: |
425 | 541 | newkey = itemgetter(1) |
426 | 542 | else: |
427 | 543 | |
428 | def newkey(x): | |
429 | return key(itemgetter(1)(x)) | |
544 | def newkey(x: Any) -> NatsortInType: | |
545 | return cast(KeyType, key)(itemgetter(1)(x)) | |
430 | 546 | |
431 | 547 | # Pair the index and sequence together, then sort by element |
432 | 548 | index_seq_pair = [(x, y) for x, y in enumerate(seq)] |
434 | 550 | return [x for x, _ in index_seq_pair] |
435 | 551 | |
436 | 552 | |
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: | |
438 | 573 | """ |
439 | 574 | This is a wrapper around ``index_natsorted(seq, alg=ns.LOCALE)``. |
440 | 575 | |
483 | 618 | return index_natsorted(seq, key, reverse, alg | ns.LOCALE) |
484 | 619 | |
485 | 620 | |
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: | |
487 | 641 | """ |
488 | 642 | This is a wrapper around ``index_natsorted(seq, alg=ns.REAL)``. |
489 | 643 | |
529 | 683 | |
530 | 684 | |
531 | 685 | # 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: | |
533 | 689 | """ |
534 | 690 | Order a given sequence by an index sequence. |
535 | 691 | |
588 | 744 | return (seq[i] for i in index) if iter else [seq[i] for i in index] |
589 | 745 | |
590 | 746 | |
591 | def numeric_regex_chooser(alg): | |
747 | def numeric_regex_chooser(alg: NSType) -> str: | |
592 | 748 | """ |
593 | 749 | Select an appropriate regex for the type of number of interest. |
594 | 750 | |
607 | 763 | return utils.regex_chooser(alg).pattern[1:-1] |
608 | 764 | |
609 | 765 | |
610 | def _split_apply(v, key=None): | |
766 | def _split_apply(v: Any, key: MaybeKeyType = None) -> Iterator[str]: | |
611 | 767 | if key is not None: |
612 | 768 | v = key(v) |
613 | 769 | return utils.path_splitter(str(v)) |
616 | 772 | # Choose the implementation based on the host OS |
617 | 773 | if platform.system() == "Windows": |
618 | 774 | |
619 | from ctypes import wintypes, windll | |
775 | from ctypes import wintypes, windll # type: ignore | |
620 | 776 | from functools import cmp_to_key |
621 | 777 | |
622 | 778 | _windows_sort_cmp = windll.Shlwapi.StrCmpLogicalW |
624 | 780 | _windows_sort_cmp.restype = wintypes.INT |
625 | 781 | _winsort_key = cmp_to_key(_windows_sort_cmp) |
626 | 782 | |
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 | ) | |
629 | 787 | |
630 | 788 | |
631 | 789 | else: |
644 | 802 | |
645 | 803 | except ImportError: |
646 | 804 | # 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), | |
650 | 809 | ) |
651 | 810 | |
652 | 811 | else: |
653 | 812 | # ICU installed |
654 | def os_sort_keygen(key=None): | |
813 | def os_sort_keygen(key: MaybeKeyType = None) -> OSSortKeyType: | |
655 | 814 | loc = natsort.compat.locale.get_icu_locale() |
656 | 815 | collator = icu.Collator.createInstance(loc) |
657 | 816 | collator.setAttribute( |
698 | 857 | """ |
699 | 858 | |
700 | 859 | |
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: | |
702 | 873 | """ |
703 | 874 | Sort elements in the same order as your operating system's file browser |
704 | 875 |
3 | 3 | what algorithm natsort uses. |
4 | 4 | """ |
5 | 5 | |
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 | |
70 | 9 | |
71 | 10 | |
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 | |
75 | 15 | """ |
76 | 16 | Enum to control the `natsort` algorithm. |
77 | 17 | |
187 | 127 | |
188 | 128 | """ |
189 | 129 | |
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) | |
190 | 145 | |
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 | ||
194 | 155 | |
195 | 156 | # The below is private for internal use only. |
196 | 157 | 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] |
37 | 37 | and thus has a slightly improved performance at runtime. |
38 | 38 | |
39 | 39 | """ |
40 | ||
41 | 40 | import re |
42 | 41 | from functools import partial, reduce |
43 | 42 | from itertools import chain as ichain |
44 | 43 | from operator import methodcaller |
45 | 44 | 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 | ) | |
46 | 60 | from unicodedata import normalize |
47 | 61 | |
48 | 62 | 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 | |
51 | 70 | from natsort.unicode_numbers import digits_no_decimals, numeric_no_decimals |
52 | 71 | |
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 | ||
53 | 127 | |
54 | 128 | class NumericalRegularExpressions: |
55 | 129 | """ |
61 | 135 | """ |
62 | 136 | |
63 | 137 | # All unicode numeric characters (minus the decimal characters). |
64 | numeric = numeric_no_decimals | |
138 | numeric: str = numeric_no_decimals | |
65 | 139 | # All unicode digit characters (minus the decimal characters). |
66 | digits = digits_no_decimals | |
140 | digits: str = digits_no_decimals | |
67 | 141 | # Regular expression to match exponential component of a float. |
68 | exp = r"(?:[eE][-+]?\d+)?" | |
142 | exp: str = r"(?:[eE][-+]?\d+)?" | |
69 | 143 | # Regular expression to match a floating point number. |
70 | float_num = r"(?:\d+\.?\d*|\.\d+)" | |
144 | float_num: str = r"(?:\d+\.?\d*|\.\d+)" | |
71 | 145 | |
72 | 146 | @classmethod |
73 | def _construct_regex(cls, fmt): | |
147 | def _construct_regex(cls, fmt: str) -> Pattern[str]: | |
74 | 148 | """Given a format string, construct the regex with class attributes.""" |
75 | 149 | return re.compile(fmt.format(**vars(cls)), flags=re.U) |
76 | 150 | |
77 | 151 | @classmethod |
78 | def int_sign(cls): | |
152 | def int_sign(cls) -> Pattern[str]: | |
79 | 153 | """Regular expression to match a signed int.""" |
80 | 154 | return cls._construct_regex(r"([-+]?\d+|[{digits}])") |
81 | 155 | |
82 | 156 | @classmethod |
83 | def int_nosign(cls): | |
157 | def int_nosign(cls) -> Pattern[str]: | |
84 | 158 | """Regular expression to match an unsigned int.""" |
85 | 159 | return cls._construct_regex(r"(\d+|[{digits}])") |
86 | 160 | |
87 | 161 | @classmethod |
88 | def float_sign_exp(cls): | |
162 | def float_sign_exp(cls) -> Pattern[str]: | |
89 | 163 | """Regular expression to match a signed float with exponent.""" |
90 | 164 | return cls._construct_regex(r"([-+]?{float_num}{exp}|[{numeric}])") |
91 | 165 | |
92 | 166 | @classmethod |
93 | def float_nosign_exp(cls): | |
167 | def float_nosign_exp(cls) -> Pattern[str]: | |
94 | 168 | """Regular expression to match an unsigned float with exponent.""" |
95 | 169 | return cls._construct_regex(r"({float_num}{exp}|[{numeric}])") |
96 | 170 | |
97 | 171 | @classmethod |
98 | def float_sign_noexp(cls): | |
172 | def float_sign_noexp(cls) -> Pattern[str]: | |
99 | 173 | """Regular expression to match a signed float without exponent.""" |
100 | 174 | return cls._construct_regex(r"([-+]?{float_num}|[{numeric}])") |
101 | 175 | |
102 | 176 | @classmethod |
103 | def float_nosign_noexp(cls): | |
177 | def float_nosign_noexp(cls) -> Pattern[str]: | |
104 | 178 | """Regular expression to match an unsigned float without exponent.""" |
105 | 179 | return cls._construct_regex(r"({float_num}|[{numeric}])") |
106 | 180 | |
107 | 181 | |
108 | def regex_chooser(alg): | |
182 | def regex_chooser(alg: NSType) -> Pattern[str]: | |
109 | 183 | """ |
110 | 184 | Select an appropriate regex for the type of number of interest. |
111 | 185 | |
135 | 209 | }[alg] |
136 | 210 | |
137 | 211 | |
138 | def _no_op(x): | |
212 | def _no_op(x: Any) -> Any: | |
139 | 213 | """A function that does nothing and returns the input as-is.""" |
140 | 214 | return x |
141 | 215 | |
142 | 216 | |
143 | def _normalize_input_factory(alg): | |
217 | def _normalize_input_factory(alg: NSType) -> StrToStr: | |
144 | 218 | """ |
145 | 219 | Create a function that will normalize unicode input data. |
146 | 220 | |
160 | 234 | return partial(normalize, normalization_form) |
161 | 235 | |
162 | 236 | |
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: | |
164 | 266 | """ |
165 | 267 | Key to sort strings and numbers naturally. |
166 | 268 | |
169 | 271 | |
170 | 272 | Parameters |
171 | 273 | ---------- |
172 | val : str | unicode | bytes | int | float | iterable | |
274 | val : str | bytes | int | float | iterable | |
173 | 275 | key : callable | None |
174 | 276 | A key to apply to the *val* before any other operations are performed. |
175 | 277 | string_func : callable |
209 | 311 | |
210 | 312 | # Assume the input are strings, which is the most common case |
211 | 313 | try: |
212 | return string_func(val) | |
314 | return string_func(cast(str, val)) | |
213 | 315 | except (TypeError, AttributeError): |
214 | 316 | |
215 | 317 | # If bytes type, use the bytes_func |
216 | 318 | if type(val) in (bytes,): |
217 | return bytes_func(val) | |
319 | return bytes_func(cast(bytes, val)) | |
218 | 320 | |
219 | 321 | # Otherwise, assume it is an iterable that must be parsed recursively. |
220 | 322 | # Do not apply the key recursively. |
221 | 323 | try: |
222 | 324 | 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) | |
224 | 327 | ) |
225 | 328 | |
226 | 329 | # If that failed, it must be a number. |
227 | 330 | 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: | |
232 | 335 | """ |
233 | 336 | Create a function that will format a *bytes* object into a tuple. |
234 | 337 | |
261 | 364 | return lambda x: (x,) |
262 | 365 | |
263 | 366 | |
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: | |
265 | 370 | """ |
266 | 371 | Create a function that will format a number (or None) into a tuple. |
267 | 372 | |
292 | 397 | """ |
293 | 398 | nan_replace = float("+inf") if alg & ns.NANLAST else float("-inf") |
294 | 399 | |
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: | |
296 | 403 | """Given a number, place it in a tuple with a leading null string.""" |
297 | 404 | return _sep, (_nan_replace if val != val or val is None else val) |
298 | 405 | |
308 | 415 | |
309 | 416 | |
310 | 417 | 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: | |
313 | 425 | """ |
314 | 426 | Create a function that will split and format a *str* into a tuple. |
315 | 427 | |
360 | 472 | original_func = input_transform if orig_after_xfrm else _no_op |
361 | 473 | normalize_input = _normalize_input_factory(alg) |
362 | 474 | |
363 | def func(x): | |
475 | def func(x: str) -> FinalTransform: | |
364 | 476 | # Apply string input transformation function and return to x. |
365 | 477 | # Original function is usually a no-op, but some algorithms require it |
366 | 478 | # 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. | |
374 | 486 | |
375 | 487 | return func |
376 | 488 | |
377 | 489 | |
378 | def parse_path_factory(str_split): | |
490 | def parse_path_factory(str_split: StrParser) -> PathSplitter: | |
379 | 491 | """ |
380 | 492 | Create a function that will properly split and format a path. |
381 | 493 | |
402 | 514 | return lambda x: tuple(map(str_split, path_splitter(x))) |
403 | 515 | |
404 | 516 | |
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 | |
412 | 524 | sep : str |
413 | 525 | The string character to be inserted between adjacent numeric objects. |
414 | 526 | |
415 | 527 | Yields |
416 | 528 | ------ |
417 | The values of *iterable* in order, with *sep* inserted where adjacent | |
529 | The values of *iterator* in order, with *sep* inserted where adjacent | |
418 | 530 | elements are numeric. If the first element in the input is numeric |
419 | 531 | then *sep* will be the first value yielded. |
420 | 532 | |
421 | 533 | """ |
422 | 534 | try: |
423 | # Get the first element. A StopIteration indicates an empty iterable. | |
535 | # Get the first element. A StopIteration indicates an empty iterator. | |
424 | 536 | # Since we are controlling the types of the input, 'type' is used |
425 | 537 | # instead of 'isinstance' for the small speed advantage it offers. |
426 | 538 | types = (int, float) |
427 | first = next(iterable) | |
539 | first = next(iterator) | |
428 | 540 | if type(first) in types: |
429 | 541 | yield sep |
430 | 542 | yield first |
431 | 543 | |
432 | 544 | # Now, check if pair of elements are both numbers. If so, add ''. |
433 | second = next(iterable) | |
545 | second = next(iterator) | |
434 | 546 | if type(first) in types and type(second) in types: |
435 | 547 | yield sep |
436 | 548 | yield second |
437 | 549 | |
438 | 550 | # Now repeat in a loop. |
439 | for x in iterable: | |
551 | for x in iterator: | |
440 | 552 | first, second = second, x |
441 | 553 | if type(first) in types and type(second) in types: |
442 | 554 | yield sep |
447 | 559 | return |
448 | 560 | |
449 | 561 | |
450 | def input_string_transform_factory(alg): | |
562 | def input_string_transform_factory(alg: NSType) -> StrToStr: | |
451 | 563 | """ |
452 | 564 | Create a function to transform a string. |
453 | 565 | |
472 | 584 | dumb = alg & NS_DUMB |
473 | 585 | |
474 | 586 | # Build the chain of functions to execute in order. |
475 | function_chain = [] | |
587 | function_chain: List[StrToStr] = [] | |
476 | 588 | if (dumb and not lowfirst) or (lowfirst and not dumb): |
477 | 589 | function_chain.append(methodcaller("swapcase")) |
478 | 590 | |
501 | 613 | strip_thousands = strip_thousands.format( |
502 | 614 | thou=re.escape(get_thousands_sep()), nodecimal=nodecimal |
503 | 615 | ) |
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, "")) | |
506 | 618 | |
507 | 619 | # Create a regular expression that will change the decimal point to |
508 | 620 | # a period if not already a period. |
510 | 622 | if alg & ns.FLOAT and decimal != ".": |
511 | 623 | switch_decimal = r"(?<=[0-9]){decimal}|{decimal}(?=[0-9])" |
512 | 624 | 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, ".")) | |
515 | 627 | |
516 | 628 | # Return the chained functions. |
517 | 629 | return chain_functions(function_chain) |
518 | 630 | |
519 | 631 | |
520 | def string_component_transform_factory(alg): | |
632 | def string_component_transform_factory(alg: NSType) -> StrTransformer: | |
521 | 633 | """ |
522 | 634 | Create a function to either transform a string or convert to a number. |
523 | 635 | |
544 | 656 | nan_val = float("+inf") if alg & ns.NANLAST else float("-inf") |
545 | 657 | |
546 | 658 | # Build the chain of functions to execute in order. |
547 | func_chain = [] | |
659 | func_chain: List[Callable[[str], StrOrBytes]] = [] | |
548 | 660 | if group_letters: |
549 | 661 | func_chain.append(groupletters) |
550 | 662 | if use_locale: |
551 | 663 | func_chain.append(get_strxfrm()) |
664 | ||
665 | # Return the correct chained functions. | |
666 | kwargs: Dict[str, Union[float, Callable[[str], StrOrBytes]]] | |
552 | 667 | kwargs = {"key": chain_functions(func_chain)} if func_chain else {} |
553 | ||
554 | # Return the correct chained functions. | |
555 | 668 | if alg & ns.FLOAT: |
556 | 669 | # noinspection PyTypeChecker |
557 | 670 | kwargs["nan"] = nan_val |
558 | return partial(fast_float, **kwargs) | |
671 | return cast(Callable[[str], StrOrBytes], partial(fast_float, **kwargs)) | |
559 | 672 | 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: | |
564 | 679 | """ |
565 | 680 | Create a function to transform a tuple. |
566 | 681 | |
588 | 703 | """ |
589 | 704 | if alg & ns.UNGROUPLETTERS and alg & ns.LOCALEALPHA: |
590 | 705 | 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: | |
594 | 715 | """ |
595 | 716 | Return a tuple with the first character of the first element |
596 | 717 | of the return value as the first element, and the return value |
605 | 726 | else: |
606 | 727 | return (_transform(val[0]),), split_val |
607 | 728 | |
608 | return func | |
609 | 729 | 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")) | |
614 | 744 | |
615 | 745 | |
616 | 746 | # noinspection PyIncorrectDocstring |
617 | def groupletters(x, _low=lower_function): | |
747 | def groupletters(x: str, _low: StrToStr = lower_function) -> str: | |
618 | 748 | """ |
619 | 749 | Double all characters, making doubled letters lowercase. |
620 | 750 | |
636 | 766 | return "".join(ichain.from_iterable((_low(y), y) for y in x)) |
637 | 767 | |
638 | 768 | |
639 | def chain_functions(functions): | |
769 | def chain_functions(functions: Iterable[AnyCall]) -> AnyCall: | |
640 | 770 | """ |
641 | 771 | Chain a list of single-argument functions together and return. |
642 | 772 | |
673 | 803 | return partial(reduce, lambda res, f: f(res), functions) |
674 | 804 | |
675 | 805 | |
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: | |
677 | 817 | """ |
678 | 818 | Helper to decode a *bytes* object, or return the object as-is. |
679 | 819 | |
691 | 831 | |
692 | 832 | """ |
693 | 833 | try: |
694 | return s.decode(encoding) | |
834 | return cast(bytes, s).decode(encoding) | |
695 | 835 | except (AttributeError, TypeError): |
696 | 836 | return s |
697 | 837 | |
698 | 838 | |
699 | 839 | # 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]: | |
701 | 843 | """ |
702 | 844 | Split a string into its path components. |
703 | 845 |
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 | natsort |
0 | 0 | [bumpversion] |
1 | current_version = 7.1.0 | |
1 | current_version = 8.0.0 | |
2 | 2 | commit = True |
3 | 3 | tag = True |
4 | 4 | tag_name = {new_version} |
24 | 24 | Natural Language :: English |
25 | 25 | Programming Language :: Python |
26 | 26 | Programming Language :: Python :: 3 |
27 | Programming Language :: Python :: 3.4 | |
28 | Programming Language :: Python :: 3.5 | |
29 | 27 | Programming Language :: Python :: 3.6 |
30 | 28 | Programming Language :: Python :: 3.7 |
31 | 29 | Programming Language :: Python :: 3.8 |
32 | 30 | Programming Language :: Python :: 3.9 |
31 | Programming Language :: Python :: 3.10 | |
33 | 32 | Topic :: Scientific/Engineering :: Information Analysis |
34 | 33 | Topic :: Utilities |
35 | 34 | Topic :: Text Processing |
63 | 62 | docs, |
64 | 63 | .venv |
65 | 64 | |
65 | [mypy] | |
66 | ||
67 | [mypy-icu] | |
68 | ignore_missing_imports = True | |
69 | ||
70 | [egg_info] | |
71 | tag_build = | |
72 | tag_date = 0 | |
73 |
0 | 0 | #! /usr/bin/env python |
1 | 1 | |
2 | 2 | from setuptools import find_packages, setup |
3 | ||
3 | 4 | setup( |
4 | name='natsort', | |
5 | version='7.1.0', | |
5 | name="natsort", | |
6 | version="8.0.0", | |
6 | 7 | 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, | |
13 | 13 | ) |
2 | 2 | """ |
3 | 3 | |
4 | 4 | import locale |
5 | from typing import Iterator | |
5 | 6 | |
6 | 7 | import hypothesis |
7 | 8 | import pytest |
11 | 12 | # For some reason it thinks that the text/binary generation is too |
12 | 13 | # slow then causes the tests to fail. |
13 | 14 | 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] | |
16 | 16 | ) |
17 | 17 | |
18 | 18 | |
19 | def load_locale(x): | |
19 | def load_locale(x: str) -> None: | |
20 | 20 | """Convenience to load a locale, trying ISO8859-1 first.""" |
21 | 21 | try: |
22 | 22 | locale.setlocale(locale.LC_ALL, str("{}.ISO8859-1".format(x))) |
25 | 25 | |
26 | 26 | |
27 | 27 | @pytest.fixture() |
28 | def with_locale_en_us(): | |
28 | def with_locale_en_us() -> Iterator[None]: | |
29 | 29 | """Convenience to load the en_US locale - reset when complete.""" |
30 | 30 | orig = locale.getlocale() |
31 | yield load_locale("en_US") | |
31 | load_locale("en_US") | |
32 | yield | |
32 | 33 | locale.setlocale(locale.LC_ALL, orig) |
33 | 34 | |
34 | 35 | |
35 | 36 | @pytest.fixture() |
36 | def with_locale_de_de(): | |
37 | def with_locale_de_de() -> Iterator[None]: | |
37 | 38 | """ |
38 | 39 | Convenience to load the de_DE locale - reset when complete - skip if missing. |
39 | 40 | """ |
6 | 6 | import cProfile |
7 | 7 | import locale |
8 | 8 | import sys |
9 | from typing import List, Union | |
9 | 10 | |
10 | 11 | try: |
11 | 12 | from natsort import ns, natsort_keygen |
12 | 13 | except ImportError: |
13 | 14 | sys.path.insert(0, ".") |
14 | 15 | from natsort import ns, natsort_keygen |
16 | ||
17 | from natsort.natsort import NatsortKeyType | |
15 | 18 | |
16 | 19 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") |
17 | 20 | |
31 | 34 | locale_key = natsort_keygen(alg=ns.LOCALE) |
32 | 35 | |
33 | 36 | |
34 | def prof_time_to_generate(): | |
37 | def prof_time_to_generate() -> None: | |
35 | 38 | print("*** Generate Plain Key ***") |
36 | 39 | for _ in range(100000): |
37 | 40 | natsort_keygen() |
40 | 43 | cProfile.run("prof_time_to_generate()", sort="time") |
41 | 44 | |
42 | 45 | |
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: | |
44 | 49 | print(msg) |
45 | 50 | for _ in range(100000): |
46 | 51 | key(a) |
4 | 4 | |
5 | 5 | import unicodedata |
6 | 6 | from math import isnan |
7 | from typing import Union, cast | |
7 | 8 | |
8 | 9 | from hypothesis import given |
9 | 10 | from hypothesis.strategies import floats, integers, text |
10 | 11 | from natsort.compat.fake_fastnumbers import fast_float, fast_int |
11 | 12 | |
12 | 13 | |
13 | def is_float(x): | |
14 | def is_float(x: str) -> bool: | |
14 | 15 | try: |
15 | 16 | float(x) |
16 | 17 | except ValueError: |
24 | 25 | return True |
25 | 26 | |
26 | 27 | |
27 | def not_a_float(x): | |
28 | def not_a_float(x: str) -> bool: | |
28 | 29 | return not is_float(x) |
29 | 30 | |
30 | 31 | |
31 | def is_int(x): | |
32 | def is_int(x: Union[str, float]) -> bool: | |
32 | 33 | try: |
33 | return x.is_integer() | |
34 | return cast(float, x).is_integer() | |
34 | 35 | except AttributeError: |
35 | 36 | try: |
36 | 37 | int(x) |
37 | 38 | except ValueError: |
38 | 39 | try: |
39 | unicodedata.digit(x) | |
40 | unicodedata.digit(cast(str, x)) | |
40 | 41 | except (ValueError, TypeError): |
41 | 42 | return False |
42 | 43 | else: |
45 | 46 | return True |
46 | 47 | |
47 | 48 | |
48 | def not_an_int(x): | |
49 | def not_an_int(x: Union[str, float]) -> bool: | |
49 | 50 | return not is_int(x) |
50 | 51 | |
51 | 52 | |
53 | 54 | # and a test that uses the hypothesis module. |
54 | 55 | |
55 | 56 | |
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: | |
57 | 58 | assert fast_float("nan", nan=7) == 7 |
58 | 59 | |
59 | 60 | |
60 | def test_fast_float_converts_float_string_to_float_example(): | |
61 | def test_fast_float_converts_float_string_to_float_example() -> None: | |
61 | 62 | assert fast_float("45.8") == 45.8 |
62 | 63 | assert fast_float("-45") == -45.0 |
63 | 64 | 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"))) | |
67 | 68 | assert fast_float("۱۲.۱۲") == 12.12 |
68 | 69 | assert fast_float("-۱۲.۱۲") == -12.12 |
69 | 70 | |
70 | 71 | |
71 | 72 | @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: | |
73 | 74 | assert fast_float(repr(x)) == x |
74 | 75 | |
75 | 76 | |
76 | def test_fast_float_leaves_string_as_is_example(): | |
77 | def test_fast_float_leaves_string_as_is_example() -> None: | |
77 | 78 | assert fast_float("invalid") == "invalid" |
78 | 79 | |
79 | 80 | |
80 | 81 | @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: | |
82 | 83 | assert fast_float(x) == x |
83 | 84 | |
84 | 85 | |
85 | def test_fast_float_with_key_applies_to_string_example(): | |
86 | def test_fast_float_with_key_applies_to_string_example() -> None: | |
86 | 87 | assert fast_float("invalid", key=len) == len("invalid") |
87 | 88 | |
88 | 89 | |
89 | 90 | @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: | |
91 | 92 | assert fast_float(x, key=len) == len(x) |
92 | 93 | |
93 | 94 | |
94 | def test_fast_int_leaves_float_string_as_is_example(): | |
95 | def test_fast_int_leaves_float_string_as_is_example() -> None: | |
95 | 96 | assert fast_int("45.8") == "45.8" |
96 | 97 | assert fast_int("nan") == "nan" |
97 | 98 | assert fast_int("inf") == "inf" |
98 | 99 | |
99 | 100 | |
100 | 101 | @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: | |
102 | 103 | assert fast_int(repr(x)) == repr(x) |
103 | 104 | |
104 | 105 | |
105 | def test_fast_int_converts_int_string_to_int_example(): | |
106 | def test_fast_int_converts_int_string_to_int_example() -> None: | |
106 | 107 | assert fast_int("-45") == -45 |
107 | 108 | assert fast_int("+45") == 45 |
108 | 109 | assert fast_int("۱۲") == 12 |
110 | 111 | |
111 | 112 | |
112 | 113 | @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: | |
114 | 115 | assert fast_int(repr(x)) == x |
115 | 116 | |
116 | 117 | |
117 | def test_fast_int_leaves_string_as_is_example(): | |
118 | def test_fast_int_leaves_string_as_is_example() -> None: | |
118 | 119 | assert fast_int("invalid") == "invalid" |
119 | 120 | |
120 | 121 | |
121 | 122 | @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: | |
123 | 124 | assert fast_int(x) == x |
124 | 125 | |
125 | 126 | |
126 | def test_fast_int_with_key_applies_to_string_example(): | |
127 | def test_fast_int_with_key_applies_to_string_example() -> None: | |
127 | 128 | assert fast_int("invalid", key=len) == len("invalid") |
128 | 129 | |
129 | 130 | |
130 | 131 | @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: | |
132 | 133 | assert fast_int(x, key=len) == len(x) |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | """These test the utils.py functions.""" |
2 | from typing import Callable, Union | |
2 | 3 | |
3 | 4 | import pytest |
4 | 5 | from hypothesis import example, given |
5 | 6 | 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 | |
7 | 8 | from natsort.utils import final_data_transform_factory |
8 | 9 | |
9 | 10 | |
10 | 11 | @pytest.mark.parametrize("alg", [ns.DEFAULT, ns.UNGROUPLETTERS, ns.LOCALE]) |
11 | 12 | @given(x=text(), y=floats(allow_nan=False, allow_infinity=False) | integers()) |
12 | 13 | @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: | |
14 | 17 | final_data_transform_func = final_data_transform_factory(alg, "", "::") |
15 | 18 | value = (x, y) |
16 | 19 | original_value = "".join(map(str, value)) |
25 | 28 | (ns.LOCALE | ns.UNGROUPLETTERS | NS_DUMB, lambda x: x), |
26 | 29 | (ns.LOCALE | ns.UNGROUPLETTERS | ns.LOWERCASEFIRST, lambda x: x), |
27 | 30 | ( |
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(), | |
30 | 33 | ), |
31 | 34 | ], |
32 | 35 | ) |
33 | 36 | @given(x=text(), y=floats(allow_nan=False, allow_infinity=False) | integers()) |
34 | 37 | @example(x="İ", y=0) |
35 | 38 | @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: | |
37 | 42 | final_data_transform_func = final_data_transform_factory(alg, "", "::") |
38 | 43 | value = (x, y) |
39 | 44 | original_value = "".join(map(str, value)) |
45 | 50 | assert result == expected |
46 | 51 | |
47 | 52 | |
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: | |
49 | 54 | final_data_transform_func = final_data_transform_factory(ns.UG | ns.L, "", "::") |
50 | 55 | assert final_data_transform_func((), "") == ((), ()) |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | """These test the utils.py functions.""" |
2 | from typing import Callable | |
2 | 3 | |
3 | 4 | import pytest |
4 | 5 | from hypothesis import example, given |
5 | 6 | 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 | |
7 | 8 | from natsort.utils import input_string_transform_factory |
8 | 9 | |
9 | 10 | |
10 | def thousands_separated_int(n): | |
11 | def thousands_separated_int(n: str) -> str: | |
11 | 12 | """Insert thousands separators in an int.""" |
12 | 13 | new_int = "" |
13 | 14 | for i, y in enumerate(reversed(n), 1): |
19 | 20 | |
20 | 21 | |
21 | 22 | @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: | |
23 | 24 | input_string_transform_func = input_string_transform_factory(ns.DEFAULT) |
24 | 25 | assert input_string_transform_func(x) is x |
25 | 26 | |
35 | 36 | ], |
36 | 37 | ) |
37 | 38 | @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: | |
39 | 42 | input_string_transform_func = input_string_transform_factory(alg) |
40 | 43 | assert input_string_transform_func(x) == example_func(x) |
41 | 44 | |
43 | 46 | @example(12543642642534980) # 12,543,642,642,534,980 => 12543642642534980 |
44 | 47 | @given(x=integers(min_value=1000)) |
45 | 48 | @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: | |
47 | 50 | int_str = str(x).rstrip("lL") |
48 | 51 | thousands_int_str = thousands_separated_int(int_str) |
49 | 52 | assert thousands_int_str.replace(",", "") != thousands_int_str |
68 | 71 | ], |
69 | 72 | ) |
70 | 73 | @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: | |
72 | 77 | input_string_transform_func = input_string_transform_factory(ns.LOCALE) |
73 | 78 | assert input_string_transform_func(x) == expected |
74 | 79 | |
82 | 87 | ], |
83 | 88 | ) |
84 | 89 | @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: | |
86 | 93 | input_string_transform_func = input_string_transform_factory(ns.LOCALE) |
87 | 94 | assert input_string_transform_func(x) == expected |
88 | 95 | |
96 | 103 | ], |
97 | 104 | ) |
98 | 105 | @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: | |
100 | 109 | input_string_transform_func = input_string_transform_factory(alg) |
101 | 110 | assert input_string_transform_func("1543,753") == expected |
102 | 111 | |
103 | 112 | |
104 | 113 | @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: | |
106 | 115 | input_string_transform_func = input_string_transform_factory(ns.LOCALE | ns.FLOAT) |
107 | 116 | expected = "154s,t53" |
108 | 117 | assert input_string_transform_func("154s,t53") == expected |
4 | 4 | |
5 | 5 | import re |
6 | 6 | import sys |
7 | from typing import Any, List, Union | |
7 | 8 | |
8 | 9 | import pytest |
9 | 10 | from hypothesis import given |
10 | from hypothesis.strategies import data, floats, integers, lists | |
11 | from hypothesis.strategies import DataObject, data, floats, integers, lists | |
11 | 12 | from natsort.__main__ import ( |
13 | TypedArgs, | |
12 | 14 | check_filters, |
13 | 15 | keep_entry_range, |
14 | 16 | keep_entry_value, |
16 | 18 | range_check, |
17 | 19 | sort_and_print_entries, |
18 | 20 | ) |
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: | |
22 | 27 | p = mocker.patch("natsort.__main__.sort_and_print_entries") |
23 | 28 | main("num-2", "num-6", "num-1") |
24 | 29 | args = p.call_args[0][1] |
25 | 30 | assert not args.paths |
26 | 31 | assert args.filter is None |
27 | 32 | assert args.reverse_filter is None |
28 | assert args.exclude is None | |
33 | assert args.exclude == [] | |
29 | 34 | assert not args.reverse |
30 | 35 | assert args.number_type == "int" |
31 | 36 | assert not args.signed |
33 | 38 | assert not args.locale |
34 | 39 | |
35 | 40 | |
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: | |
37 | 44 | arguments = ["--paths", "--reverse", "--locale"] |
38 | 45 | arguments.extend(["--filter", "4", "10"]) |
39 | 46 | arguments.extend(["--reverse-filter", "100", "110"]) |
56 | 63 | assert args.locale |
57 | 64 | |
58 | 65 | |
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 | ||
74 | 66 | mock_print = "__builtin__.print" if sys.version[0] == "2" else "builtins.print" |
75 | 67 | |
76 | 68 | entries = [ |
134 | 126 | ([None, None, False, True, True], reversed([2, 3, 1, 0, 5, 6, 4])), |
135 | 127 | ], |
136 | 128 | ) |
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: | |
138 | 132 | p = mocker.patch(mock_print) |
139 | sort_and_print_entries(entries, Args(*options)) | |
133 | sort_and_print_entries(entries, TypedArgs(*options)) | |
140 | 134 | e = [mocker.call(entries[i]) for i in order] |
141 | 135 | p.assert_has_calls(e) |
142 | 136 | |
145 | 139 | # and a test that uses the hypothesis module. |
146 | 140 | |
147 | 141 | |
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: | |
149 | 143 | assert range_check(10, 11) == (10.0, 11.0) |
150 | 144 | assert range_check(6.4, 30) == (6.4, 30.0) |
151 | 145 | |
152 | 146 | |
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: | |
155 | 151 | # Pull data such that the first is less than the second. |
156 | 152 | 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)) | |
158 | 154 | else: |
159 | 155 | y = d.draw(integers(min_value=x + 1)) |
160 | 156 | assert range_check(x, y) == (x, y) |
161 | 157 | |
162 | 158 | |
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: | |
164 | 160 | with pytest.raises(ValueError, match="low >= high"): |
165 | 161 | range_check(7, 2) |
166 | 162 | |
167 | 163 | |
168 | 164 | @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: | |
170 | 168 | # Pull data such that the first is greater than or equal to the second. |
171 | 169 | y = d.draw(floats(max_value=x, allow_nan=False)) |
172 | 170 | with pytest.raises(ValueError, match="low >= high"): |
173 | 171 | range_check(x, y) |
174 | 172 | |
175 | 173 | |
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: | |
177 | 175 | 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: | |
183 | 179 | assert check_filters([(6, 7)]) == [(6, 7)] |
184 | 180 | assert check_filters([(6, 7), (2, 8)]) == [(6, 7), (2, 8)] |
185 | 181 | |
186 | 182 | |
187 | 183 | @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: | |
189 | 187 | # ensure y is element-wise greater than x |
190 | 188 | y = [d.draw(integers(min_value=val + 1)) for val in x] |
191 | 189 | assert check_filters(list(zip(x, y))) == [(i, j) for i, j in zip(x, y)] |
192 | 190 | |
193 | 191 | |
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: | |
195 | 193 | with pytest.raises(ValueError, match="Error in --filter: low >= high"): |
196 | 194 | check_filters([(7, 2)]) |
197 | 195 | |
198 | 196 | |
199 | 197 | @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: | |
201 | 201 | # ensure y is element-wise less than or equal to x |
202 | 202 | y = [d.draw(integers(max_value=val)) for val in x] |
203 | 203 | with pytest.raises(ValueError, match="Error in --filter: low >= high"): |
211 | 211 | # 3. No portion is between the bounds => False. |
212 | 212 | [([0], [100], True), ([1, 88], [20, 90], True), ([1], [20], False)], |
213 | 213 | ) |
214 | def test_keep_entry_range(lows, highs, truth): | |
214 | def test_keep_entry_range(lows: List[int], highs: List[int], truth: bool) -> None: | |
215 | 215 | assert keep_entry_range("a56b23c89", lows, highs, int, re.compile(r"\d+")) is truth |
216 | 216 | |
217 | 217 | |
218 | 218 | # 1. Values not in entry => True. 2. Values in entry => False. |
219 | 219 | @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: | |
221 | 221 | assert keep_entry_value("a56b23c89", values, int, re.compile(r"\d+")) is truth |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | """These test the utils.py functions.""" |
2 | from typing import Any, List, NoReturn, Tuple, Union, cast | |
2 | 3 | |
3 | 4 | from hypothesis import given |
4 | 5 | from hypothesis.strategies import binary, floats, integers, lists, text |
5 | 6 | from natsort.utils import natsort_key |
6 | 7 | |
7 | 8 | |
8 | def str_func(x): | |
9 | def str_func(x: Any) -> Tuple[str]: | |
9 | 10 | if isinstance(x, str): |
10 | return x | |
11 | return (x,) | |
11 | 12 | else: |
12 | 13 | raise TypeError("Not a str!") |
13 | 14 | |
14 | 15 | |
15 | def fail(_): | |
16 | def fail(_: Any) -> NoReturn: | |
16 | 17 | raise AssertionError("This should never be reached!") |
17 | 18 | |
18 | 19 | |
19 | 20 | @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 | |
22 | 23 | |
23 | 24 | |
24 | 25 | @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 | |
27 | 28 | |
28 | 29 | |
29 | 30 | @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 | |
32 | 33 | |
33 | 34 | |
34 | 35 | @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) | |
37 | 38 | |
38 | 39 | |
39 | 40 | @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 | ) |
4 | 4 | """ |
5 | 5 | |
6 | 6 | import os |
7 | from typing import List, Tuple, Union | |
7 | 8 | |
8 | 9 | import pytest |
9 | 10 | from natsort import natsort_key, natsort_keygen, natsorted, ns |
10 | 11 | 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 | |
11 | 15 | |
12 | 16 | |
13 | 17 | @pytest.fixture |
14 | def arbitrary_input(): | |
18 | def arbitrary_input() -> List[Union[str, float]]: | |
15 | 19 | return ["6A-5.034e+1", "/Folder (1)/Foo", 56.7] |
16 | 20 | |
17 | 21 | |
18 | 22 | @pytest.fixture |
19 | def bytes_input(): | |
23 | def bytes_input() -> bytes: | |
20 | 24 | return b"6A-5.034e+1" |
21 | 25 | |
22 | 26 | |
23 | def test_natsort_keygen_demonstration(): | |
27 | def test_natsort_keygen_demonstration() -> None: | |
24 | 28 | original_list = ["a50", "a51.", "a50.31", "a50.4", "a5.034e1", "a50.300"] |
25 | 29 | copy_of_list = original_list[:] |
26 | 30 | original_list.sort(key=natsort_keygen(alg=ns.F)) |
28 | 32 | assert original_list == natsorted(copy_of_list, alg=ns.F) |
29 | 33 | |
30 | 34 | |
31 | def test_natsort_key_public(): | |
35 | def test_natsort_key_public() -> None: | |
32 | 36 | assert natsort_key("a-5.034e2") == ("a-", 5, ".", 34, "e", 2) |
33 | 37 | |
34 | 38 | |
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: | |
36 | 40 | # Invalid arguments give the correct response |
37 | 41 | with pytest.raises(ValueError, match="'alg' argument"): |
38 | natsort_keygen(None, "1") | |
42 | natsort_keygen(None, "1") # type: ignore | |
39 | 43 | |
40 | 44 | |
41 | 45 | @pytest.mark.parametrize( |
42 | 46 | "alg, expected", |
43 | 47 | [(ns.DEFAULT, ("a-", 5, ".", 34, "e", 1)), (ns.FLOAT | ns.SIGNED, ("a", -50.34))], |
44 | 48 | ) |
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: | |
46 | 52 | ns_key = natsort_keygen(alg=alg) |
47 | 53 | assert ns_key("a-5.034e1") == expected |
48 | 54 | |
77 | 83 | ), |
78 | 84 | ], |
79 | 85 | ) |
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: | |
81 | 89 | ns_key = natsort_keygen(alg=alg) |
82 | 90 | assert ns_key(arbitrary_input) == expected |
83 | 91 | |
92 | 100 | (ns.PATH | ns.GROUPLETTERS, ((b"6A-5.034e+1",),)), |
93 | 101 | ], |
94 | 102 | ) |
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: | |
96 | 106 | ns_key = natsort_keygen(alg=alg) |
97 | 107 | assert ns_key(bytes_input) == expected |
98 | 108 | |
130 | 140 | ], |
131 | 141 | ) |
132 | 142 | @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: | |
134 | 150 | # First, apply the correct strxfrm function to the string values. |
135 | 151 | strxfrm = get_strxfrm() |
136 | expected = [list(sub) for sub in expected] | |
152 | expected_tmp = [list(sub) for sub in expected] | |
137 | 153 | try: |
138 | 154 | for i in (2, 4, 6): |
139 | expected[0][i] = strxfrm(expected[0][i]) | |
155 | expected_tmp[0][i] = strxfrm(expected_tmp[0][i]) | |
140 | 156 | 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) | |
143 | 159 | 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] | |
145 | 161 | 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]) | |
147 | 163 | 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) | |
150 | 166 | |
151 | 167 | mocker.patch("natsort.compat.locale.dumb_sort", return_value=is_dumb) |
152 | 168 | ns_key = natsort_keygen(alg=alg) |
158 | 174 | [(ns.LOCALE, False), (ns.LOCALE, True), (ns.LOCALE | ns.CAPITALFIRST, False)], |
159 | 175 | ) |
160 | 176 | @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: | |
162 | 180 | expected = (b"6A-5.034e+1",) |
163 | 181 | mocker.patch("natsort.compat.locale.dumb_sort", return_value=is_dumb) |
164 | 182 | ns_key = natsort_keygen(alg=alg) |
4 | 4 | """ |
5 | 5 | |
6 | 6 | from operator import itemgetter |
7 | from typing import List, Tuple, Union | |
7 | 8 | |
8 | 9 | import pytest |
9 | 10 | from natsort import as_utf8, natsorted, ns |
11 | from natsort.ns_enum import NSType | |
10 | 12 | from pytest import raises |
11 | 13 | |
12 | 14 | |
13 | 15 | @pytest.fixture |
14 | def float_list(): | |
16 | def float_list() -> List[str]: | |
15 | 17 | return ["a50", "a51.", "a50.31", "a-50", "a50.4", "a5.034e1", "a50.300"] |
16 | 18 | |
17 | 19 | |
18 | 20 | @pytest.fixture |
19 | def fruit_list(): | |
21 | def fruit_list() -> List[str]: | |
20 | 22 | return ["Apple", "corn", "Corn", "Banana", "apple", "banana"] |
21 | 23 | |
22 | 24 | |
23 | 25 | @pytest.fixture |
24 | def mixed_list(): | |
26 | def mixed_list() -> List[Union[str, int, float]]: | |
25 | 27 | return ["Ä", "0", "ä", 3, "b", 1.5, "2", "Z"] |
26 | 28 | |
27 | 29 | |
28 | def test_natsorted_numbers_in_ascending_order(): | |
30 | def test_natsorted_numbers_in_ascending_order() -> None: | |
29 | 31 | given = ["a2", "a5", "a9", "a1", "a4", "a10", "a6"] |
30 | 32 | expected = ["a1", "a2", "a4", "a5", "a6", "a9", "a10"] |
31 | 33 | assert natsorted(given) == expected |
32 | 34 | |
33 | 35 | |
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: | |
35 | 39 | expected = ["a-50", "a50", "a50.300", "a50.31", "a5.034e1", "a50.4", "a51."] |
36 | 40 | assert natsorted(float_list, alg=ns.REAL) == expected |
37 | 41 | |
41 | 45 | "alg", |
42 | 46 | [ns.NOEXP | ns.FLOAT | ns.UNSIGNED, ns.NOEXP | ns.FLOAT], |
43 | 47 | ) |
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: | |
45 | 51 | expected = ["a5.034e1", "a50", "a50.300", "a50.31", "a50.4", "a51.", "a-50"] |
46 | 52 | assert natsorted(float_list, alg=alg) == expected |
47 | 53 | |
48 | 54 | |
49 | 55 | # DEFAULT and INT are all equivalent. |
50 | 56 | @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: | |
52 | 60 | expected = ["a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51.", "a-50"] |
53 | 61 | assert natsorted(float_list, alg=alg) == expected |
54 | 62 | |
55 | 63 | |
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: | |
57 | 65 | expected = ["a-50", "a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51."] |
58 | 66 | assert natsorted(float_list, alg=ns.SIGNED) == expected |
59 | 67 | |
62 | 70 | "alg, expected", |
63 | 71 | [(ns.UNSIGNED, ["a7", "a+2", "a-5"]), (ns.SIGNED, ["a-5", "a+2", "a7"])], |
64 | 72 | ) |
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: | |
66 | 76 | given = ["a-5", "a7", "a+2"] |
67 | 77 | assert natsorted(given, alg=alg) == expected |
68 | 78 | |
69 | 79 | |
70 | def test_natsorted_can_sort_as_version_numbers(): | |
80 | def test_natsorted_can_sort_as_version_numbers() -> None: | |
71 | 81 | given = ["1.9.9a", "1.11", "1.9.9b", "1.11.4", "1.10.1"] |
72 | 82 | expected = ["1.9.9a", "1.9.9b", "1.10.1", "1.11", "1.11.4"] |
73 | 83 | assert natsorted(given) == expected |
80 | 90 | (ns.NUMAFTER, ["Ä", "Z", "ä", "b", "0", 1.5, "2", 3]), |
81 | 91 | ], |
82 | 92 | ) |
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: | |
84 | 98 | assert natsorted(mixed_list, alg=alg) == expected |
85 | 99 | |
86 | 100 | |
87 | 101 | @pytest.mark.parametrize( |
88 | 102 | "alg, expected, slc", |
89 | 103 | [ |
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] | |
96 | 112 | # The slice is because NaN != NaN |
97 | 113 | # noinspection PyUnresolvedReferences |
98 | 114 | assert natsorted(given, alg=alg)[slc] == expected[slc] |
99 | 115 | |
100 | 116 | |
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: | |
102 | 118 | with raises(TypeError, match="bytes"): |
103 | 119 | natsorted(["ä", b"b"]) |
104 | 120 | |
106 | 122 | assert natsorted(["ä", b"b"], key=as_utf8) == ["ä", b"b"] |
107 | 123 | |
108 | 124 | |
109 | def test_natsorted_raises_type_error_for_non_iterable_input(): | |
125 | def test_natsorted_raises_type_error_for_non_iterable_input() -> None: | |
110 | 126 | 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: | |
115 | 131 | given = [["a1", "a5"], ["a1", "a40"], ["a10", "a1"], ["a2", "a5"]] |
116 | 132 | expected = [["a1", "a5"], ["a1", "a40"], ["a2", "a5"], ["a10", "a1"]] |
117 | 133 | assert natsorted(given) == expected |
118 | 134 | |
119 | 135 | |
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: | |
121 | 137 | given = [("a", "num3"), ("b", "num5"), ("c", "num2")] |
122 | 138 | expected = [("c", "num2"), ("a", "num3"), ("b", "num5")] |
123 | 139 | assert natsorted(given, key=itemgetter(1)) == expected |
124 | 140 | |
125 | 141 | |
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: | |
127 | 145 | expected = natsorted(float_list)[::-1] |
128 | 146 | assert natsorted(float_list, reverse=True) == expected |
129 | 147 | |
130 | 148 | |
131 | def test_natsorted_handles_filesystem_paths(): | |
149 | def test_natsorted_handles_filesystem_paths() -> None: | |
132 | 150 | given = [ |
133 | 151 | "/p/Folder (10)/file.tar.gz", |
134 | 152 | "/p/Folder (1)/file (1).tar.gz", |
156 | 174 | assert natsorted(given, alg=ns.FLOAT | ns.PATH) == expected_correct |
157 | 175 | |
158 | 176 | |
159 | def test_natsorted_handles_numbers_and_filesystem_paths_simultaneously(): | |
177 | def test_natsorted_handles_numbers_and_filesystem_paths_simultaneously() -> None: | |
160 | 178 | # 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"] | |
163 | 181 | assert natsorted(given, alg=ns.PATH) == expected |
164 | 182 | |
165 | 183 | |
173 | 191 | (ns.G | ns.LF, ["apple", "Apple", "banana", "Banana", "corn", "Corn"]), |
174 | 192 | ], |
175 | 193 | ) |
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: | |
177 | 197 | assert natsorted(fruit_list, alg=alg) == expected |
178 | 198 | |
179 | 199 | |
185 | 205 | (ns.IGNORECASE, [("a3", "a1"), ("A5", "a6")]), |
186 | 206 | ], |
187 | 207 | ) |
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: | |
189 | 211 | given = [("A5", "a6"), ("a3", "a1")] |
190 | 212 | assert natsorted(given, alg=alg) == expected |
191 | 213 | |
200 | 222 | ], |
201 | 223 | ) |
202 | 224 | @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: | |
204 | 228 | assert natsorted(fruit_list, alg=ns.LOCALE | alg) == expected |
205 | 229 | |
206 | 230 | |
207 | 231 | @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: | |
209 | 233 | given = ["c", "a5,467.86", "ä", "b", "a5367.86", "a5,6", "a5,50"] |
210 | 234 | expected = ["a5,6", "a5,50", "a5367.86", "a5,467.86", "ä", "b", "c"] |
211 | 235 | assert natsorted(given, alg=ns.LOCALE | ns.F) == expected |
212 | 236 | |
213 | 237 | |
214 | 238 | @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: | |
216 | 240 | given = ["c", "a5.467,86", "ä", "b", "a5367.86", "a5,6", "a5,50"] |
217 | 241 | expected = ["a5,50", "a5,6", "a5367.86", "a5.467,86", "ä", "b", "c"] |
218 | 242 | assert natsorted(given, alg=ns.LOCALE | ns.F) == expected |
219 | 243 | |
220 | 244 | |
221 | 245 | @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: | |
223 | 247 | # https://github.com/SethMMorton/natsort/issues/109 |
224 | 248 | given = ["462166", "461761"] |
225 | 249 | expected = ["461761", "462166"] |
241 | 265 | ], |
242 | 266 | ) |
243 | 267 | @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: | |
245 | 273 | assert natsorted(mixed_list, alg=ns.LOCALE | alg) == expected |
246 | 274 | |
247 | 275 | |
252 | 280 | (ns.NUMAFTER, ["Banana", "apple", "corn", "~~~~~~", "73", "5039"]), |
253 | 281 | ], |
254 | 282 | ) |
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: | |
256 | 286 | given = ["apple", "Banana", "73", "5039", "corn", "~~~~~~"] |
257 | 287 | assert natsorted(given, alg=alg) == expected |
258 | 288 | |
259 | 289 | |
260 | def test_natsorted_sorts_mixed_ascii_and_non_ascii_numbers(): | |
290 | def test_natsorted_sorts_mixed_ascii_and_non_ascii_numbers() -> None: | |
261 | 291 | given = [ |
262 | 292 | "1st street", |
263 | 293 | "10th street", |
4 | 4 | """ |
5 | 5 | |
6 | 6 | from operator import itemgetter |
7 | from typing import List | |
7 | 8 | |
8 | 9 | import pytest |
9 | 10 | from natsort import ( |
22 | 23 | |
23 | 24 | |
24 | 25 | @pytest.fixture |
25 | def version_list(): | |
26 | def version_list() -> List[str]: | |
26 | 27 | return ["1.9.9a", "1.11", "1.9.9b", "1.11.4", "1.10.1"] |
27 | 28 | |
28 | 29 | |
29 | 30 | @pytest.fixture |
30 | def float_list(): | |
31 | def float_list() -> List[str]: | |
31 | 32 | return ["a50", "a51.", "a50.31", "a-50", "a50.4", "a5.034e1", "a50.300"] |
32 | 33 | |
33 | 34 | |
34 | 35 | @pytest.fixture |
35 | def fruit_list(): | |
36 | def fruit_list() -> List[str]: | |
36 | 37 | return ["Apple", "corn", "Corn", "Banana", "apple", "banana"] |
37 | 38 | |
38 | 39 | |
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: | |
40 | 41 | func = decoder("latin1") |
41 | 42 | str_obj = "bytes" |
42 | 43 | int_obj = 14 |
43 | 44 | assert func(b"bytes") == str_obj |
44 | 45 | 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 | |
48 | 47 | |
49 | 48 | |
50 | def test_as_ascii_converts_bytes_to_ascii(): | |
49 | def test_as_ascii_converts_bytes_to_ascii() -> None: | |
51 | 50 | assert decoder("ascii")(b"bytes") == as_ascii(b"bytes") |
52 | 51 | |
53 | 52 | |
54 | def test_as_utf8_converts_bytes_to_utf8(): | |
53 | def test_as_utf8_converts_bytes_to_utf8() -> None: | |
55 | 54 | assert decoder("utf8")(b"bytes") == as_utf8(b"bytes") |
56 | 55 | |
57 | 56 | |
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: | |
59 | 60 | assert realsorted(float_list) == natsorted(float_list, alg=ns.REAL) |
60 | 61 | |
61 | 62 | |
62 | 63 | @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: | |
64 | 67 | assert humansorted(fruit_list) == natsorted(fruit_list, alg=ns.LOCALE) |
65 | 68 | |
66 | 69 | |
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: | |
68 | 71 | given = ["num3", "num5", "num2"] |
69 | 72 | other = ["foo", "bar", "baz"] |
70 | 73 | index = index_natsorted(given) |
73 | 76 | assert [other[i] for i in index] == ["baz", "foo", "bar"] |
74 | 77 | |
75 | 78 | |
76 | def test_index_natsorted_reverse(): | |
79 | def test_index_natsorted_reverse() -> None: | |
77 | 80 | given = ["num3", "num5", "num2"] |
78 | 81 | assert index_natsorted(given, reverse=True) == index_natsorted(given)[::-1] |
79 | 82 | |
80 | 83 | |
81 | def test_index_natsorted_applies_key_function_before_sorting(): | |
84 | def test_index_natsorted_applies_key_function_before_sorting() -> None: | |
82 | 85 | given = [("a", "num3"), ("b", "num5"), ("c", "num2")] |
83 | 86 | expected = [2, 0, 1] |
84 | 87 | assert index_natsorted(given, key=itemgetter(1)) == expected |
85 | 88 | |
86 | 89 | |
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: | |
88 | 93 | assert index_realsorted(float_list) == index_natsorted(float_list, alg=ns.REAL) |
89 | 94 | |
90 | 95 | |
91 | 96 | @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: | |
93 | 100 | assert index_humansorted(fruit_list) == index_natsorted(fruit_list, alg=ns.LOCALE) |
94 | 101 | |
95 | 102 | |
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: | |
97 | 104 | given = ["num3", "num5", "num2"] |
98 | 105 | index = [2, 0, 1] |
99 | 106 | expected = [given[i] for i in index] |
101 | 108 | assert order_by_index(given, index) == expected |
102 | 109 | |
103 | 110 | |
104 | def test_order_by_index_returns_generator_with_iter_true(): | |
111 | def test_order_by_index_returns_generator_with_iter_true() -> None: | |
105 | 112 | given = ["num3", "num5", "num2"] |
106 | 113 | index = [2, 0, 1] |
107 | 114 | assert order_by_index(given, index, True) != [given[i] for i in index] |
0 | import pytest | |
0 | 1 | from natsort import ns |
1 | 2 | |
2 | 3 | |
3 | def test_ns_enum(): | |
4 | enum_name_values = [ | |
4 | @pytest.mark.parametrize( | |
5 | "given, expected", | |
6 | [ | |
5 | 7 | ("FLOAT", 0x0001), |
6 | 8 | ("SIGNED", 0x0002), |
7 | 9 | ("NOEXP", 0x0004), |
39 | 41 | ("NL", 0x0400), |
40 | 42 | ("CN", 0x0800), |
41 | 43 | ("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 |
2 | 2 | Testing for the OS sorting |
3 | 3 | """ |
4 | 4 | import platform |
5 | from typing import cast | |
5 | 6 | |
6 | 7 | import natsort |
7 | 8 | import pytest |
14 | 15 | has_icu = True |
15 | 16 | |
16 | 17 | |
17 | def test_os_sorted_compound(): | |
18 | def test_os_sorted_compound() -> None: | |
18 | 19 | given = [ |
19 | 20 | "/p/Folder (10)/file.tar.gz", |
20 | 21 | "/p/Folder (1)/file (1).tar.gz", |
35 | 36 | assert result == expected |
36 | 37 | |
37 | 38 | |
38 | def test_os_sorted_misc_no_fail(): | |
39 | def test_os_sorted_misc_no_fail() -> None: | |
39 | 40 | natsort.os_sorted([9, 4.3, None, float("nan")]) |
40 | 41 | |
41 | 42 | |
42 | def test_os_sorted_key(): | |
43 | def test_os_sorted_key() -> None: | |
43 | 44 | given = ["foo0", "foo2", "goo1"] |
44 | 45 | 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")) | |
46 | 47 | assert result == expected |
47 | 48 | |
48 | 49 | |
198 | 199 | |
199 | 200 | |
200 | 201 | @pytest.mark.usefixtures("with_locale_en_us") |
201 | def test_os_sorted_corpus(): | |
202 | def test_os_sorted_corpus() -> None: | |
202 | 203 | result = natsort.os_sorted(given) |
203 | 204 | print(result) |
204 | 205 | assert result == expected |
3 | 3 | import pytest |
4 | 4 | from hypothesis import given |
5 | 5 | 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 | |
8 | 8 | |
9 | 9 | |
10 | 10 | @pytest.mark.parametrize( |
18 | 18 | ], |
19 | 19 | ) |
20 | 20 | @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: | |
22 | 24 | parse_bytes_func = parse_bytes_factory(alg) |
23 | 25 | assert parse_bytes_func(x) == example_func(x) |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | """These test the utils.py functions.""" |
2 | ||
3 | from typing import Optional, Tuple, Union | |
2 | 4 | |
3 | 5 | import pytest |
4 | 6 | from hypothesis import given |
5 | 7 | 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 | |
8 | 10 | |
9 | 11 | |
10 | 12 | @pytest.mark.usefixtures("with_locale_en_us") |
18 | 20 | ], |
19 | 21 | ) |
20 | 22 | @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: | |
22 | 26 | parse_number_func = parse_number_or_none_factory(alg, "", "xx") |
23 | 27 | assert parse_number_func(x) == example_func(x) |
24 | 28 | |
33 | 37 | (ns.NANLAST, None, ("", float("+inf"))), # NANLAST makes it +infinity |
34 | 38 | ], |
35 | 39 | ) |
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: | |
37 | 43 | parse_number_func = parse_number_or_none_factory(alg, "", "xx") |
38 | 44 | assert parse_number_func(x) == result |
1 | 1 | """These test the utils.py functions.""" |
2 | 2 | |
3 | 3 | import unicodedata |
4 | from typing import Any, Callable, Iterable, List, Tuple, Union | |
4 | 5 | |
5 | 6 | import pytest |
6 | 7 | from hypothesis import given |
7 | 8 | from hypothesis.strategies import floats, integers, lists, text |
8 | 9 | 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 | ) | |
11 | 16 | from natsort.utils import parse_string_factory |
12 | 17 | |
13 | 18 | |
14 | class CustomTuple(tuple): | |
19 | class CustomTuple(Tuple[Any, ...]): | |
15 | 20 | """Used to ensure what is given during testing is what is returned.""" |
16 | 21 | |
17 | original = None | |
22 | original: Any = None | |
18 | 23 | |
19 | 24 | |
20 | def input_transform(x): | |
25 | def input_transform(x: Any) -> Any: | |
21 | 26 | """Make uppercase.""" |
22 | 27 | try: |
23 | 28 | return x.upper() |
25 | 30 | return x |
26 | 31 | |
27 | 32 | |
28 | def final_transform(x, original): | |
33 | def final_transform(x: Iterable[Any], original: str) -> FinalTransform: | |
29 | 34 | """Make the input a CustomTuple.""" |
30 | 35 | t = CustomTuple(x) |
31 | 36 | t.original = original |
32 | 37 | return t |
33 | 38 | |
34 | 39 | |
35 | @pytest.fixture | |
36 | def parse_string_func(request): | |
40 | def parse_string_func_factory(alg: NSType) -> StrParser: | |
37 | 41 | """A parse_string_factory result with sample arguments.""" |
38 | 42 | sep = "" |
39 | 43 | return parse_string_factory( |
40 | request.param, # algorirhm | |
44 | alg, | |
41 | 45 | sep, |
42 | 46 | NumRegex.int_nosign().split, |
43 | 47 | input_transform, |
46 | 50 | ) |
47 | 51 | |
48 | 52 | |
49 | @pytest.mark.parametrize("parse_string_func", [ns.DEFAULT], indirect=True) | |
50 | 53 | @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) | |
52 | 58 | with pytest.raises(TypeError): |
53 | assert parse_string_func(x) | |
59 | assert parse_string_func(x) # type: ignore | |
54 | 60 | |
55 | 61 | |
56 | 62 | # noinspection PyCallingNonCallable |
57 | 63 | @pytest.mark.parametrize( |
58 | "parse_string_func, orig_func", | |
64 | "alg, orig_func", | |
59 | 65 | [ |
60 | 66 | (ns.DEFAULT, lambda x: x.upper()), |
61 | 67 | (ns.LOCALE, lambda x: x.upper()), |
62 | 68 | (ns.LOCALE | NS_DUMB, lambda x: x), # This changes the "original" handling. |
63 | 69 | ], |
64 | indirect=["parse_string_func"], | |
65 | 70 | ) |
66 | 71 | @given( |
67 | 72 | x=lists( |
69 | 74 | ) |
70 | 75 | ) |
71 | 76 | @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) | |
73 | 81 | # parse_string_factory is the high-level combination of several dedicated |
74 | 82 | # functions involved in splitting and manipulating a string. The details of |
75 | 83 | # what those functions do is not relevant to testing parse_string_factory. |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | """These test the splitting regular expressions.""" |
2 | 2 | |
3 | from typing import List, Pattern | |
4 | ||
3 | 5 | import pytest |
4 | 6 | from natsort import ns, numeric_regex_chooser |
7 | from natsort.ns_enum import NSType | |
5 | 8 | from natsort.utils import NumericalRegularExpressions as NumRegex |
6 | 9 | |
7 | 10 | |
80 | 83 | f_s: ["", "12", "", "①", "", "②", "", "Ⅰ", "", "Ⅱ", "", "⅓", ""], |
81 | 84 | f_ue: ["", "12", "", "①", "", "②", "", "Ⅰ", "", "Ⅱ", "", "⅓", ""], |
82 | 85 | f_se: ["", "12", "", "①", "", "②", "", "Ⅰ", "", "Ⅱ", "", "⅓", ""], |
83 | } | |
86 | }, | |
84 | 87 | } |
85 | 88 | |
86 | 89 | |
94 | 97 | |
95 | 98 | |
96 | 99 | @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: | |
98 | 103 | # noinspection PyUnresolvedReferences |
99 | 104 | assert regex.split(x) == expected |
100 | 105 | |
114 | 119 | (ns.FLOAT | ns.UNSIGNED | ns.NOEXP, NumRegex.float_nosign_noexp()), |
115 | 120 | ], |
116 | 121 | ) |
117 | def test_regex_chooser(given, expected): | |
122 | def test_regex_chooser(given: NSType, expected: Pattern[str]) -> None: | |
118 | 123 | assert numeric_regex_chooser(given) == expected.pattern[1:-1] # remove parens |
1 | 1 | """These test the utils.py functions.""" |
2 | 2 | |
3 | 3 | from functools import partial |
4 | from typing import Any, Callable, FrozenSet, Union | |
4 | 5 | |
5 | 6 | import pytest |
6 | 7 | from hypothesis import example, given |
7 | 8 | from hypothesis.strategies import floats, integers, text |
8 | 9 | from natsort.compat.fastnumbers import fast_float, fast_int |
9 | 10 | 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 | |
11 | 12 | from natsort.utils import groupletters, string_component_transform_factory |
12 | 13 | |
13 | 14 | # There are some unicode values that are known failures with the builtin locale |
14 | 15 | # library on BSD systems that has nothing to do with natsort (a ValueError is |
15 | 16 | # raised by strxfrm). Let's filter them out. |
16 | 17 | 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)) | |
20 | 19 | except ValueError: |
21 | 20 | # Narrow unicode build... no worries. |
22 | 21 | bad_uni_chars = frozenset() |
23 | 22 | |
24 | 23 | |
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: | |
26 | 25 | """Ensure text does not contain bad unicode characters""" |
27 | 26 | return not any(y in _bad_chars for y in x) |
28 | 27 | |
29 | 28 | |
30 | def no_null(x): | |
29 | def no_null(x: str) -> bool: | |
31 | 30 | """Ensure text does not contain a null character.""" |
32 | 31 | return "\0" not in x |
33 | 32 | |
66 | 65 | | text().filter(bool).filter(no_bad_uni_chars).filter(no_null) |
67 | 66 | ) |
68 | 67 | @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: | |
70 | 71 | string_component_transform_func = string_component_transform_factory(alg) |
71 | 72 | try: |
72 | 73 | assert string_component_transform_func(str(x)) == example_func(str(x)) |
13 | 13 | digits_no_decimals, |
14 | 14 | numeric, |
15 | 15 | numeric_chars, |
16 | numeric_hex, | |
17 | 16 | numeric_no_decimals, |
18 | 17 | ) |
18 | from natsort.unicode_numeric_hex import numeric_hex | |
19 | 19 | |
20 | 20 | |
21 | def test_numeric_chars_contains_only_valid_unicode_numeric_characters(): | |
21 | def test_numeric_chars_contains_only_valid_unicode_numeric_characters() -> None: | |
22 | 22 | for a in numeric_chars: |
23 | 23 | assert unicodedata.numeric(a, None) is not None |
24 | 24 | |
25 | 25 | |
26 | def test_digit_chars_contains_only_valid_unicode_digit_characters(): | |
26 | def test_digit_chars_contains_only_valid_unicode_digit_characters() -> None: | |
27 | 27 | for a in digit_chars: |
28 | 28 | assert unicodedata.digit(a, None) is not None |
29 | 29 | |
30 | 30 | |
31 | def test_decimal_chars_contains_only_valid_unicode_decimal_characters(): | |
31 | def test_decimal_chars_contains_only_valid_unicode_decimal_characters() -> None: | |
32 | 32 | for a in decimal_chars: |
33 | 33 | assert unicodedata.decimal(a, None) is not None |
34 | 34 | |
35 | 35 | |
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: | |
37 | 37 | set_numeric_chars = set(numeric_chars) |
38 | 38 | set_digit_chars = set(digit_chars) |
39 | 39 | set_decimal_chars = set(decimal_chars) |
45 | 45 | assert set_numeric_chars.issuperset(numeric_no_decimals) |
46 | 46 | |
47 | 47 | |
48 | def test_missing_unicode_number_in_collection(): | |
48 | def test_missing_unicode_number_in_collection() -> None: | |
49 | 49 | ok = True |
50 | 50 | set_numeric_hex = set(numeric_hex) |
51 | 51 | for i in range(0x110000): |
70 | 70 | ) |
71 | 71 | |
72 | 72 | |
73 | def test_combined_string_contains_all_characters_in_list(): | |
73 | def test_combined_string_contains_all_characters_in_list() -> None: | |
74 | 74 | assert numeric == "".join(numeric_chars) |
75 | 75 | assert digits == "".join(digit_chars) |
76 | 76 | assert decimals == "".join(decimal_chars) |
5 | 5 | import string |
6 | 6 | from itertools import chain |
7 | 7 | from operator import neg as op_neg |
8 | from typing import List, Pattern, Union | |
8 | 9 | |
9 | 10 | import pytest |
10 | 11 | from hypothesis import given |
11 | 12 | from hypothesis.strategies import integers, lists, sampled_from, text |
12 | 13 | from natsort import utils |
13 | from natsort.ns_enum import ns | |
14 | from natsort.ns_enum import NSType, ns | |
14 | 15 | |
15 | 16 | |
16 | def test_do_decoding_decodes_bytes_string_to_unicode(): | |
17 | def test_do_decoding_decodes_bytes_string_to_unicode() -> None: | |
17 | 18 | assert type(utils.do_decoding(b"bytes", "ascii")) is str |
18 | 19 | assert utils.do_decoding(b"bytes", "ascii") == "bytes" |
19 | 20 | assert utils.do_decoding(b"bytes", "ascii") == b"bytes".decode("ascii") |
32 | 33 | (ns.F | ns.S | ns.N, utils.NumericalRegularExpressions.float_sign_noexp()), |
33 | 34 | ], |
34 | 35 | ) |
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: | |
36 | 39 | assert utils.regex_chooser(alg).pattern == expected.pattern |
37 | 40 | |
38 | 41 | |
67 | 70 | (ns.REAL, ns.FLOAT | ns.SIGNED), |
68 | 71 | ], |
69 | 72 | ) |
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: | |
71 | 74 | assert alg == value_or_alias |
72 | 75 | |
73 | 76 | |
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: | |
75 | 78 | x = 2345 |
76 | 79 | assert utils.chain_functions([])(x) is x |
77 | 80 | |
78 | 81 | |
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: | |
80 | 83 | x = "2345" |
81 | 84 | assert utils.chain_functions([len])(x) == 4 |
82 | 85 | |
83 | 86 | |
84 | def test_chain_functions_combines_functions_in_given_order(): | |
87 | def test_chain_functions_combines_functions_in_given_order() -> None: | |
85 | 88 | x = 2345 |
86 | 89 | assert utils.chain_functions([str, len, op_neg])(x) == -len(str(x)) |
87 | 90 | |
90 | 93 | # and a test that uses the hypothesis module. |
91 | 94 | |
92 | 95 | |
93 | def test_groupletters_returns_letters_with_lowercase_transform_of_letter_example(): | |
96 | def test_groupletters_gives_letters_with_lowercase_letter_transform_example() -> None: | |
94 | 97 | assert utils.groupletters("HELLO") == "hHeElLlLoO" |
95 | 98 | assert utils.groupletters("hello") == "hheelllloo" |
96 | 99 | |
97 | 100 | |
98 | 101 | @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: | |
100 | 105 | assert utils.groupletters(x) == "".join( |
101 | 106 | chain.from_iterable([y.casefold(), y] for y in x) |
102 | 107 | ) |
103 | 108 | |
104 | 109 | |
105 | def test_sep_inserter_does_nothing_if_no_numbers_example(): | |
110 | def test_sep_inserter_does_nothing_if_no_numbers_example() -> None: | |
106 | 111 | assert list(utils.sep_inserter(iter(["a", "b", "c"]), "")) == ["a", "b", "c"] |
107 | 112 | assert list(utils.sep_inserter(iter(["a"]), "")) == ["a"] |
108 | 113 | |
109 | 114 | |
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: | |
111 | 116 | assert list(utils.sep_inserter(iter(["a", 5]), "")) == ["a", 5] |
112 | 117 | |
113 | 118 | |
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: | |
115 | 120 | assert list(utils.sep_inserter(iter([5, 9]), "")) == ["", 5, "", 9] |
116 | 121 | |
117 | 122 | |
118 | 123 | @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: | |
120 | 127 | # Rather than just replicating the results in a different algorithm, |
121 | 128 | # validate that the "shape" of the output is as expected. |
122 | 129 | result = list(utils.sep_inserter(iter(x), "")) |
126 | 133 | assert isinstance(result[i + 1], int) |
127 | 134 | |
128 | 135 | |
129 | def test_path_splitter_splits_path_string_by_separator_example(): | |
136 | def test_path_splitter_splits_path_string_by_sep_example() -> None: | |
130 | 137 | given = "/this/is/a/path" |
131 | 138 | expected = (os.sep, "this", "is", "a", "path") |
132 | 139 | 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) | |
135 | 141 | |
136 | 142 | |
137 | 143 | @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: | |
139 | 145 | z = str(pathlib.Path(*x)) |
140 | 146 | assert tuple(utils.path_splitter(z)) == tuple(pathlib.Path(z).parts) |
141 | 147 | |
142 | 148 | |
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: | |
144 | 150 | given = "/this/is/a/path/file.x1.10.tar.gz" |
145 | 151 | expected = (os.sep, "this", "is", "a", "path", "file.x1.10", ".tar", ".gz") |
146 | 152 | assert tuple(utils.path_splitter(given)) == tuple(expected) |
147 | 153 | |
148 | 154 | |
149 | 155 | @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: | |
151 | 159 | z = str(pathlib.Path(*x[:-2])) + "." + x[-1] |
152 | 160 | y = tuple(pathlib.Path(z).parts) |
153 | 161 | assert tuple(utils.path_splitter(z)) == y[:-1] + ( |
4 | 4 | |
5 | 5 | [tox] |
6 | 6 | envlist = |
7 | flake8, py35, py36, py37, py38, py39 | |
7 | flake8, mypy, py36, py37, py38, py39, py310 | |
8 | 8 | # Other valid evironments are: |
9 | 9 | # docs |
10 | 10 | # release |
19 | 19 | passenv = |
20 | 20 | WITH_EXTRAS |
21 | 21 | deps = |
22 | -r dev/requirements.txt | |
22 | coverage | |
23 | pytest | |
24 | pytest-cov | |
25 | pytest-mock | |
26 | hypothesis | |
27 | semver | |
23 | 28 | extras = |
24 | 29 | {env:WITH_EXTRAS:} |
25 | 30 | commands = |
46 | 51 | twine check dist/* |
47 | 52 | skip_install = true |
48 | 53 | |
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 | ||
49 | 66 | # Build documentation. |
50 | 67 | # sphinx and sphinx_rtd_theme not in docs/requirements.txt because they |
51 | 68 | # will already be installed on readthedocs. |
52 | 69 | [testenv:docs] |
53 | 70 | deps = |
54 | sphinx < 3.3.0 | |
71 | sphinx | |
55 | 72 | sphinx_rtd_theme |
56 | 73 | -r docs/requirements.txt |
57 | 74 | commands = |
82 | 99 | deps = |
83 | 100 | commands = {envpython} dev/clean.py |
84 | 101 | 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 |