1"""@package grass.temporal 2 3Temporal operator evaluation with PLY 4 5(C) 2013 by the GRASS Development Team 6This program is free software under the GNU General Public 7License (>=v2). Read the file COPYING that comes with GRASS 8for details. 9 10:authors: Thomas Leppelt and Soeren Gebbert 11 12.. code-block:: python 13 14 >>> p = TemporalOperatorParser() 15 >>> expression = "{equal|equivalent|cover|in|meet|contain|overlap}" 16 >>> p.parse(expression, optype = 'relation') 17 >>> print((p.relations, p.temporal, p.function)) 18 (['equal', 'equivalent', 'cover', 'in', 'meet', 'contain', 'overlap'], None, None) 19 20 >>> p = TemporalOperatorParser() 21 >>> expression = "{equal| during}" 22 >>> p.parse(expression, optype = 'relation') 23 >>> print((p.relations, p.temporal, p.function)) 24 (['equal', 'during'], None, None) 25 >>> p = TemporalOperatorParser() 26 >>> expression = "{contains | starts}" 27 >>> p.parse(expression) 28 >>> print((p.relations, p.temporal, p.function)) 29 (['contains', 'starts'], None, None) 30 >>> p = TemporalOperatorParser() 31 >>> expression = "{&&, during}" 32 >>> p.parse(expression, optype = 'boolean') 33 >>> print((p.relations, p.temporal, p.function, p.aggregate)) 34 (['during'], 'l', '&&', '&') 35 >>> p = TemporalOperatorParser() 36 >>> expression = "{||, equal | during}" 37 >>> p.parse(expression, optype = 'boolean') 38 >>> print((p.relations, p.temporal, p.function, p.aggregate)) 39 (['equal', 'during'], 'l', '||', '|') 40 >>> p = TemporalOperatorParser() 41 >>> expression = "{||, equal | during, &}" 42 >>> p.parse(expression, optype = 'boolean') 43 >>> print((p.relations, p.temporal, p.function, p.aggregate)) 44 (['equal', 'during'], 'l', '||', '&') 45 >>> p = TemporalOperatorParser() 46 >>> expression = "{&&, during, |}" 47 >>> p.parse(expression, optype = 'boolean') 48 >>> print((p.relations, p.temporal, p.function, p.aggregate)) 49 (['during'], 'l', '&&', '|') 50 >>> p = TemporalOperatorParser() 51 >>> expression = "{&&, during, |, r}" 52 >>> p.parse(expression, optype = 'boolean') 53 >>> print((p.relations, p.temporal, p.function, p.aggregate)) 54 (['during'], 'r', '&&', '|') 55 >>> p = TemporalOperatorParser() 56 >>> expression = "{&&, during, u}" 57 >>> p.parse(expression, optype = 'boolean') 58 >>> print((p.relations, p.temporal, p.function, p.aggregate)) 59 (['during'], 'u', '&&', '&') 60 >>> p = TemporalOperatorParser() 61 >>> expression = "{:, during, r}" 62 >>> p.parse(expression, optype = 'select') 63 >>> print((p.relations, p.temporal, p.function)) 64 (['during'], 'r', ':') 65 >>> p = TemporalOperatorParser() 66 >>> expression = "{!:, equal | contains, d}" 67 >>> p.parse(expression, optype = 'select') 68 >>> print((p.relations, p.temporal, p.function)) 69 (['equal', 'contains'], 'd', '!:') 70 >>> p = TemporalOperatorParser() 71 >>> expression = "{#, during, r}" 72 >>> p.parse(expression, optype = 'hash') 73 >>> print((p.relations, p.temporal, p.function)) 74 (['during'], 'r', '#') 75 >>> p = TemporalOperatorParser() 76 >>> expression = "{#, equal | contains}" 77 >>> p.parse(expression, optype = 'hash') 78 >>> print((p.relations, p.temporal, p.function)) 79 (['equal', 'contains'], 'l', '#') 80 >>> p = TemporalOperatorParser() 81 >>> expression = "{+, during, r}" 82 >>> p.parse(expression, optype = 'raster') 83 >>> print((p.relations, p.temporal, p.function)) 84 (['during'], 'r', '+') 85 >>> p = TemporalOperatorParser() 86 >>> expression = "{/, equal | contains}" 87 >>> p.parse(expression, optype = 'raster') 88 >>> print((p.relations, p.temporal, p.function)) 89 (['equal', 'contains'], 'l', '/') 90 >>> p = TemporalOperatorParser() 91 >>> expression = "{+, equal | contains,intersect}" 92 >>> p.parse(expression, optype = 'raster') 93 >>> print((p.relations, p.temporal, p.function)) 94 (['equal', 'contains'], 'i', '+') 95 >>> p = TemporalOperatorParser() 96 >>> expression = "{*, contains,disjoint}" 97 >>> p.parse(expression, optype = 'raster') 98 >>> print((p.relations, p.temporal, p.function)) 99 (['contains'], 'd', '*') 100 >>> p = TemporalOperatorParser() 101 >>> expression = "{~, equal,left}" 102 >>> p.parse(expression, optype = 'overlay') 103 >>> print((p.relations, p.temporal, p.function)) 104 (['equal'], 'l', '~') 105 >>> p = TemporalOperatorParser() 106 >>> expression = "{^, over,right}" 107 >>> p.parse(expression, optype = 'overlay') 108 >>> print((p.relations, p.temporal, p.function)) 109 (['overlaps', 'overlapped'], 'r', '^') 110 >>> p = TemporalOperatorParser() 111 >>> expression = "{&&, equal | during | contains | starts, &}" 112 >>> p.parse(expression, optype = 'boolean') 113 >>> print((p.relations, p.temporal, p.function, p.aggregate)) 114 (['equal', 'during', 'contains', 'starts'], 'l', '&&', '&') 115 >>> p = TemporalOperatorParser() 116 >>> expression = "{&&, equal | during | contains | starts, &&&&&}" 117 >>> p.parse(expression, optype = 'boolean') 118 Traceback (most recent call last): 119 SyntaxError: Unexpected syntax error in expression "{&&, equal | during | contains | starts, &&&&&}" at position 42 near & 120 >>> p = TemporalOperatorParser() 121 >>> expression = "{+, starting}" 122 >>> p.parse(expression) 123 Traceback (most recent call last): 124 SyntaxError: syntax error on line 1 position 4 near 'starting' 125 >>> p = TemporalOperatorParser() 126 >>> expression = "{nope, start, |, l}" 127 >>> p.parse(expression) 128 Traceback (most recent call last): 129 SyntaxError: syntax error on line 1 position 1 near 'nope' 130 >>> p = TemporalOperatorParser() 131 >>> expression = "{++, start, |, l}" 132 >>> p.parse(expression) 133 Traceback (most recent call last): 134 SyntaxError: Unexpected syntax error in expression "{++, start, |, l}" at position 2 near + 135 >>> p = TemporalOperatorParser() 136 >>> expression = "{^, over, right}" 137 >>> p.parse(expression, optype='rter') 138 Traceback (most recent call last): 139 SyntaxError: Unknown optype rter, must be one of ['select', 'boolean', 'raster', 'hash', 'relation', 'overlay'] 140 141""" 142from __future__ import print_function 143 144try: 145 import ply.lex as lex 146 import ply.yacc as yacc 147except: 148 pass 149 150class TemporalOperatorLexer(object): 151 """Lexical analyzer for the GRASS GIS temporal operator""" 152 153 # Functions that defines topological relations. 154 relations = { 155 # temporal relations 156 'equal' : "EQUAL", 157 'follows' : "FOLLOWS", 158 'precedes' : "PRECEDES", 159 'overlaps' : "OVERLAPS", 160 'overlapped' : "OVERLAPPED", 161 'during' : "DURING", 162 'starts' : "STARTS", 163 'finishes' : "FINISHES", 164 'contains' : "CONTAINS", 165 'started' : "STARTED", 166 'finished' : "FINISHED", 167 'over' : "OVER", 168 # spatial relations 169 'equivalent' : "EQUIVALENT", 170 'cover' : "COVER", 171 'overlap' : "OVERLAP", 172 'in' : "IN", 173 'contain' : "CONTAIN", 174 'meet' : "MEET" 175 } 176 177 # This is the list of token names. 178 tokens = ( 179 'COMMA', 180 'LEFTREF', 181 'RIGHTREF', 182 'UNION', 183 'DISJOINT', 184 'INTERSECT', 185 'HASH', 186 'OR', 187 'AND', 188 'DISOR', 189 'XOR', 190 'NOT', 191 'MOD', 192 'DIV', 193 'MULT', 194 'ADD', 195 'SUB', 196 'T_SELECT', 197 'T_NOT_SELECT', 198 'CLPAREN', 199 'CRPAREN', 200 ) 201 202 # Build the token list 203 tokens = tokens + tuple(relations.values()) 204 205 # Regular expression rules for simple tokens 206 t_T_SELECT = r':' 207 t_T_NOT_SELECT = r'!:' 208 t_COMMA = r',' 209 t_LEFTREF = '^[l|left]' 210 t_RIGHTREF = '^[r|right]' 211 t_UNION = '^[u|union]' 212 t_DISJOINT = '^[d|disjoint]' 213 t_INTERSECT = '^[i|intersect]' 214 t_HASH = r'\#' 215 t_OR = r'[\|]' 216 t_AND = r'[&]' 217 t_DISOR = r'\+' 218 t_XOR = r'\^' 219 t_NOT = r'\~' 220 t_MOD = r'[\%]' 221 t_DIV = r'[\/]' 222 t_MULT = r'[\*]' 223 t_ADD = r'[\+]' 224 t_SUB = r'[-]' 225 t_CLPAREN = r'\{' 226 t_CRPAREN = r'\}' 227 228 # These are the things that should be ignored. 229 t_ignore = ' \t\n' 230 231 # Track line numbers. 232 def t_newline(self, t): 233 r'\n+' 234 t.lineno += len(t.value) 235 236 def t_NAME(self, t): 237 r'[a-zA-Z_][a-zA-Z_0-9]*' 238 return self.temporal_symbol(t) 239 240 # Parse symbols 241 def temporal_symbol(self, t): 242 # Check for reserved words 243 if t.value in TemporalOperatorLexer.relations.keys(): 244 t.type = TemporalOperatorLexer.relations.get(t.value) 245 elif t.value == 'l' or t.value == 'left': 246 t.value = 'l' 247 t.type = 'LEFTREF' 248 elif t.value == 'r' or t.value == 'right': 249 t.value = 'r' 250 t.type = 'RIGHTREF' 251 elif t.value == 'u' or t.value == 'union': 252 t.value = 'u' 253 t.type = 'UNION' 254 elif t.value == 'd' or t.value == 'disjoint': 255 t.value = 'd' 256 t.type = 'DISJOINT' 257 elif t.value == 'i' or t.value == 'intersect': 258 t.value = 'i' 259 t.type = 'INTERSECT' 260 else: 261 self.t_error(t) 262 return(t) 263 264 # Handle errors. 265 def t_error(self, t): 266 raise SyntaxError("syntax error on line %d position %i near '%s'" % 267 (t.lineno, t.lexpos, t.value)) 268 269 # Build the lexer 270 def build(self,**kwargs): 271 self.lexer = lex.lex(module=self, optimize=False, 272 nowarn=True, debug=0, **kwargs) 273 274 # Just for testing 275 def test(self,data): 276 self.name_list = {} 277 print(data) 278 self.lexer.input(data) 279 while True: 280 tok = self.lexer.token() 281 if not tok: break 282 print(tok) 283 284############################################################################### 285 286class TemporalOperatorParser(object): 287 """The temporal operator class""" 288 289 def __init__(self): 290 self.lexer = TemporalOperatorLexer() 291 self.lexer.build() 292 self.parser = yacc.yacc(module=self, debug=0) 293 self.relations = None # Temporal relations (equals, contain, during, ...) 294 self.temporal = None # Temporal operation (intersect, left, right, ...) 295 self.function = None # Actual operation (+, -, /, *, ... ) 296 self.aggregate = None # Aggregation function (|, &) 297 298 self.optype_list = ["select", "boolean", "raster", "hash", "relation", "overlay"] 299 300 def parse(self, expression, optype='relation'): 301 """Parse the expression and fill the object variables 302 303 :param expression: 304 :param optype: The parameter optype can be of type: 305 - select { :, during, r} 306 - boolean {&&, contains, |} 307 - raster { *, equal, |} 308 - overlay { |, starts, &} 309 - hash { #, during, l} 310 - relation {during} 311 :return: 312 """ 313 self.optype = optype 314 315 if optype not in self.optype_list: 316 raise SyntaxError("Unknown optype %s, must be one of %s"%(self.optype, str(self.optype_list))) 317 self.expression = expression 318 self.parser.parse(expression) 319 320 # Error rule for syntax errors. 321 def p_error(self, t): 322 raise SyntaxError("Unexpected syntax error in expression" 323 " \"%s\" at position %i near %s"%(self.expression, 324 t.lexpos, 325 t.value)) 326 327 # Get the tokens from the lexer class 328 tokens = TemporalOperatorLexer.tokens 329 330 def p_relation_operator(self, t): 331 # {during} 332 # {during | equal | starts} 333 """ 334 operator : CLPAREN relation CRPAREN 335 | CLPAREN relationlist CRPAREN 336 """ 337 # Check for correct type. 338 if not self.optype == 'relation': 339 raise SyntaxError("Wrong optype \"%s\" must be \"relation\""%self.optype) 340 else: 341 # Set three operator components. 342 if isinstance(t[2], list): 343 self.relations = t[2] 344 else: 345 self.relations = [t[2]] 346 self.temporal = None 347 self.function = None 348 349 t[0] = t[2] 350 351 def p_relation_bool_operator(self, t): 352 # {||, during} 353 # {&&, during | equal | starts} 354 """ 355 operator : CLPAREN OR OR COMMA relation CRPAREN 356 | CLPAREN AND AND COMMA relation CRPAREN 357 | CLPAREN OR OR COMMA relationlist CRPAREN 358 | CLPAREN AND AND COMMA relationlist CRPAREN 359 """ 360 if not self.optype == 'boolean': 361 raise SyntaxError("Wrong optype \"%s\" must be \"boolean\""%self.optype) 362 else: 363 # Set three operator components. 364 if isinstance(t[5], list): 365 self.relations = t[5] 366 else: 367 self.relations = [t[5]] 368 self.temporal = "l" 369 self.function = t[2] + t[3] 370 self.aggregate = t[2] 371 372 t[0] = t[2] 373 374 def p_relation_bool_combi_operator(self, t): 375 # {||, during, &} 376 # {&&, during | equal | starts, |} 377 """ 378 operator : CLPAREN OR OR COMMA relation COMMA OR CRPAREN 379 | CLPAREN OR OR COMMA relation COMMA AND CRPAREN 380 | CLPAREN AND AND COMMA relation COMMA OR CRPAREN 381 | CLPAREN AND AND COMMA relation COMMA AND CRPAREN 382 | CLPAREN OR OR COMMA relationlist COMMA OR CRPAREN 383 | CLPAREN OR OR COMMA relationlist COMMA AND CRPAREN 384 | CLPAREN AND AND COMMA relationlist COMMA OR CRPAREN 385 | CLPAREN AND AND COMMA relationlist COMMA AND CRPAREN 386 """ 387 if not self.optype == 'boolean': 388 raise SyntaxError("Wrong optype \"%s\" must be \"boolean\""%self.optype) 389 else: 390 # Set three operator components. 391 if isinstance(t[5], list): 392 self.relations = t[5] 393 else: 394 self.relations = [t[5]] 395 self.temporal = "l" 396 self.function = t[2] + t[3] 397 self.aggregate = t[7] 398 399 t[0] = t[2] 400 401 def p_relation_bool_combi_operator2(self, t): 402 # {||, during, left} 403 # {&&, during | equal | starts, union} 404 """ 405 operator : CLPAREN OR OR COMMA relation COMMA temporal CRPAREN 406 | CLPAREN AND AND COMMA relation COMMA temporal CRPAREN 407 | CLPAREN OR OR COMMA relationlist COMMA temporal CRPAREN 408 | CLPAREN AND AND COMMA relationlist COMMA temporal CRPAREN 409 """ 410 if not self.optype == 'boolean': 411 raise SyntaxError("Wrong optype \"%s\" must be \"boolean\""%self.optype) 412 else: 413 # Set three operator components. 414 if isinstance(t[5], list): 415 self.relations = t[5] 416 else: 417 self.relations = [t[5]] 418 self.temporal = t[7] 419 self.function = t[2] + t[3] 420 self.aggregate = t[2] 421 422 t[0] = t[2] 423 424 def p_relation_bool_combi_operator3(self, t): 425 # {||, during, |, left} 426 # {&&, during | equal | starts, &, union} 427 """ 428 operator : CLPAREN OR OR COMMA relation COMMA OR COMMA temporal CRPAREN 429 | CLPAREN OR OR COMMA relation COMMA AND COMMA temporal CRPAREN 430 | CLPAREN AND AND COMMA relation COMMA OR COMMA temporal CRPAREN 431 | CLPAREN AND AND COMMA relation COMMA AND COMMA temporal CRPAREN 432 | CLPAREN OR OR COMMA relationlist COMMA OR COMMA temporal CRPAREN 433 | CLPAREN OR OR COMMA relationlist COMMA AND COMMA temporal CRPAREN 434 | CLPAREN AND AND COMMA relationlist COMMA OR COMMA temporal CRPAREN 435 | CLPAREN AND AND COMMA relationlist COMMA AND COMMA temporal CRPAREN 436 """ 437 if not self.optype == 'boolean': 438 raise SyntaxError("Wrong optype \"%s\" must be \"relation\""%self.optype) 439 else: 440 # Set three operator components. 441 if isinstance(t[5], list): 442 self.relations = t[5] 443 else: 444 self.relations = [t[5]] 445 self.temporal = t[9] 446 self.function = t[2] + t[3] 447 self.aggregate = t[7] 448 449 t[0] = t[2] 450 451 def p_select_relation_operator(self, t): 452 # {!:} 453 # { :, during} 454 # {!:, during | equal | starts} 455 # { :, during | equal | starts, l} 456 """ 457 operator : CLPAREN select CRPAREN 458 | CLPAREN select COMMA relation CRPAREN 459 | CLPAREN select COMMA relationlist CRPAREN 460 | CLPAREN select COMMA relation COMMA temporal CRPAREN 461 | CLPAREN select COMMA relationlist COMMA temporal CRPAREN 462 """ 463 if not self.optype == 'select': 464 raise SyntaxError("Wrong optype \"%s\" must be \"select\""%self.optype) 465 else: 466 if len(t) == 4: 467 # Set three operator components. 468 self.relations = ['equal', 'equivalent'] 469 self.temporal = "l" 470 self.function = t[2] 471 elif len(t) == 6: 472 if isinstance(t[4], list): 473 self.relations = t[4] 474 else: 475 self.relations = [t[4]] 476 self.temporal = "l" 477 self.function = t[2] 478 elif len(t) == 8: 479 if isinstance(t[4], list): 480 self.relations = t[4] 481 else: 482 self.relations = [t[4]] 483 self.temporal = t[6] 484 self.function = t[2] 485 t[0] = t[2] 486 487 def p_hash_relation_operator(self, t): 488 # {#} 489 # {#, during} 490 # {#, during | equal | starts} 491 # {#, during | equal | starts, l} 492 """ 493 operator : CLPAREN HASH CRPAREN 494 | CLPAREN HASH COMMA relation CRPAREN 495 | CLPAREN HASH COMMA relationlist CRPAREN 496 | CLPAREN HASH COMMA relation COMMA temporal CRPAREN 497 | CLPAREN HASH COMMA relationlist COMMA temporal CRPAREN 498 """ 499 if not self.optype == 'hash': 500 raise SyntaxError("Wrong optype \"%s\" must be \"hash\""%self.optype) 501 else: 502 if len(t) == 4: 503 # Set three operator components. 504 self.relations = ['equal'] 505 self.temporal = "l" 506 self.function = t[2] 507 elif len(t) == 6: 508 if isinstance(t[4], list): 509 self.relations = t[4] 510 else: 511 self.relations = [t[4]] 512 self.temporal = "l" 513 self.function = t[2] 514 elif len(t) == 8: 515 if isinstance(t[4], list): 516 self.relations = t[4] 517 else: 518 self.relations = [t[4]] 519 self.temporal = t[6] 520 self.function = t[2] 521 t[0] = t[2] 522 523 def p_raster_relation_operator(self, t): 524 # {+} 525 # {-, during} 526 # {*, during | equal | starts} 527 # {/, during | equal | starts, l} 528 """ 529 operator : CLPAREN arithmetic CRPAREN 530 | CLPAREN arithmetic COMMA relation CRPAREN 531 | CLPAREN arithmetic COMMA relationlist CRPAREN 532 | CLPAREN arithmetic COMMA relation COMMA temporal CRPAREN 533 | CLPAREN arithmetic COMMA relationlist COMMA temporal CRPAREN 534 """ 535 if not self.optype == 'raster': 536 raise SyntaxError("Wrong optype \"%s\" must be \"raster\""%self.optype) 537 else: 538 if len(t) == 4: 539 # Set three operator components. 540 self.relations = ['equal'] 541 self.temporal = "l" 542 self.function = t[2] 543 elif len(t) == 6: 544 if isinstance(t[4], list): 545 self.relations = t[4] 546 else: 547 self.relations = [t[4]] 548 self.temporal = "l" 549 self.function = t[2] 550 elif len(t) == 8: 551 if isinstance(t[4], list): 552 self.relations = t[4] 553 else: 554 self.relations = [t[4]] 555 self.temporal = t[6] 556 self.function = t[2] 557 t[0] = t[2] 558 559 def p_overlay_relation_operator(self, t): 560 # {+} 561 # {-, during} 562 # {~, during | equal | starts} 563 # {^, during | equal | starts, l} 564 """ 565 operator : CLPAREN overlay CRPAREN 566 | CLPAREN overlay COMMA relation CRPAREN 567 | CLPAREN overlay COMMA relationlist CRPAREN 568 | CLPAREN overlay COMMA relation COMMA temporal CRPAREN 569 | CLPAREN overlay COMMA relationlist COMMA temporal CRPAREN 570 """ 571 if not self.optype == 'overlay': 572 raise SyntaxError("Wrong optype \"%s\" must be \"overlay\""%self.optype) 573 else: 574 if len(t) == 4: 575 # Set three operator components. 576 self.relations = ['equal'] 577 self.temporal = "l" 578 self.function = t[2] 579 elif len(t) == 6: 580 if isinstance(t[4], list): 581 self.relations = t[4] 582 else: 583 self.relations = [t[4]] 584 self.temporal = "l" 585 self.function = t[2] 586 elif len(t) == 8: 587 if isinstance(t[4], list): 588 self.relations = t[4] 589 else: 590 self.relations = [t[4]] 591 self.temporal = t[6] 592 self.function = t[2] 593 t[0] = t[2] 594 595 def p_relation(self, t): 596 # The list of relations. Temporal and spatial relations are supported 597 """ 598 relation : EQUAL 599 | FOLLOWS 600 | PRECEDES 601 | OVERLAPS 602 | OVERLAPPED 603 | DURING 604 | STARTS 605 | FINISHES 606 | CONTAINS 607 | STARTED 608 | FINISHED 609 | EQUIVALENT 610 | COVER 611 | OVERLAP 612 | IN 613 | CONTAIN 614 | MEET 615 """ 616 t[0] = t[1] 617 618 def p_over(self, t): 619 # The the over keyword 620 """ 621 relation : OVER 622 """ 623 over_list = ["overlaps", "overlapped"] 624 t[0] = over_list 625 626 def p_relationlist(self, t): 627 # The list of relations. 628 """ 629 relationlist : relation OR relation 630 | relation OR relationlist 631 """ 632 rel_list = [] 633 rel_list.append(t[1]) 634 if isinstance(t[3], list): 635 rel_list = rel_list + t[3] 636 else: 637 rel_list.append(t[3]) 638 t[0] = rel_list 639 640 def p_temporal_operator(self, t): 641 # The list of relations. 642 """ 643 temporal : LEFTREF 644 | RIGHTREF 645 | UNION 646 | DISJOINT 647 | INTERSECT 648 """ 649 t[0] = t[1] 650 651 def p_select_operator(self, t): 652 # The list of relations. 653 """ 654 select : T_SELECT 655 | T_NOT_SELECT 656 """ 657 t[0] = t[1] 658 659 def p_arithmetic_operator(self, t): 660 # The list of relations. 661 """ 662 arithmetic : MOD 663 | DIV 664 | MULT 665 | ADD 666 | SUB 667 """ 668 t[0] = t[1] 669 670 def p_overlay_operator(self, t): 671 # The list of relations. 672 """ 673 overlay : AND 674 | OR 675 | XOR 676 | DISOR 677 | NOT 678 """ 679 t[0] = t[1] 680 681############################################################################### 682 683if __name__ == "__main__": 684 import doctest 685 doctest.testmod() 686