Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/sort.py : 35%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# cardinal_pythonlib/sort.py
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 https://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
23===============================================================================
25**Support functions for sorting.**
27"""
29from functools import partial, total_ordering
30import re
31from typing import Any, List, Union
34# =============================================================================
35# Natural sorting, e.g. for COM ports
36# =============================================================================
37# https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside # noqa
39def atoi(text: str) -> Union[int, str]:
40 """
41 Converts strings to integers if they're composed of digits; otherwise
42 returns the strings unchanged. One way of sorting strings with numbers;
43 it will mean that ``"11"`` is more than ``"2"``.
44 """
45 return int(text) if text.isdigit() else text
48def natural_keys(text: str) -> List[Union[int, str]]:
49 """
50 Sort key function.
51 Returns text split into string/number parts, for natural sorting; as per
52 https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside
54 Example (as per the source above):
56 .. code-block:: python
58 >>> from cardinal_pythonlib.sort import natural_keys
59 >>> alist=[
60 ... "something1",
61 ... "something12",
62 ... "something17",
63 ... "something2",
64 ... "something25",
65 ... "something29"
66 ... ]
67 >>> alist.sort(key=natural_keys)
68 >>> alist
69 ['something1', 'something2', 'something12', 'something17', 'something25', 'something29']
71 """ # noqa
72 return [atoi(c) for c in re.split(r'(\d+)', text)]
75# =============================================================================
76# Sorting where None counts as the minimum
77# =============================================================================
79@total_ordering
80class MinType(object):
81 """
82 An object that compares less than anything else.
83 """
84 def __le__(self, other: Any) -> bool:
85 return True
87 def __eq__(self, other: Any) -> bool:
88 return self is other
90 def __str__(self) -> str:
91 return "MinType"
94MINTYPE_SINGLETON = MinType()
97# noinspection PyPep8Naming
98class attrgetter_nonesort:
99 """
100 Modification of ``operator.attrgetter``.
101 Returns an object's attributes, or the ``mintype_singleton`` if the
102 attribute is ``None``.
103 """
104 __slots__ = ('_attrs', '_call')
106 def __init__(self, attr, *attrs):
107 if not attrs:
108 if not isinstance(attr, str):
109 raise TypeError('attribute name must be a string')
110 self._attrs = (attr,)
111 names = attr.split('.')
113 def func(obj):
114 for name in names:
115 obj = getattr(obj, name)
116 if obj is None: # MODIFIED HERE
117 return MINTYPE_SINGLETON
118 return obj
120 self._call = func
121 else:
122 self._attrs = (attr,) + attrs
123 # MODIFIED HERE:
124 getters = tuple(map(attrgetter_nonesort, self._attrs))
126 def func(obj):
127 return tuple(getter(obj) for getter in getters)
129 self._call = func
131 def __call__(self, obj):
132 return self._call(obj)
134 def __repr__(self):
135 return '%s.%s(%s)' % (self.__class__.__module__,
136 self.__class__.__qualname__,
137 ', '.join(map(repr, self._attrs)))
139 def __reduce__(self):
140 return self.__class__, self._attrs
143# noinspection PyPep8Naming
144class methodcaller_nonesort:
145 """
146 As per :class:`attrgetter_nonesort` (q.v.), but for ``methodcaller``.
147 """
148 __slots__ = ('_name', '_args', '_kwargs')
150 def __init__(*args, **kwargs):
151 if len(args) < 2:
152 msg = "methodcaller needs at least one argument, the method name"
153 raise TypeError(msg)
154 self = args[0]
155 self._name = args[1]
156 if not isinstance(self._name, str):
157 raise TypeError('method name must be a string')
158 self._args = args[2:]
159 self._kwargs = kwargs
161 def __call__(self, obj):
162 # MODIFICATION HERE
163 result = getattr(obj, self._name)(*self._args, **self._kwargs)
164 if result is None:
165 return MINTYPE_SINGLETON
166 return result
168 def __repr__(self):
169 args = [repr(self._name)]
170 args.extend(map(repr, self._args))
171 args.extend('%s=%r' % (k, v) for k, v in self._kwargs.items())
172 return '%s.%s(%s)' % (self.__class__.__module__,
173 self.__class__.__name__,
174 ', '.join(args))
176 def __reduce__(self):
177 if not self._kwargs:
178 return self.__class__, (self._name,) + self._args
179 else:
180 return (
181 partial(self.__class__, self._name, **self._kwargs),
182 self._args
183 )