Fixed / overloading when enabling true division.
[tx] / misc.py
1 # -*- coding:utf-8 -*-
2
3 # Misc - Various functions not directly related to TX
4 # Copyright (C) 2005  Frédéric Jolliton <frederic@jolliton.com>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 __all__ = [
21         # Exception
22         'NotReached' ,
23         # Functions
24         'decodeData' ,
25         'iterSingle' ,
26         'identity' ,
27         'constantly' ,
28         'plural' ,
29         'bench' ,
30         'typeOf' ,
31         'shortenText' ,
32         'stringValuesStartsWith' ,
33         'stringValuesCompare' ,
34         'stringValuesLength' ,
35         'isNotANumber' ,
36         'isInfinity' ,
37         'isPositiveInfinity' ,
38         'isNegativeInfinity' ,
39         'isSpecialFloat'
40         ]
41
42 import time
43 import re
44
45 class NotReached( Exception ) :
46
47         '''Error when reaching a point in the program that should'nt in normal
48         condition.'''
49
50         pass
51
52 #
53 # From Appendix F of http://www.w3.org/TR/REC-xml/
54 # "Autodetection of Character Encodings (Non-Normative)"
55 #
56 encodingsBom = [
57         # With a Byte Order Mark:
58         ( '\x00\x00\xfe\xff' , 'UTF-32/BE'   ) , # codecs.BOM_UTF32_BE
59         ( '\xff\xfe\x00\x00' , 'UTF-32/LE'   ) , # codecs.BOM_UTF32_LE
60         ( '\x00\x00\xff\xfe' , 'UTF-32/2143' ) ,
61         ( '\xfe\xff\x00\x00' , 'UTF-32/3412' ) ,
62         ( '\xfe\xff'         , 'UTF-16/BE'   ) , # codecs.BOM_UTF16_BE
63         ( '\xff\xfe'         , 'UTF-16/LE'   ) , # codecs.BOM_UTF16_LE
64         ( '\xef\xbb\xbf'     , 'UTF-8'       )   # codecs.BOM_UTF8
65 ]
66
67 encodingsXml = [
68         # Without a Byte Order Mark:
69         ( '\x00\x00\x00\x3c' , '32BIT/BE'   ) ,
70         ( '\x3c\x00\x00\x00' , '32BIT/LE'   ) ,
71         ( '\x00\x00\x3c\x00' , '32BIT/2143' ) ,
72         ( '\x00\x3c\x00\x00' , '32BIT/3412' ) ,
73         ( '\x00\x3c\x00\x3f' , '16BIT/BE'   ) ,
74         ( '\x3c\x00\x3f\x00' , '16BIT/LE'   ) ,
75         ( '\x3c\x3f\x78\x6d' , '8BIT'       ) ,
76         ( '\x4c\x6f\xa7\x94' , 'EBCDIC'     ) ,
77         ( ''                 , 'UNKNOWN'    )
78 ]
79
80 def guessXmlCharacterEncoding( s ) :
81
82         '''Guess character encoding up to the first 4 characters of string 's'.
83         Return encoding name (or None if no guess can be made) and how many
84         characters to skip to get beginning of real data.'''
85
86         if isinstance( s , unicode ) :
87                 return 'UNICODE' , 0
88         else :
89                 for encoding , name in encodingsBom :
90                         if s[ : len( encoding ) ] == encoding :
91                                 return name , len( encoding )
92                 for encoding , name in encodingsXml :
93                         if s[ : len( encoding ) ] == encoding :
94                                 return name , 0
95                 return None , 0
96
97 rtXmlDeclVersionInfo    = r'''(?:\s+version=(?P<version>(?:'[^']*'|"[^"]*")))'''
98 rtXmlDeclEncodingDecl   = r'''(?:\s+encoding=(?P<encoding>(?:'[^']*'|"[^"]*")))'''
99 rtXmlDeclStandaloneDecl = r'''(?:\s+standalone=(?P<standalone>(?:'[^']*'|"[^"]*")))'''
100 reXmlDecl = re.compile( r'<\?xml(?:%s|%s|%s)*\s*\??>' \
101                                                 % ( rtXmlDeclVersionInfo ,
102                                                         rtXmlDeclEncodingDecl ,
103                                                         rtXmlDeclStandaloneDecl ) )
104 def getXmlEncoding( data ) :
105
106         r = reXmlDecl.search( data )
107         if r is not None :
108                 enc = r.groupdict()[ 'encoding' ]
109                 if enc is not None :
110                         enc = enc[ 1 : -1 ].lower()
111                         return enc
112
113 reLookupMeta  = re.compile( r'<meta\s+([^>]+)>' , re.I )
114 reAttributes  = re.compile( '''(http-equiv|content)=('[^']*'|"[^"]*")''' , re.I )
115 def getHtmlEncoding( data ) :
116
117         p = 0
118         while p < len( data ) :
119                 r = reLookupMeta.search( data , p )
120                 if r is None :
121                         break
122                 attributes = r.group( 1 )
123                 attributes = dict( ( name.lower() , value[ 1 : -1 ].lower() )
124                                                    for name , value
125                                                    in reAttributes.findall( attributes ) )
126                 if attributes.get( 'http-equiv' ) == 'content-type' :
127                         ct = attributes.get( 'content' )
128                         if ct is not None :
129                                 for item in ct.split( ';' )[ 1 : ] :
130                                         item = item.split( '=' )
131                                         if len( item ) == 2 and item[ 0 ].strip().lower() == 'charset' :
132                                                 return item[ 1 ].strip().lower()
133                         break
134                 p = r.end( 0 )
135
136 encodingName = {
137         'UTF-32/BE'   : None ,
138         'UTF-32/LE'   : None ,
139         'UTF-32/2143' : None ,
140         'UTF-32/3412' : None ,
141         'UTF-16/BE'   : 'utf_16_be' ,
142         'UTF-16/LE'   : 'utf_16_le' ,
143         'UTF-8'       : 'utf_8' ,
144         '32BIT/BE'    : None ,
145         '32BIT/LE'    : None ,
146         '32BIT/2143'  : None ,
147         '32BIT/3412'  : None ,
148         'EBCDIC'      : None
149         }
150
151 def decodeData( data ) :
152
153         '''Decode XML/HTML text in 'data' and return an Unicode string,
154         guessing encoding from various way (BOM header, XML declaration or
155         ContentType META element in HTML header.)'''
156
157         defaultEncoding = 'iso-8859-1'
158         encoding , skip = guessXmlCharacterEncoding( data )
159         if encoding == 'UNICODE' :
160                 data = data[ skip : ] # Do nothing, already Unicode.
161         elif encoding in encodingName :
162                 name = encodingName[ encoding ]
163                 if name is None :
164                         raise Error( 'Unsupported encoding %s' % encoding )
165                 data = data[ skip : ].decode( encoding )
166         elif encoding == '16BIT/BE' :
167                 data = decode16bitBe( data[ skip : ] )
168         elif encoding == '16BIT/LE' :
169                 data = decode16bitLe( data[ skip : ] )
170         elif encoding in [ '8BIT' , 'UNKNOWN' ] :
171                 data = data[ skip : ]
172                 encoding = getXmlEncoding( data )
173                 if encoding is not None :
174                         data = data.decode( encoding )
175                 else :
176                         encoding = getHtmlEncoding( data ) or defaultEncoding
177                         data = data.decode( encoding )
178         else :
179                 raise Error( 'Unexpected encoding %s' % encoding )
180         return data
181
182 #----------------------------------------------------------------------------
183
184 def isNotANumber( n ) :
185
186         '''Test if 'n' is a "not-a-number" float.'''
187
188         return isinstance( n , float ) and n != n
189
190 def isInfinity( n ) :
191
192         '''Test if 'n' is either a positive or negative infinity float.'''
193
194         return isinstance( n , float ) and n != 0.0 and 2 * n == n
195
196 def isPositiveInfinity( n ) :
197
198         return isInfinity( n ) and n > 0
199
200 def isNegativeInfinity( n ) :
201
202         return isInfinity( n ) and n < 0
203
204 def isSpecialFloat( n ) :
205
206         '''Test if 'n' is one of the special float value (nan,+inf,-inf).'''
207
208         return isinstance( n , float ) \
209                 and ( n != n                           # NaN
210                           or ( n != 0.0 and 2 * n == n ) ) # +inf or -inf
211
212 #----------------------------------------------------------------------------
213
214 def stringValuesLength( s ) :
215
216         '''Compute length of string 's'.'''
217
218         if isinstance( s , basestring ) :
219                 return len( s )
220         else :
221                 return sum( len( item ) for item in s )
222
223 def stringValuesStartsWith( s1 , s2 ) :
224
225         '''
226         Compare strings 's1' and 's2'. This function also accept iterators
227         returning string. This is usefull to compare two nodes without
228         first converting them to strings for performance reason.
229         '''
230
231         if isinstance( s1 , basestring ) :
232                 if isinstance( s2 , basestring ) :
233                         return s1.startswith( s2 )
234                 else :
235                         s1 = iterSingle( s1 )
236         elif isinstance( s2 , basestring ) :
237                 s2 = iterSingle( s2 )
238
239         result = 0
240         d1 = ''
241         d2 = ''
242         while 1 :
243                 if not d1 :
244                         try :
245                                 d1 = s1.next()
246                         except StopIteration :
247                                 d1 = ''
248                 if not d2 :
249                         try :
250                                 d2 = s2.next()
251                         except StopIteration :
252                                 d2 = ''
253                 if not d1 :
254                         if not d2 :
255                                 r = True
256                         else :
257                                 r = False
258                         break
259                 elif not d2 :
260                         r = True
261                         break
262                 m = min( len( d1 ) , len( d2 ) )
263                 r = cmp( d1[ : m ] , d2[ : m ] )
264                 if r != 0 :
265                         r = False
266                         break
267                 d1 = d1[ m : ]
268                 d2 = d2[ m : ]
269         return r
270
271 def stringValuesCompare( s1 , s2 ) :
272
273         '''
274         Compare strings 's1' and 's2'. This function also accept iterators
275         returning string. This is usefull to compare two nodes without
276         first converting them to strings for performance reason.
277         '''
278
279         if isinstance( s1 , basestring ) :
280                 if isinstance( s2 , basestring ) :
281                         return cmp( s1 , s2 )
282                 else :
283                         s1 = iterSingle( s1 )
284         elif isinstance( s2 , basestring ) :
285                 s2 = iterSingle( s2 )
286
287         result = 0
288         d1 = ''
289         d2 = ''
290         while 1 :
291                 if not d1 :
292                         try :
293                                 d1 = s1.next()
294                         except StopIteration :
295                                 d1 = ''
296                 if not d2 :
297                         try :
298                                 d2 = s2.next()
299                         except StopIteration :
300                                 d2 = ''
301                 if not d1 :
302                         if not d2 :
303                                 r = 0
304                         else :
305                                 r = -1
306                         break
307                 elif not d2 :
308                         r = 1
309                         break
310                 m = min( len( d1 ) , len( d2 ) )
311                 r = cmp( d1[ : m ] , d2[ : m ] )
312                 if r != 0 :
313                         break
314                 d1 = d1[ m : ]
315                 d2 = d2[ m : ]
316         return r
317
318 def iterSingle( o ) :
319
320         '''Iterate over singleton 'o'.'''
321
322         yield o
323
324 def identity( o ) :
325
326     '''Return its argument.'''
327
328     return o
329
330 def constantly( value ) :
331
332     '''Construct a function returning the same value all the time,
333     discarding its argument.'''
334
335     return lambda o : value
336
337 def plural( n , s = 's' , alt = '' ) :
338
339         '''
340         Return 's' if 'n' is equal to 1, otherwise return 'alt'.
341
342         '%d bottle%s' % ( n , plural( n ) )
343         '%d child%s' % ( n , plural( n , 'ren' ) )
344         '%d %s' % ( n , plural( n , 'children' , 'child' ) )
345         '''
346
347         if abs( n ) != 1 :
348                 return s
349         else :
350                 return alt
351
352 def bench( name , f , n = 1 ) :
353
354         '''Evaluate 'n' times the function 'f', printing
355         timing details including 'name' in the output.
356
357         The result of the last evaluation of function 'f' is
358         returned.'''
359
360         if n == 1 :
361                 t = time.time()
362                 # Avoid for/in overhead for n = 1 for better timing
363                 # results (if that really matter..)
364                 r = f()
365                 d = time.time() - t
366         else :
367                 t = time.time()
368                 for i in range( n ) :
369                         r = f()
370                 d = time.time() - t
371         print 'BENCH[%s] (x%d) %fs' % ( name , n , d / n )
372         return r
373
374 def typeOf( o ) :
375
376         '''Return type name of object 'o'.'''
377
378         # FIXME: Find a better way to extract type name !
379
380         r = `o.__class__`
381         if r.startswith( '<' ) and r.endswith( '>' ) :
382                 r = r[ 1 : -1 ]
383         r = r.split()
384         if len( r ) >= 2 :
385                 r = r[ 1 ]
386                 if r.startswith( '"' ) or r.startswith( "'" ) :
387                         r = r[ 1 : -1 ]
388         elif len( r ) >= 1 :
389                 r = r[ 0 ]
390         else :
391                 r = '???'
392         return r
393
394 def shortenText( string , size , suffix = '[..]' ) :
395
396         '''Shorten text to 'size' characters if 'string' is larger.
397         In such case, the suffix 'suffix' is pasted at the end of
398         string. Note that the result contains at least as much as
399         characters in suffix.
400
401         >>> shortenText( 'foobarbaz' , 12 )
402         'foobarbaz'
403         >>> shortenText( 'foobarbaz' , 8 )
404         'foob[..]'
405         >>> shortenText( 'foobarbaz' , 2 )
406         '[..]'
407         '''
408
409         if size is None :
410                 result = string
411         elif len( string ) > size :
412                 if len( string ) <= len( suffix ) :
413                         result = suffix
414                 else :
415                         result = string[ : max( 0 , size - len( suffix ) ) ] + suffix
416         else :
417                 result = string
418         return result
419
420 # Local Variables:
421 # tab-width: 4
422 # python-indent: 4
423 # End: