Skip to content

Commit 5ff574c

Browse files
v0.1.5
1 parent 2fb3b6f commit 5ff574c

File tree

7 files changed

+342
-178
lines changed

7 files changed

+342
-178
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,29 @@ rows_sorted = sorted(rows_before, key=cmp_func(cmp_student), reverse=True)
232232

233233
<br><br>
234234

235+
### Basic sorting
236+
`multisort` can be used as a basic non-destructive sorter of lists where a traditional sort does so destructively:
237+
```
238+
_orig = [1, 4, 3, 6, 5]
239+
_orig.sort(reverse=True)
240+
```
241+
This will sort `_orig` in-place
242+
243+
In builtin python, to do a non-destructive sort it takes two lines:
244+
```
245+
_orig = [1, 4, 3, 6, 5]
246+
_clone = [:]
247+
_clone.sort(reverse=True)
248+
```
249+
250+
With Multisort its just one line:
251+
```
252+
_orig = [1, 4, 3, 6, 5]
253+
_sorted = multisort(_orig, reverse=True)
254+
```
255+
Where `_orig` is left unchanged
256+
257+
<br>
235258

236259
### `multisort` library Test / Sample files (/tests)
237260
Name|Descr|Other

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "multisort"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
description = "NoneType Safe Multi Column Sorting For Python"
55
license = "MIT"
66
authors = ["Timothy C. Quinn"]

src/multisort/multisort.py

Lines changed: 102 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,134 +8,182 @@
88
# Licence: MIT
99
#########################################
1010
from functools import cmp_to_key
11+
from typing import Union
1112
cmp_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+
98128
class MultiSortBaseExc(Exception):
99129
def __init__(self, msg, row, cause):
100130
self.message = msg
101131
self.row = row
102132
self.cause = cause
103-
133+
134+
104135
class MSIndexError(MultiSortBaseExc):
105136
def __init__(self, msg, row, cause):
106137
super(MSIndexError, self).__init__(msg, row, cause)
107138

139+
108140
class MSKeyError(MultiSortBaseExc):
109141
def __init__(self, msg, row, cause):
110142
super(MSKeyError, self).__init__(msg, row, cause)
111143

144+
112145
class 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+
128175
class 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

139188
def 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

Comments
 (0)