[utils] Allow attrsorted to work on dicts
[blip:blinq.git] / blinq / utils.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2008-2010  Shaun McCance  <shaunm@gnome.org>
3 #
4 # Blinq is free software; you can redistribute it and/or modify it under the
5 # terms of the GNU General Public License as published by the Free Software
6 # Foundation; either version 2 of the License, or (at your option) any later
7 # version.
8 #
9 # Blinq is distributed in the hope that it will be useful, but WITHOUT ANY
10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
12 # details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with Blinq; if not, write to the Free Software Foundation, 59 Temple Place,
16 # Suite 330, Boston, MA  0211-1307  USA.
17 #
18
19 """Various utility functions"""
20
21 import codecs
22
23 def utf8dec (s):
24     """
25     Decode a string to UTF-8, or don't if it already is
26     """
27     if isinstance(s, str):
28         return codecs.getdecoder('utf-8')(s, 'replace')[0]
29     else:
30         return s
31
32 def utf8enc (s):
33     """
34     Encode a unicode object to UTF-8, or don't if it already is
35     """
36     if isinstance(s, unicode):
37         return codecs.getencoder('utf-8')(s)[0]
38     else:
39         return s
40
41 def attrsorted (lst, *attrs):
42     """
43     Sort a list of objects based on given object attributes
44
45     The list is first sorted by comparing the value of the first attribute
46     for each object.  When the values for two objects are equal, they are
47     sorted according to the second attribute, third attribute, and so on.
48
49     An attribute may also be a tuple or list.  In this case, the value to
50     be compared for an object is obtained by successively extracting the
51     attributes.  For example, an attribute of ('foo', 'bar') would use the
52     value of obj.foo.bar for the object obj.
53
54     All string comparisons are case-insensitive.
55     """
56     def attrget (obj, attr):
57         """Get an attribute or list of attributes from an object"""
58         if isinstance (attr, tuple) or isinstance (attr, list):
59             if len(attr) > 1:
60                 return attrget (attrget (obj, attr[0]), attr[1:])
61             else:
62                 return attrget (obj, attr[0])
63         elif isinstance (obj, dict):
64             return obj.get (attr)
65         elif isinstance (attr, basestring):
66             return getattr (obj, attr)
67         elif isinstance (attr, int):
68             return obj.__getitem__ (attr)
69         elif isinstance (obj, basestring):
70             return obj.lower()
71         else:
72             return obj
73
74     def lcmp (val1, val2):
75         """Compare two objects, case-insensitive if strings"""
76         if isinstance (val1, unicode):
77             v1 = val1.lower()
78         elif isinstance (val1, basestring):
79             v1 = val1.decode('utf-8').lower()
80         else:
81             v1 = val1
82         if isinstance (val2, unicode):
83             v2 = val2.lower()
84         elif isinstance (val1, basestring):
85             v2 = val2.decode('utf-8').lower()
86         else:
87             v2 = val2
88         return cmp (v1, v2)
89
90     def attrcmp (val1, val2, attrs):
91         """Compare two objects based on some attributes"""
92         attr = attrs[0]
93         try:
94             attrf = attr[0]
95         except:
96             attrf = None
97         if attrf == '-':
98             attr = attr[1:]
99             cmpval = lcmp (attrget(val2, attr), attrget(val1, attr))
100         else:
101             cmpval = lcmp (attrget(val1, attr), attrget(val2, attr))
102         if cmpval == 0 and len(attrs) > 1:
103             return attrcmp (val1, val2, attrs[1:])
104         else:
105             return cmpval
106
107     return sorted (lst, lambda val1, val2: attrcmp (val1, val2, attrs))