88# Licence: MIT
99#########################################
1010from functools import cmp_to_key
11+ from typing import Union
1112cmp_func = cmp_to_key
1213
1314
14- # .: multisort :.
15- # spec is a list one of the following
16- # <key>
17- # spec
18- # spec options:
19- # key Property, Key or Index for 'column' in row
20- # reverse: opt - reversed sort (defaults to False)
21- # clean: opt - callback to clean / alter data in 'field'
22- # default: Value to default if None is found or required = False
23- # required: Will not fail if key not found
24- # Use mscol helper to ease passing of variables or just pass lists of args:
25- # spec=mscol('colname1', reverse=True), mscol('colname2', reverse=True)]
26- # -or-
27- # spec=[('colname1', True),('colname2',True)]
28-
29- def mscol (key , reverse = False , clean = None , default = None , required = True ):
30- return (key , reverse , clean , default , required )
31-
32- def multisort (rows , spec , reverse :bool = False ):
33- rows_sorted = None
34- if isinstance (spec , (int , str )): spec = [mscol (spec )]
15+ # multisort - Non-destructive sorter with multi column support
16+ # [rows] list of records to sort
17+ # [spec] list/tuple one of <key> or <spec> or list/tuple(of <spec>)
18+ # items by order in tuple:
19+ # [key] Key or Index for 'column' in row
20+ # [reverse] reversed sort (defaults to False) (opt)
21+ # [clean] callback to clean / alter data in 'field' (opt)
22+ # [default] Value to default if None is found or required = False (opt)
23+ # [required] Will not fail if key not found (opt)
24+ # [reverse] reverse the sort (defaults to False)
25+ # Other:
26+ # mscol: Helper to simplify construction of <spec> record(s) eg:
27+ # multisort(rows, [mscol('colname1', reverse=True),
28+ # mscol('colname2', reverse=True, default=1)]
29+ # # as opposed to:
30+ # multisort(rows, [('colname1', True),
31+ # ('colname2', True, None, 1)]
32+ def multisort (rows : list ,
33+ spec : Union [int , str , list , tuple ] = None ,
34+ reverse : bool = False ):
35+
36+ if spec is None :
37+ _clone = rows [:]
38+ _clone .sort (reverse = reverse )
39+ return _clone
40+
41+ rows_sorted = None
42+ if isinstance (spec , (int , str )):
43+ spec = [mscol (spec )]
3544 for spec_c in reversed (spec ):
3645 spec_c_t = type (spec_c )
37- if spec_c_t in (int , str ):
38- (key , col_reverse , clean , default , required ) = (spec_c , False , None , None , True )
46+ if spec_c_t in (int , str ):
47+ (key , col_reverse , clean , default , required ) \
48+ = (spec_c , False , None , None , True )
3949 else :
40- assert spec_c_t in (list , tuple ), f"Invalid spec. Got: { spec_c_t .__name__ } . See docs"
41- if len (spec_c ) < 5 : spec_c = mscol (* spec_c )
50+ assert spec_c_t in (list , tuple ), \
51+ f"Invalid spec. Got: { spec_c_t .__name__ } . See docs"
52+ if len (spec_c ) < 5 :
53+ spec_c = mscol (* spec_c )
4254 (key , col_reverse , clean , default , required ) = spec_c
43- def _sort_column (row ): # Throws MSIndexError, MSKeyError
44- ex1 = None
55+
56+ def _sort_column (row ): # Throws MSIndexError, MSKeyError
57+ ex1 = None
4558 try :
4659 try :
47- v = row [key ]
60+ v = row [key ]
4861 except Exception as ex :
4962 ex1 = ex
5063 v = getattr (row , key )
5164 except Exception as ex2 :
52- if isinstance (row , (list , tuple )): # failfast for tuple / list
65+ if isinstance (row , (list , tuple )): # failfast for tuple / list
5366 raise MSIndexError (ex1 .args [0 ], row , ex1 )
5467
5568 elif required :
5669 raise MSKeyError (ex2 .args [0 ], row , ex2 )
5770
5871 else :
59- if default is None :
72+ if default is None :
6073 v = None
6174 else :
6275 v = default
6376
6477 if default :
65- if v is None : return default
78+ if v is None :
79+ return default
6680 return clean (v ) if clean else v
6781 else :
68- if v is None : return True , None
69- if clean : return False , clean (v )
82+ if v is None :
83+ return True , None
84+ if clean :
85+ return False , clean (v )
7086 return False , v
7187
7288 try :
7389 if rows_sorted is None :
74- rows_sorted = sorted (rows , key = _sort_column , reverse = col_reverse )
90+ rows_sorted = sorted (rows ,
91+ key = _sort_column ,
92+ reverse = col_reverse )
7593 else :
7694 rows_sorted .sort (key = _sort_column , reverse = col_reverse )
7795
78-
7996 except Exception as ex :
80- msg = None
81- row = None
82- key_is_int = isinstance (key , int )
97+ sb = []
98+ msg = None
99+ row = None
100+ key_is_int = isinstance (key , int )
83101
84102 if isinstance (ex , MultiSortBaseExc ):
85103 row = ex .row
86104 if isinstance (ex , MSIndexError ):
87- msg = f"Invalid index for { row .__class__ .__name__ } row of length { len (row )} . Row: { row } "
88- else : # MSKeyError
89- msg = f"Invalid key/property for row of type { row .__class__ .__name__ } . Row: { row } "
105+ sb .append (f"Invalid index for { row .__class__ .__name__ } " )
106+ sb .append (f" row of length { len (row )} . Row: { row } " )
107+ else : # MSKeyError
108+ sb .append ("Invalid key/property for row of type" )
109+ sb .append (f" { row .__class__ .__name__ } . Row: { row } " )
110+ msg = ' ' .join (sb )
90111 else :
91112 msg = ex .args [0 ]
92-
93- raise MultiSortError (f"""Sort failed on key { "int" if key_is_int else "str '" } { key } { '' if key_is_int else "' " } . { msg } """ , row , ex )
94113
114+ msg = "Sort failed on key {0}{1}{2}. {3}" .format (
115+ "int" if key_is_int else "str '" ,
116+ key ,
117+ '' if key_is_int else "' " ,
118+ msg )
119+ raise MultiSortError (msg , row , ex )
95120
96121 return reversed (rows_sorted ) if reverse else rows_sorted
97122
123+
124+ def mscol (key , reverse = False , clean = None , default = None , required = True ):
125+ return (key , reverse , clean , default , required )
126+
127+
98128class MultiSortBaseExc (Exception ):
99129 def __init__ (self , msg , row , cause ):
100130 self .message = msg
101131 self .row = row
102132 self .cause = cause
103-
133+
134+
104135class MSIndexError (MultiSortBaseExc ):
105136 def __init__ (self , msg , row , cause ):
106137 super (MSIndexError , self ).__init__ (msg , row , cause )
107138
139+
108140class MSKeyError (MultiSortBaseExc ):
109141 def __init__ (self , msg , row , cause ):
110142 super (MSKeyError , self ).__init__ (msg , row , cause )
111143
144+
112145class MultiSortError (MultiSortBaseExc ):
113146 def __init__ (self , msg , row , cause ):
114147 super (MultiSortError , self ).__init__ (msg , row , cause )
148+
115149 def __str__ (self ):
116150 return self .message
151+
117152 def __repr__ (self ):
118153 return f"<MultiSortError> { self .__str__ ()} "
119154
120- # For use in the multi column sorted syntax to sort by 'grade' and then 'attend' descending
121- # dict example:
122- # rows_sorted = sorted(rows, key=lambda o: ((None if o['grade'] is None else o['grade'].lower()), reversor(o['attend'])), reverse=True)
123- # object example:
124- # rows_sorted = sorted(rows, key=lambda o: ((None if o.grade is None else o.grade.lower()), reversor(o.attend)), reverse=True)
125- # list, tuple example:
126- # rows_sorted = sorted(rows, key=lambda o: ((None if o[COL_GRADE] is None else o[COL_GRADE].lower()), reversor(o[COL_ATTEND])), reverse=True)
127- # where: COL_GRADE and COL_ATTEND are column indexes for values
155+
156+ # reversor() For use in the multi column sorted
157+ # syntax to sort by 'grade'and then 'attend' descending
158+ # Dict example:
159+ # rows_sorted = sorted(rows,
160+ # key=lambda o: (\
161+ # (None if o['grade'] is None else o['grade'].lower()),\
162+ # reversor(o['attend'])), reverse=True)
163+ # Object example:
164+ # rows_sorted = sorted(rows,
165+ # key = lambda o: ((None if o.grade is None else o.grade.lower()), \
166+ # reversor(o.attend)),
167+ # reverse = True)
168+ # List, Tuple example:
169+ # rows_sorted = sorted(rows, key=lambda o:\
170+ # ((None if o[COL_GRADE] is None else o[COL_GRADE].lower()),
171+ # reversor(o[COL_ATTEND])), reverse=True)
172+ # where: COL_GRADE and COL_ATTEND are column indexes for values
173+
174+
128175class reversor :
129176 def __init__ (self , obj ):
130177 self .obj = obj
178+
131179 def __eq__ (self , other ):
132180 return other .obj == self .obj
181+
133182 def __lt__ (self , other ):
134183 return False if self .obj is None else \
135184 True if other .obj is None else \
136185 other .obj < self .obj
137186
138187
139188def getClassName (o ):
140- return None if o == None else type (o ).__name__
141-
189+ return None if o is None else type (o ).__name__
0 commit comments