diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 0fb0e8e..e99cf00 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -22,11 +22,10 @@ jobs: with: python-version: '3.8' - - name: Install black - run: pip install black - - - name: Run black - run: black --quiet --check --diff . + - name: Run ruff + uses: astral-sh/ruff-action@v1 + with: + args: 'format --check' static-analysis: name: Static Analysis @@ -40,11 +39,8 @@ jobs: with: python-version: '3.8' - - name: Install Flake8 - run: pip install flake8 flake8-import-order flake8-bugbear pep8-naming - - - name: Run Flake8 - run: flake8 + - name: Run ruff + uses: astral-sh/ruff-action@v1 type-checking: name: Type Checking @@ -59,7 +55,10 @@ jobs: python-version: '3.8' - name: Install MyPy - run: pip install mypy hypothesis pytest pytest-mock fastnumbers + run: pip install mypy hypothesis pytest pytest-mock fastnumbers setuptools_scm + + - name: Create _version.py file + run: python -m setuptools_scm --force-write-version-files - name: Run MyPy run: mypy --strict natsort tests @@ -77,10 +76,9 @@ jobs: python-version: '3.8' - name: Install Validators - run: pip install twine check-manifest + run: pip install twine build - name: Run Validation run: | - check-manifest --ignore ".github*,*.md,.coveragerc" - python setup.py sdist + python -m build twine check dist/* diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 70c3730..de96bf3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,9 +21,8 @@ jobs: - name: Build Source Distribution and Wheel run: | - pip install wheel - python setup.py sdist --format=gztar - pip wheel . -w dist + pip install build + python -m build - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fde6e9f..765439a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] os: [ubuntu-latest] extras: [false] include: diff --git a/.gitignore b/.gitignore index 0b25526..339c4ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[co] +_version.py # Packages *.egg diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c6bc7d..96bc47b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,23 @@ Unreleased --- +### Changed + +- Modernize development infrastructure + (PR [#162](https://github.com/SethMMorton/natsort/issues/177)) + - Use `ruff` instead of `flake8` and `black` + - Move all config to `pyproject.toml` + - Use `setuptools-scm` to track `__version__` + - Remove `bumpversion` for creating new releases + ### Fixed - Eliminated mypy failure related to string literals +### Removed + +- Support for EOL Python 3.7 + [8.4.0] - 2023-06-19 --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 696d60d..78124cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ a good reason not to be). Located in the `dev/` folder is development collateral such as formatting and patching scripts. The only development collateral not in the `dev/` folder are those files that are expected to exist in the the top-level directory -(such as `setup.py`, `tox.ini`, and CI configuration). All of these scripts +(such as `pyproject.toml`, `tox.ini`, and CI configuration). All of these scripts can either be run with the python stdandard library, or have hooks in `tox`. I do not have strong opinions on how one should contribute, so diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 27d7981..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,10 +0,0 @@ -include LICENSE -include CHANGELOG.md -include tox.ini -include RELEASING.md -recursive-include mypy_stubs *.pyi -graft dev -graft docs -graft natsort -graft tests -global-exclude *.py[cod] __pycache__ *.so diff --git a/README.rst b/README.rst index d196140..d226ebf 100644 --- a/README.rst +++ b/README.rst @@ -362,7 +362,7 @@ from the command line with ``python -m natsort``. Check out the Requirements ------------ -`natsort`_ requires Python 3.7 or greater. +`natsort`_ requires Python 3.8 or greater. Optional Dependencies --------------------- diff --git a/dev/bump.py b/dev/bump.py index 9bb2138..d9ec89f 100755 --- a/dev/bump.py +++ b/dev/bump.py @@ -5,9 +5,24 @@ INTENDED TO BE CALLED FROM PROJECT ROOT, NOT FROM dev/! """ +import datetime import subprocess import sys +from setuptools_scm import get_version + +# Ensure a clean repo before moving on. +ret = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=no"], + check=True, + capture_output=True, + text=True, +) +if ret.stdout: + sys.exit("Cannot bump unless the git repo has no changes.") + + +# A valid bump must have been given. try: bump_type = sys.argv[1] except IndexError: @@ -28,44 +43,45 @@ def git(cmd, *args): sys.exit(e.returncode) -def bumpversion(severity, *args, catch=False): - """Wrapper for calling bumpversion""" - cmd = ["bump2version", *args, severity] - try: - if catch: - return subprocess.run( - cmd, check=True, capture_output=True, text=True - ).stdout - else: - subprocess.run(cmd, check=True, text=True) - except subprocess.CalledProcessError as e: - print("Call to bump2version failed!", file=sys.stderr) - print("STDOUT:", e.stdout, file=sys.stderr) - print("STDERR:", e.stderr, file=sys.stderr) - sys.exit(e.returncode) +# Use setuptools-scm to identify the current version +current_version = get_version( + root="..", + relative_to=__file__, + local_scheme="no-local-version", + version_scheme="only-version", +) +# Increment the version according to the bump type +version_components = list(map(int, current_version.split("."))) +incr_index = {"major": 0, "minor": 1, "patch": 2}[bump_type] +version_components[incr_index] += 1 +for i in range(incr_index + 1, 3): + version_components[i] = 0 +next_version = ".".join(map(str, version_components)) -# Do a dry run of the bump to find what the current version is and what it will become. -data = bumpversion(bump_type, "--dry-run", "--list", catch=True) -data = dict(x.split("=") for x in data.splitlines()) +# Update the changelog. +with open("CHANGELOG.md") as fl: + changelog = fl.read() -# Execute the bumpversion. -bumpversion(bump_type) + # Add a date to this entry. + changelog = changelog.replace( + "Unreleased", + f"Unreleased\n---\n\n[{next_version}] - {datetime.datetime.now():%Y-%m-%d}", + ) -# Post-process the changelog with things that bumpversion is not good at updating. -with open("CHANGELOG.md") as fl: - changelog = fl.read().replace( + # Add links to the entries. + changelog = changelog.replace( "", "\n[{new}]: {url}/{current}...{new}".format( - new=data["new_version"], - current=data["current_version"], + new=next_version, + current=current_version, url="https://github.com/SethMMorton/natsort/compare", ), ) with open("CHANGELOG.md", "w") as fl: fl.write(changelog) -# Finally, add the CHANGELOG.md changes to the previous commit. +# Add the CHANGELOG.md changes and commit & tag. git("add", "CHANGELOG.md") -git("commit", "--amend", "--no-edit") -git("tag", "--force", data["new_version"], "HEAD") +git("commit", "--message", f"Bump version: {current_version} → {next_version}") +git("tag", next_version, "HEAD") diff --git a/dev/clean.py b/dev/clean.py index ac2c4a0..5e6105d 100755 --- a/dev/clean.py +++ b/dev/clean.py @@ -13,8 +13,12 @@ pathlib.Path("build"), pathlib.Path("dist"), pathlib.Path(".pytest_cache"), + pathlib.Path(".ruff_cache"), + pathlib.Path(".mypy_cache"), + pathlib.Path(".pytest_cache"), pathlib.Path(".hypothesis"), pathlib.Path(".tox"), + pathlib.Path(".coverage"), ] dirs += pathlib.Path.cwd().glob("*.egg-info") for d in dirs: @@ -30,3 +34,8 @@ # Shouldn't be any .pyc left, but just in case for f in pathlib.Path.cwd().rglob("*.pyc"): f.unlink() + +# Remove _version.py +version = pathlib.Path("natsort/_version.py") +if version.is_file(): + version.unlink() diff --git a/dev/generate_new_unicode_numbers.py b/dev/generate_new_unicode_numbers.py index 42ce3ce..22b835b 100755 --- a/dev/generate_new_unicode_numbers.py +++ b/dev/generate_new_unicode_numbers.py @@ -1,8 +1,8 @@ #! /usr/bin/env python -# -*- coding: utf-8 -*- """ Generate the numeric hex list of unicode numerals """ + import os import os.path import sys @@ -40,6 +40,6 @@ if a in "0123456789": continue if unicodedata.numeric(a, None) is not None: - print(" 0x{:X},".format(i), file=fl) + print(f" 0x{i:X},", file=fl) print(")", file=fl) diff --git a/docs/conf.py b/docs/conf.py index 405b3da..084c5bd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,8 @@ import os +import natsort + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -58,7 +60,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "8.4.0" +release = natsort.__version__ # The short X.Y version. version = ".".join(release.split(".")[0:2]) diff --git a/natsort/__init__.py b/natsort/__init__.py index 31d3298..8f321cb 100644 --- a/natsort/__init__.py +++ b/natsort/__init__.py @@ -1,5 +1,10 @@ -# -*- coding: utf-8 -*- - +try: + # The redundant "as" tells mypy to treat as explict import + from natsort._version import __version__ as __version__ + from natsort._version import __version_tuple__ as __version_tuple__ +except ImportError: + __version__ = "unknown version" + __version_tuple__ = (0, 0, "unknown version") from natsort.natsort import ( NatsortKeyType, OSSortKeyType, @@ -23,8 +28,6 @@ from natsort.ns_enum import NSType, ns from natsort.utils import KeyType, NatsortInType, NatsortOutType, chain_functions -__version__ = "8.4.0" - __all__ = [ "natsort_key", "natsort_keygen", @@ -53,4 +56,4 @@ ] # Add the ns keys to this namespace for convenience. -globals().update({name: value for name, value in ns.__members__.items()}) +globals().update(dict(ns.__members__.items())) diff --git a/natsort/__main__.py b/natsort/__main__.py index 4dffc4b..8a2b51a 100644 --- a/natsort/__main__.py +++ b/natsort/__main__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import argparse import sys from typing import Callable, Iterable, List, Optional, Pattern, Tuple, Union, cast @@ -64,7 +62,7 @@ def main(*arguments: str) -> None: parser.add_argument( "--version", action="version", - version="%(prog)s {}".format(natsort.__version__), + version=f"%(prog)s {natsort.__version__}", ) parser.add_argument( "-p", @@ -201,9 +199,9 @@ def range_check(low: Num, high: Num) -> NumPair: """ if low >= high: - raise ValueError("low >= high") - else: - return low, high + msg = "low >= high" + raise ValueError(msg) + return low, high def check_filters(filters: Optional[NumPairIter]) -> Optional[List[NumPair]]: @@ -231,7 +229,7 @@ def check_filters(filters: Optional[NumPairIter]) -> Optional[List[NumPair]]: try: return [range_check(f[0], f[1]) for f in filters] except ValueError as err: - raise ValueError("Error in --filter: " + str(err)) + raise ValueError("Error in --filter: " + str(err)) from None def keep_entry_range( @@ -273,7 +271,10 @@ def keep_entry_range( def keep_entry_value( - entry: str, values: NumIter, converter: NumConverter, regex: Pattern[str] + entry: str, + values: NumIter, + converter: NumConverter, + regex: Pattern[str], ) -> bool: """ Check if an entry does not match a given value. diff --git a/natsort/compat/fake_fastnumbers.py b/natsort/compat/fake_fastnumbers.py index 430345a..f5b926c 100644 --- a/natsort/compat/fake_fastnumbers.py +++ b/natsort/compat/fake_fastnumbers.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- """ This module is intended to replicate some of the functionality from the fastnumbers module in the event that module is not installed. """ + import unicodedata from typing import Callable, FrozenSet, Union diff --git a/natsort/compat/fastnumbers.py b/natsort/compat/fastnumbers.py index f37ee84..b5d5298 100644 --- a/natsort/compat/fastnumbers.py +++ b/natsort/compat/fastnumbers.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- """ Interface for natsort to access fastnumbers functions without having to worry if it is actually installed. """ + import re from typing import Callable, Iterable, Iterator, Tuple, Union @@ -13,7 +13,8 @@ def is_supported_fastnumbers( - fastnumbers_version: str, minimum: Tuple[int, int, int] = (2, 0, 0) + fastnumbers_version: str, + minimum: Tuple[int, int, int] = (2, 0, 0), ) -> bool: match = re.match( r"^(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?$", @@ -22,9 +23,8 @@ def is_supported_fastnumbers( ) if not match: - raise ValueError( - "Invalid fastnumbers version number '{}'".format(fastnumbers_version) - ) + msg = f"Invalid fastnumbers version number '{fastnumbers_version}'" + raise ValueError(msg) (major, minor, patch) = match.group(1, 2, 4) @@ -35,7 +35,8 @@ def is_supported_fastnumbers( # benefits. If not, we use the simulated functions that come with natsort. try: # noinspection PyPackageRequirements - from fastnumbers import fast_float, fast_int, __version__ as fn_ver + from fastnumbers import __version__ as fn_ver + from fastnumbers import fast_float, fast_int # Require >= version 2.0.0. if not is_supported_fastnumbers(fn_ver): @@ -53,7 +54,7 @@ def is_supported_fastnumbers( # then there is nothing to do. if "try_float" not in globals(): - def try_float( # type: ignore[no-redef] # noqa: F811 + def try_float( # type: ignore[no-redef] x: Iterable[str], map: bool, nan: float = float("inf"), @@ -65,7 +66,7 @@ def try_float( # type: ignore[no-redef] # noqa: F811 if "try_int" not in globals(): - def try_int( # type: ignore[no-redef] # noqa: F811 + def try_int( # type: ignore[no-redef] x: Iterable[str], map: bool, on_fail: Callable[[str], str] = lambda x: x, diff --git a/natsort/compat/locale.py b/natsort/compat/locale.py index b1b7c00..7f73670 100644 --- a/natsort/compat/locale.py +++ b/natsort/compat/locale.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- """ Interface for natsort to access locale functionality without having to worry about if it is using PyICU or the built-in locale. """ + import sys from typing import Callable, Union, cast @@ -22,10 +22,11 @@ # strxfrm can be buggy (especially on OSX and *possibly* some other # BSD-based systems), so prefer icu if available. -try: # noqa: C901 - import icu +try: from locale import getlocale + import icu + null_string_locale = b"" # This string should in theory be sorted after any other byte @@ -115,8 +116,7 @@ def get_thousands_sep() -> str: "fr_FR.UTF-8": "\xa0", "fr_CA.ISO8859-15": "\xa0", }.get(loc, sep) - else: - return sep + return sep def get_decimal_point() -> str: return cast(str, locale.localeconv()["decimal_point"]) diff --git a/natsort/natsort.py b/natsort/natsort.py index 43006d5..a69a376 100644 --- a/natsort/natsort.py +++ b/natsort/natsort.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Along with ns_enum.py, this module contains all of the natsort public API. @@ -25,7 +24,7 @@ import natsort.compat.locale from natsort import utils -from natsort.ns_enum import NSType, NS_DUMB, ns +from natsort.ns_enum import NS_DUMB, NSType, ns from natsort.utils import NatsortInType, NatsortOutType # Common input and output types @@ -64,16 +63,16 @@ def decoder(encoding: str) -> Callable[[Any], Any]: Examples -------- - >>> f = decoder('utf8') - >>> f(b'bytes') == 'bytes' + >>> f = decoder("utf8") + >>> f(b"bytes") == "bytes" True >>> f(12345) == 12345 True >>> # On Python 3, without decoder this would return [b'a10', b'a2'] - >>> natsorted([b'a10', b'a2'], key=decoder('utf8')) == [b'a2', b'a10'] + >>> natsorted([b"a10", b"a2"], key=decoder("utf8")) == [b"a2", b"a10"] True >>> # On Python 3, without decoder this would raise a TypeError. - >>> natsorted([b'a10', 'a2'], key=decoder('utf8')) == ['a2', b'a10'] + >>> natsorted([b"a10", "a2"], key=decoder("utf8")) == ["a2", b"a10"] True """ @@ -127,7 +126,8 @@ def as_utf8(s: Any) -> Any: def natsort_keygen( - key: Optional[Callable[[Any], NatsortInType]] = None, alg: NSType = ns.DEFAULT + key: Optional[Callable[[Any], NatsortInType]] = None, + alg: NSType = ns.DEFAULT, ) -> Callable[[Any], NatsortOutType]: """ Generate a key to sort strings and numbers naturally. @@ -178,7 +178,7 @@ def natsort_keygen( ns.DEFAULT | alg except TypeError: msg = "natsort_keygen: 'alg' argument must be from the enum 'ns'" - raise ValueError(msg + ", got {}".format(str(alg))) + raise ValueError(msg + f", got {alg!s}") from None # Add the NS_DUMB option if the locale library is broken. if alg & ns.LOCALEALPHA and natsort.compat.locale.dumb_sort(): @@ -206,7 +206,12 @@ def natsort_keygen( # Create the high-level parsing functions for strings, bytes, and numbers. string_func = utils.parse_string_factory( - alg, sep, regex.split, input_transform, component_transform, final_transform + alg, + sep, + regex.split, + input_transform, + component_transform, + final_transform, ) if alg & ns.PATH: string_func = utils.parse_path_factory(string_func) @@ -478,7 +483,7 @@ def newkey(x: Tuple[int, T]) -> NatsortInType: return key(itemgetter(1)(x)) # Pair the index and sequence together, then sort by element - index_seq_pair = [(x, y) for x, y in enumerate(seq)] + index_seq_pair = list(enumerate(seq)) if alg & ns.PRESORT: index_seq_pair.sort(reverse=reverse, key=lambda x: str(itemgetter(1)(x))) index_seq_pair.sort(reverse=reverse, key=natsort_keygen(newkey, alg)) @@ -590,7 +595,9 @@ def index_realsorted( def order_by_index( - seq: Sequence[Any], index: Iterable[int], iter: bool = False + seq: Sequence[Any], + index: Iterable[int], + iter: bool = False, ) -> Iterable[Any]: """ Order a given sequence by an index sequence. @@ -670,7 +677,9 @@ def numeric_regex_chooser(alg: NSType) -> str: def _split_apply( - v: Any, key: Optional[Callable[[T], NatsortInType]] = None, treat_base: bool = True + v: Any, + key: Optional[Callable[[T], NatsortInType]] = None, + treat_base: bool = True, ) -> Iterator[str]: if key is not None: v = key(v) @@ -681,7 +690,7 @@ def _split_apply( # Choose the implementation based on the host OS if platform.system() == "Windows": - from ctypes import wintypes, windll # type: ignore + from ctypes import windll, wintypes # type: ignore from functools import cmp_to_key _windows_sort_cmp = windll.Shlwapi.StrCmpLogicalW @@ -690,7 +699,7 @@ def _split_apply( _winsort_key = cmp_to_key(_windows_sort_cmp) def os_sort_keygen( - key: Optional[Callable[[Any], NatsortInType]] = None + key: Optional[Callable[[Any], NatsortInType]] = None, ) -> Callable[[Any], NatsortOutType]: return cast( Callable[[Any], NatsortOutType], @@ -713,19 +722,20 @@ def os_sort_keygen( except ImportError: # No ICU installed def os_sort_keygen( - key: Optional[Callable[[Any], NatsortInType]] = None + key: Optional[Callable[[Any], NatsortInType]] = None, ) -> Callable[[Any], NatsortOutType]: return natsort_keygen(key=key, alg=ns.LOCALE | ns.PATH | ns.IGNORECASE) else: # ICU installed def os_sort_keygen( - key: Optional[Callable[[Any], NatsortInType]] = None + key: Optional[Callable[[Any], NatsortInType]] = None, ) -> Callable[[Any], NatsortOutType]: loc = natsort.compat.locale.get_icu_locale() collator = icu.Collator.createInstance(loc) collator.setAttribute( - icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON + icu.UCollAttribute.NUMERIC_COLLATION, + icu.UCollAttributeValue.ON, ) return lambda x: tuple(map(collator.getSortKey, _split_apply(x, key))) diff --git a/natsort/ns_enum.py b/natsort/ns_enum.py index 02f970f..b55e212 100644 --- a/natsort/ns_enum.py +++ b/natsort/ns_enum.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ This module defines the "ns" enum for natsort is used to determine what algorithm natsort uses. @@ -8,7 +7,6 @@ import itertools import typing - _counter = itertools.count(0) @@ -130,7 +128,7 @@ class ns(enum.IntEnum): # noqa: N801 available as top-level imports. >>> import natsort as ns - >>> a = ['num5.10', 'num-3', 'num5.3', 'num2'] + >>> a = ["num5.10", "num-3", "num5.3", "num2"] >>> ns.natsorted(a, alg=ns.REAL) == ns.natsorted(a, alg=ns.ns.REAL) True diff --git a/natsort/unicode_numbers.py b/natsort/unicode_numbers.py index 94cf0ff..a632b2a 100644 --- a/natsort/unicode_numbers.py +++ b/natsort/unicode_numbers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Pre-determine the collection of unicode decimals, digits, and numerals. """ diff --git a/natsort/unicode_numeric_hex.py b/natsort/unicode_numeric_hex.py index 2134c25..48d6fb0 100644 --- a/natsort/unicode_numeric_hex.py +++ b/natsort/unicode_numeric_hex.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Contains all possible non-ASCII unicode numbers. """ diff --git a/natsort/utils.py b/natsort/utils.py index 0345ceb..bfc756b 100644 --- a/natsort/utils.py +++ b/natsort/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Utilities and definitions for natsort, mostly all used to define the natsort_key function. @@ -26,11 +25,12 @@ have signatures similar to the following >>> def factory(parameter): - ... val = 'yes' if parameter else 'no' + ... val = "yes" if parameter else "no" + ... ... def closure(x, _val=val): - ... return '{} {}'.format(_val, x) - ... return closure + ... return "{} {}".format(_val, x) ... + ... return closure The variable value is passed as the default to a keyword argument. This is a micro-optimization @@ -38,12 +38,14 @@ and thus has a slightly improved performance at runtime. """ + import re from functools import partial, reduce from itertools import chain as ichain from operator import methodcaller from pathlib import PurePath from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -53,7 +55,6 @@ Match, Optional, Pattern, - TYPE_CHECKING, Tuple, Union, cast, @@ -68,7 +69,7 @@ get_strxfrm, get_thousands_sep, ) -from natsort.ns_enum import NSType, NS_DUMB, ns +from natsort.ns_enum import NS_DUMB, NSType, ns from natsort.unicode_numbers import digits_no_decimals, numeric_no_decimals if TYPE_CHECKING: @@ -239,8 +240,7 @@ def _normalize_input_factory(alg: NSType) -> StrToStr: """ if alg & ns.COMPATIBILITYNORMALIZE: return partial(normalize, "NFKD") - else: - return partial(normalize, "NFD") + return partial(normalize, "NFD") def _compose_input_factory(alg: NSType) -> StrToStr: @@ -260,8 +260,7 @@ def _compose_input_factory(alg: NSType) -> StrToStr: """ if alg & ns.COMPATIBILITYNORMALIZE: return partial(normalize, "NFKC") - else: - return partial(normalize, "NFC") + return partial(normalize, "NFC") @overload @@ -339,15 +338,15 @@ def natsort_key( if isinstance(val, (str, PurePath)): return string_func(val) - elif isinstance(val, bytes): + if isinstance(val, bytes): return bytes_func(val) - elif isinstance(val, Iterable): + if isinstance(val, Iterable): # Must be parsed recursively, but do not apply the key recursively. return tuple( natsort_key(x, None, string_func, bytes_func, num_func) for x in val ) - else: # Anything else goes here - return num_func(val) + # Anything else goes here + return num_func(val) def parse_bytes_factory(alg: NSType) -> BytesTransformer: @@ -375,16 +374,17 @@ def parse_bytes_factory(alg: NSType) -> BytesTransformer: # bytes cannot be compared to strings. if alg & ns.PATH and alg & ns.IGNORECASE: return lambda x: ((x.lower(),),) - elif alg & ns.PATH: + if alg & ns.PATH: return lambda x: ((x,),) - elif alg & ns.IGNORECASE: + if alg & ns.IGNORECASE: return lambda x: (x.lower(),) - else: - return lambda x: (x,) + return lambda x: (x,) def parse_number_or_none_factory( - alg: NSType, sep: StrOrBytes, pre_sep: str + alg: NSType, + sep: StrOrBytes, + pre_sep: str, ) -> NumTransformer: """ Create a function that will format a number (or None) into a tuple. @@ -428,22 +428,20 @@ def func( # None comes first, then NaN, then the replacement value. if val != val: return _sep, _nan_replace, "3" if reverse else "1" - elif val is None: + if val is None: return _sep, _nan_replace, "2" - elif val == _nan_replace: + if val == _nan_replace: return _sep, _nan_replace, "1" if reverse else "3" - else: - return _sep, val + return _sep, val # Return the function, possibly wrapping in tuple if PATH is selected. if alg & ns.PATH and alg & ns.UNGROUPLETTERS and alg & ns.LOCALEALPHA: return lambda x: (((pre_sep,), func(x)),) - elif alg & ns.UNGROUPLETTERS and alg & ns.LOCALEALPHA: + if alg & ns.UNGROUPLETTERS and alg & ns.LOCALEALPHA: return lambda x: ((pre_sep,), func(x)) - elif alg & ns.PATH: + if alg & ns.PATH: return lambda x: (func(x),) - else: - return func + return func def parse_string_factory( @@ -649,7 +647,8 @@ def input_string_transform_factory(alg: NSType) -> StrToStr: nodecimal += r"(? StrTransformer: if alg & ns.FLOAT: kwargs["nan"] = nan_val return cast(StrTransformer, partial(try_float, **kwargs)) - else: - return cast(StrTransformer, partial(try_int, **kwargs)) + return cast(StrTransformer, partial(try_int, **kwargs)) def final_data_transform_factory( - alg: NSType, sep: StrOrBytes, pre_sep: str + alg: NSType, + sep: StrOrBytes, + pre_sep: str, ) -> FinalTransformer: """ Create a function to transform a tuple. @@ -759,16 +759,15 @@ def func( split_val = tuple(split_val) if not split_val: return (), () - elif split_val[0] == _sep: + if split_val[0] == _sep: return (_pre_sep,), split_val - else: - return (_transform(val[0]),), split_val + return (_transform(val[0]),), split_val else: def func( split_val: Iterable[NatsortInType], - val: str, + val: str, # noqa: ARG001 _transform: StrToStr = _no_op, _sep: StrOrBytes = sep, _pre_sep: str = pre_sep, @@ -827,18 +826,17 @@ def chain_functions(functions: Iterable[AnyCall]) -> AnyCall: >>> funcs = [lambda x: x * 4, len, lambda x: x + 5] >>> func = chain_functions(funcs) - >>> func('hey') + >>> func("hey") 17 """ functions = list(functions) if not functions: return _no_op - elif len(functions) == 1: + if len(functions) == 1: return functions[0] - else: - # See https://stackoverflow.com/a/39123400/1399279 - return partial(reduce, lambda res, f: f(res), functions) + # See https://stackoverflow.com/a/39123400/1399279 + return partial(reduce, lambda res, f: f(res), functions) @overload @@ -868,13 +866,14 @@ def do_decoding(s: Any, encoding: str) -> Any: """ if isinstance(s, bytes): return s.decode(encoding) - else: - return s + return s # noinspection PyIncorrectDocstring def path_splitter( - s: PathArg, treat_base: bool = True, _d_match: MatchFn = re.compile(r"\.\d").match + s: PathArg, + treat_base: bool = True, + _d_match: MatchFn = re.compile(r"\.\d").match, ) -> Iterator[str]: """ Split a string into its path components. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a228c8d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,112 @@ +[build-system] +requires = ["setuptools>=64", "setuptools-scm>=8.0"] +build-backend = "setuptools.build_meta" + +[project] +name="natsort" +authors = [ + {name = "Seth M. Morton", email = "drtuba78@gmail.com"}, +] +dynamic = ["version"] +requires-python = ">=3.8" +description = "Simple yet flexible natural sorting in Python." +readme = "README.rst" +license = {text = "MIT"} +keywords = ["sort", "sorting", "natural sort", "natsort"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Intended Audience :: System Administrators", + "Intended Audience :: Information Technology", + "Intended Audience :: Financial and Insurance Industry", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Utilities", + "Topic :: Text Processing", +] + +[project.optional-dependencies] +fast = ["fastnumbers >= 2.0.0"] +icu = ["PyICU >= 1.0.0"] + +[project.urls] +Homepage = "https://github.com/SethMMorton/natsort" +Documentation = "https://natsort.readthedocs.io/" +Issues = "https://github.com/SethMMorton/natsort/issues" +Changelog = "https://github.com/SethMMorton/natsort/blob/main/CHANGELOG.md" + +[project.scripts] +natsort = "natsort.__main__:main" + +[tool.setuptools_scm] +version_file = "natsort/_version.py" + +[tool.setuptools.packages] +find = {namespaces = false} + +[tool.mypy] +mypy_path = "mypy_stubs" + +[tool.ruff] +target-version = "py38" +extend-exclude = ["build", "dist", "docs", "mypy_stubs", "_version.py"] + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +fixable = ["ALL"] +select = [ + "F", + "E", + "W", + "C90", # mccabe + "B", # flake8-bugbear + "N", # pep8-naming + "I", # isort + "UP", # pyupgrade + "YTT", # flake8-2020 + "BLE", # flake8-blind-except + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "EM", # flake8-errmsg + "EXE", # flake8-executable + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "PIE", # flake8-pie + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "ARG", # flake8-unused-arguments + "ERA", # eradicate + "RUF", # ruff-specific-rules +] +ignore = [ + "A002", # arguments shadow builtins + "PT004", # leading underscore in fixtures that return nothing + "RUF001", # ambiguous-unicode-character-string + "COM812", # missing trailing comma + "ISC001", # single line implicit string concatenation +] +# doctests = true # enable when/if available + +[tool.ruff.lint.mccabe] +max-complexity = 10 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1155262..0000000 --- a/setup.cfg +++ /dev/null @@ -1,68 +0,0 @@ -[bumpversion] -current_version = 8.4.0 -commit = True -tag = True -tag_name = {new_version} - -[metadata] -author = Seth M. Morton -author_email = drtuba78@gmail.com -url = https://github.com/SethMMorton/natsort -description = Simple yet flexible natural sorting in Python. -long_description = file: README.rst -long_description_content_type = text/x-rst -license = MIT -license_files = LICENSE -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Intended Audience :: Science/Research - Intended Audience :: System Administrators - Intended Audience :: Information Technology - Intended Audience :: Financial and Insurance Industry - Operating System :: OS Independent - License :: OSI Approved :: MIT License - Natural Language :: English - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Topic :: Scientific/Engineering :: Information Analysis - Topic :: Utilities - Topic :: Text Processing - -[bumpversion:file:setup.py] - -[bumpversion:file:natsort/__init__.py] - -[bumpversion:file:docs/conf.py] - -[bumpversion:file:CHANGELOG.md] -search = Unreleased -replace = Unreleased - --- - - [{new_version}] - {now:%%Y-%%m-%%d} - -[flake8] -max-line-length = 89 -import-order-style = pycharm -doctests = True -max-complexity = 10 -exclude = - natsort.egg-info, - .tox, - .cache, - .git, - __pycache__, - build, - dist, - docs, - .venv - -[mypy] -mypy_path = mypy_stubs diff --git a/setup.py b/setup.py deleted file mode 100644 index ac91d51..0000000 --- a/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -#! /usr/bin/env python - -from setuptools import find_packages, setup - -setup( - name="natsort", - version="8.4.0", - packages=find_packages(), - entry_points={"console_scripts": ["natsort = natsort.__main__:main"]}, - python_requires=">=3.7", - extras_require={"fast": ["fastnumbers >= 2.0.0"], "icu": ["PyICU >= 1.0.0"]}, - package_data={"": ["py.typed"]}, - zip_safe=False, -) diff --git a/tests/conftest.py b/tests/conftest.py index 2ae04fc..588281c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,23 +7,24 @@ import hypothesis import pytest -from natsort.compat.locale import dumb_sort +from natsort.compat.locale import dumb_sort # This disables the "too slow" hypothesis heath check globally. # For some reason it thinks that the text/binary generation is too # slow then causes the tests to fail. hypothesis.settings.register_profile( - "slow-tests", suppress_health_check=[hypothesis.HealthCheck.too_slow] + "slow-tests", + suppress_health_check=[hypothesis.HealthCheck.too_slow], ) def load_locale(x: str) -> None: """Convenience to load a locale.""" - locale.setlocale(locale.LC_ALL, str("{}.UTF-8".format(x))) + locale.setlocale(locale.LC_ALL, str(f"{x}.UTF-8")) -@pytest.fixture() +@pytest.fixture def with_locale_en_us() -> Iterator[None]: """Convenience to load the en_US locale - reset when complete.""" orig = locale.getlocale() @@ -32,7 +33,7 @@ def with_locale_en_us() -> Iterator[None]: locale.setlocale(locale.LC_ALL, orig) -@pytest.fixture() +@pytest.fixture def with_locale_de_de() -> Iterator[None]: """ Convenience to load the de_DE locale - reset when complete - skip if missing. @@ -48,7 +49,7 @@ def with_locale_de_de() -> Iterator[None]: locale.setlocale(locale.LC_ALL, orig) -@pytest.fixture() +@pytest.fixture def with_locale_cs_cz() -> Iterator[None]: """ Convenience to load the cs_CZ locale - reset when complete - skip if missing. diff --git a/tests/profile_natsorted.py b/tests/profile_natsorted.py index f6580a3..da55784 100644 --- a/tests/profile_natsorted.py +++ b/tests/profile_natsorted.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """\ This file contains functions to profile natsorted with different inputs and different settings. @@ -10,10 +9,10 @@ from typing import List, Union try: - from natsort import ns, natsort_keygen + from natsort import natsort_keygen, ns except ImportError: sys.path.insert(0, ".") - from natsort import ns, natsort_keygen + from natsort import natsort_keygen, ns from natsort.natsort import NatsortKeyType @@ -45,7 +44,9 @@ def prof_time_to_generate() -> None: def prof_parsing( - a: Union[str, int, bytes, List[str]], msg: str, key: NatsortKeyType = basic_key + a: Union[str, int, bytes, List[str]], + msg: str, + key: NatsortKeyType = basic_key, ) -> None: print(msg) for _ in range(100000): @@ -53,15 +54,18 @@ def prof_parsing( cProfile.run( - 'prof_parsing(int_string, "*** Basic Call, Int as String ***")', sort="time" + 'prof_parsing(int_string, "*** Basic Call, Int as String ***")', + sort="time", ) cProfile.run( - 'prof_parsing(float_string, "*** Basic Call, Float as String ***")', sort="time" + 'prof_parsing(float_string, "*** Basic Call, Float as String ***")', + sort="time", ) cProfile.run('prof_parsing(float_string, "*** Real Call ***", real_key)', sort="time") cProfile.run('prof_parsing(number, "*** Basic Call, Number ***")', sort="time") cProfile.run( - 'prof_parsing(fancy_string, "*** Basic Call, Mixed String ***")', sort="time" + 'prof_parsing(fancy_string, "*** Basic Call, Mixed String ***")', + sort="time", ) cProfile.run('prof_parsing(some_bytes, "*** Basic Call, Byte String ***")', sort="time") cProfile.run('prof_parsing(a_path, "*** Path Call ***", path_key)', sort="time") diff --git a/tests/test_fake_fastnumbers.py b/tests/test_fake_fastnumbers.py index 6324c64..95381be 100644 --- a/tests/test_fake_fastnumbers.py +++ b/tests/test_fake_fastnumbers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """\ Test the fake fastnumbers module. """ @@ -9,6 +8,7 @@ from hypothesis import given from hypothesis.strategies import floats, integers, text + from natsort.compat.fake_fastnumbers import fast_float, fast_int diff --git a/tests/test_final_data_transform_factory.py b/tests/test_final_data_transform_factory.py index 36607b6..023dc5f 100644 --- a/tests/test_final_data_transform_factory.py +++ b/tests/test_final_data_transform_factory.py @@ -1,11 +1,12 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" -from typing import Callable, Union + +from typing import Callable import pytest from hypothesis import example, given from hypothesis.strategies import floats, integers, text -from natsort.ns_enum import NSType, NS_DUMB, ns + +from natsort.ns_enum import NS_DUMB, NSType, ns from natsort.utils import final_data_transform_factory @@ -13,7 +14,9 @@ @given(x=text(), y=floats(allow_nan=False, allow_infinity=False) | integers()) @pytest.mark.usefixtures("with_locale_en_us") def test_final_data_transform_factory_default( - x: str, y: Union[int, float], alg: NSType + x: str, + y: float, + alg: NSType, ) -> None: final_data_transform_func = final_data_transform_factory(alg, "", "::") value = (x, y) @@ -23,7 +26,7 @@ def test_final_data_transform_factory_default( @pytest.mark.parametrize( - "alg, func", + ("alg", "func"), [ (ns.UNGROUPLETTERS | ns.LOCALE, lambda x: x), (ns.LOCALE | ns.UNGROUPLETTERS | NS_DUMB, lambda x: x), @@ -38,13 +41,16 @@ def test_final_data_transform_factory_default( @example(x="İ", y=0) @pytest.mark.usefixtures("with_locale_en_us") def test_final_data_transform_factory_ungroup_and_locale( - x: str, y: Union[int, float], alg: NSType, func: Callable[[str], str] + x: str, + y: float, + alg: NSType, + func: Callable[[str], str], ) -> None: final_data_transform_func = final_data_transform_factory(alg, "", "::") value = (x, y) original_value = "".join(map(str, value)) result = final_data_transform_func(value, original_value) - if x: + if x: # noqa: SIM108 expected = ((func(original_value[:1]),), value) else: expected = (("::",), value) diff --git a/tests/test_input_string_transform_factory.py b/tests/test_input_string_transform_factory.py index 6a08318..5d132da 100644 --- a/tests/test_input_string_transform_factory.py +++ b/tests/test_input_string_transform_factory.py @@ -1,11 +1,12 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" + from typing import Callable import pytest from hypothesis import example, given from hypothesis.strategies import integers, text -from natsort.ns_enum import NSType, NS_DUMB, ns + +from natsort.ns_enum import NS_DUMB, NSType, ns from natsort.utils import input_string_transform_factory @@ -27,7 +28,7 @@ def test_input_string_transform_factory_is_no_op_for_no_alg_options(x: str) -> N @pytest.mark.parametrize( - "alg, example_func", + ("alg", "example_func"), [ (ns.IGNORECASE, lambda x: x.casefold()), (NS_DUMB, lambda x: x.swapcase()), @@ -38,7 +39,9 @@ def test_input_string_transform_factory_is_no_op_for_no_alg_options(x: str) -> N ) @given(x=text()) def test_input_string_transform_factory( - x: str, alg: NSType, example_func: Callable[[str], str] + x: str, + alg: NSType, + example_func: Callable[[str], str], ) -> None: input_string_transform_func = input_string_transform_factory(alg) assert input_string_transform_func(x) == example_func(x) @@ -64,7 +67,7 @@ def test_input_string_transform_factory_cleans_thousands(x: int) -> None: @pytest.mark.parametrize( - "x, expected", + ("x", "expected"), [ ("12,543,642642.5345,34980", "12543,642642.5345,34980"), ("12,59443,642,642.53,4534980", "12,59443,642642.53,4534980"), # No change @@ -73,14 +76,15 @@ def test_input_string_transform_factory_cleans_thousands(x: int) -> None: ) @pytest.mark.usefixtures("with_locale_en_us") def test_input_string_transform_factory_handles_us_locale( - x: str, expected: str + x: str, + expected: str, ) -> None: input_string_transform_func = input_string_transform_factory(ns.LOCALE) assert input_string_transform_func(x) == expected @pytest.mark.parametrize( - "x, expected", + ("x", "expected"), [ ("12.543.642642,5345.34980", "12543.642642,5345.34980"), ("12.59443.642.642,53.4534980", "12.59443.642642,53.4534980"), # No change @@ -89,14 +93,15 @@ def test_input_string_transform_factory_handles_us_locale( ) @pytest.mark.usefixtures("with_locale_de_de") def test_input_string_transform_factory_handles_de_locale( - x: str, expected: str + x: str, + expected: str, ) -> None: input_string_transform_func = input_string_transform_factory(ns.LOCALE) assert input_string_transform_func(x) == expected @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.LOCALE, "1543,753"), # Does nothing without FLOAT (ns.LOCALE | ns.FLOAT, "1543.753"), @@ -105,7 +110,8 @@ def test_input_string_transform_factory_handles_de_locale( ) @pytest.mark.usefixtures("with_locale_de_de") def test_input_string_transform_factory_handles_german_locale( - alg: NSType, expected: str + alg: NSType, + expected: str, ) -> None: input_string_transform_func = input_string_transform_factory(alg) assert input_string_transform_func("1543,753") == expected diff --git a/tests/test_main.py b/tests/test_main.py index 2f784a1..10332d6 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,15 +1,16 @@ -# -*- coding: utf-8 -*- """\ Test the natsort command-line tool functions. """ import re import sys -from typing import Any, List, Union +from typing import Any, List import pytest from hypothesis import given from hypothesis.strategies import DataObject, data, floats, integers, lists +from pytest_mock import MockerFixture + from natsort.__main__ import ( TypedArgs, check_filters, @@ -19,7 +20,6 @@ range_check, sort_and_print_entries, ) -from pytest_mock import MockerFixture def test_main_passes_default_arguments_with_no_command_line_options( @@ -64,7 +64,7 @@ def test_main_passes_arguments_with_all_command_line_options( assert args.locale -mock_print = "__builtin__.print" if sys.version[0] == "2" else "builtins.print" +mock_print = "__builtin__.print" if sys.version_info[0] == 2 else "builtins.print" entries = [ "tmp/a57/path2", @@ -78,10 +78,10 @@ def test_main_passes_arguments_with_all_command_line_options( @pytest.mark.parametrize( - "options, order", + ("options", "order"), [ # Defaults, all options false - # tmp/a1 (1)/path1 + # tmp/a1 (1)/path1 # noqa: ERA001 # tmp/a1/path1 # tmp/a23/path1 # tmp/a57/path2 @@ -91,7 +91,7 @@ def test_main_passes_arguments_with_all_command_line_options( ([None, None, False, False, False], [3, 2, 1, 0, 5, 6, 4]), # Path option True # tmp/a1/path1 - # tmp/a1 (1)/path1 + # tmp/a1 (1)/path1 # noqa: ERA001 # tmp/a23/path1 # tmp/a57/path2 # tmp/a64/path1 @@ -106,12 +106,12 @@ def test_main_passes_arguments_with_all_command_line_options( ([[(20, 100)], None, False, False, False], [1, 0, 5, 6]), # Reverse filter, exclude in range # tmp/a1/path1 - # tmp/a1 (1)/path1 + # tmp/a1 (1)/path1 # noqa: ERA001 # tmp/a130/path1 ([None, [(20, 100)], False, True, False], [2, 3, 4]), # Exclude given values with exclude list # tmp/a1/path1 - # tmp/a1 (1)/path1 + # tmp/a1 (1)/path1 # noqa: ERA001 # tmp/a57/path2 # tmp/a64/path1 # tmp/a64/path2 @@ -122,13 +122,15 @@ def test_main_passes_arguments_with_all_command_line_options( # tmp/a64/path1 # tmp/a57/path2 # tmp/a23/path1 - # tmp/a1 (1)/path1 + # tmp/a1 (1)/path1 # noqa: ERA001 # tmp/a1/path1 ([None, None, False, True, True], reversed([2, 3, 1, 0, 5, 6, 4])), ], ) def test_sort_and_print_entries( - options: List[Any], order: List[int], mocker: MockerFixture + options: List[Any], + order: List[int], + mocker: MockerFixture, ) -> None: p = mocker.patch(mock_print) sort_and_print_entries(entries, TypedArgs(*options)) @@ -147,7 +149,8 @@ def test_range_check_returns_range_as_is_but_with_floats_example() -> None: @given(x=floats(allow_nan=False, min_value=-1e8, max_value=1e8) | integers(), d=data()) def test_range_check_returns_range_as_is_if_first_is_less_than_second( - x: Union[int, float], d: DataObject + x: float, + d: DataObject, ) -> None: # Pull data such that the first is less than the second. if isinstance(x, float): @@ -164,7 +167,8 @@ def test_range_check_raises_value_error_if_second_is_less_than_first_example() - @given(x=floats(allow_nan=False), d=data()) def test_range_check_raises_value_error_if_second_is_less_than_first( - x: float, d: DataObject + x: float, + d: DataObject, ) -> None: # Pull data such that the first is greater than or equal to the second. y = d.draw(floats(max_value=x, allow_nan=False)) @@ -183,11 +187,12 @@ def test_check_filters_returns_input_as_is_if_filter_is_valid_example() -> None: @given(x=lists(integers(), min_size=1), d=data()) def test_check_filters_returns_input_as_is_if_filter_is_valid( - x: List[int], d: DataObject + x: List[int], + d: DataObject, ) -> None: # ensure y is element-wise greater than x y = [d.draw(integers(min_value=val + 1)) for val in x] - assert check_filters(list(zip(x, y))) == [(i, j) for i, j in zip(x, y)] + assert check_filters(list(zip(x, y))) == list(zip(x, y)) def test_check_filters_raises_value_error_if_filter_is_invalid_example() -> None: @@ -197,7 +202,8 @@ def test_check_filters_raises_value_error_if_filter_is_invalid_example() -> None @given(x=lists(integers(), min_size=1), d=data()) def test_check_filters_raises_value_error_if_filter_is_invalid( - x: List[int], d: DataObject + x: List[int], + d: DataObject, ) -> None: # ensure y is element-wise less than or equal to x y = [d.draw(integers(max_value=val)) for val in x] @@ -206,7 +212,7 @@ def test_check_filters_raises_value_error_if_filter_is_invalid( @pytest.mark.parametrize( - "lows, highs, truth", + ("lows", "highs", "truth"), # 1. Any portion is between the bounds => True. # 2. Any portion is between any bounds => True. # 3. No portion is between the bounds => False. @@ -217,6 +223,6 @@ def test_keep_entry_range(lows: List[int], highs: List[int], truth: bool) -> Non # 1. Values not in entry => True. 2. Values in entry => False. -@pytest.mark.parametrize("values, truth", [([100, 45], True), ([23], False)]) +@pytest.mark.parametrize(("values", "truth"), [([100, 45], True), ([23], False)]) def test_keep_entry_value(values: List[int], truth: bool) -> None: assert keep_entry_value("a56b23c89", values, int, re.compile(r"\d+")) is truth diff --git a/tests/test_natsort_key.py b/tests/test_natsort_key.py index cdfdc67..40e4174 100644 --- a/tests/test_natsort_key.py +++ b/tests/test_natsort_key.py @@ -1,25 +1,27 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" -from typing import Any, List, NoReturn, Tuple, Union, cast + +from typing import Any, List, NoReturn, Tuple, cast from hypothesis import given from hypothesis.strategies import binary, floats, integers, lists, text + from natsort.utils import natsort_key def str_func(x: Any) -> Tuple[str]: if isinstance(x, str): return (x,) - else: - raise TypeError("Not a str!") + msg = "Not a str!" + raise TypeError(msg) def fail(_: Any) -> NoReturn: - raise AssertionError("This should never be reached!") + msg = "This should never be reached!" + raise AssertionError(msg) @given(floats(allow_nan=False) | integers()) -def test_natsort_key_with_numeric_input_takes_number_path(x: Union[float, int]) -> None: +def test_natsort_key_with_numeric_input_takes_number_path(x: float) -> None: assert natsort_key(x, None, str_func, fail, lambda y: ("", y))[1] is x @@ -41,5 +43,5 @@ def test_natsort_key_with_nested_input_takes_nested_path(x: List[str]) -> None: @given(text()) def test_natsort_key_with_key_argument_applies_key_before_processing(x: str) -> None: assert natsort_key(x, len, str_func, fail, lambda y: ("", cast(int, y)))[1] == len( - x + x, ) diff --git a/tests/test_natsort_keygen.py b/tests/test_natsort_keygen.py index d4da2e3..0c073d4 100644 --- a/tests/test_natsort_keygen.py +++ b/tests/test_natsort_keygen.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """\ Here are a collection of examples of how this module can be used. See the README or the natsort homepage for more details. @@ -8,11 +7,12 @@ from typing import List, Tuple, Union import pytest +from pytest_mock import MockerFixture + from natsort import natsort_key, natsort_keygen, natsorted, ns from natsort.compat.locale import get_strxfrm, null_string_locale from natsort.ns_enum import NSType from natsort.utils import BytesTransform, FinalTransform -from pytest_mock import MockerFixture @pytest.fixture @@ -44,18 +44,19 @@ def test_natsort_keygen_with_invalid_alg_input_raises_value_error() -> None: @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [(ns.DEFAULT, ("a-", 5, ".", 34, "e", 1)), (ns.FLOAT | ns.SIGNED, ("a", -50.34))], ) def test_natsort_keygen_returns_natsort_key_that_parses_input( - alg: NSType, expected: Tuple[Union[str, int, float], ...] + alg: NSType, + expected: Tuple[Union[str, int, float], ...], ) -> None: ns_key = natsort_keygen(alg=alg) assert ns_key("a-5.034e1") == expected @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ ( ns.DEFAULT, @@ -85,14 +86,16 @@ def test_natsort_keygen_returns_natsort_key_that_parses_input( ], ) def test_natsort_keygen_handles_arbitrary_input( - arbitrary_input: List[Union[str, float]], alg: NSType, expected: FinalTransform + arbitrary_input: List[Union[str, float]], + alg: NSType, + expected: FinalTransform, ) -> None: ns_key = natsort_keygen(alg=alg) assert ns_key(arbitrary_input) == expected @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, (b"6A-5.034e+1",)), (ns.IGNORECASE, (b"6a-5.034e+1",)), @@ -102,14 +105,16 @@ def test_natsort_keygen_handles_arbitrary_input( ], ) def test_natsort_keygen_handles_bytes_input( - bytes_input: bytes, alg: NSType, expected: BytesTransform + bytes_input: bytes, + alg: NSType, + expected: BytesTransform, ) -> None: ns_key = natsort_keygen(alg=alg) assert ns_key(bytes_input) == expected @pytest.mark.parametrize( - "alg, expected, is_dumb", + ("alg", "expected", "is_dumb"), [ ( ns.LOCALE, @@ -171,12 +176,15 @@ def test_natsort_keygen_with_locale( @pytest.mark.parametrize( - "alg, is_dumb", + ("alg", "is_dumb"), [(ns.LOCALE, False), (ns.LOCALE, True), (ns.LOCALE | ns.CAPITALFIRST, False)], ) @pytest.mark.usefixtures("with_locale_en_us") def test_natsort_keygen_with_locale_bytes( - mocker: MockerFixture, bytes_input: bytes, alg: NSType, is_dumb: bool + mocker: MockerFixture, + bytes_input: bytes, + alg: NSType, + is_dumb: bool, ) -> None: expected = (b"6A-5.034e+1",) mocker.patch("natsort.compat.locale.dumb_sort", return_value=is_dumb) diff --git a/tests/test_natsorted.py b/tests/test_natsorted.py index bd1cc39..662ecb4 100644 --- a/tests/test_natsorted.py +++ b/tests/test_natsorted.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """\ Here are a collection of examples of how this module can be used. See the README or the natsort homepage for more details. @@ -10,9 +9,9 @@ from typing import List, Tuple, Union import pytest + from natsort import as_utf8, natsorted, ns from natsort.ns_enum import NSType -from pytest import raises @pytest.fixture @@ -49,7 +48,8 @@ def test_natsorted_can_sort_as_signed_floats_with_exponents( [ns.NOEXP | ns.FLOAT | ns.UNSIGNED, ns.NOEXP | ns.FLOAT], ) def test_natsorted_can_sort_as_unsigned_and_ignore_exponents( - float_list: List[str], alg: NSType + float_list: List[str], + alg: NSType, ) -> None: expected = ["a5.034e1", "a50", "a50.300", "a50.31", "a50.4", "a51.", "a-50"] assert natsorted(float_list, alg=alg) == expected @@ -58,7 +58,8 @@ def test_natsorted_can_sort_as_unsigned_and_ignore_exponents( # DEFAULT and INT are all equivalent. @pytest.mark.parametrize("alg", [ns.DEFAULT, ns.INT]) def test_natsorted_can_sort_as_unsigned_ints_which_is_default( - float_list: List[str], alg: NSType + float_list: List[str], + alg: NSType, ) -> None: expected = ["a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51.", "a-50"] assert natsorted(float_list, alg=alg) == expected @@ -70,11 +71,12 @@ def test_natsorted_can_sort_as_signed_ints(float_list: List[str]) -> None: @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [(ns.UNSIGNED, ["a7", "a+2", "a-5"]), (ns.SIGNED, ["a-5", "a+2", "a7"])], ) def test_natsorted_can_sort_with_or_without_accounting_for_sign( - alg: NSType, expected: List[str] + alg: NSType, + expected: List[str], ) -> None: given = ["a-5", "a7", "a+2"] assert natsorted(given, alg=alg) == expected @@ -96,7 +98,7 @@ def test_natsorted_can_sorts_paths_same_as_strings() -> None: @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, ["0", 1.5, "2", 3, "Ä", "Z", "ä", "b"]), (ns.NUMAFTER, ["Ä", "Z", "ä", "b", "0", 1.5, "2", 3]), @@ -111,14 +113,15 @@ def test_natsorted_handles_mixed_types( @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, [float("nan"), None, float("-inf"), 5, "25", 1e40, float("inf")]), (ns.NANLAST, [float("-inf"), 5, "25", 1e40, float("inf"), None, float("nan")]), ], ) def test_natsorted_consistent_ordering_with_nan_and_friends( - alg: NSType, expected: List[Union[str, float, None, int]] + alg: NSType, + expected: List[Union[str, float, None, int]], ) -> None: sentinel = math.pi expected = [sentinel if x != x else x for x in expected] @@ -137,7 +140,7 @@ def test_natsorted_consistent_ordering_with_nan_and_friends( def test_natsorted_with_mixed_bytes_and_str_input_raises_type_error() -> None: - with raises(TypeError, match="bytes"): + with pytest.raises(TypeError, match="bytes"): natsorted(["ä", b"b"]) # ...unless you use as_utf (or some other decoder). @@ -145,7 +148,7 @@ def test_natsorted_with_mixed_bytes_and_str_input_raises_type_error() -> None: def test_natsorted_raises_type_error_for_non_iterable_input() -> None: - with raises(TypeError, match="'int' object is not iterable"): + with pytest.raises(TypeError, match="'int' object is not iterable"): natsorted(100) # type: ignore @@ -219,7 +222,7 @@ def test_natsorted_path_extensions_heuristic() -> None: @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, ["Apple", "Banana", "Corn", "apple", "banana", "corn"]), (ns.IGNORECASE, ["Apple", "apple", "Banana", "banana", "corn", "Corn"]), @@ -229,13 +232,15 @@ def test_natsorted_path_extensions_heuristic() -> None: ], ) def test_natsorted_supports_case_handling( - alg: NSType, expected: List[str], fruit_list: List[str] + alg: NSType, + expected: List[str], + fruit_list: List[str], ) -> None: assert natsorted(fruit_list, alg=alg) == expected @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, [("A5", "a6"), ("a3", "a1")]), (ns.LOWERCASEFIRST, [("a3", "a1"), ("A5", "a6")]), @@ -243,14 +248,15 @@ def test_natsorted_supports_case_handling( ], ) def test_natsorted_supports_nested_case_handling( - alg: NSType, expected: List[Tuple[str, str]] + alg: NSType, + expected: List[Tuple[str, str]], ) -> None: given = [("A5", "a6"), ("a3", "a1")] assert natsorted(given, alg=alg) == expected @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, ["apple", "Apple", "banana", "Banana", "corn", "Corn"]), (ns.CAPITALFIRST, ["Apple", "Banana", "Corn", "apple", "banana", "corn"]), @@ -260,7 +266,9 @@ def test_natsorted_supports_nested_case_handling( ) @pytest.mark.usefixtures("with_locale_en_us") def test_natsorted_can_sort_using_locale( - fruit_list: List[str], alg: NSType, expected: List[str] + fruit_list: List[str], + alg: NSType, + expected: List[str], ) -> None: assert natsorted(fruit_list, alg=ns.LOCALE | alg) == expected @@ -296,7 +304,7 @@ def test_natsorted_locale_bug_regression_test_140() -> None: @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, ["0", 1.5, "2", 3, "ä", "Ä", "b", "Z"]), (ns.NUMAFTER, ["ä", "Ä", "b", "Z", "0", 1.5, "2", 3]), @@ -319,14 +327,15 @@ def test_natsorted_handles_mixed_types_with_locale( @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.DEFAULT, ["73", "5039", "Banana", "apple", "corn", "~~~~~~"]), (ns.NUMAFTER, ["Banana", "apple", "corn", "~~~~~~", "73", "5039"]), ], ) def test_natsorted_sorts_an_odd_collection_of_strings( - alg: NSType, expected: List[str] + alg: NSType, + expected: List[str], ) -> None: given = ["apple", "Banana", "73", "5039", "corn", "~~~~~~"] assert natsorted(given, alg=alg) == expected diff --git a/tests/test_natsorted_convenience.py b/tests/test_natsorted_convenience.py index 81bdf5c..853b19e 100644 --- a/tests/test_natsorted_convenience.py +++ b/tests/test_natsorted_convenience.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """\ Here are a collection of examples of how this module can be used. See the README or the natsort homepage for more details. @@ -8,6 +7,7 @@ from typing import List import pytest + from natsort import ( as_ascii, as_utf8, diff --git a/tests/test_ns_enum.py b/tests/test_ns_enum.py index c950812..0e7bf14 100644 --- a/tests/test_ns_enum.py +++ b/tests/test_ns_enum.py @@ -1,9 +1,10 @@ import pytest + from natsort import ns @pytest.mark.parametrize( - "given, expected", + ("given", "expected"), [ ("FLOAT", 0x0001), ("SIGNED", 0x0002), diff --git a/tests/test_os_sorted.py b/tests/test_os_sorted.py index 6ec7727..336a8b8 100644 --- a/tests/test_os_sorted.py +++ b/tests/test_os_sorted.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- """ Testing for the OS sorting """ + import platform -import natsort import pytest +import natsort + try: import icu # noqa: F401 except ImportError: diff --git a/tests/test_parse_bytes_function.py b/tests/test_parse_bytes_function.py index 318c4aa..5de4caf 100644 --- a/tests/test_parse_bytes_function.py +++ b/tests/test_parse_bytes_function.py @@ -1,15 +1,15 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" import pytest from hypothesis import given from hypothesis.strategies import binary + from natsort.ns_enum import NSType, ns from natsort.utils import BytesTransformer, parse_bytes_factory @pytest.mark.parametrize( - "alg, example_func", + ("alg", "example_func"), [ (ns.DEFAULT, lambda x: (x,)), (ns.IGNORECASE, lambda x: (x.lower(),)), @@ -20,7 +20,9 @@ ) @given(x=binary()) def test_parse_bytest_factory_makes_function_that_returns_tuple( - x: bytes, alg: NSType, example_func: BytesTransformer + x: bytes, + alg: NSType, + example_func: BytesTransformer, ) -> None: parse_bytes_func = parse_bytes_factory(alg) assert parse_bytes_func(x) == example_func(x) diff --git a/tests/test_parse_number_function.py b/tests/test_parse_number_function.py index 24ee714..18c87e2 100644 --- a/tests/test_parse_number_function.py +++ b/tests/test_parse_number_function.py @@ -1,18 +1,18 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" -from typing import Optional, Tuple, Union +from typing import Optional, Tuple import pytest from hypothesis import given from hypothesis.strategies import floats, integers + from natsort.ns_enum import NSType, ns from natsort.utils import NumTransformer, parse_number_or_none_factory @pytest.mark.usefixtures("with_locale_en_us") @pytest.mark.parametrize( - "alg, example_func", + ("alg", "example_func"), [ (ns.DEFAULT, lambda x: ("", x)), (ns.PATH, lambda x: (("", x),)), @@ -22,14 +22,16 @@ ) @given(x=floats(allow_nan=False, allow_infinity=False) | integers()) def test_parse_number_factory_makes_function_that_returns_tuple( - x: Union[float, int], alg: NSType, example_func: NumTransformer + x: float, + alg: NSType, + example_func: NumTransformer, ) -> None: parse_number_func = parse_number_or_none_factory(alg, "", "xx") assert parse_number_func(x) == example_func(x) @pytest.mark.parametrize( - "alg, x, result", + ("alg", "x", "result"), [ (ns.DEFAULT, 57, ("", 57)), ( @@ -49,7 +51,9 @@ def test_parse_number_factory_makes_function_that_returns_tuple( ], ) def test_parse_number_factory_treats_nan_and_none_special( - alg: NSType, x: Optional[Union[float, int]], result: Tuple[str, Union[float, int]] + alg: NSType, + x: Optional[float], + result: Tuple[str, float], ) -> None: parse_number_func = parse_number_or_none_factory(alg, "", "xx") assert parse_number_func(x) == result diff --git a/tests/test_parse_string_function.py b/tests/test_parse_string_function.py index d2d33a4..aa2470a 100644 --- a/tests/test_parse_string_function.py +++ b/tests/test_parse_string_function.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" import unicodedata @@ -7,14 +6,17 @@ import pytest from hypothesis import given from hypothesis.strategies import floats, integers, lists, text + from natsort.compat.fastnumbers import try_float -from natsort.ns_enum import NSType, NS_DUMB, ns +from natsort.ns_enum import NS_DUMB, NSType, ns from natsort.utils import ( FinalTransform, - NumericalRegularExpressions as NumRegex, StrParser, + parse_string_factory, +) +from natsort.utils import ( + NumericalRegularExpressions as NumRegex, ) -from natsort.utils import parse_string_factory class CustomTuple(Tuple[Any, ...]): @@ -53,7 +55,7 @@ def parse_string_func_factory(alg: NSType) -> StrParser: @given(x=floats() | integers()) def test_parse_string_factory_raises_type_error_if_given_number( - x: Union[int, float] + x: float, ) -> None: parse_string_func = parse_string_func_factory(ns.DEFAULT) with pytest.raises(TypeError): @@ -62,7 +64,7 @@ def test_parse_string_factory_raises_type_error_if_given_number( # noinspection PyCallingNonCallable @pytest.mark.parametrize( - "alg, orig_func", + ("alg", "orig_func"), [ (ns.DEFAULT, lambda x: x.upper()), (ns.LOCALE, lambda x: x.upper()), @@ -71,12 +73,16 @@ def test_parse_string_factory_raises_type_error_if_given_number( ) @given( x=lists( - elements=floats(allow_nan=False) | text() | integers(), min_size=1, max_size=10 - ) + elements=floats(allow_nan=False) | text() | integers(), + min_size=1, + max_size=10, + ), ) @pytest.mark.usefixtures("with_locale_en_us") def test_parse_string_factory_invariance( - x: List[Union[float, str, int]], alg: NSType, orig_func: Callable[[str], str] + x: List[Union[float, str, int]], + alg: NSType, + orig_func: Callable[[str], str], ) -> None: parse_string_func = parse_string_func_factory(alg) # parse_string_factory is the high-level combination of several dedicated diff --git a/tests/test_regex.py b/tests/test_regex.py index 08314b5..5caf640 100644 --- a/tests/test_regex.py +++ b/tests/test_regex.py @@ -1,14 +1,13 @@ -# -*- coding: utf-8 -*- """These test the splitting regular expressions.""" from typing import List, Pattern import pytest + from natsort import ns, numeric_regex_chooser from natsort.ns_enum import NSType from natsort.utils import NumericalRegularExpressions as NumRegex - regex_names = { NumRegex.int_nosign(): "int_nosign", NumRegex.int_sign(): "int_sign", @@ -94,19 +93,21 @@ for given, values in regex_tests.items() for regex, expected in values.items() ] -labels = ["{}-{}".format(given, regex_names[regex]) for given, _, regex in regex_params] +labels = [f"{given}-{regex_names[regex]}" for given, _, regex in regex_params] -@pytest.mark.parametrize("x, expected, regex", regex_params, ids=labels) +@pytest.mark.parametrize(("x", "expected", "regex"), regex_params, ids=labels) def test_regex_splits_correctly( - x: str, expected: List[str], regex: Pattern[str] + x: str, + expected: List[str], + regex: Pattern[str], ) -> None: # noinspection PyUnresolvedReferences assert regex.split(x) == expected @pytest.mark.parametrize( - "given, expected", + ("given", "expected"), [ (ns.INT, NumRegex.int_nosign()), (ns.INT | ns.UNSIGNED, NumRegex.int_nosign()), diff --git a/tests/test_string_component_transform_factory.py b/tests/test_string_component_transform_factory.py index 03dea8c..a070902 100644 --- a/tests/test_string_component_transform_factory.py +++ b/tests/test_string_component_transform_factory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" from functools import partial @@ -7,9 +6,10 @@ import pytest from hypothesis import assume, example, given from hypothesis.strategies import floats, integers, text + from natsort.compat.fastnumbers import try_float, try_int from natsort.compat.locale import get_strxfrm -from natsort.ns_enum import NSType, NS_DUMB, ns +from natsort.ns_enum import NS_DUMB, NSType, ns from natsort.utils import groupletters, string_component_transform_factory # There are some unicode values that are known failures with the builtin locale @@ -44,7 +44,7 @@ def input_is_ok_with_locale(x: str) -> bool: @pytest.mark.parametrize( - "alg, example_func", + ("alg", "example_func"), [ (ns.INT, partial(try_int, map=True)), (ns.DEFAULT, partial(try_int, map=True)), @@ -55,13 +55,17 @@ def input_is_ok_with_locale(x: str) -> bool: ( ns.GROUPLETTERS | ns.LOCALE, partial( - try_int, map=True, on_fail=lambda x: get_strxfrm()(groupletters(x)) + try_int, + map=True, + on_fail=lambda x: get_strxfrm()(groupletters(x)), ), ), ( NS_DUMB | ns.LOCALE, partial( - try_int, map=True, on_fail=lambda x: get_strxfrm()(groupletters(x)) + try_int, + map=True, + on_fail=lambda x: get_strxfrm()(groupletters(x)), ), ), ( @@ -80,11 +84,13 @@ def input_is_ok_with_locale(x: str) -> bool: @given( x=integers() | floats() - | text().filter(bool).filter(no_bad_uni_chars).filter(no_null) + | text().filter(bool).filter(no_bad_uni_chars).filter(no_null), ) @pytest.mark.usefixtures("with_locale_en_us") def test_string_component_transform_factory( - x: Union[str, float, int], alg: NSType, example_func: Callable[[str], Any] + x: Union[str, float], + alg: NSType, + example_func: Callable[[str], Any], ) -> None: string_component_transform_func = string_component_transform_factory(alg) x = str(x) diff --git a/tests/test_unicode_numbers.py b/tests/test_unicode_numbers.py index be867ee..4f4a099 100644 --- a/tests/test_unicode_numbers.py +++ b/tests/test_unicode_numbers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """\ Test the Unicode numbers module. """ @@ -58,7 +57,7 @@ def test_missing_unicode_number_in_collection() -> None: break if a in "0123456789": continue - if unicodedata.numeric(a, None) is not None: + if unicodedata.numeric(a, None) is not None: # noqa: SIM102 if i not in set_numeric_hex: ok = False if not ok: diff --git a/tests/test_utils.py b/tests/test_utils.py index b140682..22da2e5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """These test the utils.py functions.""" import os @@ -11,6 +10,7 @@ import pytest from hypothesis import given from hypothesis.strategies import integers, lists, sampled_from, text + from natsort import utils from natsort.ns_enum import NSType, ns @@ -22,7 +22,7 @@ def test_do_decoding_decodes_bytes_string_to_unicode() -> None: @pytest.mark.parametrize( - "alg, expected", + ("alg", "expected"), [ (ns.I, utils.NumericalRegularExpressions.int_nosign()), (ns.I | ns.N, utils.NumericalRegularExpressions.int_nosign()), @@ -35,13 +35,14 @@ def test_do_decoding_decodes_bytes_string_to_unicode() -> None: ], ) def test_regex_chooser_returns_correct_regular_expression_object( - alg: NSType, expected: Pattern[str] + alg: NSType, + expected: Pattern[str], ) -> None: assert utils.regex_chooser(alg).pattern == expected.pattern @pytest.mark.parametrize( - "alg, value_or_alias", + ("alg", "value_or_alias"), [ # Defaults (ns.DEFAULT, 0), @@ -104,7 +105,7 @@ def test_groupletters_gives_letters_with_lowercase_letter_transform( x: str, ) -> None: assert utils.groupletters(x) == "".join( - chain.from_iterable([y.casefold(), y] for y in x) + chain.from_iterable([y.casefold(), y] for y in x), ) @@ -123,7 +124,7 @@ def test_sep_inserter_inserts_separator_string_between_two_numbers_example() -> @given(lists(elements=text().filter(bool) | integers(), min_size=3)) def test_sep_inserter_inserts_separator_between_two_numbers( - x: List[Union[str, int]] + x: List[Union[str, int]], ) -> None: # Rather than just replicating the results in a different algorithm, # validate that the "shape" of the output is as expected. @@ -156,7 +157,7 @@ def test_path_splitter_splits_path_string_by_sep(x: List[str]) -> None: @pytest.mark.parametrize( - "given, expected", + ("given", "expected"), [ ( "/this/is/a/path/file.x1.10.tar.gz", @@ -173,7 +174,8 @@ def test_path_splitter_splits_path_string_by_sep(x: List[str]) -> None: ], ) def test_path_splitter_splits_path_string_by_sep_and_removes_extension_example( - given: str, expected: Tuple[str, ...] + given: str, + expected: Tuple[str, ...], ) -> None: assert tuple(utils.path_splitter(given)) == tuple(expected) diff --git a/tox.ini b/tox.ini index e60b4b7..f8f4281 100644 --- a/tox.ini +++ b/tox.ini @@ -5,10 +5,9 @@ [tox] envlist = - flake8, mypy, py37, py38, py39, py310, py311, py312 + lint, mypy, py38, py39, py310, py311, py312 # Other valid environments are: # docs -# release # clean # bump @@ -36,19 +35,15 @@ commands = pytest --hypothesis-profile=slow-tests --tb=short --cov {envsitepackagesdir}/natsort --cov-report term-missing {posargs:} # Check code quality. -[testenv:flake8] +[testenv:lint] deps = - flake8 - flake8-import-order - flake8-bugbear - pep8-naming - check-manifest + ruff twine + build commands = - {envpython} setup.py sdist - pip wheel . -w dist - flake8 - check-manifest --ignore ".github*,*.md,.coveragerc" + ruff format --check + ruff check + {envpython} -m build twine check dist/* skip_install = true @@ -73,28 +68,18 @@ deps = sphinx sphinx_rtd_theme commands = - {envpython} setup.py build_sphinx -skip_install = true + sphinx-build docs build/sphinx/html # Bump version [testenv:bump] passenv = HOME deps = - bump2version + setuptools_scm commands = {envpython} dev/bump.py {posargs:} skip_install = true -# Release the code to PyPI -[testenv:release] -deps = - twine -commands = - {envpython} setup.py sdist --format=gztar bdist_wheel - twine upload --skip-existing dist/* -skip_install = true - # Clean up the working directory [testenv:clean] deps = @@ -104,7 +89,6 @@ skip_install = true # Get GitHub actions to run the correct tox environment [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310