feat: referentiel projets dynamique + validation nommage by design
Durcit la convention de nommage des projets (dérive constatée : 'Sliding Automation', 'code_versioning'... au lieu des formes canoniques). - trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label projet sur les notes de type=projet (source unique, pas de constante en dur). Note-projet CodeVersioning créée (manquait). - mcp_server.py : _valider_projet() branché dans les 6 tools de création (add_decision/history/backlog, new_conversation, create_entite, add_skill). Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au processus de création de projet). Ne verrouille pas si référentiel illisible. - lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace ET snake_case ; l'ancien 'contient un espace' ratait code_versioning). - Données : 79 notes ré-étiquetées vers les 3 formes canoniques. Quality by design : l'erreur de nommage devient impossible à l'écriture, le Lint n'est plus que le filet de sécurité.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from natsort.natsort import (
|
||||
NatsortKeyType,
|
||||
OSSortKeyType,
|
||||
as_ascii,
|
||||
as_utf8,
|
||||
decoder,
|
||||
humansorted,
|
||||
index_humansorted,
|
||||
index_natsorted,
|
||||
index_realsorted,
|
||||
natsort_key,
|
||||
natsort_keygen,
|
||||
natsorted,
|
||||
numeric_regex_chooser,
|
||||
order_by_index,
|
||||
os_sort_key,
|
||||
os_sort_keygen,
|
||||
os_sorted,
|
||||
realsorted,
|
||||
)
|
||||
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",
|
||||
"natsorted",
|
||||
"humansorted",
|
||||
"realsorted",
|
||||
"index_natsorted",
|
||||
"index_humansorted",
|
||||
"index_realsorted",
|
||||
"order_by_index",
|
||||
"decoder",
|
||||
"as_ascii",
|
||||
"as_utf8",
|
||||
"ns",
|
||||
"chain_functions",
|
||||
"numeric_regex_chooser",
|
||||
"os_sort_key",
|
||||
"os_sort_keygen",
|
||||
"os_sorted",
|
||||
"NatsortKeyType",
|
||||
"OSSortKeyType",
|
||||
"KeyType",
|
||||
"NatsortInType",
|
||||
"NatsortOutType",
|
||||
"NSType",
|
||||
]
|
||||
|
||||
# Add the ns keys to this namespace for convenience.
|
||||
globals().update({name: value for name, value in ns.__members__.items()})
|
||||
@@ -0,0 +1,363 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Callable, Iterable, List, Optional, Pattern, Tuple, Union, cast
|
||||
|
||||
import natsort
|
||||
from natsort.utils import regex_chooser
|
||||
|
||||
Num = Union[float, int]
|
||||
NumIter = Iterable[Num]
|
||||
NumPair = Tuple[Num, Num]
|
||||
NumPairIter = Iterable[NumPair]
|
||||
NumConverter = Callable[[str], Num]
|
||||
|
||||
|
||||
class TypedArgs(argparse.Namespace):
|
||||
paths: bool
|
||||
filter: Optional[List[NumPair]]
|
||||
reverse_filter: Optional[List[NumPair]]
|
||||
exclude: List[Num]
|
||||
reverse: bool
|
||||
number_type: str
|
||||
nosign: bool
|
||||
sign: bool
|
||||
noexp: bool
|
||||
locale: bool
|
||||
entries: List[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filter: Optional[List[NumPair]] = None,
|
||||
reverse_filter: Optional[List[NumPair]] = None,
|
||||
exclude: Optional[List[Num]] = None,
|
||||
paths: bool = False,
|
||||
reverse: bool = False,
|
||||
) -> None:
|
||||
"""Used by testing only"""
|
||||
self.filter = filter
|
||||
self.reverse_filter = reverse_filter
|
||||
self.exclude = [] if exclude is None else exclude
|
||||
self.paths = paths
|
||||
self.reverse = reverse
|
||||
self.number_type = "int"
|
||||
self.signed = False
|
||||
self.exp = True
|
||||
self.locale = False
|
||||
|
||||
|
||||
def main(*arguments: str) -> None:
|
||||
"""
|
||||
Performs a natural sort on entries given on the command-line.
|
||||
|
||||
Arguments are read from sys.argv.
|
||||
"""
|
||||
|
||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||
from textwrap import dedent
|
||||
|
||||
parser = ArgumentParser(
|
||||
description=dedent(cast(str, main.__doc__)),
|
||||
formatter_class=RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version="%(prog)s {}".format(natsort.__version__),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--paths",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Interpret the input as file paths. This is not "
|
||||
"strictly necessary to sort all file paths, but in cases "
|
||||
'where there are OS-generated file paths like "Folder/" '
|
||||
'and "Folder (1)/", this option is needed to make the '
|
||||
'paths sorted in the order you expect ("Folder/" before '
|
||||
'"Folder (1)/").',
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--filter",
|
||||
nargs=2,
|
||||
type=float,
|
||||
metavar=("LOW", "HIGH"),
|
||||
action="append",
|
||||
help="Used for keeping only the entries that have a number "
|
||||
"falling in the given range.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-F",
|
||||
"--reverse-filter",
|
||||
nargs=2,
|
||||
type=float,
|
||||
metavar=("LOW", "HIGH"),
|
||||
action="append",
|
||||
dest="reverse_filter",
|
||||
help="Used for excluding the entries that have a number "
|
||||
"falling in the given range.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--exclude",
|
||||
type=float,
|
||||
action="append",
|
||||
help="Used to exclude an entry that contains a specific number.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--reverse",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Returns in reversed order.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--number-type",
|
||||
"--number_type",
|
||||
dest="number_type",
|
||||
choices=("int", "float", "real", "f", "i", "r"),
|
||||
default="int",
|
||||
help='Choose the type of number to search for. "float" will search '
|
||||
'for floating-point numbers. "int" will only search for '
|
||||
'integers. "real" is a shortcut for "float" with --sign. '
|
||||
'"i" is a synonym for "int", "f" is a synonym for '
|
||||
'"float", and "r" is a synonym for "real".'
|
||||
"The default is %(default)s.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nosign",
|
||||
default=False,
|
||||
action="store_false",
|
||||
dest="signed",
|
||||
help='Do not consider "+" or "-" as part of a number, i.e. do not '
|
||||
"take sign into consideration. This is the default.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--sign",
|
||||
default=False,
|
||||
action="store_true",
|
||||
dest="signed",
|
||||
help='Consider "+" or "-" as part of a number, i.e. '
|
||||
"take sign into consideration. The default is unsigned.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noexp",
|
||||
default=True,
|
||||
action="store_false",
|
||||
dest="exp",
|
||||
help="Do not consider an exponential as part of a number, i.e. 1e4, "
|
||||
'would be considered as 1, "e", and 4, not as 10000. This only '
|
||||
"effects the --number-type=float.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--locale",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Causes natsort to use locale-aware sorting. You will get the "
|
||||
"best results if you install PyICU.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"entries",
|
||||
nargs="*",
|
||||
default=sys.stdin,
|
||||
help="The entries to sort. Taken from stdin if nothing is given on "
|
||||
"the command line.",
|
||||
)
|
||||
args = parser.parse_args(arguments or None, namespace=TypedArgs())
|
||||
|
||||
# Make sure the filter range is given properly. Does nothing if no filter
|
||||
args.filter = check_filters(args.filter)
|
||||
args.reverse_filter = check_filters(args.reverse_filter)
|
||||
|
||||
# Remove trailing whitespace from all the entries
|
||||
entries = [e.strip() for e in args.entries]
|
||||
|
||||
# Sort by directory then by file within directory and print.
|
||||
sort_and_print_entries(entries, args)
|
||||
|
||||
|
||||
def range_check(low: Num, high: Num) -> NumPair:
|
||||
"""
|
||||
Verify that that given range has a low lower than the high.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
low : {float, int}
|
||||
high : {float, int}
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple : low, high
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Low is greater than or equal to high.
|
||||
|
||||
"""
|
||||
if low >= high:
|
||||
raise ValueError("low >= high")
|
||||
else:
|
||||
return low, high
|
||||
|
||||
|
||||
def check_filters(filters: Optional[NumPairIter]) -> Optional[List[NumPair]]:
|
||||
"""
|
||||
Execute range_check for every element of an iterable.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filters : iterable
|
||||
The collection of filters to check. Each element
|
||||
must be a two-element tuple of floats or ints.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The input as-is, or None if it evaluates to False.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Low is greater than or equal to high for any element.
|
||||
|
||||
"""
|
||||
if not filters:
|
||||
return None
|
||||
try:
|
||||
return [range_check(f[0], f[1]) for f in filters]
|
||||
except ValueError as err:
|
||||
raise ValueError("Error in --filter: " + str(err))
|
||||
|
||||
|
||||
def keep_entry_range(
|
||||
entry: str,
|
||||
lows: NumIter,
|
||||
highs: NumIter,
|
||||
converter: NumConverter,
|
||||
regex: Pattern[str],
|
||||
) -> bool:
|
||||
"""
|
||||
Check if an entry falls into a desired range.
|
||||
|
||||
Every number in the entry will be extracted using *regex*,
|
||||
if any are within a given low to high range the entry will
|
||||
be kept.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
entry : str
|
||||
lows : iterable
|
||||
Collection of low values against which to compare the entry.
|
||||
highs : iterable
|
||||
Collection of high values against which to compare the entry.
|
||||
converter : callable
|
||||
Function to convert a string to a number.
|
||||
regex : regex object
|
||||
Regular expression to locate numbers in a string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
True if the entry should be kept, False otherwise.
|
||||
|
||||
"""
|
||||
return any(
|
||||
low <= converter(num) <= high
|
||||
for num in regex.findall(entry)
|
||||
for low, high in zip(lows, highs)
|
||||
)
|
||||
|
||||
|
||||
def keep_entry_value(
|
||||
entry: str, values: NumIter, converter: NumConverter, regex: Pattern[str]
|
||||
) -> bool:
|
||||
"""
|
||||
Check if an entry does not match a given value.
|
||||
|
||||
Every number in the entry will be extracted using *regex*,
|
||||
if any match a given value the entry will not be kept.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
entry : str
|
||||
values : iterable
|
||||
Collection of values against which to compare the entry.
|
||||
converter : callable
|
||||
Function to convert a string to a number.
|
||||
regex : regex object
|
||||
Regular expression to locate numbers in a string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
True if the entry should be kept, False otherwise.
|
||||
|
||||
"""
|
||||
return not any(converter(num) in values for num in regex.findall(entry))
|
||||
|
||||
|
||||
def sort_and_print_entries(entries: List[str], args: TypedArgs) -> None:
|
||||
"""Sort the entries, applying the filters first if necessary."""
|
||||
|
||||
# Extract the proper number type.
|
||||
is_float = args.number_type in ("float", "real", "f", "r")
|
||||
signed = args.signed or args.number_type in ("real", "r")
|
||||
alg: int = (
|
||||
natsort.ns.FLOAT * is_float
|
||||
| natsort.ns.SIGNED * signed
|
||||
| natsort.ns.NOEXP * (not args.exp)
|
||||
| natsort.ns.PATH * args.paths
|
||||
| natsort.ns.LOCALE * args.locale
|
||||
)
|
||||
|
||||
# Pre-remove entries that don't pass the filtering criteria
|
||||
# Make sure we use the same searching algorithm for filtering
|
||||
# as for sorting.
|
||||
do_filter = args.filter is not None or args.reverse_filter is not None
|
||||
if do_filter or args.exclude:
|
||||
inp_options = (
|
||||
natsort.ns.FLOAT * is_float
|
||||
| natsort.ns.SIGNED * signed
|
||||
| natsort.ns.NOEXP * (not args.exp)
|
||||
)
|
||||
regex = regex_chooser(inp_options)
|
||||
if args.filter is not None:
|
||||
lows, highs = ([f[0] for f in args.filter], [f[1] for f in args.filter])
|
||||
entries = [
|
||||
entry
|
||||
for entry in entries
|
||||
if keep_entry_range(entry, lows, highs, float, regex)
|
||||
]
|
||||
if args.reverse_filter is not None:
|
||||
lows, highs = (
|
||||
[f[0] for f in args.reverse_filter],
|
||||
[f[1] for f in args.reverse_filter],
|
||||
)
|
||||
entries = [
|
||||
entry
|
||||
for entry in entries
|
||||
if not keep_entry_range(entry, lows, highs, float, regex)
|
||||
]
|
||||
if args.exclude:
|
||||
exclude = set(args.exclude)
|
||||
entries = [
|
||||
entry
|
||||
for entry in entries
|
||||
if keep_entry_value(entry, exclude, float, regex)
|
||||
]
|
||||
|
||||
# Print off the sorted results
|
||||
for entry in natsort.natsorted(entries, reverse=args.reverse, alg=alg):
|
||||
print(entry)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except ValueError as a:
|
||||
sys.exit(str(a))
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- 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
|
||||
|
||||
from natsort.unicode_numbers import decimal_chars
|
||||
|
||||
_NAN_INF = [
|
||||
"INF",
|
||||
"INf",
|
||||
"Inf",
|
||||
"inF",
|
||||
"iNF",
|
||||
"InF",
|
||||
"inf",
|
||||
"iNf",
|
||||
"NAN",
|
||||
"nan",
|
||||
"NaN",
|
||||
"nAn",
|
||||
"naN",
|
||||
"NAn",
|
||||
"nAN",
|
||||
"Nan",
|
||||
]
|
||||
_NAN_INF.extend(["+" + x[:2] for x in _NAN_INF] + ["-" + x[:2] for x in _NAN_INF])
|
||||
NAN_INF = frozenset(_NAN_INF)
|
||||
ASCII_NUMS = "0123456789+-"
|
||||
POTENTIAL_FIRST_CHAR = frozenset(decimal_chars + list(ASCII_NUMS + "."))
|
||||
|
||||
StrOrFloat = Union[str, float]
|
||||
StrOrInt = Union[str, int]
|
||||
|
||||
|
||||
def fast_float(
|
||||
x: str,
|
||||
key: Callable[[str], str] = lambda x: x,
|
||||
nan: float = float("inf"),
|
||||
_uni: Callable[[str, StrOrFloat], StrOrFloat] = unicodedata.numeric,
|
||||
_nan_inf: FrozenSet[str] = NAN_INF,
|
||||
_first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
|
||||
) -> StrOrFloat:
|
||||
"""
|
||||
Convert a string to a float quickly, return input as-is if not possible.
|
||||
|
||||
We don't need to accept all input that the real fast_int accepts because
|
||||
natsort is controlling what is passed to this function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : str
|
||||
String to attempt to convert to a float.
|
||||
key : callable
|
||||
Single-argument function to apply to *x* if conversion fails.
|
||||
nan : float
|
||||
Value to return instead of NaN if NaN would be returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
*str* or *float*
|
||||
|
||||
"""
|
||||
if x[0] in _first_char or x.lstrip()[:3] in _nan_inf:
|
||||
try:
|
||||
ret = float(x)
|
||||
return nan if ret != ret else ret
|
||||
except ValueError:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
else:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
|
||||
|
||||
def fast_int(
|
||||
x: str,
|
||||
key: Callable[[str], str] = lambda x: x,
|
||||
_uni: Callable[[str, StrOrInt], StrOrInt] = unicodedata.digit,
|
||||
_first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
|
||||
) -> StrOrInt:
|
||||
"""
|
||||
Convert a string to a int quickly, return input as-is if not possible.
|
||||
|
||||
We don't need to accept all input that the real fast_int accepts because
|
||||
natsort is controlling what is passed to this function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : str
|
||||
String to attempt to convert to an int.
|
||||
key : callable
|
||||
Single-argument function to apply to *x* if conversion fails.
|
||||
|
||||
Returns
|
||||
-------
|
||||
*str* or *int*
|
||||
|
||||
"""
|
||||
if x[0] in _first_char:
|
||||
try:
|
||||
return int(x)
|
||||
except ValueError:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
else:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- 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
|
||||
|
||||
StrOrFloat = Union[str, float]
|
||||
StrOrInt = Union[str, int]
|
||||
|
||||
__all__ = ["try_float", "try_int"]
|
||||
|
||||
|
||||
def is_supported_fastnumbers(
|
||||
fastnumbers_version: str, minimum: Tuple[int, int, int] = (2, 0, 0)
|
||||
) -> bool:
|
||||
match = re.match(
|
||||
r"^(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?$",
|
||||
fastnumbers_version,
|
||||
flags=re.ASCII,
|
||||
)
|
||||
|
||||
if not match:
|
||||
raise ValueError(
|
||||
"Invalid fastnumbers version number '{}'".format(fastnumbers_version)
|
||||
)
|
||||
|
||||
(major, minor, patch) = match.group(1, 2, 4)
|
||||
|
||||
return (int(major), int(minor), int(patch)) >= minimum
|
||||
|
||||
|
||||
# If the user has fastnumbers installed, they will get great speed
|
||||
# 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
|
||||
|
||||
# Require >= version 2.0.0.
|
||||
if not is_supported_fastnumbers(fn_ver):
|
||||
raise ImportError # pragma: no cover
|
||||
|
||||
# For versions of fastnumbers with mapping capability, use that
|
||||
if is_supported_fastnumbers(fn_ver, (5, 0, 0)):
|
||||
del fast_float, fast_int
|
||||
from fastnumbers import try_float, try_int
|
||||
except ImportError:
|
||||
from natsort.compat.fake_fastnumbers import fast_float, fast_int # type: ignore
|
||||
|
||||
# Re-map the old-or-compatibility functions fast_float/fast_int to the
|
||||
# newer API of try_float/try_int. If we already imported try_float/try_int
|
||||
# then there is nothing to do.
|
||||
if "try_float" not in globals():
|
||||
|
||||
def try_float( # type: ignore[no-redef] # noqa: F811
|
||||
x: Iterable[str],
|
||||
map: bool,
|
||||
nan: float = float("inf"),
|
||||
on_fail: Callable[[str], str] = lambda x: x,
|
||||
) -> Iterator[StrOrFloat]:
|
||||
assert map is True
|
||||
return (fast_float(y, nan=nan, key=on_fail) for y in x)
|
||||
|
||||
|
||||
if "try_int" not in globals():
|
||||
|
||||
def try_int( # type: ignore[no-redef] # noqa: F811
|
||||
x: Iterable[str],
|
||||
map: bool,
|
||||
on_fail: Callable[[str], str] = lambda x: x,
|
||||
) -> Iterator[StrOrInt]:
|
||||
assert map is True
|
||||
return (fast_int(y, key=on_fail) for y in x)
|
||||
@@ -0,0 +1,122 @@
|
||||
# -*- 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
|
||||
|
||||
StrOrBytes = Union[str, bytes]
|
||||
TrxfmFunc = Callable[[str], StrOrBytes]
|
||||
|
||||
# This string should be sorted after any other byte string because
|
||||
# it contains the max unicode character repeated 20 times.
|
||||
# You would need some odd data to come after that.
|
||||
null_string = ""
|
||||
null_string_max = chr(sys.maxunicode) * 20
|
||||
|
||||
# This variable could be str or bytes depending on the locale library
|
||||
# being used, so give the type-checker this information.
|
||||
null_string_locale: StrOrBytes
|
||||
null_string_locale_max: StrOrBytes
|
||||
|
||||
# strxfrm can be buggy (especially on OSX and *possibly* some other
|
||||
# BSD-based systems), so prefer icu if available.
|
||||
try: # noqa: C901
|
||||
import icu
|
||||
from locale import getlocale
|
||||
|
||||
null_string_locale = b""
|
||||
|
||||
# This string should in theory be sorted after any other byte
|
||||
# string because it contains the max byte char repeated many times.
|
||||
# You would need some odd data to come after that.
|
||||
null_string_locale_max = b"x7f" * 50
|
||||
|
||||
def dumb_sort() -> bool:
|
||||
return False
|
||||
|
||||
# If using icu, get the locale from the current global locale,
|
||||
def get_icu_locale() -> str:
|
||||
language_code, encoding = getlocale()
|
||||
if language_code is None or encoding is None: # pragma: no cover
|
||||
return icu.Locale()
|
||||
return icu.Locale(f"{language_code}.{encoding}")
|
||||
|
||||
def get_strxfrm() -> TrxfmFunc:
|
||||
return icu.Collator.createInstance(get_icu_locale()).getSortKey
|
||||
|
||||
def get_thousands_sep() -> str:
|
||||
sep = icu.DecimalFormatSymbols.kGroupingSeparatorSymbol
|
||||
return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
|
||||
|
||||
def get_decimal_point() -> str:
|
||||
sep = icu.DecimalFormatSymbols.kDecimalSeparatorSymbol
|
||||
return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
|
||||
|
||||
except ImportError:
|
||||
import locale
|
||||
from locale import strxfrm
|
||||
|
||||
null_string_locale = null_string
|
||||
null_string_locale_max = null_string_max
|
||||
|
||||
# On some systems, locale is broken and does not sort in the expected
|
||||
# order. We will try to detect this and compensate.
|
||||
def dumb_sort() -> bool:
|
||||
return strxfrm("A") < strxfrm("a")
|
||||
|
||||
def get_strxfrm() -> TrxfmFunc:
|
||||
return strxfrm
|
||||
|
||||
def get_thousands_sep() -> str:
|
||||
sep = cast(str, locale.localeconv()["thousands_sep"])
|
||||
# If this locale library is broken, some of the thousands separator
|
||||
# characters are incorrectly blank. Here is a lookup table of the
|
||||
# corrections I am aware of.
|
||||
if dumb_sort():
|
||||
language_code, encoding = locale.getlocale()
|
||||
if language_code is None or encoding is None:
|
||||
# No locale loaded, default to ','
|
||||
return ","
|
||||
loc = f"{language_code}.{encoding}"
|
||||
return {
|
||||
"de_DE.ISO8859-15": ".",
|
||||
"es_ES.ISO8859-1": ".",
|
||||
"de_AT.ISO8859-1": ".",
|
||||
"de_at": "\xa0",
|
||||
"nl_NL.UTF-8": ".",
|
||||
"es_es": ".",
|
||||
"fr_CH.ISO8859-15": "\xa0",
|
||||
"fr_CA.ISO8859-1": "\xa0",
|
||||
"de_CH.ISO8859-1": ".",
|
||||
"fr_FR.ISO8859-15": "\xa0",
|
||||
"nl_NL.ISO8859-1": ".",
|
||||
"ca_ES.UTF-8": ".",
|
||||
"nl_NL.ISO8859-15": ".",
|
||||
"de_ch": "'",
|
||||
"ca_es": ".",
|
||||
"de_AT.ISO8859-15": ".",
|
||||
"ca_ES.ISO8859-1": ".",
|
||||
"de_AT.UTF-8": ".",
|
||||
"es_ES.UTF-8": ".",
|
||||
"fr_fr": "\xa0",
|
||||
"es_ES.ISO8859-15": ".",
|
||||
"de_DE.ISO8859-1": ".",
|
||||
"nl_nl": ".",
|
||||
"fr_ch": "\xa0",
|
||||
"fr_ca": "\xa0",
|
||||
"de_DE.UTF-8": ".",
|
||||
"ca_ES.ISO8859-15": ".",
|
||||
"de_CH.ISO8859-15": ".",
|
||||
"fr_FR.ISO8859-1": "\xa0",
|
||||
"fr_CH.ISO8859-1": "\xa0",
|
||||
"de_de": ".",
|
||||
"fr_FR.UTF-8": "\xa0",
|
||||
"fr_CA.ISO8859-15": "\xa0",
|
||||
}.get(loc, sep)
|
||||
else:
|
||||
return sep
|
||||
|
||||
def get_decimal_point() -> str:
|
||||
return cast(str, locale.localeconv()["decimal_point"])
|
||||
@@ -0,0 +1,839 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Along with ns_enum.py, this module contains all of the
|
||||
natsort public API.
|
||||
|
||||
The majority of the "work" is defined in utils.py.
|
||||
"""
|
||||
|
||||
import platform
|
||||
from functools import partial
|
||||
from operator import itemgetter
|
||||
from pathlib import PurePath
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
import natsort.compat.locale
|
||||
from natsort import utils
|
||||
from natsort.ns_enum import NSType, NS_DUMB, ns
|
||||
from natsort.utils import NatsortInType, NatsortOutType
|
||||
|
||||
# Common input and output types
|
||||
T = TypeVar("T")
|
||||
NatsortInTypeT = TypeVar("NatsortInTypeT", bound=NatsortInType)
|
||||
|
||||
# The type that natsort_key returns
|
||||
NatsortKeyType = Callable[[NatsortInType], NatsortOutType]
|
||||
|
||||
# Types for os_sorted
|
||||
OSSortKeyType = Callable[[NatsortInType], NatsortOutType]
|
||||
|
||||
|
||||
def decoder(encoding: str) -> Callable[[Any], Any]:
|
||||
"""
|
||||
Return a function that can be used to decode bytes to unicode.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
encoding : str
|
||||
The codec to use for decoding. This must be a valid unicode codec.
|
||||
|
||||
Returns
|
||||
-------
|
||||
decode_function
|
||||
A function that takes a single argument and attempts to decode
|
||||
it using the supplied codec. Any `UnicodeErrors` are raised.
|
||||
If the argument was not of `bytes` type, it is simply returned
|
||||
as-is.
|
||||
|
||||
See Also
|
||||
--------
|
||||
as_ascii
|
||||
as_utf8
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> 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']
|
||||
True
|
||||
>>> # On Python 3, without decoder this would raise a TypeError.
|
||||
>>> natsorted([b'a10', 'a2'], key=decoder('utf8')) == ['a2', b'a10']
|
||||
True
|
||||
|
||||
"""
|
||||
return partial(utils.do_decoding, encoding=encoding)
|
||||
|
||||
|
||||
def as_ascii(s: Any) -> Any:
|
||||
"""
|
||||
Function to decode an input with the ASCII codec, or return as-is.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : object
|
||||
|
||||
Returns
|
||||
-------
|
||||
output
|
||||
If the input was of type `bytes`, the return value is a `str` decoded
|
||||
with the ASCII codec. Otherwise, the return value is identically the
|
||||
input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
decoder
|
||||
|
||||
"""
|
||||
return utils.do_decoding(s, "ascii")
|
||||
|
||||
|
||||
def as_utf8(s: Any) -> Any:
|
||||
"""
|
||||
Function to decode an input with the UTF-8 codec, or return as-is.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : object
|
||||
|
||||
Returns
|
||||
-------
|
||||
output
|
||||
If the input was of type `bytes`, the return value is a `str` decoded
|
||||
with the UTF-8 codec. Otherwise, the return value is identically the
|
||||
input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
decoder
|
||||
|
||||
"""
|
||||
return utils.do_decoding(s, "utf-8")
|
||||
|
||||
|
||||
def natsort_keygen(
|
||||
key: Optional[Callable[[Any], NatsortInType]] = None, alg: NSType = ns.DEFAULT
|
||||
) -> Callable[[Any], NatsortOutType]:
|
||||
"""
|
||||
Generate a key to sort strings and numbers naturally.
|
||||
|
||||
This key is designed for use as the `key` argument to
|
||||
functions such as the `sorted` builtin.
|
||||
|
||||
The user may customize the generated function with the
|
||||
arguments to `natsort_keygen`, including an optional
|
||||
`key` function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : callable, optional
|
||||
A key used to manipulate the input value before parsing for
|
||||
numbers. It is **not** applied recursively.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
alg : ns enum, optional
|
||||
This option is used to control which algorithm `natsort`
|
||||
uses when sorting. For details into these options, please see
|
||||
the :class:`ns` class documentation. The default is `ns.INT`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : function
|
||||
A function that parses input for natural sorting that is
|
||||
suitable for passing as the `key` argument to functions
|
||||
such as `sorted`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsorted
|
||||
natsort_key
|
||||
|
||||
Examples
|
||||
--------
|
||||
`natsort_keygen` is a convenient way to create a custom key
|
||||
to sort lists in-place (for example).::
|
||||
|
||||
>>> a = ['num5.10', 'num-3', 'num5.3', 'num2']
|
||||
>>> a.sort(key=natsort_keygen(alg=ns.REAL))
|
||||
>>> a
|
||||
['num-3', 'num2', 'num5.10', 'num5.3']
|
||||
|
||||
"""
|
||||
try:
|
||||
ns.DEFAULT | alg
|
||||
except TypeError:
|
||||
msg = "natsort_keygen: 'alg' argument must be from the enum 'ns'"
|
||||
raise ValueError(msg + ", got {}".format(str(alg)))
|
||||
|
||||
# Add the NS_DUMB option if the locale library is broken.
|
||||
if alg & ns.LOCALEALPHA and natsort.compat.locale.dumb_sort():
|
||||
alg |= NS_DUMB
|
||||
|
||||
# Set some variables that will be passed to the factory functions
|
||||
if alg & ns.NUMAFTER:
|
||||
if alg & ns.LOCALEALPHA:
|
||||
sep = natsort.compat.locale.null_string_locale_max
|
||||
else:
|
||||
sep = natsort.compat.locale.null_string_max
|
||||
pre_sep = natsort.compat.locale.null_string_max
|
||||
else:
|
||||
if alg & ns.LOCALEALPHA:
|
||||
sep = natsort.compat.locale.null_string_locale
|
||||
else:
|
||||
sep = natsort.compat.locale.null_string
|
||||
pre_sep = natsort.compat.locale.null_string
|
||||
regex = utils.regex_chooser(alg)
|
||||
|
||||
# Create the functions that will be used to split strings.
|
||||
input_transform = utils.input_string_transform_factory(alg)
|
||||
component_transform = utils.string_component_transform_factory(alg)
|
||||
final_transform = utils.final_data_transform_factory(alg, sep, pre_sep)
|
||||
|
||||
# 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
|
||||
)
|
||||
if alg & ns.PATH:
|
||||
string_func = utils.parse_path_factory(string_func)
|
||||
bytes_func = utils.parse_bytes_factory(alg)
|
||||
num_func = utils.parse_number_or_none_factory(alg, sep, pre_sep)
|
||||
|
||||
# Return the natsort key with the parsing path pre-chosen.
|
||||
return partial(
|
||||
utils.natsort_key,
|
||||
key=key,
|
||||
string_func=string_func,
|
||||
bytes_func=bytes_func,
|
||||
num_func=num_func,
|
||||
)
|
||||
|
||||
|
||||
# Exposed for simplicity if one needs the default natsort key.
|
||||
natsort_key = natsort_keygen()
|
||||
natsort_key.__doc__ = """\
|
||||
natsort_key(val)
|
||||
The default natural sorting key.
|
||||
|
||||
This is the output of :func:`natsort_keygen` with default values.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsort_keygen
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def natsorted(
|
||||
seq: Iterable[T],
|
||||
key: Optional[Callable[[T], NatsortInType]] = None,
|
||||
reverse: bool = False,
|
||||
alg: NSType = ns.DEFAULT,
|
||||
) -> List[T]:
|
||||
"""
|
||||
Sorts an iterable naturally.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq : iterable
|
||||
The input to sort.
|
||||
|
||||
key : callable, optional
|
||||
A key used to determine how to sort each element of the iterable.
|
||||
It is **not** applied recursively.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
reverse : {{True, False}}, optional
|
||||
Return the list in reversed sorted order. The default is
|
||||
`False`.
|
||||
|
||||
alg : ns enum, optional
|
||||
This option is used to control which algorithm `natsort`
|
||||
uses when sorting. For details into these options, please see
|
||||
the :class:`ns` class documentation. The default is `ns.INT`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: list
|
||||
The sorted input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsort_keygen : Generates the key that makes natural sorting possible.
|
||||
realsorted : A wrapper for ``natsorted(seq, alg=ns.REAL)``.
|
||||
humansorted : A wrapper for ``natsorted(seq, alg=ns.LOCALE)``.
|
||||
index_natsorted : Returns the sorted indexes from `natsorted`.
|
||||
os_sorted : Sort according to your operating system's rules.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Use `natsorted` just like the builtin `sorted`::
|
||||
|
||||
>>> a = ['num3', 'num5', 'num2']
|
||||
>>> natsorted(a)
|
||||
['num2', 'num3', 'num5']
|
||||
|
||||
"""
|
||||
if alg & ns.PRESORT:
|
||||
seq = sorted(seq, reverse=reverse, key=str)
|
||||
return sorted(seq, reverse=reverse, key=natsort_keygen(key, alg))
|
||||
|
||||
|
||||
def humansorted(
|
||||
seq: Iterable[T],
|
||||
key: Optional[Callable[[T], NatsortInType]] = None,
|
||||
reverse: bool = False,
|
||||
alg: NSType = ns.DEFAULT,
|
||||
) -> List[T]:
|
||||
"""
|
||||
Convenience function to properly sort non-numeric characters.
|
||||
|
||||
This is a wrapper around ``natsorted(seq, alg=ns.LOCALE)``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq : iterable
|
||||
The input to sort.
|
||||
|
||||
key : callable, optional
|
||||
A key used to determine how to sort each element of the sequence.
|
||||
It is **not** applied recursively.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
reverse : {{True, False}}, optional
|
||||
Return the list in reversed sorted order. The default is
|
||||
`False`.
|
||||
|
||||
alg : ns enum, optional
|
||||
This option is used to control which algorithm `natsort`
|
||||
uses when sorting. For details into these options, please see
|
||||
the :class:`ns` class documentation. The default is `ns.LOCALE`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : list
|
||||
The sorted input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
index_humansorted : Returns the sorted indexes from `humansorted`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Please read :ref:`locale_issues` before using `humansorted`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Use `humansorted` just like the builtin `sorted`::
|
||||
|
||||
>>> a = ['Apple', 'Banana', 'apple', 'banana']
|
||||
>>> natsorted(a)
|
||||
['Apple', 'Banana', 'apple', 'banana']
|
||||
>>> humansorted(a)
|
||||
['apple', 'Apple', 'banana', 'Banana']
|
||||
|
||||
"""
|
||||
return natsorted(seq, key, reverse, alg | ns.LOCALE)
|
||||
|
||||
|
||||
def realsorted(
|
||||
seq: Iterable[T],
|
||||
key: Optional[Callable[[T], NatsortInType]] = None,
|
||||
reverse: bool = False,
|
||||
alg: NSType = ns.DEFAULT,
|
||||
) -> List[T]:
|
||||
"""
|
||||
Convenience function to properly sort signed floats.
|
||||
|
||||
A signed float in a string could be "a-5.7". This is a wrapper around
|
||||
``natsorted(seq, alg=ns.REAL)``.
|
||||
|
||||
The behavior of :func:`realsorted` for `natsort` version >= 4.0.0
|
||||
was the default behavior of :func:`natsorted` for `natsort`
|
||||
version < 4.0.0.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq : iterable
|
||||
The input to sort.
|
||||
|
||||
key : callable, optional
|
||||
A key used to determine how to sort each element of the sequence.
|
||||
It is **not** applied recursively.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
reverse : {{True, False}}, optional
|
||||
Return the list in reversed sorted order. The default is
|
||||
`False`.
|
||||
|
||||
alg : ns enum, optional
|
||||
This option is used to control which algorithm `natsort`
|
||||
uses when sorting. For details into these options, please see
|
||||
the :class:`ns` class documentation. The default is `ns.REAL`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : list
|
||||
The sorted input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
index_realsorted : Returns the sorted indexes from `realsorted`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Use `realsorted` just like the builtin `sorted`::
|
||||
|
||||
>>> a = ['num5.10', 'num-3', 'num5.3', 'num2']
|
||||
>>> natsorted(a)
|
||||
['num2', 'num5.3', 'num5.10', 'num-3']
|
||||
>>> realsorted(a)
|
||||
['num-3', 'num2', 'num5.10', 'num5.3']
|
||||
|
||||
"""
|
||||
return natsorted(seq, key, reverse, alg | ns.REAL)
|
||||
|
||||
|
||||
def index_natsorted(
|
||||
seq: Iterable[T],
|
||||
key: Optional[Callable[[T], NatsortInType]] = None,
|
||||
reverse: bool = False,
|
||||
alg: NSType = ns.DEFAULT,
|
||||
) -> List[int]:
|
||||
"""
|
||||
Determine the list of the indexes used to sort the input sequence.
|
||||
|
||||
Sorts a sequence naturally, but returns a list of sorted the
|
||||
indexes and not the sorted list itself. This list of indexes
|
||||
can be used to sort multiple lists by the sorted order of the
|
||||
given sequence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq : iterable
|
||||
The input to sort.
|
||||
|
||||
key : callable, optional
|
||||
A key used to determine how to sort each element of the sequence.
|
||||
It is **not** applied recursively.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
reverse : {{True, False}}, optional
|
||||
Return the list in reversed sorted order. The default is
|
||||
`False`.
|
||||
|
||||
alg : ns enum, optional
|
||||
This option is used to control which algorithm `natsort`
|
||||
uses when sorting. For details into these options, please see
|
||||
the :class:`ns` class documentation. The default is `ns.INT`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : tuple
|
||||
The ordered indexes of the input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsorted
|
||||
order_by_index
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Use index_natsorted if you want to sort multiple lists by the
|
||||
sorted order of one list::
|
||||
|
||||
>>> a = ['num3', 'num5', 'num2']
|
||||
>>> b = ['foo', 'bar', 'baz']
|
||||
>>> index = index_natsorted(a)
|
||||
>>> index
|
||||
[2, 0, 1]
|
||||
>>> # Sort both lists by the sort order of a
|
||||
>>> order_by_index(a, index)
|
||||
['num2', 'num3', 'num5']
|
||||
>>> order_by_index(b, index)
|
||||
['baz', 'foo', 'bar']
|
||||
|
||||
"""
|
||||
newkey: Callable[[Tuple[int, T]], NatsortInType]
|
||||
if key is None:
|
||||
newkey = itemgetter(1)
|
||||
else:
|
||||
|
||||
def newkey(x: Tuple[int, T]) -> NatsortInType:
|
||||
return cast(Callable[[T], NatsortInType], 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)]
|
||||
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))
|
||||
return [x for x, _ in index_seq_pair]
|
||||
|
||||
|
||||
def index_humansorted(
|
||||
seq: Iterable[T],
|
||||
key: Optional[Callable[[T], NatsortInType]] = None,
|
||||
reverse: bool = False,
|
||||
alg: NSType = ns.DEFAULT,
|
||||
) -> List[int]:
|
||||
"""
|
||||
This is a wrapper around ``index_natsorted(seq, alg=ns.LOCALE)``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq: iterable
|
||||
The input to sort.
|
||||
|
||||
key: callable, optional
|
||||
A key used to determine how to sort each element of the sequence.
|
||||
It is **not** applied recursively.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
reverse : {{True, False}}, optional
|
||||
Return the list in reversed sorted order. The default is
|
||||
`False`.
|
||||
|
||||
alg : ns enum, optional
|
||||
This option is used to control which algorithm `natsort`
|
||||
uses when sorting. For details into these options, please see
|
||||
the :class:`ns` class documentation. The default is `ns.LOCALE`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : tuple
|
||||
The ordered indexes of the input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
humansorted
|
||||
order_by_index
|
||||
|
||||
Notes
|
||||
-----
|
||||
Please read :ref:`locale_issues` before using `humansorted`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Use `index_humansorted` just like the builtin `sorted`::
|
||||
|
||||
>>> a = ['Apple', 'Banana', 'apple', 'banana']
|
||||
>>> index_humansorted(a)
|
||||
[2, 0, 3, 1]
|
||||
|
||||
"""
|
||||
return index_natsorted(seq, key, reverse, alg | ns.LOCALE)
|
||||
|
||||
|
||||
def index_realsorted(
|
||||
seq: Iterable[T],
|
||||
key: Optional[Callable[[T], NatsortInType]] = None,
|
||||
reverse: bool = False,
|
||||
alg: NSType = ns.DEFAULT,
|
||||
) -> List[int]:
|
||||
"""
|
||||
This is a wrapper around ``index_natsorted(seq, alg=ns.REAL)``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq: iterable
|
||||
The input to sort.
|
||||
|
||||
key: callable, optional
|
||||
A key used to determine how to sort each element of the sequence.
|
||||
It is **not** applied recursively.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
reverse : {{True, False}}, optional
|
||||
Return the list in reversed sorted order. The default is
|
||||
`False`.
|
||||
|
||||
alg : ns enum, optional
|
||||
This option is used to control which algorithm `natsort`
|
||||
uses when sorting. For details into these options, please see
|
||||
the :class:`ns` class documentation. The default is `ns.REAL`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : tuple
|
||||
The ordered indexes of the input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
realsorted
|
||||
order_by_index
|
||||
|
||||
Examples
|
||||
--------
|
||||
Use `index_realsorted` just like the builtin `sorted`::
|
||||
|
||||
>>> a = ['num5.10', 'num-3', 'num5.3', 'num2']
|
||||
>>> index_realsorted(a)
|
||||
[1, 3, 0, 2]
|
||||
|
||||
"""
|
||||
return index_natsorted(seq, key, reverse, alg | ns.REAL)
|
||||
|
||||
|
||||
def order_by_index(
|
||||
seq: Sequence[Any], index: Iterable[int], iter: bool = False
|
||||
) -> Iterable[Any]:
|
||||
"""
|
||||
Order a given sequence by an index sequence.
|
||||
|
||||
The output of `index_natsorted` is a
|
||||
sequence of integers (index) that correspond to how its input
|
||||
sequence **would** be sorted. The idea is that this index can
|
||||
be used to reorder multiple sequences by the sorted order of the
|
||||
first sequence. This function is a convenient wrapper to
|
||||
apply this ordering to a sequence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq : sequence
|
||||
The sequence to order.
|
||||
|
||||
index : iterable
|
||||
The iterable that indicates how to order `seq`.
|
||||
It should be the same length as `seq` and consist
|
||||
of integers only.
|
||||
|
||||
iter : {{True, False}}, optional
|
||||
If `True`, the ordered sequence is returned as a
|
||||
iterator; otherwise it is returned as a
|
||||
list. The default is `False`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : {{list, iterator}}
|
||||
The sequence ordered by `index`, as a `list` or as an
|
||||
iterator (depending on the value of `iter`).
|
||||
|
||||
See Also
|
||||
--------
|
||||
index_natsorted
|
||||
index_humansorted
|
||||
index_realsorted
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
`order_by_index` is a convenience function that helps you apply
|
||||
the result of `index_natsorted`::
|
||||
|
||||
>>> a = ['num3', 'num5', 'num2']
|
||||
>>> b = ['foo', 'bar', 'baz']
|
||||
>>> index = index_natsorted(a)
|
||||
>>> index
|
||||
[2, 0, 1]
|
||||
>>> # Sort both lists by the sort order of a
|
||||
>>> order_by_index(a, index)
|
||||
['num2', 'num3', 'num5']
|
||||
>>> order_by_index(b, index)
|
||||
['baz', 'foo', 'bar']
|
||||
|
||||
"""
|
||||
return (seq[i] for i in index) if iter else [seq[i] for i in index]
|
||||
|
||||
|
||||
def numeric_regex_chooser(alg: NSType) -> str:
|
||||
"""
|
||||
Select an appropriate regex for the type of number of interest.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Used to indicate the regular expression to select.
|
||||
|
||||
Returns
|
||||
-------
|
||||
regex : str
|
||||
Regular expression string that matches the desired number type.
|
||||
|
||||
"""
|
||||
# Remove the leading and trailing parens
|
||||
return utils.regex_chooser(alg).pattern[1:-1]
|
||||
|
||||
|
||||
def _split_apply(
|
||||
v: Any, key: Optional[Callable[[T], NatsortInType]] = None, treat_base: bool = True
|
||||
) -> Iterator[str]:
|
||||
if key is not None:
|
||||
v = key(v)
|
||||
if not isinstance(v, (str, PurePath)):
|
||||
v = str(v)
|
||||
return utils.path_splitter(v, treat_base=treat_base)
|
||||
|
||||
|
||||
# Choose the implementation based on the host OS
|
||||
if platform.system() == "Windows":
|
||||
from ctypes import wintypes, windll # type: ignore
|
||||
from functools import cmp_to_key
|
||||
|
||||
_windows_sort_cmp = windll.Shlwapi.StrCmpLogicalW
|
||||
_windows_sort_cmp.argtypes = [wintypes.LPWSTR, wintypes.LPWSTR]
|
||||
_windows_sort_cmp.restype = wintypes.INT
|
||||
_winsort_key = cmp_to_key(_windows_sort_cmp)
|
||||
|
||||
def os_sort_keygen(
|
||||
key: Optional[Callable[[Any], NatsortInType]] = None
|
||||
) -> Callable[[Any], NatsortOutType]:
|
||||
return cast(
|
||||
Callable[[Any], NatsortOutType],
|
||||
lambda x: tuple(map(_winsort_key, _split_apply(x, key, treat_base=False))),
|
||||
)
|
||||
|
||||
else:
|
||||
# For UNIX-based platforms, ICU performs MUCH better than locale
|
||||
# at replicating the file explorer's sort order. We will use
|
||||
# ICU's ability to do basic natural sorting as it also better
|
||||
# replicates than what natsort does by default.
|
||||
#
|
||||
# However, if the user does not have ICU installed then fall back
|
||||
# on natsort's default handling for paths with locale turned on
|
||||
# which will give good results in most cases (e.g. when there aren't
|
||||
# a bunch of special characters).
|
||||
try:
|
||||
import icu
|
||||
|
||||
except ImportError:
|
||||
# No ICU installed
|
||||
def os_sort_keygen(
|
||||
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
|
||||
) -> Callable[[Any], NatsortOutType]:
|
||||
loc = natsort.compat.locale.get_icu_locale()
|
||||
collator = icu.Collator.createInstance(loc)
|
||||
collator.setAttribute(
|
||||
icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON
|
||||
)
|
||||
return lambda x: tuple(map(collator.getSortKey, _split_apply(x, key)))
|
||||
|
||||
|
||||
os_sort_keygen.__doc__ = """
|
||||
Generate a sorting key to replicate your file browser's sort order
|
||||
|
||||
See :func:`os_sorted` for description and caveats.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : function
|
||||
A function that parses input for OS path sorting that is
|
||||
suitable for passing as the `key` argument to functions
|
||||
such as `sorted`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
os_sort_key
|
||||
os_sorted
|
||||
|
||||
Notes
|
||||
-----
|
||||
On Windows, this will implicitly coerce all inputs to str before
|
||||
collating.
|
||||
|
||||
"""
|
||||
|
||||
os_sort_key = os_sort_keygen()
|
||||
os_sort_key.__doc__ = """
|
||||
os_sort_key(val)
|
||||
The default key to replicate your file browser's sort order
|
||||
|
||||
This is the output of :func:`os_sort_keygen` with default values.
|
||||
|
||||
See Also
|
||||
--------
|
||||
os_sort_keygen
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def os_sorted(
|
||||
seq: Iterable[T],
|
||||
key: Optional[Callable[[T], NatsortInType]] = None,
|
||||
reverse: bool = False,
|
||||
presort: bool = False,
|
||||
) -> List[T]:
|
||||
"""
|
||||
Sort elements in the same order as your operating system's file browser
|
||||
|
||||
.. warning::
|
||||
|
||||
The resulting function will generate results that will be
|
||||
different depending on your platform. This is intentional.
|
||||
|
||||
On Windows, this will sort with the same order as Windows Explorer.
|
||||
|
||||
On MacOS/Linux, you will get different results depending on whether
|
||||
or not you have :mod:`pyicu` installed.
|
||||
|
||||
- If you have :mod:`pyicu` installed, you will get results that are
|
||||
the same as (or very close to) the same order as your operating
|
||||
system's file browser.
|
||||
- If you do not have :mod:`pyicu` installed, then this will give
|
||||
the same results as if you used ``ns.LOCALE``, ``ns.PATH``,
|
||||
and ``ns.IGNORECASE`` with :func:`natsorted`. If you do not have
|
||||
special characters this will give correct results, but once
|
||||
special characters are added you should lower your expectations.
|
||||
|
||||
It is *strongly* recommended to have :mod:`pyicu` installed on
|
||||
MacOS/Linux if you want correct sort results.
|
||||
|
||||
It does *not* take into account if a path is a directory or a file
|
||||
when sorting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seq : iterable
|
||||
The input to sort. Each element must be of type str.
|
||||
|
||||
key : callable, optional
|
||||
A key used to determine how to sort each element of the sequence.
|
||||
It should accept a single argument and return a single value.
|
||||
|
||||
reverse : {{True, False}}, optional
|
||||
Return the list in reversed sorted order. The default is
|
||||
`False`.
|
||||
|
||||
presort : {{True, False}}, optional
|
||||
Equivalent to adding ``ns.PRESORT``, see :class:`ns` for
|
||||
documentation. The default is `False`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : list
|
||||
The sorted input.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsorted
|
||||
os_sort_keygen
|
||||
|
||||
Notes
|
||||
-----
|
||||
This will implicitly coerce all inputs to str before collating.
|
||||
|
||||
"""
|
||||
if presort:
|
||||
seq = sorted(seq, reverse=reverse, key=str)
|
||||
return sorted(seq, reverse=reverse, key=os_sort_keygen(key))
|
||||
@@ -0,0 +1,171 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module defines the "ns" enum for natsort is used to determine
|
||||
what algorithm natsort uses.
|
||||
"""
|
||||
|
||||
import enum
|
||||
import itertools
|
||||
import typing
|
||||
|
||||
|
||||
_counter = itertools.count(0)
|
||||
|
||||
|
||||
class ns(enum.IntEnum): # noqa: N801
|
||||
"""
|
||||
Enum to control the `natsort` algorithm.
|
||||
|
||||
This class acts like an enum to control the `natsort` algorithm. The
|
||||
user may select several options simultaneously by or'ing the options
|
||||
together. For example, to choose ``ns.INT``, ``ns.PATH``, and
|
||||
``ns.LOCALE``, you could do ``ns.INT | ns.LOCALE | ns.PATH``. Each
|
||||
function in the :mod:`natsort` package has an `alg` option that accepts
|
||||
this enum to allow fine control over how your input is sorted.
|
||||
|
||||
Each option has a shortened 1- or 2-letter form.
|
||||
|
||||
.. note:: Please read :ref:`locale_issues` before using ``ns.LOCALE``.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
INT, I (default)
|
||||
The default - parse numbers as integers.
|
||||
FLOAT, F
|
||||
Tell `natsort` to parse numbers as floats.
|
||||
UNSIGNED, U (default)
|
||||
Tell `natsort` to ignore any sign (i.e. "-" or "+") to the immediate
|
||||
left of a number. This is the default.
|
||||
SIGNED, S
|
||||
Tell `natsort` to take into account any sign (i.e. "-" or "+")
|
||||
to the immediate left of a number.
|
||||
REAL, R
|
||||
This is a shortcut for ``ns.FLOAT | ns.SIGNED``, which is useful
|
||||
when attempting to sort real numbers.
|
||||
NOEXP, N
|
||||
Tell `natsort` to not search for exponents as part of a float number.
|
||||
For example, with `NOEXP` the number "5.6E5" would be interpreted
|
||||
as `5.6`, `"E"`, and `5` instead of `560000`.
|
||||
NUMAFTER, NA
|
||||
Tell `natsort` to sort numbers after non-numbers. By default
|
||||
numbers will be ordered before non-numbers.
|
||||
PATH, P
|
||||
Tell `natsort` to interpret strings as filesystem paths, so they
|
||||
will be split according to the filesystem separator
|
||||
(i.e. '/' on UNIX, '\\' on Windows), as well as splitting on the
|
||||
file extension, if any. Without this, lists of file paths like
|
||||
``['Folder/', 'Folder (1)/', 'Folder (10)/']`` will not be
|
||||
sorted properly; 'Folder/' will be placed at the end, not at the
|
||||
front. It is the same as setting the old `as_path` option to
|
||||
`True`.
|
||||
COMPATIBILITYNORMALIZE, CN
|
||||
Use the "NFKD" unicode normalization form on input rather than the
|
||||
default "NFD". This will transform characters such as '⑦' into
|
||||
'7'. Please see https://stackoverflow.com/a/7934397/1399279,
|
||||
https://stackoverflow.com/a/7931547/1399279,
|
||||
and https://unicode.org/reports/tr15/ for full details into unicode
|
||||
normalization.
|
||||
LOCALE, L
|
||||
Tell `natsort` to be locale-aware when sorting. This includes both
|
||||
proper sorting of alphabetical characters as well as proper
|
||||
handling of locale-dependent decimal separators and thousands
|
||||
separators. This is a shortcut for
|
||||
``ns.LOCALEALPHA | ns.LOCALENUM``.
|
||||
Your sorting results will vary depending on your current locale.
|
||||
LOCALEALPHA, LA
|
||||
Tell `natsort` to be locale-aware when sorting, but only for
|
||||
alphabetical characters.
|
||||
LOCALENUM, LN
|
||||
Tell `natsort` to be locale-aware when sorting, but only for
|
||||
decimal separators and thousands separators.
|
||||
IGNORECASE, IC
|
||||
Tell `natsort` to ignore case when sorting. For example,
|
||||
``['Banana', 'apple', 'banana', 'Apple']`` would be sorted as
|
||||
``['apple', 'Apple', 'Banana', 'banana']``.
|
||||
LOWERCASEFIRST, LF
|
||||
Tell `natsort` to put lowercase letters before uppercase letters
|
||||
when sorting. For example,
|
||||
``['Banana', 'apple', 'banana', 'Apple']`` would be sorted as
|
||||
``['apple', 'banana', 'Apple', 'Banana']`` (the default order
|
||||
would be ``['Apple', 'Banana', 'apple', 'banana']`` which is
|
||||
the order from a purely ordinal sort).
|
||||
Useless when used with `IGNORECASE`. Please note that if used
|
||||
with ``LOCALE``, this actually has the reverse effect and will
|
||||
put uppercase first (this is because ``LOCALE`` already puts
|
||||
lowercase first); you may use this to your advantage if you
|
||||
need to modify the order returned with ``LOCALE``.
|
||||
GROUPLETTERS, G
|
||||
Tell `natsort` to group lowercase and uppercase letters together
|
||||
when sorting. For example,
|
||||
``['Banana', 'apple', 'banana', 'Apple']`` would be sorted as
|
||||
``['Apple', 'apple', 'Banana', 'banana']``.
|
||||
Useless when used with `IGNORECASE`; use with `LOWERCASEFIRST`
|
||||
to reverse the order of upper and lower case. Generally not
|
||||
needed with `LOCALE`.
|
||||
CAPITALFIRST, C
|
||||
Only used when `LOCALE` is enabled. Tell `natsort` to put all
|
||||
capitalized words before non-capitalized words. This is essentially
|
||||
the inverse of `GROUPLETTERS`, and is the default Python sorting
|
||||
behavior without `LOCALE`.
|
||||
UNGROUPLETTERS, UG
|
||||
An alias for `CAPITALFIRST`.
|
||||
NANLAST, NL
|
||||
If an NaN shows up in the input, this instructs `natsort` to
|
||||
treat these as +Infinity and place them after all the other numbers.
|
||||
By default, an NaN be treated as -Infinity and be placed first.
|
||||
Note that this ``None`` is treated like NaN internally.
|
||||
PRESORT, PS
|
||||
Sort the input as strings before sorting with the `nasort`
|
||||
algorithm. This can help eliminate inconsistent sorting in cases
|
||||
where two different strings represent the same number. For example,
|
||||
"a1" and "a01" both are internally represented as ("a", "1), so
|
||||
without `PRESORT` the order of these two values would depend on
|
||||
the order they appeared in the input (because Python's `sorted`
|
||||
is a stable sorting algorithm).
|
||||
|
||||
Notes
|
||||
-----
|
||||
If you prefer to use `import natsort as ns` as opposed to
|
||||
`from natsort import natsorted, ns`, the `ns` options are
|
||||
available as top-level imports.
|
||||
|
||||
>>> import natsort as ns
|
||||
>>> a = ['num5.10', 'num-3', 'num5.3', 'num2']
|
||||
>>> ns.natsorted(a, alg=ns.REAL) == ns.natsorted(a, alg=ns.ns.REAL)
|
||||
True
|
||||
|
||||
"""
|
||||
|
||||
# The below are the base ns options. The values will be stored as powers
|
||||
# of two so bitmasks can be used to extract the user's requested options.
|
||||
FLOAT = F = 1 << next(_counter)
|
||||
SIGNED = S = 1 << next(_counter)
|
||||
NOEXP = N = 1 << next(_counter)
|
||||
PATH = P = 1 << next(_counter)
|
||||
LOCALEALPHA = LA = 1 << next(_counter)
|
||||
LOCALENUM = LN = 1 << next(_counter)
|
||||
IGNORECASE = IC = 1 << next(_counter)
|
||||
LOWERCASEFIRST = LF = 1 << next(_counter)
|
||||
GROUPLETTERS = G = 1 << next(_counter)
|
||||
UNGROUPLETTERS = CAPITALFIRST = C = UG = 1 << next(_counter)
|
||||
NANLAST = NL = 1 << next(_counter)
|
||||
COMPATIBILITYNORMALIZE = CN = 1 << next(_counter)
|
||||
NUMAFTER = NA = 1 << next(_counter)
|
||||
PRESORT = PS = 1 << next(_counter)
|
||||
|
||||
# Following were previously options but are now defaults.
|
||||
DEFAULT = 0
|
||||
INT = I = 0 # noqa: E741
|
||||
UNSIGNED = U = 0
|
||||
|
||||
# The following are bitwise-OR combinations of other fields.
|
||||
REAL = R = FLOAT | SIGNED
|
||||
LOCALE = L = LOCALEALPHA | LOCALENUM
|
||||
|
||||
|
||||
# The below is private for internal use only.
|
||||
NS_DUMB = 1 << 31
|
||||
|
||||
# An integer can be used in place of the ns enum so make the
|
||||
# type to use for this enum a union of it and an inteter.
|
||||
NSType = typing.Union[ns, int]
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Pre-determine the collection of unicode decimals, digits, and numerals.
|
||||
"""
|
||||
|
||||
import unicodedata
|
||||
|
||||
from natsort.unicode_numeric_hex import numeric_hex
|
||||
|
||||
# Convert each hex into the literal Unicode character.
|
||||
# Stop if a ValueError is raised in case of a narrow Unicode build.
|
||||
# The extra check with unicodedata is in case this Python version
|
||||
# does not support some characters.
|
||||
numeric_chars = []
|
||||
for a in numeric_hex:
|
||||
try:
|
||||
character = chr(a)
|
||||
except ValueError: # pragma: no cover
|
||||
break
|
||||
if unicodedata.numeric(character, None) is None:
|
||||
continue # pragma: no cover
|
||||
numeric_chars.append(character)
|
||||
|
||||
# The digit characters are a subset of the numerals.
|
||||
digit_chars = [a for a in numeric_chars if unicodedata.digit(a, None) is not None]
|
||||
|
||||
# The decimal characters are a subset of the numerals
|
||||
# (probably of the digits, but let's be safe).
|
||||
decimal_chars = [a for a in numeric_chars if unicodedata.decimal(a, None) is not None]
|
||||
|
||||
# Create a single string with the above data.
|
||||
decimals = "".join(decimal_chars)
|
||||
digits = "".join(digit_chars)
|
||||
numeric = "".join(numeric_chars)
|
||||
digits_no_decimals = "".join([x for x in digits if x not in decimals])
|
||||
numeric_no_decimals = "".join([x for x in numeric if x not in decimals])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,934 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Utilities and definitions for natsort, mostly all used to define
|
||||
the natsort_key function.
|
||||
|
||||
SOME CONVENTIONS USED IN THIS FILE.
|
||||
|
||||
1 - Factory Functions
|
||||
|
||||
Most of the logic of natsort revolves around factory functions
|
||||
that create branchless transformation functions. For example, rather
|
||||
than making a string transformation function that has an if
|
||||
statement to determine whether or not to perform .lowercase() at
|
||||
runtime for each element to transform, there is a string transformation
|
||||
factory function that will return a function that either calls
|
||||
.lowercase() or does nothing. In this way, all the branches and
|
||||
decisions are taken care of once, up front. In addition to a slight
|
||||
speed improvement, this provides a more extensible infrastructure.
|
||||
|
||||
Each of these factory functions will end with the suffix "_factory"
|
||||
to indicate that they themselves return a function.
|
||||
|
||||
2 - Keyword Parameters For Local Scope
|
||||
|
||||
Many of the closures that are created by the factory functions
|
||||
have signatures similar to the following
|
||||
|
||||
>>> def factory(parameter):
|
||||
... val = 'yes' if parameter else 'no'
|
||||
... def closure(x, _val=val):
|
||||
... return '{} {}'.format(_val, x)
|
||||
... return closure
|
||||
...
|
||||
|
||||
The variable value is passed as the default to a keyword argument.
|
||||
This is a micro-optimization
|
||||
that ensures "val" is a local variable instead of global variable
|
||||
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 (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Match,
|
||||
Optional,
|
||||
Pattern,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from unicodedata import normalize
|
||||
|
||||
from natsort.compat.fastnumbers import try_float, try_int
|
||||
from natsort.compat.locale import (
|
||||
StrOrBytes,
|
||||
get_decimal_point,
|
||||
get_strxfrm,
|
||||
get_thousands_sep,
|
||||
)
|
||||
from natsort.ns_enum import NSType, NS_DUMB, ns
|
||||
from natsort.unicode_numbers import digits_no_decimals, numeric_no_decimals
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Protocol
|
||||
else:
|
||||
Protocol = object
|
||||
|
||||
#
|
||||
# Pre-define a slew of aggregate types which makes the type hinting below easier
|
||||
#
|
||||
|
||||
|
||||
class SupportsDunderLT(Protocol):
|
||||
def __lt__(self, __other: Any) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsDunderGT(Protocol):
|
||||
def __gt__(self, __other: Any) -> bool:
|
||||
...
|
||||
|
||||
|
||||
Sortable = Union[SupportsDunderLT, SupportsDunderGT]
|
||||
|
||||
StrToStr = Callable[[str], str]
|
||||
AnyCall = Callable[[Any], Any]
|
||||
|
||||
# For the bytes transform factory
|
||||
BytesTuple = Tuple[bytes]
|
||||
NestedBytesTuple = Tuple[Tuple[bytes]]
|
||||
BytesTransform = Union[BytesTuple, NestedBytesTuple]
|
||||
BytesTransformer = Callable[[bytes], BytesTransform]
|
||||
|
||||
# For the number transform factory
|
||||
BasicTuple = Tuple[Any, ...]
|
||||
NestedAnyTuple = Tuple[BasicTuple, ...]
|
||||
AnyTuple = Union[BasicTuple, NestedAnyTuple]
|
||||
NumTransform = AnyTuple
|
||||
NumTransformer = Callable[[Any], NumTransform]
|
||||
|
||||
# For the string component transform factory
|
||||
StrBytesNum = Union[str, bytes, float, int]
|
||||
StrTransformer = Callable[[Iterable[str]], Iterator[StrBytesNum]]
|
||||
|
||||
# For the final data transform factory
|
||||
FinalTransform = AnyTuple
|
||||
FinalTransformer = Callable[[Iterable[Any], str], FinalTransform]
|
||||
|
||||
PathArg = Union[str, PurePath]
|
||||
MatchFn = Callable[[str], Optional[Match]]
|
||||
|
||||
# For the string parsing factory
|
||||
StrSplitter = Callable[[str], Iterable[str]]
|
||||
StrParser = Callable[[PathArg], FinalTransform]
|
||||
|
||||
# For the path parsing factory
|
||||
PathSplitter = Callable[[PathArg], Tuple[FinalTransform, ...]]
|
||||
|
||||
# For the natsort key
|
||||
NatsortInType = Optional[Sortable]
|
||||
NatsortOutType = Tuple[Sortable, ...]
|
||||
KeyType = Callable[[Any], NatsortInType]
|
||||
MaybeKeyType = Optional[KeyType]
|
||||
|
||||
|
||||
class NumericalRegularExpressions:
|
||||
"""
|
||||
Container of regular expressions that match numbers.
|
||||
|
||||
The numbers also account for unicode non-decimal characters.
|
||||
|
||||
Not intended to be made an instance - use class methods only.
|
||||
"""
|
||||
|
||||
# All unicode numeric characters (minus the decimal characters).
|
||||
numeric: str = numeric_no_decimals
|
||||
# All unicode digit characters (minus the decimal characters).
|
||||
digits: str = digits_no_decimals
|
||||
# Regular expression to match exponential component of a float.
|
||||
exp: str = r"(?:[eE][-+]?\d+)?"
|
||||
# Regular expression to match a floating point number.
|
||||
float_num: str = r"(?:\d+\.?\d*|\.\d+)"
|
||||
|
||||
@classmethod
|
||||
def _construct_regex(cls, fmt: str) -> Pattern[str]:
|
||||
"""Given a format string, construct the regex with class attributes."""
|
||||
return re.compile(fmt.format(**vars(cls)), flags=re.U)
|
||||
|
||||
@classmethod
|
||||
def int_sign(cls) -> Pattern[str]:
|
||||
"""Regular expression to match a signed int."""
|
||||
return cls._construct_regex(r"([-+]?\d+|[{digits}])")
|
||||
|
||||
@classmethod
|
||||
def int_nosign(cls) -> Pattern[str]:
|
||||
"""Regular expression to match an unsigned int."""
|
||||
return cls._construct_regex(r"(\d+|[{digits}])")
|
||||
|
||||
@classmethod
|
||||
def float_sign_exp(cls) -> Pattern[str]:
|
||||
"""Regular expression to match a signed float with exponent."""
|
||||
return cls._construct_regex(r"([-+]?{float_num}{exp}|[{numeric}])")
|
||||
|
||||
@classmethod
|
||||
def float_nosign_exp(cls) -> Pattern[str]:
|
||||
"""Regular expression to match an unsigned float with exponent."""
|
||||
return cls._construct_regex(r"({float_num}{exp}|[{numeric}])")
|
||||
|
||||
@classmethod
|
||||
def float_sign_noexp(cls) -> Pattern[str]:
|
||||
"""Regular expression to match a signed float without exponent."""
|
||||
return cls._construct_regex(r"([-+]?{float_num}|[{numeric}])")
|
||||
|
||||
@classmethod
|
||||
def float_nosign_noexp(cls) -> Pattern[str]:
|
||||
"""Regular expression to match an unsigned float without exponent."""
|
||||
return cls._construct_regex(r"({float_num}|[{numeric}])")
|
||||
|
||||
|
||||
def regex_chooser(alg: NSType) -> Pattern[str]:
|
||||
"""
|
||||
Select an appropriate regex for the type of number of interest.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Used to indicate the regular expression to select.
|
||||
|
||||
Returns
|
||||
-------
|
||||
regex : compiled regex object
|
||||
Regular expression object that matches the desired number type.
|
||||
|
||||
"""
|
||||
if alg & ns.FLOAT:
|
||||
alg &= ns.FLOAT | ns.SIGNED | ns.NOEXP
|
||||
else:
|
||||
alg &= ns.INT | ns.SIGNED
|
||||
|
||||
return {
|
||||
ns.INT: NumericalRegularExpressions.int_nosign(),
|
||||
ns.FLOAT: NumericalRegularExpressions.float_nosign_exp(),
|
||||
ns.INT | ns.SIGNED: NumericalRegularExpressions.int_sign(),
|
||||
ns.FLOAT | ns.SIGNED: NumericalRegularExpressions.float_sign_exp(),
|
||||
ns.FLOAT | ns.NOEXP: NumericalRegularExpressions.float_nosign_noexp(),
|
||||
ns.FLOAT | ns.SIGNED | ns.NOEXP: NumericalRegularExpressions.float_sign_noexp(),
|
||||
}[alg]
|
||||
|
||||
|
||||
def _no_op(x: Any) -> Any:
|
||||
"""A function that does nothing and returns the input as-is."""
|
||||
return x
|
||||
|
||||
|
||||
def _normalize_input_factory(alg: NSType) -> StrToStr:
|
||||
"""
|
||||
Create a function that will normalize unicode input data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Used to indicate how to normalize unicode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function that accepts string (unicode) input and returns the
|
||||
the input normalized with the desired normalization scheme.
|
||||
|
||||
"""
|
||||
normalization_form = "NFKD" if alg & ns.COMPATIBILITYNORMALIZE else "NFD"
|
||||
return partial(normalize, normalization_form)
|
||||
|
||||
|
||||
def _compose_input_factory(alg: NSType) -> StrToStr:
|
||||
"""
|
||||
Create a function that will compose unicode input data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Used to indicate how to compose unicode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function that accepts string (unicode) input and returns the
|
||||
the input normalized with the desired composition scheme.
|
||||
"""
|
||||
normalization_form = "NFKC" if alg & ns.COMPATIBILITYNORMALIZE else "NFC"
|
||||
return partial(normalize, normalization_form)
|
||||
|
||||
|
||||
@overload
|
||||
def natsort_key(
|
||||
val: NatsortInType,
|
||||
key: None,
|
||||
string_func: Union[StrParser, PathSplitter],
|
||||
bytes_func: BytesTransformer,
|
||||
num_func: NumTransformer,
|
||||
) -> NatsortOutType:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def natsort_key(
|
||||
val: Any,
|
||||
key: KeyType,
|
||||
string_func: Union[StrParser, PathSplitter],
|
||||
bytes_func: BytesTransformer,
|
||||
num_func: NumTransformer,
|
||||
) -> NatsortOutType:
|
||||
...
|
||||
|
||||
|
||||
def natsort_key(
|
||||
val: Union[NatsortInType, Any],
|
||||
key: MaybeKeyType,
|
||||
string_func: Union[StrParser, PathSplitter],
|
||||
bytes_func: BytesTransformer,
|
||||
num_func: NumTransformer,
|
||||
) -> NatsortOutType:
|
||||
"""
|
||||
Key to sort strings and numbers naturally.
|
||||
|
||||
It works by splitting the string into components of strings and numbers,
|
||||
and then converting the numbers into actual ints or floats.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
val : str | bytes | int | float | iterable
|
||||
key : callable | None
|
||||
A key to apply to the *val* before any other operations are performed.
|
||||
string_func : callable
|
||||
If *val* (or the output of *key* if given) is of type *str*, this
|
||||
function will be applied to it. The function must return
|
||||
a tuple.
|
||||
bytes_func : callable
|
||||
If *val* (or the output of *key* if given) is of type *bytes*, this
|
||||
function will be applied to it. The function must return
|
||||
a tuple.
|
||||
num_func : callable
|
||||
If *val* (or the output of *key* if given) is not of type *bytes*,
|
||||
*str*, nor is iterable, this function will be applied to it.
|
||||
The function must return a tuple.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : tuple
|
||||
The string split into its string and numeric components.
|
||||
It *always* starts with a string, and then alternates
|
||||
between numbers and strings (unless it was applied
|
||||
recursively, in which case it will return tuples of tuples,
|
||||
but the lowest-level tuples will then *always* start with
|
||||
a string etc.).
|
||||
|
||||
See Also
|
||||
--------
|
||||
parse_string_factory
|
||||
parse_bytes_factory
|
||||
parse_number_or_none_factory
|
||||
|
||||
"""
|
||||
|
||||
# Apply key if needed
|
||||
if key is not None:
|
||||
val = key(val)
|
||||
|
||||
if isinstance(val, (str, PurePath)):
|
||||
return string_func(val)
|
||||
elif isinstance(val, bytes):
|
||||
return bytes_func(val)
|
||||
elif 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)
|
||||
|
||||
|
||||
def parse_bytes_factory(alg: NSType) -> BytesTransformer:
|
||||
"""
|
||||
Create a function that will format a *bytes* object into a tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Indicate how to format the *bytes*.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function that accepts *bytes* input and returns a tuple
|
||||
with the formatted *bytes*. Intended to be used as the
|
||||
*bytes_func* argument to *natsort_key*.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsort_key
|
||||
|
||||
"""
|
||||
# We don't worry about ns.UNGROUPLETTERS | ns.LOCALEALPHA because
|
||||
# bytes cannot be compared to strings.
|
||||
if alg & ns.PATH and alg & ns.IGNORECASE:
|
||||
return lambda x: ((x.lower(),),)
|
||||
elif alg & ns.PATH:
|
||||
return lambda x: ((x,),)
|
||||
elif alg & ns.IGNORECASE:
|
||||
return lambda x: (x.lower(),)
|
||||
else:
|
||||
return lambda x: (x,)
|
||||
|
||||
|
||||
def parse_number_or_none_factory(
|
||||
alg: NSType, sep: StrOrBytes, pre_sep: str
|
||||
) -> NumTransformer:
|
||||
"""
|
||||
Create a function that will format a number (or None) into a tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Indicate how to format the *bytes*.
|
||||
sep : str
|
||||
The string character to be inserted before the number
|
||||
in the returned tuple.
|
||||
pre_sep : str
|
||||
In the event that *alg* contains ``UNGROUPLETTERS``, this
|
||||
string will be placed in a single-element tuple at the front
|
||||
of the returned nested tuple.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function that accepts numeric input (e.g. *int* or *float*)
|
||||
and returns a tuple containing the number with the leading string
|
||||
*sep*. Intended to be used as the *num_func* argument to
|
||||
*natsort_key*.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsort_key
|
||||
|
||||
"""
|
||||
nan_replace = float("+inf") if alg & ns.NANLAST else float("-inf")
|
||||
|
||||
def func(
|
||||
val: Any,
|
||||
_nan_replace: float = nan_replace,
|
||||
_sep: StrOrBytes = sep,
|
||||
reverse: bool = nan_replace == float("+inf"),
|
||||
) -> BasicTuple:
|
||||
"""Given a number, place it in a tuple with a leading null string."""
|
||||
# Add a trailing string numbers equaling _nan_replace. This will make
|
||||
# the ordering between None NaN, and the NaN replacement value...
|
||||
# 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:
|
||||
return _sep, _nan_replace, "2"
|
||||
elif val == _nan_replace:
|
||||
return _sep, _nan_replace, "1" if reverse else "3"
|
||||
else:
|
||||
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:
|
||||
return lambda x: ((pre_sep,), func(x))
|
||||
elif alg & ns.PATH:
|
||||
return lambda x: (func(x),)
|
||||
else:
|
||||
return func
|
||||
|
||||
|
||||
def parse_string_factory(
|
||||
alg: NSType,
|
||||
sep: StrOrBytes,
|
||||
splitter: StrSplitter,
|
||||
input_transform: StrToStr,
|
||||
component_transform: StrTransformer,
|
||||
final_transform: FinalTransformer,
|
||||
) -> StrParser:
|
||||
"""
|
||||
Create a function that will split and format a *str* into a tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Indicate how to format and split the *str*.
|
||||
sep : str
|
||||
The string character to be inserted between adjacent numeric
|
||||
objects in the returned tuple.
|
||||
splitter : callable
|
||||
A function the will accept a string and returns an iterable
|
||||
of strings where the numbers are separated from the non-numbers.
|
||||
input_transform : callable
|
||||
A function to apply to the string input *before* applying
|
||||
the *splitter* function. Must return a string.
|
||||
component_transform : callable
|
||||
A function that is operated elementwise on the output of
|
||||
*splitter*. It must accept a single string and return either
|
||||
a string or a number.
|
||||
final_transform : callable
|
||||
A function to operate on the return value as a whole. It
|
||||
must accept a tuple and a string argument - the tuple
|
||||
should be the result of applying the above functions, and the
|
||||
string is the original input value. It must return a tuple.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function that accepts string input and returns a tuple
|
||||
containing the string split into numeric and non-numeric
|
||||
components, where the numeric components are converted into
|
||||
numeric objects. The first element is *always* a string,
|
||||
and then alternates number then string. Intended to be
|
||||
used as the *string_func* argument to *natsort_key*.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsort_key
|
||||
input_string_transform_factory
|
||||
string_component_transform_factory
|
||||
final_data_transform_factory
|
||||
|
||||
"""
|
||||
# Sometimes we store the "original" input before transformation,
|
||||
# sometimes after.
|
||||
orig_after_xfrm = not (alg & NS_DUMB and alg & ns.LOCALEALPHA)
|
||||
original_func = input_transform if orig_after_xfrm else _no_op
|
||||
normalize_input = _normalize_input_factory(alg)
|
||||
compose_input = _compose_input_factory(alg) if alg & ns.LOCALEALPHA else _no_op
|
||||
|
||||
def func(x: PathArg) -> FinalTransform:
|
||||
if isinstance(x, PurePath):
|
||||
# While paths are technically not strings, it is natural for them
|
||||
# to be treated the same.
|
||||
x = str(x)
|
||||
# Apply string input transformation function and return to x.
|
||||
# Original function is usually a no-op, but some algorithms require it
|
||||
# to also be the transformation function.
|
||||
a = normalize_input(x)
|
||||
b, original = input_transform(a), original_func(a)
|
||||
c = compose_input(b) # Decompose unicode if using LOCALE
|
||||
d = splitter(c) # Split string into components.
|
||||
e = filter(None, d) # Remove empty strings.
|
||||
f = component_transform(e) # Apply transform on components.
|
||||
g = sep_inserter(f, sep) # Insert '' between numbers.
|
||||
return final_transform(g, original) # Apply the final transform.
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def parse_path_factory(str_split: StrParser) -> PathSplitter:
|
||||
"""
|
||||
Create a function that will properly split and format a path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
str_split : callable
|
||||
The output of the *parse_string_factory* function.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function that accepts a string or path-like object
|
||||
and splits it into its path components, then passes
|
||||
each component to *str_split* and returns the result
|
||||
as a nested tuple. Can be used as the *string_func*
|
||||
argument to *natsort_key*.
|
||||
|
||||
See Also
|
||||
--------
|
||||
natsort_key
|
||||
parse_string_factory
|
||||
|
||||
"""
|
||||
return lambda x: tuple(map(str_split, path_splitter(x)))
|
||||
|
||||
|
||||
def sep_inserter(iterator: Iterator[Any], sep: StrOrBytes) -> Iterator[Any]:
|
||||
"""
|
||||
Insert '' between numbers in an iterator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterator
|
||||
sep : str
|
||||
The string character to be inserted between adjacent numeric objects.
|
||||
|
||||
Yields
|
||||
------
|
||||
The values of *iterator* in order, with *sep* inserted where adjacent
|
||||
elements are numeric. If the first element in the input is numeric
|
||||
then *sep* will be the first value yielded.
|
||||
|
||||
"""
|
||||
try:
|
||||
# Get the first element. A StopIteration indicates an empty iterator.
|
||||
# Since we are controlling the types of the input, 'type' is used
|
||||
# instead of 'isinstance' for the small speed advantage it offers.
|
||||
types = (int, float)
|
||||
first = next(iterator)
|
||||
if type(first) in types:
|
||||
yield sep
|
||||
yield first
|
||||
|
||||
# Now, check if pair of elements are both numbers. If so, add ''.
|
||||
second = next(iterator)
|
||||
if type(first) in types and type(second) in types:
|
||||
yield sep
|
||||
yield second
|
||||
|
||||
# Now repeat in a loop.
|
||||
for x in iterator:
|
||||
first, second = second, x
|
||||
if type(first) in types and type(second) in types:
|
||||
yield sep
|
||||
yield second
|
||||
except StopIteration:
|
||||
# Catch StopIteration per deprecation in PEP 479:
|
||||
# "Change StopIteration handling inside generators"
|
||||
return
|
||||
|
||||
|
||||
def input_string_transform_factory(alg: NSType) -> StrToStr:
|
||||
"""
|
||||
Create a function to transform a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Indicate how to format the *str*.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function to be used as the *input_transform* argument to
|
||||
*parse_string_factory*.
|
||||
|
||||
See Also
|
||||
--------
|
||||
parse_string_factory
|
||||
|
||||
"""
|
||||
# Shortcuts.
|
||||
lowfirst = alg & ns.LOWERCASEFIRST
|
||||
dumb = alg & NS_DUMB
|
||||
|
||||
# Build the chain of functions to execute in order.
|
||||
function_chain: List[StrToStr] = []
|
||||
if (dumb and not lowfirst) or (lowfirst and not dumb):
|
||||
function_chain.append(methodcaller("swapcase"))
|
||||
|
||||
if alg & ns.IGNORECASE:
|
||||
function_chain.append(methodcaller("casefold"))
|
||||
|
||||
if alg & ns.LOCALENUM:
|
||||
# Create a regular expression that will remove thousands separators.
|
||||
strip_thousands = r"""
|
||||
(?<=[0-9]{{1}}) # At least 1 number
|
||||
(?<![0-9]{{4}}) # No more than 3 numbers
|
||||
{nodecimal} # Cannot follow decimal
|
||||
{thou} # The thousands separator
|
||||
(?=[0-9]{{3}} # Three numbers must follow
|
||||
([^0-9]|$) # But a non-number after that
|
||||
)
|
||||
"""
|
||||
nodecimal = r""
|
||||
if alg & ns.FLOAT:
|
||||
# Make a regular expression component that will ensure no
|
||||
# separators are removed after a decimal point.
|
||||
d = re.escape(get_decimal_point())
|
||||
nodecimal += r"(?<!" + d + r"[0-9])"
|
||||
nodecimal += r"(?<!" + d + r"[0-9]{2})"
|
||||
nodecimal += r"(?<!" + d + r"[0-9]{3})"
|
||||
strip_thousands = strip_thousands.format(
|
||||
thou=re.escape(get_thousands_sep()), nodecimal=nodecimal
|
||||
)
|
||||
strip_thousands_re = re.compile(strip_thousands, flags=re.VERBOSE)
|
||||
function_chain.append(partial(strip_thousands_re.sub, ""))
|
||||
|
||||
# Create a regular expression that will change the decimal point to
|
||||
# a period if not already a period.
|
||||
decimal = get_decimal_point()
|
||||
if alg & ns.FLOAT and decimal != ".":
|
||||
switch_decimal = r"(?<=[0-9]){decimal}|{decimal}(?=[0-9])"
|
||||
switch_decimal = switch_decimal.format(decimal=re.escape(decimal))
|
||||
switch_decimal_re = re.compile(switch_decimal)
|
||||
function_chain.append(partial(switch_decimal_re.sub, "."))
|
||||
|
||||
# Return the chained functions.
|
||||
return chain_functions(function_chain)
|
||||
|
||||
|
||||
def string_component_transform_factory(alg: NSType) -> StrTransformer:
|
||||
"""
|
||||
Create a function to either transform a string or convert to a number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Indicate how to format the *str*.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function to be used as the *component_transform* argument to
|
||||
*parse_string_factory*.
|
||||
|
||||
See Also
|
||||
--------
|
||||
parse_string_factory
|
||||
|
||||
"""
|
||||
# Shortcuts.
|
||||
use_locale = alg & ns.LOCALEALPHA
|
||||
dumb = alg & NS_DUMB
|
||||
group_letters = (alg & ns.GROUPLETTERS) or (use_locale and dumb)
|
||||
nan_val = float("+inf") if alg & ns.NANLAST else float("-inf")
|
||||
|
||||
# Build the chain of functions to execute in order.
|
||||
func_chain: List[Callable[[str], StrOrBytes]] = []
|
||||
if group_letters:
|
||||
func_chain.append(groupletters)
|
||||
if use_locale:
|
||||
func_chain.append(get_strxfrm())
|
||||
|
||||
# Return the correct chained functions.
|
||||
kwargs: Dict[str, Union[float, Callable[[str], StrOrBytes], bool]]
|
||||
kwargs = {"on_fail": chain_functions(func_chain)} if func_chain else {}
|
||||
kwargs["map"] = True
|
||||
if alg & ns.FLOAT:
|
||||
kwargs["nan"] = nan_val
|
||||
return cast(StrTransformer, partial(try_float, **kwargs))
|
||||
else:
|
||||
return cast(StrTransformer, partial(try_int, **kwargs))
|
||||
|
||||
|
||||
def final_data_transform_factory(
|
||||
alg: NSType, sep: StrOrBytes, pre_sep: str
|
||||
) -> FinalTransformer:
|
||||
"""
|
||||
Create a function to transform a tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alg : ns enum
|
||||
Indicate how to format the *str*.
|
||||
sep : str
|
||||
Separator that was passed to *parse_string_factory*.
|
||||
pre_sep : str
|
||||
String separator to insert at the at the front
|
||||
of the return tuple in the case that the first element
|
||||
is *sep*.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A function to be used as the *final_transform* argument to
|
||||
*parse_string_factory*.
|
||||
|
||||
See Also
|
||||
--------
|
||||
parse_string_factory
|
||||
|
||||
"""
|
||||
if alg & ns.UNGROUPLETTERS and alg & ns.LOCALEALPHA:
|
||||
swap = alg & NS_DUMB and alg & ns.LOWERCASEFIRST
|
||||
transform = cast(StrToStr, methodcaller("swapcase") if swap else _no_op)
|
||||
|
||||
def func(
|
||||
split_val: Iterable[NatsortInType],
|
||||
val: str,
|
||||
_transform: StrToStr = transform,
|
||||
_sep: StrOrBytes = sep,
|
||||
_pre_sep: str = pre_sep,
|
||||
) -> FinalTransform:
|
||||
"""
|
||||
Return a tuple with the first character of the first element
|
||||
of the return value as the first element, and the return value
|
||||
as the second element. This will be used to perform gross sorting
|
||||
by the first letter.
|
||||
"""
|
||||
split_val = tuple(split_val)
|
||||
if not split_val:
|
||||
return (), ()
|
||||
elif split_val[0] == _sep:
|
||||
return (_pre_sep,), split_val
|
||||
else:
|
||||
return (_transform(val[0]),), split_val
|
||||
|
||||
else:
|
||||
|
||||
def func(
|
||||
split_val: Iterable[NatsortInType],
|
||||
val: str,
|
||||
_transform: StrToStr = _no_op,
|
||||
_sep: StrOrBytes = sep,
|
||||
_pre_sep: str = pre_sep,
|
||||
) -> FinalTransform:
|
||||
return tuple(split_val)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
lower_function: StrToStr = cast(StrToStr, methodcaller("casefold"))
|
||||
|
||||
|
||||
# noinspection PyIncorrectDocstring
|
||||
def groupletters(x: str, _low: StrToStr = lower_function) -> str:
|
||||
"""
|
||||
Double all characters, making doubled letters lowercase.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : str
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> groupletters("Apple")
|
||||
'aAppppllee'
|
||||
|
||||
"""
|
||||
return "".join(ichain.from_iterable((_low(y), y) for y in x))
|
||||
|
||||
|
||||
def chain_functions(functions: Iterable[AnyCall]) -> AnyCall:
|
||||
"""
|
||||
Chain a list of single-argument functions together and return.
|
||||
|
||||
The functions are applied in list order, and the output of the
|
||||
previous functions is passed to the next function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
functions : list
|
||||
A list of single-argument functions to chain together.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : callable
|
||||
A single argument function.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Chain several functions together!
|
||||
|
||||
>>> funcs = [lambda x: x * 4, len, lambda x: x + 5]
|
||||
>>> func = chain_functions(funcs)
|
||||
>>> func('hey')
|
||||
17
|
||||
|
||||
"""
|
||||
functions = list(functions)
|
||||
if not functions:
|
||||
return _no_op
|
||||
elif len(functions) == 1:
|
||||
return functions[0]
|
||||
else:
|
||||
# See https://stackoverflow.com/a/39123400/1399279
|
||||
return partial(reduce, lambda res, f: f(res), functions)
|
||||
|
||||
|
||||
@overload
|
||||
def do_decoding(s: bytes, encoding: str) -> str:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def do_decoding(s: Any, encoding: str) -> Any:
|
||||
...
|
||||
|
||||
|
||||
def do_decoding(s: Any, encoding: str) -> Any:
|
||||
"""
|
||||
Helper to decode a *bytes* object, or return the object as-is.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : bytes | object
|
||||
encoding : str
|
||||
The encoding to use to decode *s*.
|
||||
|
||||
Returns
|
||||
-------
|
||||
decoded
|
||||
*str* if *s* was *bytes* and the decoding was successful.
|
||||
*s* if *s* was not *bytes*.
|
||||
|
||||
"""
|
||||
if isinstance(s, bytes):
|
||||
return s.decode(encoding)
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
# noinspection PyIncorrectDocstring
|
||||
def path_splitter(
|
||||
s: PathArg, treat_base: bool = True, _d_match: MatchFn = re.compile(r"\.\d").match
|
||||
) -> Iterator[str]:
|
||||
"""
|
||||
Split a string into its path components.
|
||||
|
||||
Assumes a string is a path or is path-like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : str | pathlib.Path
|
||||
treat_base: bool, optional
|
||||
If True, treat the base of component of the file path as
|
||||
special and split off extensions. If False, do not do this.
|
||||
The default is True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
split : tuple
|
||||
The path split by directory components and extensions.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> tuple(path_splitter("this/thing.ext"))
|
||||
('this', 'thing', '.ext')
|
||||
|
||||
"""
|
||||
if not isinstance(s, PurePath):
|
||||
s = PurePath(s)
|
||||
|
||||
# Split the path into parts.
|
||||
try:
|
||||
*path_parts, base = s.parts
|
||||
except ValueError:
|
||||
path_parts = []
|
||||
base = str(s)
|
||||
|
||||
suffixes = []
|
||||
if treat_base:
|
||||
# Now, split off the file extensions until
|
||||
# - we reach a decimal number at the beginning of the suffix
|
||||
# - more than two suffixes have been seen
|
||||
# - a suffix is more than five characters (including leading ".")
|
||||
# - there are no more extensions
|
||||
for i, suffix in enumerate(reversed(PurePath(base).suffixes)):
|
||||
if _d_match(suffix) or i > 1 or len(suffix) > 5:
|
||||
break
|
||||
suffixes.append(suffix)
|
||||
suffixes.reverse()
|
||||
|
||||
# Remove the suffixes from the base component
|
||||
base = base.replace("".join(suffixes), "")
|
||||
base_component = [base] if base else []
|
||||
|
||||
# Join all path comonents in an iterator
|
||||
return filter(None, ichain(path_parts, base_component, suffixes))
|
||||
Reference in New Issue
Block a user