Hide keyboard shortcuts

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 

3 

4""" 

5=============================================================================== 

6 

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

8 

9 This file is part of cardinal_pythonlib. 

10 

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 

14 

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

16 

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. 

22 

23=============================================================================== 

24 

25**Support functions for sorting.** 

26 

27""" 

28 

29from functools import partial, total_ordering 

30import re 

31from typing import Any, List, Union 

32 

33 

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 

38 

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 

46 

47 

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 

53  

54 Example (as per the source above): 

55  

56 .. code-block:: python 

57  

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'] 

70  

71 """ # noqa 

72 return [atoi(c) for c in re.split(r'(\d+)', text)] 

73 

74 

75# ============================================================================= 

76# Sorting where None counts as the minimum 

77# ============================================================================= 

78 

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 

86 

87 def __eq__(self, other: Any) -> bool: 

88 return self is other 

89 

90 def __str__(self) -> str: 

91 return "MinType" 

92 

93 

94MINTYPE_SINGLETON = MinType() 

95 

96 

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') 

105 

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('.') 

112 

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 

119 

120 self._call = func 

121 else: 

122 self._attrs = (attr,) + attrs 

123 # MODIFIED HERE: 

124 getters = tuple(map(attrgetter_nonesort, self._attrs)) 

125 

126 def func(obj): 

127 return tuple(getter(obj) for getter in getters) 

128 

129 self._call = func 

130 

131 def __call__(self, obj): 

132 return self._call(obj) 

133 

134 def __repr__(self): 

135 return '%s.%s(%s)' % (self.__class__.__module__, 

136 self.__class__.__qualname__, 

137 ', '.join(map(repr, self._attrs))) 

138 

139 def __reduce__(self): 

140 return self.__class__, self._attrs 

141 

142 

143# noinspection PyPep8Naming 

144class methodcaller_nonesort: 

145 """ 

146 As per :class:`attrgetter_nonesort` (q.v.), but for ``methodcaller``. 

147 """ 

148 __slots__ = ('_name', '_args', '_kwargs') 

149 

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 

160 

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 

167 

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)) 

175 

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 )