Fixed / overloading when enabling true division.
[tx] / xpathfn.py
1 # -*- coding:utf-8 -*-
2
3 # XPath/XQuery functions
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 # XO = http://www.w3.org/TR/xquery-operators/
21
22 from __future__ import division
23
24 # __all__ = [
25 #       'functions'
26 #       ]
27
28 import re
29 import operator as op
30 import string
31 import math
32 import time
33
34 from error import Error, XPathError
35 from sequence import *
36 from sequence import _Empty, _EmptyString, _False, _True, _Boolean
37 from nodes import *
38 from misc import typeOf, identity, stringValuesStartsWith, stringValuesCompare, stringValuesLength
39 from misc import isNotANumber, isInfinity, isPositiveInfinity, isNegativeInfinity, isSpecialFloat
40 from iterators import iterDescendantOrSelfFull
41
42 functions = {}
43
44 _debug = True
45
46 _NAN    = float( 'NaN' )
47 _POSINF = float( 'inf' )
48 _NEGINF = float( '-inf' )
49
50 _Nan = Sequence( _NAN )
51 _PositiveInfinity = Sequence( _POSINF )
52 _NegativeInfinity = Sequence( _NEGINF )
53
54 def functionNames( qname ) :
55
56         '''
57         'foo'     -> ['foo']
58         'bar:foo' -> ['bar:foo']
59         'fn:foo'  -> ['fn:foo', 'foo']
60         '''
61
62         names = [ qname ]
63         parts = qname.split( ':' )
64         if len( parts ) == 1 :
65                 pass
66         elif len( parts ) == 2 :
67                 if parts[ 0 ] == 'fn' :
68                     names.append( parts[ 1 ] )
69         else :
70                 raise Error( 'invalid function name %r' % qname )
71         return names
72
73 def registerFast( qname , hold = False ) :
74
75         names = functionNames( qname )
76
77         def fun( f ) :
78                 f.hold = hold
79                 if _debug :
80                         def wrapper( *args , **kwargs ) :
81                                 result = f( *args , **kwargs )
82                                 if not isinstance( result , Sequence ) :
83                                         print 'WARNING: Function %r doesn\'t return a Sequence !' % ( qname , )
84                                         result = Sequence( result )
85                                 return result
86                         wrapper.hold = False
87                         wrapper.__name__ = 'wrapper_%s' % ( f.__name__ or '<??>' )
88                         wrapper.__dict__ = f.__dict__
89                         wrapper.__doc__  = f.__doc__
90                 else :
91                         wrapper = f
92                 for name in names :
93                         functions[ name ] = wrapper
94                 return wrapper
95         return fun
96
97 def register( qname ) :
98
99         names = functionNames( qname )
100
101         def fun( f ) :
102                 #
103                 # Wrapper ensure than return value is a Sequence
104                 #
105                 def wrapper( *args , **kwargs ) :
106                         result = f( *args , **kwargs )
107                         if not isinstance( result , Sequence ) :
108                                 result = Sequence( result )
109                         return result
110                 wrapper.hold = False
111                 wrapper.__name__ = 'wrapper_%s' % ( f.__name__ or '<??>' )
112                 wrapper.__dict__ = f.__dict__
113                 wrapper.__doc__  = f.__doc__
114                 for name in names :
115                         functions[ name ] = wrapper
116                 return wrapper
117         return fun
118
119 #----------------------------------------------------------------------------
120
121 def isBoolean( item ) :
122
123         return isinstance( item , bool )
124
125 def isNumber( item ) :
126
127         return isinstance( item , ( int , float , long ) ) \
128                 and not isinstance( item , bool )
129
130 def isString( item ) :
131
132         return isinstance( item , basestring )
133
134 def isAtomic( item ) :
135
136         return not isinstance( item , Node )
137
138 def isAttribute( item ) :
139
140         return isinstance( item , Attribute )
141
142 def isElement( item ) :
143
144         return isinstance( item , Element )
145
146 def isDocument( item ) :
147
148         return isinstance( item , Document )
149
150 def isNode( item ) :
151
152         return isinstance( item , Node )
153
154 def isItem( item ) :
155
156         return True
157
158 #----------------------------------------------------------------------------
159
160 def ZeroOrOne( t ) :
161
162         def fun( sequence ) :
163                 if len( sequence ) == 0 :
164                         return None
165                 elif len( sequence ) == 1 :
166                         item = sequence[ 0 ]
167                         if t( item ) :
168                                 return item
169                 raise XPathError( 'XPTY0004' , 'Expected a sequence of 0 or 1 item (%r)' % t )
170         return fun
171
172 def OneOrMore( t ) :
173
174         def fun( sequence ) :
175                 if len( sequence ) >= 1 :
176                         for item in sequence :
177                                 if not t( item ) :
178                                         break
179                         else :
180                                 return sequence
181                 raise XPathError( 'XPTY0004' , 'Expected a sequence of 0 or 1 item (%r)' % t )
182         return fun
183
184 def One( t ) :
185
186         def fun( sequence ) :
187                 if len( sequence ) == 1 :
188                         item = sequence[ 0 ]
189                         if t( item ) :
190                                 return item
191                 raise XPathError( 'XPTY0004' , 'Expected a sequence of 1 item (%r)' % t )
192         return fun
193
194 def zeroOrMoreNodes( sequence ) :
195
196         if sequence.type not in ( SEQUENCE_EMPTY , SEQUENCE_NODES ) :
197                 raise XPathError( 'XPTY0004' , 'Expected a sequence of nodes' ) # really XPTY0004 here ?
198         return sequence
199
200 zeroOrOneNode  = ZeroOrOne( isNode )
201 zeroOrOneItem  = ZeroOrOne( isItem )
202 zeroOrMoreItem = identity
203 oneOrMoreItem  = OneOrMore( isItem )
204 oneNumber      = One( isNumber )
205 oneAtomic      = One( isAtomic )
206 oneItem        = One( isItem )
207 oneNode        = One( isNode )
208
209 #----------------------------------------------------------------------------
210
211 def atomize( item ) :
212
213         '''Convert node to string, and keep inchanged atomic value.'''
214
215         if isNode( item ) :
216                 return item.dmStringValue()
217         else :
218                 return item
219
220 def asString( item ) :
221
222         '''Convert item (node or atomic value) to a string.'''
223
224         result = atomize( item )
225         if result is None :
226                 result = ''
227         elif not isinstance( result , basestring ) :
228                 result = str( result )
229         return result
230
231 def asStringOrStream( item ) :
232
233         '''Return either a string if item is note a node,
234         or a string iterator if item is a node.'''
235
236         if isNode( item ) :
237                 return item.iterStringValue()
238         else :
239                 return asString( item )
240
241 def sequenceToData( sequence ) :
242
243         '''Convert sequence to a tuple of atomized items.'''
244
245         return tuple( atomize( item ) for item in sequence )
246
247 def asNumber( item ) :
248
249         '''
250         Convert item (node or atomic value) as a number.
251         Return None if no conversion can be done.
252         '''
253
254         if isNumber( item ) :
255                 return item
256         else :
257                 item = atomize( item )
258                 try :
259                         return int( item )
260                 except :
261                         try :
262                                 r = float( item )
263                                 if isSpecialFloat( r ) :
264                                         r = _NAN # Don't allow inf/-inf/NaN construction from string
265                                 return r
266                         except :
267                                 return _NAN
268
269 def asInteger( item ) :
270
271         n = asNumber( item )
272         try :
273                 return int( n )
274         except :
275                 return long( n )
276
277 def asBoolean( item ) :
278
279         return sequenceToBoolean( ( item , ) )
280
281 def oneAtomizedItem( sequence ) :
282
283         return atomize( oneItem( sequence ) )
284
285 #----------------------------------------------------------------------------
286
287 # Used to implement XO/15.1.1
288 def sequenceToBoolean( sequence ) :
289
290         '''Compute the effective boolean value of the sequence 'sequence'.'''
291
292         if not sequence :
293                 return False
294         else :
295                 item = sequence[ 0 ]
296                 if isNode( item ) :
297                         return True
298                 elif len( sequence ) == 1 :
299                         if isBoolean( item ) :
300                                 return item
301                         elif isString( item ) :
302                                 return bool( item )
303                         elif isNumber( item ) :
304                                 return item != 0
305         raise XPathError( 'FORG0006' , 'Cannot convert sequence to boolean' )
306
307 #--[ Extensions ]------------------------------------------------------------
308
309 @registerFast( 'ext:descendant-attribute' )
310 def extDescendantAttribute( context , arg ) :
311
312         '''Find all attributes with name 'arg' of context node and its
313         descendant.'''
314
315         name = arg[ 0 ]
316         item = context.item
317         if isDocument( item ) :
318                 if name == '*' :
319                         attrs = sum( item.attributesByName.values() , [] )
320                         attrs.sort( lambda a , b : cmp( a.position , b.position ) )
321                         return Sequence( attrs )
322                 else :
323                         return Sequence( item.attributesByName.get( name , () ) )
324         else :
325                 if name == '*' :
326                         return Sequence( attribute
327                                                          for attribute in iterDescendantOrSelfFull( item )
328                                                          if isAttribute( attribute ) )
329                 else :
330                         return Sequence( attribute
331                                                          for attribute in iterDescendantOrSelfFull( item )
332                                                          if isAttribute( attribute ) and attribute.name == name )
333
334 @registerFast( 'ext:filter-by-id' )
335 def extFilterById( context , arg ) :
336
337         arg = asString( oneAtomizedItem( arg ) )
338         item = context.item
339         if isElement( item ) :
340                 for attribute in item.attributes :
341                         if attribute.name == 'id' and attribute.dmStringValue() == arg :
342                                 return Sequence( item )
343         return _Empty
344
345 #--[ XPath/XQuery functions ]------------------------------------------------
346
347 # XO/2.1
348 @registerFast( 'fn:node-name' )
349 def fnNodeName( context , arg ) :
350
351         node = zeroOrOneNode( arg )
352         if node is None :
353                 return _Empty
354         else :
355                 return Sequence( node.dmNodeName() )
356
357 # XO/2.3
358 @registerFast( 'fn:string' )
359 def fnString( context , arg = None ) :
360
361         if arg is None :
362                 item = context.item
363                 if item is None :
364                         raise XPathError( 'FONC0001' , 'Context item undefined' )
365         else :
366                 item = zeroOrOneItem( arg )
367                 if item is None :
368                         return _EmptyString
369         return Sequence( asString( item ) )
370
371 @registerFast( 'fn:data' )
372 def fnData( context , arg ) :
373
374         return Sequence( sequenceToData( arg ) )
375
376 @registerFast( 'fn:document-uri' )
377 def fnDocumentUri( context , node ) :
378
379         node = zeroOrOneNode( node )
380         if node is None :
381                 return _Empty
382         else :
383                 if isNode( node ) :
384                         return Sequence( node.dmDocumentUri() )
385                 else :
386                         return _Empty
387
388 # XO/4
389 @registerFast( 'fn:trace' )
390 def fnTrace( context , value , label ) :
391
392         label = atomize( oneItem( label ) )
393         print '[TRACE] %s = %s' % ( label , value )
394         return Sequence( value )
395
396 # XO/6.4.1
397 @registerFast( 'fn:abs' )
398 def fnAbs( context , arg ) :
399
400         if not arg :
401                 return arg
402         else :
403                 item = oneItem( arg )
404                 return Sequence( abs( asNumber( item ) ) )
405
406 # XO/6.4.2
407 @registerFast( 'fn:ceiling' )
408 def fnCeiling( context , arg ) :
409
410         if not arg :
411                 return arg
412         else :
413                 item = asNumber( oneItem( arg ) )
414                 if item == _POSINF :
415                         return _PositiveInfinity
416                 elif item == _NEGINF :
417                         return _NegativeInfinity
418                 else :
419                         return Sequence( math.ceil( item ) )
420
421 # XO/6.4.3
422 @registerFast( 'fn:floor' )
423 def fnFloor( context , arg ) :
424
425         if not arg :
426                 return arg
427         else :
428                 item = asNumber( oneItem( arg ) )
429                 if item == _POSINF :
430                         return _PositiveInfinity
431                 elif item == _NEGINF :
432                         return _NegativeInfinity
433                 else :
434                         return Sequence( math.floor( item ) )
435
436 # XO/6.4.4
437 @registerFast( 'fn:round' )
438 def fnRound( context , arg ) :
439
440         if not arg :
441                 return arg
442         else :
443                 item = asNumber( oneItem( arg ) )
444                 if item == _POSINF :
445                         return _PositiveInfinity
446                 elif item == _NEGINF :
447                         return _NegativeInfinity
448                 else :
449                         return Sequence( math.floor( item + 0.5 ) )
450
451 # XO/7.2.1
452 @registerFast( 'fn:codepoints-to-string' )
453 def fnCodepointsToString( context , arg ) :
454
455         return Sequence( ''.join( map( lambda item : unichr( asInteger( item ) ) , arg ) ) )
456
457 # XO/7.2.2
458 @registerFast( 'fn:string-to-codepoints' )
459 def fnStringToCodepoints( context , arg ) :
460
461         item = zeroOrOneItem( arg )
462         if item is None :
463                 return _Empty
464         else :
465                 item = asString( item )
466                 if not item :
467                         return _Empty
468                 else :
469                         return Sequence( map( ord , asString( item ) ) )
470
471 # XO/7.4.1
472 @registerFast( 'fn:concat' )
473 def fnConcat( context , *arg ) :
474
475         return Sequence( ''.join( map( lambda item : asString( zeroOrOneItem( item ) or '' ) , arg ) ) )
476
477 # XO/7.4.2
478 @registerFast( 'fn:string-join' )
479 def fnStringJoin( context , arg1 , arg2 ) :
480
481         arg2 = oneItem( arg2 )
482         if not arg1 :
483                 return _EmptyString
484         else :
485                 return Sequence( asString( arg2 ).join( map( asString , arg1 ) ) )
486
487 # XO/7.4.3
488 @registerFast( 'fn:substring' )
489 def fnSubstring( context , sourceString , startingLoc , length = None ) :
490
491         string = zeroOrOneItem( sourceString )
492         if string is None :
493                 return _EmptyString
494         else :
495                 string = asString( string )
496                 startingLoc = oneItem( startingLoc )
497                 a = asNumber( startingLoc )
498                 if length is None :
499                         b = _POSINF
500                 else :
501                         b = asNumber( oneItem( length ) )
502                 if isNaN( a ) or isNan( b ) :
503                         return _EmptyString
504                 elif isNegativeInfinity( a ) and isPositiveInfinity( b ) :
505                         return _EmptyString # because a + b is NaN
506                 elif isPositiveInfinity( a ) :
507                         return _EmptyString
508                 elif b <= 0 :
509                         return _EmptyString
510                 else :
511                         if isNegativeInfinity( a ) :
512                                 a = 1
513                         else :
514                                 a = math.floor( a + 0.5 )
515                         if isPositiveInfinity( b ) :
516                                 return Sequence( string[ max( 0 , int( a - 1 ) ) : ] )
517                         else :
518                                 b = math.floor( b + 0.5 )
519                                 return Sequence( string[ max( 0 , int( a - 1 ) ) : max( 0 , int( a + b - 1 ) ) ] )
520
521 # FO/7.4.4
522 @registerFast( 'fn:string-length' )
523 def fnStringLength( context , arg = None ) :
524
525         if arg is None :
526                 item = context.item
527                 if item is None :
528                         raise XPathError( 'FONC0001' , 'Context item undefined' )
529         else :
530                 item = zeroOrOneItem( arg )
531         if item is None :
532                 return Sequence( 0 )
533         else :
534                 return Sequence( stringValuesLength( asStringOrStream( item ) ) )
535
536 # XO/7.4.5
537 @registerFast( 'fn:normalize-space' )
538 def fnNormalizeSpace( context , arg = None ) :
539
540         if arg is None :
541                 item = context.item
542                 if item is None :
543                         raise XPathError( 'FONC0001' , 'Context item undefined' )
544                 item = asString( item )
545         else :
546                 item = zeroOrOneItem( arg )
547                 if item is None :
548                         return _EmptyString
549                 item = asString( item )
550         if not item :
551                 return _EmptyString
552         else :
553                 return Sequence( ' '.join( item.split() ) )
554
555 # FO/7.4.7
556 @registerFast( 'fn:upper-case' )
557 def fnUpperCase( context , arg ) :
558
559         arg = zeroOrOneItem( arg )
560         if arg is None :
561                 return _EmptyString
562         else :
563                 return Sequence( asString( arg ).upper() )
564
565 # FO/7.4.8
566 @registerFast( 'fn:lower-case' )
567 def fnLowerCase( context , arg ) :
568
569         arg = zeroOrOneItem( arg )
570         if arg is None :
571                 return _EmptyString
572         else :
573                 return Sequence( asString( arg ).lower() )
574
575 # FO/7.4.9
576 @registerFast( 'fn:translate' )
577 def fnTranslate( context , arg , mapString , transString ) :
578
579         arg = zeroOrOneItem( arg )
580         if arg is None :
581                 return _EmptyString
582         else :
583                 mapString = asString( oneItem( mapString) )
584                 transString = asString( oneItem( transString ) )
585                 # FIXME: Slow !
586                 result = ''
587                 for ch in arg :
588                         p = mapString.find( ch )
589                         if p == -1 :
590                                 result += ch
591                         elif p >= len( transString ) :
592                                 pass
593                         else :
594                                 result += transString[ p ]
595                 return Sequence( result )
596
597 # [XF] 7.4.10
598 _escapeUri1 = string.letters + string.digits + '%#-_.!~*"()'
599 _escapeUri2 = _escapeUri1 + ';/?:@&=+$,[]'
600
601 @registerFast( 'fn:escape-uri' )
602 def fnEscapeUri( context , uriPart , escapeReserved ) :
603
604         uriPart = zeroOrOneItem( uriPart )
605         if uriPart is None :
606                 return _EmptyString
607         else :
608                 uriPart = asString( uriPart )
609                 escapeReserved = asBoolean( oneItem( escapeReserved ) )
610
611                 # FIXME: Slow !
612                 result = ''
613                 if isinstance( uriPart , unicode ) :
614                         uriPart = uriPart.encode( 'utf8' )
615                 if escapeReserved :
616                         for ch in uriPart :
617                                 if ch not in _escapeUri1 :
618                                         result += '%%%02X' % ord( ch )
619                                 else :
620                                         result += ch
621                 else :
622                         for ch in uriPart :
623                                 if ch not in _escapeUri2 :
624                                         result += '%%%02X' % ord( ch )
625                                 else :
626                                         result += ch
627                 return Sequence( result )
628
629 # XO/7.5.1
630 @registerFast( 'fn:contains' )
631 def fnContains( context , arg1 , arg2 , collation = None ) :
632
633         if collation is not None :
634                 raise XPathError( 'FOCH0004' , 'Collation not supported' )
635         arg1 = zeroOrOneItem( arg1 )
636         arg2 = zeroOrOneItem( arg2 )
637         if arg1 is arg2 :
638                 return _True
639         else :
640                 arg2 = asString( arg2 )
641                 if arg2 == '' :
642                         return _True
643                 arg1 = asString( arg1 )
644                 if arg1 == '' :
645                         return _False
646                 return _Boolean[ arg2 in arg1 ]
647
648 # XO/7.5.2
649 @registerFast( 'fn:starts-with' )
650 def fnStartsWith( context , arg1 , arg2 , collation = None ) :
651
652         if collation is not None :
653                 raise XPathError( 'FOCH0004' , 'Collation not supported' )
654         arg1 = zeroOrOneItem( arg1 )
655         if arg1 is None :
656                 arg1 = ''
657         arg2 = zeroOrOneItem( arg2 )
658         if arg2 is None :
659                 arg2 = ''
660         if arg1 is arg2 :
661                 return _True
662         else :
663                 return _Boolean[ stringValuesStartsWith( asStringOrStream( arg1 ) ,
664                                                                                                  asStringOrStream( arg2 ) ) ]
665
666 # XO/7.5.3
667 @registerFast( 'fn:ends-with' )
668 def fnEndsWith( context , arg1 , arg2 , collation = None ) :
669
670         if collation is not None :
671                 raise XPathError( 'FOCH0004' , 'Collation not supported' )
672         arg1 = zeroOrOneItem( arg1 )
673         if arg1 is None :
674                 arg1 = ''
675         arg2 = zeroOrOneItem( arg2 )
676         if arg2 is None :
677                 arg2 = ''
678         if arg1 is arg2 or arg2 == '' :
679                 return _True
680         elif arg1 == '' :
681                 return _False
682         else :
683                 return _Boolean[ asString( arg1 ).endswith( asString( arg2 ) ) ]
684
685 # XO/7.5.4
686 @registerFast( 'fn:substring-before' )
687 def fnSubstringBefore( context , arg1 , arg2 , collation = None ) :
688
689         if collation is not None :
690                 raise XPathError( 'FOCH0004' , 'Collation not supported' )
691         arg1 = asString( zeroOrOneItem( arg1 ) )
692         arg2 = asString( zeroOrOneItem( arg2 ) )
693
694         if not arg2 :
695                 return _EmptyString
696         else :
697                 p = arg1.find( arg2 )
698                 if p == -1 :
699                         return _EmptyString
700                 else :
701                         return Sequence( arg1[ : p ] )
702
703 # XO/7.5.5
704 @registerFast( 'fn:substring-after' )
705 def fnSubstringAfter( context , arg1 , arg2 , collation = None ) :
706
707         if collation is not None :
708                 raise XPathError( 'FOCH0004' , 'Collation not supported' )
709         arg1 = asString( zeroOrOneItem( arg1 ) )
710         arg2 = asString( zeroOrOneItem( arg2 ) )
711
712         if not arg2 :
713                 return Sequence( arg1 )
714         else :
715                 p = arg1.find( arg2 )
716                 if p == -1 :
717                         return _EmptyString
718                 else :
719                         return Sequence( arg1[ p + len( arg2 ) : ] )
720
721 def _patternFlags( flags ) :
722
723         if flags is None :
724                 r = 0
725         else :
726                 flags = asString( oneItem( flags ) )
727                 r = 0
728                 if 'i' in flags :
729                         r |= re.I
730                 elif 'm' in flags :
731                         r |= re.M
732                 elif 's' in flags :
733                         r |= re.S
734         return r
735
736 # XO/7.6.2
737 @registerFast( 'fn:matches' )
738 def fnMatches( context , input , pattern , flags = None ) :
739
740         input = asString( zeroOrOneItem( input ) )
741         pattern = asString( oneItem( pattern ) )
742
743         return _Boolean[ re.search( pattern , input , _patternFlags( flags ) ) is not None ]
744
745 # XO/7.6.3
746 # FIXME: Add support for $n
747 @registerFast( 'fn:replace' )
748 def fnReplace( context , input , pattern , replacement , flags = None ) :
749
750         input = asString( zeroOrOneItem( input ) )
751         pattern = asString( oneItem( pattern ) )
752         replacement = asString( oneItem( replacement ) )
753
754         return Sequence( re.sub( pattern , replacement , input , _patternFlags( flags ) ) )
755
756 # XO/7.6.4
757 @registerFast( 'fn:tokenize' )
758 def fnTokenize( context , input , pattern , flags = None ) :
759
760         input = asString( zeroOrOneItem( input ) )
761         pattern = asString( oneItem( pattern ) )
762
763         return Sequence( re.compile( pattern , _patternFlags( flags ) ).split( input ) )
764
765 # XO/9.1.1
766 @registerFast( 'fn:true' )
767 def fnTrue( context ) :
768
769         return _True
770
771 # XO/9.1.2
772 @registerFast( 'fn:false' )
773 def fnFalse( context ) :
774
775         return _False
776
777 # XO/9.3.1
778 @registerFast( 'fn:not' )
779 def fnNot( context , arg ) :
780
781         return _Boolean[ not sequenceToBoolean( arg ) ]
782
783 # XO/14.1
784 @registerFast( 'fn:name' )
785 def fnName( context , arg = None ) :
786
787         if arg is None :
788                 item = context.item
789                 if item is None :
790                         raise XPathError( 'FONC0001' , 'Context item undefined' )
791                 if not isNode( item ) :
792                         raise XPathError( 'XPTY0006' , 'Expected a node as context item' )
793         else :
794                 item = zeroOrOneNode( arg )
795                 if item is None :
796                         return _EmptyString
797         return Sequence( item.dmNodeName() )
798
799 # XO/14.4
800 @registerFast( 'fn:number' )
801 def fnNumber( context , arg = None ) :
802
803         if arg is None :
804                 item = context.item
805                 if item is None :
806                         raise XPathError( 'FONC0001' , 'Context item undefined' )
807         else :
808                 item = zeroOrOneItem( arg )
809         return Sequence( asNumber( item ) )
810
811 # XO/14.5
812 @registerFast( 'fn:lang' )
813 def fnLang( context , testlang , node = None ) :
814
815         testlang = asString( zeroOrOneItem( testlang ) ).lower()
816         if node is None :
817                 node = context.item
818                 if node is None :
819                         raise XPathError( 'FONC0001' , 'Context item undefined' )
820                 if not isNode( item ) :
821                         raise XPathError( 'XPTY0006' , 'Expected a node as context item' )
822         else :
823                 node = oneNode( node )
824
825         while node is not None :
826                 if isinstance( node , Element ) :
827                         att = node.getAttribute( 'xml:lang' )
828                         if att is not None :
829                                 v = att.dmStringValue().lower()
830                                 if v == testlang :
831                                         return _True
832                                 if v.split( '-' , 1 )[ 0 ] == testlang :
833                                         return _True
834                 node = node.parent
835         return _False
836
837 # XO/14.9
838 @registerFast( 'fn:root' )
839 def fnRoot( context , sequence = None ) :
840
841         if sequence is None :
842                 item = context.item
843                 if item is None :
844                         raise XPathError( 'FONC0001' , 'Context item undefined' )
845                 elif not isNode( item ) :
846                         raise XPathError( 'XPTY0006' , 'Expected a node as context item' )
847                 elif isDocument( item ) :
848                         return Sequence( item )
849                 else :
850                         return Sequence( item.root )
851         else :
852                 item = zeroOrOneNode( sequence )
853                 if item is None :
854                         return _Empty
855                 elif isDocument( item ) :
856                         return sequence
857                 else :
858                         return Sequence( item.root )
859
860 # XO/15.1.1
861 @registerFast( 'fn:boolean' )
862 def fnBoolean( context , arg ) :
863
864         return Sequence( sequenceToBoolean( arg ) )
865
866 # XO/15.1.3
867 @registerFast( 'fn:index-of' )
868 def fnIndexOf( context , seqParam , srchParam , collation = None ) :
869
870         if collation is not None :
871                 raise XPathError( 'FOCH0004' , 'Collation not supported' )
872
873         srchParam = oneItem( srchParam )
874
875         return Sequence( i + 1 for i , item in enumerate( seqParam ) if compareValue( item , srchParam ) == 0 )
876
877 # XO/15.1.4
878 @registerFast( 'fn:empty' )
879 def fnEmpty( context , sequence ) :
880
881         return _Boolean[ len( sequence ) == 0 ]
882
883 # XO/15.1.5
884 @registerFast( 'fn:exists' )
885 def fnExists( context , sequence ) :
886
887         return _Boolean[ len( sequence ) > 0 ]
888
889 # XO/15.1.6
890 @registerFast( 'fn:distinct-values' )
891 def fnDistinctValues( context , arg ) :
892
893         if not arg :
894                 return arg
895         elif arg.type == SEQUENCE_ATOMICS :
896                 result = {}
897                 for item in arg :
898                         result[ item ] = True
899                 return Sequence( result.keys() )
900         else :
901                 # FIXME: Naive implementation O(n^2)
902                 result = []
903                 for item in arg :
904                         for existingItem in result :
905                                 if compareValue( existingItem , item ) == 0 :
906                                         break
907                         else :
908                                 result.append( item )
909                 return Sequence( result )
910
911 # XO/15.1.7
912 @registerFast( 'fn:insert-before' )
913 def fnInsertBefore( context , target , position , inserts ) :
914
915         position = asNumber( oneItem( position ) )
916
917         position = max( 0 , int( position - 1 ) )
918
919         return target[ : position ] + inserts + target[ position : ]
920
921 # XO/15.1.8
922 @registerFast( 'fn:remove' )
923 def fnRemove( context , target , position ) :
924
925         position = asNumber( oneItem( position ) )
926
927         position = int( position - 1 )
928         if position < 0 :
929                 return target
930         else :
931                 return target[ : position ] + target[ position + 1 : ]
932
933 # XO/15.1.9
934 @registerFast( 'fn:reverse' )
935 def fnReverse( context , arg ) :
936
937         return Sequence( arg[ ::-1 ] , type = arg.type )
938
939 # XO/15.1.10
940 @registerFast( 'fn:subsequence' )
941 def fnSubstring( context , sourceSeq , startingLoc , length = None ) :
942
943         if sourceSeq is None :
944                 return _EmptyString
945         else :
946                 startingLoc = oneItem( startingLoc )
947                 a = asNumber( startingLoc )
948                 if length is None :
949                         b = _POSINF
950                 else :
951                         b = asNumber( oneItem( length ) )
952                 if isNotANumber( a ) or isNotANumber( b ) :
953                         return _Empty
954                 elif isNegativeInfinity( a ) and isPositiveInfinity( b ) :
955                         return _Empty # because a + b is NaN
956                 elif isPositiveInfinity( a ) :
957                         return _Empty
958                 elif b <= 0 :
959                         return _Empty
960                 else :
961                         if isNegativeInfinity( a ) :
962                                 a = 1
963                         else :
964                                 a = math.floor( a + 0.5 )
965                         if isPositiveInfinity( b ) :
966                                 return Sequence( sourceSeq[ max( 0 , int( a - 1 ) ) : ] , type = sourceSeq.type )
967                         else :
968                                 b = math.floor( b + 0.5 )
969                                 return Sequence( sourceSeq[ max( 0 , int( a - 1 ) ) : max( 0 , int( a + b - 1 ) ) ] , type = sourceSeq.type )
970
971 # XO/15.1.11
972 @registerFast( 'fn:unordered' )
973 def fnUnordered( context , sourceSeq ) :
974
975         return sourceSeq
976
977 # XO/15.2.1
978 @registerFast( 'fn:zero-or-one' )
979 def fnZeroOrOne( context , arg ) :
980
981         zeroOrOneItem( arg )
982         return arg
983
984 # XO/15.2.2
985 @registerFast( 'fn:one-or-more' )
986 def fnOneOrMore( context , arg ) :
987
988         oneOrMoreItem( arg )
989         return arg
990
991 # XO/15.2.3
992 @registerFast( 'fn:exactly-one' )
993 def fnExactlyONe( context , arg ) :
994
995         oneItem( arg )
996         return arg
997
998 # FIXME: Rewrite it as iterative (following one tree iteratively, and
999 # trying to match the other tree at each step) ?
1000 def treeDeepEqual( t1 , t2 ) :
1001
1002         if t1 is t2 :
1003                 #
1004                 # Same node
1005                 #
1006                 return True
1007         elif type( t1 ) is not type( t2 ) :
1008                 #
1009                 # Different type
1010                 #
1011                 return False
1012         elif isinstance( t1 , Document ) :
1013                 #
1014                 # Document
1015                 #
1016                 if len( t1.children ) != len( t2.children() ) :
1017                         return False
1018                 for c1 , c2 in zip( t1.children , t2.children ) :
1019                         if not treeDeepEqual( c1 , c2 ) :
1020                                 return False
1021                 return True
1022         elif isinstance( t1 , Element ) :
1023                 #
1024                 # Element
1025                 #
1026                 if t1.name != t2.name :
1027                         return False
1028                 #
1029                 # Same attributes ?
1030                 #
1031                 if len( t1.attributes ) != len( t2.attributes ) :
1032                         return False
1033                 for a1 , a2 in zip( sorted( t1.attributes , key = op.attrgetter( 'name' ) ) ,
1034                                                         sorted( t2.attributes , key = op.attrgetter( 'name' ) ) ) :
1035                         if a1.name != a2.name or a1.value != a2.value :
1036                                 return False
1037                 #
1038                 # Same children ?
1039                 #
1040                 if len( t1.children ) != len( t2.children ) :
1041                         return False
1042                 for c1 , c2 in zip( t1.children , t2.children ) :
1043                         if not treeDeepEqual( c1 , c2 ) :
1044                                 return False
1045                 return True
1046         elif isinstance( t1 , Text ) :
1047                 #
1048                 # Text
1049                 #
1050                 return t1.contents == t2.contents
1051         elif isinstance( t1 , Comment ) :
1052                 #
1053                 # Comment
1054                 #
1055                 return t1.contents == t2.contents
1056         else :
1057                 raise Error( 'Unexpected comparison between item of type %s' % typeOf( t1 ) )
1058
1059 # XO/15.3.1
1060 @registerFast( 'fn:deep-equal' )
1061 def fnDeepEqual( context , parameter1 , parameter2 , collation = None ) :
1062
1063         if collation is not None :
1064                 raise XPathError( 'FOCH0004' , 'Collation not supported' )
1065
1066         if not parameter1 and not parameter2 :
1067                 return _True
1068         if len( parameter1 ) != len( parameter2 ) :
1069                 return _False
1070         for i1 , i2 in zip( parameter1 , parameter2 ) :
1071                 if isNode( i1 ) :
1072                         if isNode( i2 ) :
1073                                 if not treeDeepEqual( i1 , i2 ) :
1074                                         return _False
1075                         else :
1076                                 return _False
1077                 else :
1078                         if isNode( i2 ) :
1079                                 return _False
1080                         else :
1081                                 if compareValue( i1 , i2 ) != 0 :
1082                                         return _False
1083         return _True
1084
1085 # XO/15.4.1
1086 @registerFast( 'fn:count' )
1087 def fnCount( context , arg ) :
1088
1089         return Sequence( len( arg ) )
1090
1091 # XO/15.4.2
1092 @registerFast( 'fn:avg' )
1093 def fnAvg( context , arg ) :
1094
1095         if not arg :
1096                 return _Empty
1097         else :
1098                 return Sequence( sum( map( asNumber , arg ) ) / float( len( arg ) ) )
1099
1100 def _makeFnMinMax( comparator ) :
1101
1102         def fun( context , arg , collation = None ) :
1103
1104                 if collation is not None :
1105                         raise XPathError( 'FOCH0004' , 'Collation not supported' )
1106
1107                 if not arg :
1108                         return _Empty
1109                 else :
1110                         r = arg[ 0 ]
1111                         if isNode( r ) :
1112                                 r = asNumber( r )
1113                         if isNotANumber( r ) :
1114                                 return _Nan
1115
1116                         for item in arg[ 1 : ] :
1117                                 if isNode( item ) :
1118                                         item = asNumber( item )
1119                                 test = compareValue( item , r )
1120                                 if test is None :
1121                                         raise XPathError( 'FORG0006' , 'inconsistency' )
1122                                 if comparator( test , 0 ) :
1123                                         r = item
1124
1125                         return Sequence( r )
1126
1127         return fun
1128
1129 # XO/15.4.3
1130 fnMax = _makeFnMinMax( op.gt )
1131
1132 # XO/15.4.4
1133 fnMin = _makeFnMinMax( op.lt )
1134
1135 registerFast( 'fn:max' )( fnMax )
1136 registerFast( 'fn:min' )( fnMin )
1137
1138 @registerFast( 'fn:sum' )
1139 def fnSum( context , arg , zero = None ) :
1140
1141         if not arg :
1142                 if zero is not None :
1143                         zero = zeroOrOneItem( zero )
1144                         if zero is None :
1145                                 return _Empty
1146                         else :
1147                                 return Sequence( zero )
1148                 else :
1149                         return Sequence( 0 )
1150
1151         return Sequence( sum( map( asNumber , arg ) ) )
1152
1153 # XO/15.5.2
1154 @registerFast( 'fn:id' )
1155 def fnId( context , arg , node = None ) :
1156
1157         if node is None :
1158                 item = context.item
1159                 if item is None :
1160                         raise XPathError( 'FONC0001' , 'Context item undefined' )
1161         else :
1162                 item = oneNode( node )
1163         doc = item.root
1164         if doc is None or not isDocument( doc ) :
1165                 raise XPathError( 'FODC0001' , '..' ) # unsure
1166         ids = map( asString , arg )
1167         result = []
1168         for id in ids :
1169                 n = doc.ids.get( id )
1170                 if n is not None :
1171                         result.append( n )
1172         result.sort( lambda a , b : cmp( a.position , b.position ) )
1173         return Sequence( result )
1174
1175 # XO/16.1
1176 @registerFast( 'fn:position' )
1177 def fnPosition( context ) :
1178
1179         if context.item is None :
1180                 raise XPathError( 'FONC0001' , 'Context item undefined' )
1181         return Sequence( context.position )
1182
1183 # XO/16.2
1184 @registerFast( 'fn:last' )
1185 def fnLast( context ) :
1186
1187         if context.item is None :
1188                 raise XPathError( 'FONC0001' , 'Context item undefined' )
1189         return Sequence( context.last )
1190
1191 # XO/16.3
1192 @registerFast( 'fn:current-dateTime' )
1193 def fnCurrentDatetime( context ) :
1194
1195         t = context.currentDatetime
1196         return '%04d-%02d-%02dT%02d:%02d:%02d.%06dZ' \
1197                 % ( time.gmtime( t )[ : 6 ] + ( int( math.modf( t )[ 0 ] * 1e6 ) , ) )
1198
1199 #--[ XPath/XQuery operators ]------------------------------------------------
1200
1201 @registerFast( 'op:is-same-node' )
1202 def opIsSameNode( context , arg1 , arg2 ) :
1203
1204         arg1 = oneNode( arg1 )
1205         arg2 = oneNode( arg2 )
1206         return _Boolean[ arg1 is arg2 ]
1207
1208 @registerFast( 'op:node-before' )
1209 def opNodeBefore( context , arg1 , arg2 ) :
1210
1211         arg1 = oneNode( arg1 )
1212         arg2 = oneNode( arg2 )
1213         return _Boolean[ arg1.position < arg2.position ]
1214
1215 @registerFast( 'op:node-after' )
1216 def opNodeAfter( context , arg1 , arg2 ) :
1217
1218         arg1 = oneNode( arg1 )
1219         arg2 = oneNode( arg2 )
1220         return _Boolean[ arg1.position > arg2.position ]
1221
1222 @registerFast( 'op:or' , hold = True )
1223 def opOr( context , arg1 , arg2 ) :
1224
1225         return _Boolean[ sequenceToBoolean( arg1( context ) ) \
1226                                          or sequenceToBoolean( arg2( context ) ) ]
1227
1228 @registerFast( 'op:and' , hold = True )
1229 def opAnd( context , arg1 , arg2 ) :
1230
1231         return _Boolean[ sequenceToBoolean( arg1( context ) ) \
1232                                          and sequenceToBoolean( arg2( context ) ) ]
1233
1234 @registerFast( 'op:to' )
1235 def opTo( context , arg1 , arg2 ) :
1236
1237         arg1 = oneNumber( arg1 )
1238         arg2 = oneNumber( arg2 )
1239         return Sequence( range( int( arg1 ) , int( arg2 ) + 1 ) )
1240
1241 @registerFast( 'op:union' )
1242 def opUnion( context , arg1 , arg2 ) :
1243
1244         if arg1.type not in ( SEQUENCE_EMPTY , SEQUENCE_NODES ) \
1245                 or arg2.type not in ( SEQUENCE_EMPTY , SEQUENCE_NODES ) :
1246                 raise XPathError( 'XPTY0004' , '\'union\' operator expect sequence of nodes only' )
1247         if not arg1 :
1248                 return arg2
1249         if not arg2 :
1250                 return arg1
1251         result = list( set( arg1 ) | set( arg2 ) )
1252         result.sort( lambda a , b : cmp( a.position , b.position ) )
1253         return Sequence( result )
1254
1255 @registerFast( 'op:except' )
1256 def opExcept( context , arg1 , arg2 ) :
1257
1258         if arg1.type not in ( SEQUENCE_EMPTY , SEQUENCE_NODES ) \
1259                 or arg2.type not in ( SEQUENCE_EMPTY , SEQUENCE_NODES ) :
1260                 raise XPathError( 'XPTY0004' , '\'except\' operator expect sequence of nodes only' )
1261         if not arg1 :
1262                 return _Empty
1263         if not arg2 :
1264                 return arg1
1265         result = list( set( arg1 ) - set( arg2 ) )
1266         result.sort( lambda a , b : cmp( a.position , b.position ) )
1267         return Sequence( result )
1268
1269 @registerFast( 'op:intersection' )
1270 def opIntersection( context , arg1 , arg2 ) :
1271
1272         if arg1.type not in ( SEQUENCE_EMPTY , SEQUENCE_NODES ) \
1273                 or arg2.type not in ( SEQUENCE_EMPTY , SEQUENCE_NODES ) :
1274                 raise XPathError( 'XPTY0004' , '\'intersect\' operator expect sequence of nodes only' )
1275         if not arg1 or not arg2 :
1276                 return _Empty
1277         result = list( set( arg1 ) & set( arg2 ) )
1278         result.sort( lambda a , b : cmp( a.position , b.position ) )
1279         return Sequence( result )
1280
1281 @registerFast( 'op:plus' )
1282 def opPlus( context , arg ) :
1283
1284         arg = oneAtomizedItem( arg )
1285         if isNumber( arg ) :
1286                 return Sequence( arg )
1287         else :
1288                 arg = asNumber( arg )
1289                 if arg is not None :
1290                         return Sequence( arg )
1291                 else :
1292                         return _Empty
1293
1294 @registerFast( 'op:minus' )
1295 def opMinus( context , arg ) :
1296
1297         arg = oneAtomizedItem( arg )
1298         if isNumber( arg ) :
1299                 return Sequence( -arg )
1300         else :
1301                 arg = asNumber( arg )
1302                 if arg is not None :
1303                         return Sequence( -arg )
1304                 else :
1305                         return _Empty
1306
1307 @registerFast( 'op:add' )
1308 def opAdd( context , arg1 , arg2 ) :
1309
1310         arg1 = asNumber( oneItem( arg1 ) )
1311         arg2 = asNumber( oneItem( arg2 ) )
1312         if arg1 is None or arg2 is None :
1313                 return _Empty
1314         else :
1315                 return Sequence( arg1 + arg2 )
1316
1317 @registerFast( 'op:substract' )
1318 def opSubstract( context , arg1 , arg2 ) :
1319
1320         arg1 = asNumber( oneItem( arg1 ) )
1321         arg2 = asNumber( oneItem( arg2 ) )
1322         if arg1 is None or arg2 is None :
1323                 return _Empty
1324         else :
1325                 return Sequence( arg1 - arg2 )
1326
1327 @registerFast( 'op:multiply' )
1328 def opMultiply( context , arg1 , arg2 ) :
1329
1330         arg1 = asNumber( oneItem( arg1 ) )
1331         arg2 = asNumber( oneItem( arg2 ) )
1332         if arg1 is None or arg2 is None :
1333                 return _Empty
1334         else :
1335                 return Sequence( arg1 * arg2 )
1336
1337 @registerFast( 'op:divide' )
1338 def opDivide( context , arg1 , arg2 ) :
1339
1340         arg1 = asNumber( oneItem( arg1 ) )
1341         arg2 = asNumber( oneItem( arg2 ) )
1342         if arg1 is None or arg2 is None :
1343                 return _Empty
1344         else :
1345                 if arg2 == 0 :
1346                         if arg1 > 0 :
1347                                 return _PositiveInfinity
1348                         elif arg1 < 0 :
1349                                 return _NegativeInfinity
1350                         else :
1351                                 return _Nan
1352                 else :
1353                         return Sequence( arg1 / float( arg2 ) )
1354
1355 @registerFast( 'op:integer-divide' )
1356 def opIntegerDivide( context , arg1 , arg2 ) :
1357
1358         arg1 = asNumber( oneItem( arg1 ) )
1359         arg2 = asNumber( oneItem( arg2 ) )
1360         if arg1 is None or arg2 is None :
1361                 return _Empty
1362         else :
1363                 return Sequence( arg1 // arg2 )
1364
1365 # Return -1, 0, 1 or None
1366 def compareValue( a , b ) :
1367
1368         if isNode( a ) :
1369                 if isNode( b ) :
1370                         return stringValuesCompare( a.iterStringValue() , b.iterStringValue() )
1371                 elif isString( b ) :
1372                         return stringValuesCompare( a.iterStringValue() , b )
1373         elif isString( a ) :
1374                 if isNode( b ) :
1375                         return stringValuesCompare( a , b.iterStringValue() )
1376                 elif isString( b ) :
1377                         return cmp( a , b )
1378         elif isNumber( a ) :
1379                 if isNumber( b ) :
1380                         return cmp( a , b )
1381         elif isBoolean( a ) :
1382                 if isBoolean( b ) :
1383                         return cmp( a , b )
1384
1385 # eq, ne, lt, le, gt, ge
1386 def makeValueComparator( comparator ) :
1387
1388         def fun( context , arg1 , arg2 ) :
1389                 if not arg1 or not arg2 :
1390                         return _Empty
1391                 elif len( arg1 ) == 1 and len( arg2 ) == 1 :
1392                         item1 = arg1[ 0 ]
1393                         item2 = arg2[ 0 ]
1394                         r = compareValue( item1 , item2 )
1395                         if r is not None :
1396                                 return _Boolean[ comparator( r , 0 ) ]
1397                         else :
1398                                 raise XPathError( 'XPTY0004' , 'cannot compare %s and %s' % ( typeOf( item1 ) , typeOf( item2 ) ) )
1399                 else :
1400                         raise XPathError( 'XPTY0004' , 'Value comparison operators expect arguments of 0 or 1 items' )
1401         return fun
1402
1403 # !=, =, <, <=, >, >=
1404 def makeGeneralComparator( comparator ) :
1405
1406         def fun( context , arg1 , arg2 ) :
1407                 if arg1 and arg2 :
1408                         for item1 in arg1 :
1409                                 for item2 in arg2 :
1410                                         r = False
1411                                         if item1 is item2 :
1412                                                 r = comparator( 0 , 0 )
1413                                         elif isNumber( item1 ) :
1414                                                 if isNumber( item2 ) :
1415                                                         r = comparator( item1 , item2 )
1416                                                 else :
1417                                                         item2 = asNumber( item2 )
1418                                                         if item2 is None : # NaN
1419                                                                 r = False
1420                                                         else :
1421                                                                 r = comparator( item1 , item2 )
1422                                         elif isNode( item1 ) :
1423                                                 if isNumber( item2 ) :
1424                                                         item1 = asNumber( item1 )
1425                                                         if item1 is None :
1426                                                                 r = False
1427                                                         else :
1428                                                                 r = comparator( item1 , item2 )
1429                                                 elif isNode( item2 ) :
1430                                                         r = comparator( stringValuesCompare( item1.iterStringValue() ,
1431                                                                                                                                  item2.iterStringValue() ) ,
1432                                                                                         0 )
1433                                                 elif isString( item2 ) :
1434                                                         r = comparator( stringValuesCompare( item1.iterStringValue() ,
1435                                                                                                                                  item2 ) ,
1436                                                                                         0 )
1437                                                 else :
1438                                                         raise XPathError( 'XPTY0004' , 'cannot compare %s and %s' % ( typeOf( item1 ) , typeOf( item2 ) ) )
1439                                         elif isString( item1 ) :
1440                                                 if isString( item2 ) :
1441                                                         r = comparator( item1 , item2 )
1442                                                 elif isNode( item2 ) :
1443                                                         r = comparator( stringValuesCompare( item1 ,
1444                                                                                                                                  item2.iterStringValue() ) ,
1445                                                                                         0 )
1446                                                 else :
1447                                                         raise XPathError( 'XPTY0004' , 'cannot compare %s and %s' % ( typeOf( item1 ) , typeOf( item2 ) ) )
1448                                         else :
1449                                                 raise XPathError( 'XPTY0004' , 'cannot compare %s and %s' % ( typeOf( item1 ) , typeOf( item2 ) ) )
1450                                         if r :
1451                                                 return _True
1452                 return _False
1453         return fun
1454
1455 #
1456 # Not really XPath operators, but rather dispatcher to the right
1457 # operator.
1458 #
1459 opValueEqual          = makeValueComparator( op.eq )
1460 opValueNotEqual       = makeValueComparator( op.ne )
1461 opValueLessThan       = makeValueComparator( op.lt )
1462 opValueGreaterThan    = makeValueComparator( op.gt )
1463 opValueLessOrEqual    = makeValueComparator( op.le )
1464 opValueGreaterOrEqual = makeValueComparator( op.ge )
1465
1466 opGeneralEqual          = makeGeneralComparator( op.eq )
1467 opGeneralNotEqual       = makeGeneralComparator( op.ne )
1468 opGeneralLessThan       = makeGeneralComparator( op.lt )
1469 opGeneralGreaterThan    = makeGeneralComparator( op.gt )
1470 opGeneralLessOrEqual    = makeGeneralComparator( op.le )
1471 opGeneralGreaterOrEqual = makeGeneralComparator( op.ge )
1472
1473 # Local Variables:
1474 # tab-width: 4
1475 # python-indent: 4
1476 # End: