Source code for cardinal_pythonlib.sort

#!/usr/bin/env python
# cardinal_pythonlib/sort.py

"""
===============================================================================

    Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of cardinal_pythonlib.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        https://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

===============================================================================

**Support functions for sorting.**

"""

from functools import partial, total_ordering
import re
from typing import Any, List, Union


# =============================================================================
# Natural sorting, e.g. for COM ports
# =============================================================================
# https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside  # noqa


[docs]def atoi(text: str) -> Union[int, str]: """ Converts strings to integers if they're composed of digits; otherwise returns the strings unchanged. One way of sorting strings with numbers; it will mean that ``"11"`` is more than ``"2"``. """ return int(text) if text.isdigit() else text
[docs]def natural_keys(text: str) -> List[Union[int, str]]: """ Sort key function. Returns text split into string/number parts, for natural sorting; as per https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside Example (as per the source above): .. code-block:: python >>> from cardinal_pythonlib.sort import natural_keys >>> alist=[ ... "something1", ... "something12", ... "something17", ... "something2", ... "something25", ... "something29" ... ] >>> alist.sort(key=natural_keys) >>> alist ['something1', 'something2', 'something12', 'something17', 'something25', 'something29'] """ # noqa return [atoi(c) for c in re.split(r"(\d+)", text)]
# ============================================================================= # Sorting where None counts as the minimum # =============================================================================
[docs]@total_ordering class MinType(object): """ An object that compares less than anything else. """ def __le__(self, other: Any) -> bool: return True def __eq__(self, other: Any) -> bool: return self is other def __str__(self) -> str: return "MinType"
MINTYPE_SINGLETON = MinType() # noinspection PyPep8Naming
[docs]class attrgetter_nonesort: """ Modification of ``operator.attrgetter``. Returns an object's attributes, or the ``mintype_singleton`` if the attribute is ``None``. """ __slots__ = ("_attrs", "_call") def __init__(self, attr, *attrs): if not attrs: if not isinstance(attr, str): raise TypeError("attribute name must be a string") self._attrs = (attr,) names = attr.split(".") def func(obj): for name in names: obj = getattr(obj, name) if obj is None: # MODIFIED HERE return MINTYPE_SINGLETON return obj self._call = func else: self._attrs = (attr,) + attrs # MODIFIED HERE: getters = tuple(map(attrgetter_nonesort, self._attrs)) def func(obj): return tuple(getter(obj) for getter in getters) self._call = func def __call__(self, obj): return self._call(obj) def __repr__(self): return "%s.%s(%s)" % ( self.__class__.__module__, self.__class__.__qualname__, ", ".join(map(repr, self._attrs)), ) def __reduce__(self): return self.__class__, self._attrs
# noinspection PyPep8Naming
[docs]class methodcaller_nonesort: """ As per :class:`attrgetter_nonesort` (q.v.), but for ``methodcaller``. """ __slots__ = ("_name", "_args", "_kwargs") def __init__(*args, **kwargs): if len(args) < 2: msg = "methodcaller needs at least one argument, the method name" raise TypeError(msg) self = args[0] self._name = args[1] if not isinstance(self._name, str): raise TypeError("method name must be a string") self._args = args[2:] self._kwargs = kwargs def __call__(self, obj): # MODIFICATION HERE result = getattr(obj, self._name)(*self._args, **self._kwargs) if result is None: return MINTYPE_SINGLETON return result def __repr__(self): args = [repr(self._name)] args.extend(map(repr, self._args)) args.extend("%s=%r" % (k, v) for k, v in self._kwargs.items()) return "%s.%s(%s)" % ( self.__class__.__module__, self.__class__.__name__, ", ".join(args), ) def __reduce__(self): if not self._kwargs: return self.__class__, (self._name,) + self._args else: return ( partial(self.__class__, self._name, **self._kwargs), self._args, )