exploration.parsing
- Authors: Peter Mawhorter
- Consulted:
- Date: 2023-12-27
- Purpose: Common code for parsing things, including conversions to custom string formats and JSON for some types.
1""" 2- Authors: Peter Mawhorter 3- Consulted: 4- Date: 2023-12-27 5- Purpose: Common code for parsing things, including conversions to 6 custom string formats and JSON for some types. 7""" 8 9from typing import ( 10 Union, Dict, Optional, get_args, Tuple, List, cast, Set, TypeVar, 11 Literal, TypeAlias, Generator, TypedDict, TextIO, Any, Callable, 12 Type, Sequence 13) 14 15import enum 16import collections 17import copy 18import warnings 19import json 20import types 21 22import networkx # type: ignore 23 24from . import base 25from . import core 26from . import utils 27from . import commands 28 29 30#----------------# 31# Format Details # 32#----------------# 33 34Lexeme = enum.IntEnum( 35 "Lexeme", 36 [ 37 'domainSeparator', 38 'zoneSeparator', 39 'partSeparator', 40 'stateOn', 41 'stateOff', 42 'tokenCount', 43 'effectCharges', 44 'sepOrDelay', 45 'consequenceSeparator', 46 'inCommon', 47 'isHidden', 48 'skillLevel', 49 'wigglyLine', 50 'withDetails', 51 'reciprocalSeparator', 52 'mechanismSeparator', 53 'openCurly', 54 'closeCurly', 55 'openParen', 56 'closeParen', 57 'angleLeft', 58 'angleRight', 59 'doubleQuestionmark', 60 'ampersand', 61 'orBar', 62 'notMarker', 63 ] 64) 65""" 66These are the different separators, grouping characters, and keywords 67used as part of parsing. The characters that are actually recognized are 68defined as part of a `Format`. 69""" 70 71Format = Dict[Lexeme, str] 72""" 73A journal format is specified using a dictionary with keys that denote 74journal marker types and values which are one-to-several-character 75strings indicating the markup used for that entry/info type. 76""" 77 78DEFAULT_FORMAT: Format = { 79 # Separator 80 Lexeme.domainSeparator: '//', 81 Lexeme.zoneSeparator: '::', 82 Lexeme.partSeparator: '%%', 83 Lexeme.stateOn: '=on', # TODO :Lexing issue! 84 Lexeme.stateOff: '=off', 85 Lexeme.tokenCount: '*', 86 Lexeme.effectCharges: '=', 87 Lexeme.sepOrDelay: ',', 88 Lexeme.consequenceSeparator: ';', 89 Lexeme.inCommon: '+c', 90 Lexeme.isHidden: '+h', 91 Lexeme.skillLevel: '^', 92 Lexeme.wigglyLine: '~', 93 Lexeme.withDetails: '%', 94 Lexeme.reciprocalSeparator: '/', 95 Lexeme.mechanismSeparator: ':', 96 Lexeme.openCurly: '{', 97 Lexeme.closeCurly: '}', 98 Lexeme.openParen: '(', 99 Lexeme.closeParen: ')', 100 Lexeme.angleLeft: '<', 101 Lexeme.angleRight: '>', 102 Lexeme.doubleQuestionmark: '??', 103 Lexeme.ampersand: '&', 104 Lexeme.orBar: '|', 105 Lexeme.notMarker: '!', 106} 107""" 108The default parsing format. 109""" 110 111DEFAULT_EFFECT_NAMES: Dict[str, base.EffectType] = { 112 x: x for x in get_args(base.EffectType) 113} 114""" 115Default names for each effect type. Maps names to canonical effect type 116strings. A different mapping could be used to allow for writing effect 117names in another language, for example. 118""" 119 120DEFAULT_FOCALIZATION_NAMES: Dict[str, base.DomainFocalization] = { 121 x: x for x in get_args(base.DomainFocalization) 122} 123""" 124Default names for each domain focalization type. Maps each focalization 125type string to itself. 126""" 127 128DEFAULT_SF_INDICATORS: Tuple[str, str] = ('s', 'f') 129""" 130Default characters used to indicate success/failure when transcribing a 131`TransitionWithOutcomes`. 132""" 133 134 135#-------------------# 136# Errors & Warnings # 137#-------------------# 138 139class ParseWarning(Warning): 140 """ 141 Represents a warning encountered when parsing something. 142 """ 143 pass 144 145 146class ParseError(ValueError): 147 """ 148 Represents a error encountered when parsing. 149 """ 150 pass 151 152 153class DotParseError(ParseError): 154 """ 155 An error raised during parsing when incorrectly-formatted graphviz 156 "dot" data is provided. See `parseDot`. 157 """ 158 pass 159 160 161class InvalidFeatureSpecifierError(ParseError): 162 """ 163 An error used when a feature specifier is in the wrong format. 164 Errors with part specifiers also use this. 165 """ 166 167 168#--------# 169# Lexing # 170#--------# 171 172LexedTokens: TypeAlias = List[Union[Lexeme, str]] 173""" 174When lexing, we pull apart a string into pieces, but when we recognize 175lexemes, we use their integer IDs in the list instead of strings, so we 176get a list that's a mix of ints and strings. 177""" 178 179GroupedTokens: TypeAlias = List[Union[Lexeme, str, 'GroupedTokens']] 180""" 181Some parsing processes group tokens into sub-lists. This type represents 182`LexedTokens` which might also contain sub-lists, to arbitrary depth. 183""" 184 185GroupedRequirementParts: TypeAlias = List[ 186 Union[Lexeme, base.Requirement, 'GroupedRequirementParts'] 187] 188""" 189Another intermediate parsing result during requirement parsing: a list 190of `base.Requirements` possibly with some sub-lists and/or `Lexeme`s 191mixed in. 192""" 193 194 195def lex( 196 characters: str, 197 tokenMap: Optional[Dict[str, Lexeme]] = None 198) -> LexedTokens: 199 """ 200 Lexes a list of tokens from a characters string. Recognizes any 201 special characters you provide in the token map, as well as 202 collections of non-mapped characters. Recognizes double-quoted 203 strings which can contain any of those (and which use 204 backslash-escapes for internal double quotes) and includes quoted 205 versions of those strings as tokens (any token string starting with a 206 double quote will be such a string). Breaks tokens on whitespace 207 outside of quotation marks, and ignores that whitespace. 208 209 Examples: 210 211 >>> lex('abc') 212 ['abc'] 213 >>> lex('(abc)', {'(': 0, ')': 1}) 214 [0, 'abc', 1] 215 >>> lex('{(abc)}', {'(': 0, ')': 1, '{': 2, '}': 3}) 216 [2, 0, 'abc', 1, 3] 217 >>> lex('abc def') 218 ['abc', 'def'] 219 >>> lex('abc def') 220 ['abc', 'def'] 221 >>> lex('abc \\n def') 222 ['abc', 'def'] 223 >>> lex ('"quoted"') 224 ['"quoted"'] 225 >>> lex ('"quoted pair"') 226 ['"quoted pair"'] 227 >>> lex (' oneWord | "two words"|"three words words" ', {'|': 0}) 228 ['oneWord', 0, '"two words"', 0, '"three words words"'] 229 >>> tokenMap = { c: i for (i, c) in enumerate("(){}~:;>,") } 230 >>> tokenMap['::'] = 9 231 >>> tokenMap['~~'] = 10 232 >>> lex( 233 ... '{~~2:best(brains, brawn)>{set switch on}' 234 ... '{deactivate ,1; bounce}}', 235 ... tokenMap 236 ... ) 237 [2, 10, '2', 5, 'best', 0, 'brains', 8, 'brawn', 1, 7, 2, 'set',\ 238 'switch', 'on', 3, 2, 'deactivate', 8, '1', 6, 'bounce', 3, 3] 239 >>> lex('set where::mechanism state', tokenMap) 240 ['set', 'where', 9, 'mechanism', 'state'] 241 >>> # Note r' doesn't take full effect 'cause we're in triple quotes 242 >>> esc = r'"escape \\\\a"' 243 >>> result = [ r'"escape \\\\a"' ] # 'quoted' doubles the backslash 244 >>> len(esc) 245 12 246 >>> len(result[0]) 247 12 248 >>> lex(esc) == result 249 True 250 >>> quoteInQuote = r'before "hello \\\\ \\" goodbye"after' 251 >>> # Note r' doesn't take full effect 'cause we're in triple quotes 252 >>> expect = ['before', r'"hello \\\\ \\" goodbye"', 'after'] 253 >>> lex(quoteInQuote) == expect 254 True 255 >>> lex('O\\'Neill') 256 ["O'Neill"] 257 >>> lex('one "quote ') 258 ['one', '"quote "'] 259 >>> lex('geo*15', {'*': 0}) 260 ['geo', 0, '15'] 261 """ 262 if tokenMap is None: 263 tokenMap = {} 264 tokenStarts: Dict[str, List[str]] = {} 265 for key in sorted(tokenMap.keys(), key=lambda x: -len(x)): 266 tokenStarts.setdefault(key[:1], []).append(key) 267 tokens: LexedTokens = [] 268 sofar = '' 269 inQuote = False 270 escaped = False 271 skip = 0 272 for i in range(len(characters)): 273 if skip > 0: 274 skip -= 1 275 continue 276 277 char = characters[i] 278 if escaped: 279 # TODO: Escape sequences? 280 sofar += char 281 escaped = False 282 283 elif char == '\\': 284 if inQuote: 285 escaped = True 286 else: 287 sofar += char 288 289 elif char == '"': 290 if sofar != '': 291 if inQuote: 292 tokens.append(utils.quoted(sofar)) 293 else: 294 tokens.append(sofar) 295 sofar = '' 296 inQuote = not inQuote 297 298 elif inQuote: 299 sofar += char 300 301 elif char in tokenStarts: 302 options = tokenStarts[char] 303 hit: Optional[str] = None 304 for possibility in options: 305 lp = len(possibility) 306 if ( 307 (lp == 1 and char == possibility) 308 or characters[i:i + lp] == possibility 309 ): 310 hit = possibility 311 break 312 313 if hit is not None: 314 if sofar != '': 315 tokens.append(sofar) 316 tokens.append(tokenMap[possibility]) 317 sofar = '' 318 skip = len(hit) - 1 319 else: # Not actually a recognized token 320 sofar += char 321 322 elif char.isspace(): 323 if sofar != '': 324 tokens.append(sofar) 325 sofar = '' 326 327 else: 328 sofar += char 329 330 if sofar != '': 331 if inQuote: 332 tokens.append(utils.quoted(sofar)) 333 else: 334 tokens.append(sofar) 335 336 return tokens 337 338 339def unLex( 340 tokens: LexedTokens, 341 tokenMap: Optional[Dict[str, Lexeme]] = None 342) -> str: 343 """ 344 Turns lexed stuff back into a string, substituting strings back into 345 token spots by reversing the given token map. Adds quotation marks to 346 complex tokens where necessary to prevent them from re-lexing into 347 multiple tokens (but `lex` doesn't remove those, so in some cases 348 there's not a perfect round-trip unLex -> lex). 349 350 For example: 351 352 >>> unLex(['a', 'b']) 353 'a b' 354 >>> tokens = {'(': 0, ')': 1, '{': 2, '}': 3, '::': 4} 355 >>> unLex([0, 'hi', 1], tokens) 356 '(hi)' 357 >>> unLex([0, 'visit', 'zone', 4, 'decision', 1], tokens) 358 '(visit zone::decision)' 359 >>> q = unLex(['a complex token', '\\'single\\' and "double" quotes']) 360 >>> q # unLex adds quotes 361 '"a complex token" "\\'single\\' and \\\\"double\\\\" quotes"' 362 >>> lex(q) # Not the same as the original list 363 ['"a complex token"', '"\\'single\\' and \\\\"double\\\\" quotes"'] 364 >>> lex(unLex(lex(q))) # But further round-trips work 365 ['"a complex token"', '"\\'single\\' and \\\\"double\\\\" quotes"'] 366 367 TODO: Fix this: 368 For now, it generates incorrect results when token combinations can 369 be ambiguous. These ambiguous token combinations should not ever be 370 generated by `lex` at least. For example: 371 372 >>> ambiguous = {':': 0, '::': 1} 373 >>> u = unLex(['a', 0, 0, 'b'], ambiguous) 374 >>> u 375 'a::b' 376 >>> l = lex(u, ambiguous) 377 >>> l 378 ['a', 1, 'b'] 379 >>> l == u 380 False 381 """ 382 if tokenMap is None: 383 nTokens = 0 384 revMap = {} 385 else: 386 nTokens = len(tokenMap) 387 revMap = {y: x for (x, y) in tokenMap.items()} 388 389 prevRaw = False 390 # TODO: add spaces where necessary to disambiguate token sequences... 391 if len(revMap) != nTokens: 392 warnings.warn( 393 ( 394 "Irreversible token map! Two or more tokens have the same" 395 " integer value." 396 ), 397 ParseWarning 398 ) 399 400 result = "" 401 for item in tokens: 402 if isinstance(item, int): 403 try: 404 result += revMap[item] 405 except KeyError: 406 raise ValueError( 407 f"Tokens list contains {item} but the token map" 408 f" does not have any entry which maps to {item}." 409 ) 410 prevRaw = False 411 elif isinstance(item, str): 412 if prevRaw: 413 result += ' ' 414 if len(lex(item)) > 1: 415 result += utils.quoted(item) 416 else: 417 result += item 418 prevRaw = True 419 else: 420 raise TypeError( 421 f"Token list contained non-int non-str item:" 422 f" {repr(item)}" 423 ) 424 425 return result 426 427 428#-------------------# 429# ParseFormat class # 430#-------------------# 431 432def normalizeEnds( 433 tokens: List, 434 start: int, 435 end: int 436) -> Tuple[int, int, int]: 437 """ 438 Given a tokens list and start & end integers, does some bounds 439 checking and normalization on the integers: converts negative 440 indices to positive indices, and raises an `IndexError` if they're 441 out-of-bounds after conversion. Returns a tuple containing the 442 normalized start & end indices, along with the number of tokens they 443 cover. 444 """ 445 totalTokens = len(tokens) 446 if start < -len(tokens): 447 raise IndexError( 448 f"Negative start index out of bounds (got {start} for" 449 f" {totalTokens} tokens)." 450 ) 451 elif start >= totalTokens: 452 raise IndexError( 453 f"Start index out of bounds (got {start} for" 454 f" {totalTokens} tokens)." 455 ) 456 elif start < 0: 457 start = totalTokens + start 458 459 if end < -len(tokens): 460 raise IndexError( 461 f"Negative end index out of bounds (got {end} for" 462 f" {totalTokens} tokens)." 463 ) 464 elif end >= totalTokens: 465 raise IndexError( 466 f"Start index out of bounds (got {end} for" 467 f" {totalTokens} tokens)." 468 ) 469 elif end < 0: 470 end = totalTokens + end 471 472 if end >= len(tokens): 473 end = len(tokens) - 1 474 475 return (start, end, (end - start) + 1) 476 477 478def findSeparatedParts( 479 tokens: LexedTokens, 480 sep: Union[str, int], 481 start: int = 0, 482 end: int = -1, 483 groupStart: Union[str, int, None] = None, 484 groupEnd: Union[str, int, None] = None 485) -> Generator[Tuple[int, int], None, None]: 486 """ 487 Finds parts separated by a separator lexeme, such as ';' or ',', but 488 ignoring separators nested within groupStart/groupEnd pairs (if 489 those arguments are supplied). For each token sequence found, yields 490 a tuple containing the start index and end index for that part, with 491 separators not included in the parts. 492 493 If two separators appear in a row, the start/end pair will have a 494 start index one after the end index. 495 496 If there are no separators, yields one pair containing the start and 497 end of the entire tokens sequence. 498 499 Raises a `ParseError` if there are unbalanced grouping elements. 500 501 For example: 502 503 >>> list(findSeparatedParts( 504 ... [ 'one' ], 505 ... Lexeme.sepOrDelay, 506 ... 0, 507 ... 0, 508 ... Lexeme.openParen, 509 ... Lexeme.closeParen 510 ... )) 511 [(0, 0)] 512 >>> list(findSeparatedParts( 513 ... [ 514 ... 'best', 515 ... Lexeme.openParen, 516 ... 'chess', 517 ... Lexeme.sepOrDelay, 518 ... 'checkers', 519 ... Lexeme.closeParen 520 ... ], 521 ... Lexeme.sepOrDelay, 522 ... 2, 523 ... 4, 524 ... Lexeme.openParen, 525 ... Lexeme.closeParen 526 ... )) 527 [(2, 2), (4, 4)] 528 """ 529 start, end, n = normalizeEnds(tokens, start, end) 530 level = 0 531 thisStart = start 532 for i in range(start, end + 1): 533 token = tokens[i] 534 if token == sep and level == 0: 535 yield (thisStart, i - 1) 536 thisStart = i + 1 537 elif token == groupStart: 538 level += 1 539 elif token == groupEnd: 540 level -= 1 541 if level < 0: 542 raise ParseError("Unbalanced grouping tokens.") 543 if level < 0: 544 raise ParseError("Unbalanced grouping tokens.") 545 yield (thisStart, end) 546 547 548K = TypeVar('K') 549"Type variable for dictionary keys." 550V = TypeVar('V') 551"Type variable for dictionary values." 552 553 554def checkCompleteness( 555 name, 556 mapping: Dict[K, V], 557 keysSet: Optional[Set[K]] = None, 558 valuesSet: Optional[Set[V]] = None 559): 560 """ 561 Checks that a dictionary has a certain exact set of keys (or 562 values). Raises a `ValueError` if it finds an extra or missing key 563 or value. 564 """ 565 if keysSet is not None: 566 for key in mapping.keys(): 567 if key not in keysSet: 568 raise ValueError("{name} has extra key {repr(key)}.") 569 570 for key in keysSet: 571 if key not in mapping: 572 raise ValueError("{name} is missing key {repr(key)}.") 573 574 if valuesSet is not None: 575 for value in mapping.values(): 576 if value not in valuesSet: 577 raise ValueError("{name} has extra value {repr(value)}.") 578 579 checkVals = mapping.values() 580 for value in valuesSet: 581 if value not in checkVals: 582 raise ValueError("{name} is missing value {repr(value)}.") 583 584 585class ParseFormat: 586 """ 587 A ParseFormat manages the mapping from markers to entry types and 588 vice versa. 589 """ 590 def __init__( 591 self, 592 formatDict: Format = DEFAULT_FORMAT, 593 effectNames: Dict[str, base.EffectType] = DEFAULT_EFFECT_NAMES, 594 focalizationNames: Dict[ 595 str, 596 base.DomainFocalization 597 ] = DEFAULT_FOCALIZATION_NAMES, 598 successFailureIndicators: Tuple[str, str] = DEFAULT_SF_INDICATORS 599 ): 600 """ 601 Sets up the parsing format. Requires a `Format` dictionary to 602 define the specifics. Raises a `ValueError` unless the keys of 603 the `Format` dictionary exactly match the `Lexeme` values. 604 """ 605 self.formatDict = formatDict 606 self.effectNames = effectNames 607 self.focalizationNames = focalizationNames 608 if ( 609 len(successFailureIndicators) != 2 610 or any(len(i) != 1 for i in successFailureIndicators) 611 ): 612 raise ValueError( 613 f"Invalid success/failure indicators: must be a pair of" 614 f" length-1 strings. Got: {successFailureIndicators!r}" 615 ) 616 self.successIndicator, self.failureIndicator = ( 617 successFailureIndicators 618 ) 619 620 # Check completeness for each dictionary 621 checkCompleteness('formatDict', self.formatDict, set(Lexeme)) 622 checkCompleteness( 623 'effectNames', 624 self.effectNames, 625 valuesSet=set(get_args(base.EffectType)) 626 ) 627 checkCompleteness( 628 'focalizationNames', 629 self.focalizationNames, 630 valuesSet=set(get_args(base.DomainFocalization)) 631 ) 632 633 # Build some reverse lookup dictionaries for specific 634 self.reverseFormat = {y: x for (x, y) in self.formatDict.items()} 635 636 # circumstances: 637 self.effectModMap = { 638 self.formatDict[x]: x 639 for x in [ 640 Lexeme.effectCharges, 641 Lexeme.sepOrDelay, 642 Lexeme.inCommon, 643 Lexeme.isHidden 644 ] 645 } 646 647 def lex(self, content: str) -> LexedTokens: 648 """ 649 Applies `lex` using this format's lexeme mapping. 650 """ 651 return lex(content, self.reverseFormat) 652 653 def onOff(self, word: str) -> Optional[bool]: 654 """ 655 Parse an on/off indicator and returns a boolean (`True` for on 656 and `False` for off). Returns `None` if the word isn't either 657 the 'on' or the 'off' word. Generates a `ParseWarning` 658 (and still returns `None`) if the word is a case-swapped version 659 of the 'on' or 'off' word and is not equal to either of them. 660 """ 661 onWord = self.formatDict[Lexeme.stateOn] 662 offWord = self.formatDict[Lexeme.stateOff] 663 664 # Generate warning if we suspect a case error 665 if ( 666 word.casefold() in (onWord, offWord) 667 and word not in (onWord, offWord) 668 ): 669 warnings.warn( 670 ( 671 f"Word '{word}' cannot be interpreted as an on/off" 672 f" value, although it is almost one (the correct" 673 f" values are '{onWord}' and '{offWord}'." 674 ), 675 ParseWarning 676 ) 677 678 # return the appropriate value 679 if word == onWord: 680 return True 681 elif word == offWord: 682 return False 683 else: 684 return None 685 686 def matchingBrace( 687 self, 688 tokens: LexedTokens, 689 where: int, 690 opener: int = Lexeme.openCurly, 691 closer: int = Lexeme.closeCurly 692 ) -> int: 693 """ 694 Returns the index within the given tokens list of the closing 695 curly brace which matches the open brace at the specified index. 696 You can specify custom `opener` and/or `closer` lexemes to find 697 matching pairs of other things. Raises a `ParseError` if there 698 is no opening brace at the specified index, or if there isn't a 699 matching closing brace. Handles nested braces of the specified 700 type. 701 702 Examples: 703 >>> pf = ParseFormat() 704 >>> ob = Lexeme.openCurly 705 >>> cb = Lexeme.closeCurly 706 >>> pf.matchingBrace([ob, cb], 0) 707 1 708 >>> pf.matchingBrace([ob, cb], 1) 709 Traceback (most recent call last): 710 ... 711 exploration.parsing.ParseError: ... 712 >>> pf.matchingBrace(['hi', ob, cb], 0) 713 Traceback (most recent call last): 714 ... 715 exploration.parsing.ParseError: ... 716 >>> pf.matchingBrace(['hi', ob, cb], 1) 717 2 718 >>> pf.matchingBrace(['hi', ob, 'lo', cb], 1) 719 3 720 >>> pf.matchingBrace([ob, 'hi', 'lo', cb], 1) 721 Traceback (most recent call last): 722 ... 723 exploration.parsing.ParseError: ... 724 >>> pf.matchingBrace([ob, 'hi', 'lo', cb], 0) 725 3 726 >>> pf.matchingBrace([ob, ob, cb, cb], 0) 727 3 728 >>> pf.matchingBrace([ob, ob, cb, cb], 1) 729 2 730 >>> pf.matchingBrace([ob, cb, ob, cb], 0) 731 1 732 >>> pf.matchingBrace([ob, cb, ob, cb], 2) 733 3 734 >>> pf.matchingBrace([ob, cb, cb, cb], 0) 735 1 736 >>> pf.matchingBrace([ob, ob, ob, cb], 0) 737 Traceback (most recent call last): 738 ... 739 exploration.parsing.ParseError: ... 740 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 0) 741 7 742 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 1) 743 6 744 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 2) 745 Traceback (most recent call last): 746 ... 747 exploration.parsing.ParseError: ... 748 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 3) 749 4 750 >>> op = Lexeme.openParen 751 >>> cp = Lexeme.closeParen 752 >>> pf.matchingBrace([ob, op, ob, cp], 1, op, cp) 753 3 754 """ 755 if where >= len(tokens): 756 raise ParseError( 757 f"Out-of-bounds brace start: index {where} with" 758 f" {len(tokens)} tokens." 759 ) 760 if tokens[where] != opener: 761 raise ParseError( 762 f"Can't find matching brace for token" 763 f" {repr(tokens[where])} at index {where} because it's" 764 f" not an open brace." 765 ) 766 767 level = 1 768 for i in range(where + 1, len(tokens)): 769 token = tokens[i] 770 if token == opener: 771 level += 1 772 elif token == closer: 773 level -= 1 774 if level == 0: 775 return i 776 777 raise ParseError( 778 f"Failed to find matching curly brace from index {where}." 779 ) 780 781 def parseFocalization(self, word: str) -> base.DomainFocalization: 782 """ 783 Parses a focalization type for a domain, recognizing 784 'domainFocalizationSingular', 'domainFocalizationPlural', and 785 'domainFocalizationSpreading'. 786 """ 787 try: 788 return self.focalizationNames[word] 789 except KeyError: 790 raise ParseError( 791 f"Invalid domain focalization name {repr(word)}. Valid" 792 f" name are: {repr(list(self.focalizationNames))}'." 793 ) 794 795 def parseTagValue(self, value: str) -> base.TagValue: 796 """ 797 Converts a string to a tag value, following these rules: 798 799 1. If the string is exactly one of 'None', 'True', or 'False', we 800 convert it to the corresponding Python value. 801 2. If the string can be converted to an integer without raising a 802 ValueError, we use that integer. 803 3. If the string can be converted to a float without raising a 804 ValueError, we use that float. 805 4. Otherwise, it remains a string. 806 807 Note that there is currently no syntax for using list, dictionary, 808 Requirement, or Consequence tag values. 809 TODO: Support those types? 810 811 Examples: 812 813 >>> pf = ParseFormat() 814 >>> pf.parseTagValue('hi') 815 'hi' 816 >>> pf.parseTagValue('3') 817 3 818 >>> pf.parseTagValue('3.0') 819 3.0 820 >>> pf.parseTagValue('True') 821 True 822 >>> pf.parseTagValue('False') 823 False 824 >>> pf.parseTagValue('None') is None 825 True 826 >>> pf.parseTagValue('none') 827 'none' 828 """ 829 # TODO: Allow these keywords to be redefined? 830 if value == 'True': 831 return True 832 elif value == 'False': 833 return False 834 elif value == 'None': 835 return None 836 else: 837 try: 838 return int(value) 839 except ValueError: 840 try: 841 return float(value) 842 except ValueError: 843 return value 844 845 def unparseTagValue(self, value: base.TagValue) -> str: 846 """ 847 Converts a tag value into a string that would be parsed back into a 848 tag value via `parseTagValue`. Currently does not work for list, 849 dictionary, Requirement, or Consequence values. 850 TODO: Those 851 """ 852 return str(value) 853 854 def hasZoneParts(self, name: str) -> bool: 855 """ 856 Returns true if the specified name contains zone parts (using 857 the `zoneSeparator`). 858 """ 859 return self.formatDict[Lexeme.zoneSeparator] in name 860 861 def splitZone( 862 self, 863 name: str 864 ) -> Tuple[List[base.Zone], base.DecisionName]: 865 """ 866 Splits a decision name that includes zone information into the 867 list-of-zones part and the decision part. If there is no zone 868 information in the name, the list-of-zones will be an empty 869 list. 870 """ 871 sep = self.formatDict[Lexeme.zoneSeparator] 872 parts = name.split(sep) 873 return (list(parts[:-1]), parts[-1]) 874 875 def prefixWithZone( 876 self, 877 name: base.DecisionName, 878 zone: base.Zone 879 ) -> base.DecisionName: 880 """ 881 Returns the given decision name, prefixed with the given zone 882 name. Does NOT check whether the decision name already includes 883 a prefix or not. 884 """ 885 return zone + self.formatDict[Lexeme.zoneSeparator] + name 886 887 def parseAnyTransitionFromTokens( 888 self, 889 tokens: LexedTokens, 890 start: int = 0 891 ) -> Tuple[base.TransitionWithOutcomes, int]: 892 """ 893 Parses a `base.TransitionWithOutcomes` from a tokens list, 894 accepting either a transition name or a transition name followed 895 by a `Lexeme.withDetails` followed by a string of success and 896 failure indicator characters. Returns a tuple containing a 897 `base.TransitionWithOutcomes` and an integer indicating the end 898 index of the parsed item within the tokens. 899 """ 900 # Normalize start index so we can do index math 901 if start < 0: 902 useIndex = len(tokens) + start 903 else: 904 useIndex = start 905 906 try: 907 first = tokens[useIndex] 908 except IndexError: 909 raise ParseError( 910 f"Invalid token index: {start!r} among {len(tokens)}" 911 f" tokens." 912 ) 913 914 if isinstance(first, Lexeme): 915 raise ParseError( 916 f"Expecting a transition name (possibly with a" 917 f" success/failure indicator string) but first token is" 918 f" {first!r}." 919 ) 920 921 try: 922 second = tokens[useIndex + 1] 923 third = tokens[useIndex + 2] 924 except IndexError: 925 return ((first, []), useIndex) 926 927 if second != Lexeme.withDetails or isinstance(third, Lexeme): 928 return ((first, []), useIndex) 929 930 outcomes = [] 931 for char in third: 932 if char == self.successIndicator: 933 outcomes.append(True) 934 elif char == self.failureIndicator: 935 outcomes.append(False) 936 else: 937 return ((first, []), useIndex) 938 939 return ((first, outcomes), useIndex + 2) 940 941 def parseTransitionWithOutcomes( 942 self, 943 content: str 944 ) -> base.TransitionWithOutcomes: 945 """ 946 Takes a transition that may have outcomes listed as a series of 947 s/f strings after a colon and returns the corresponding 948 `TransitionWithOutcomes` tuple. Calls `lex` and then 949 `parseAnyTransitionFromTokens`. 950 """ 951 return self.parseAnyTransitionFromTokens(self.lex(content))[0] 952 953 def unparseTransitionWithOutocmes( 954 self, 955 transition: base.AnyTransition 956 ) -> str: 957 """ 958 Turns a `base.AnyTransition` back into a string that would parse 959 to an equivalent `base.TransitionWithOutcomes` via 960 `parseTransitionWithOutcomes`. If a bare `base.Transition` is 961 given, returns a string that would result in a 962 `base.TransitionWithOutcomes` that has an empty outcomes 963 sequence. 964 """ 965 if isinstance(transition, base.Transition): 966 return transition 967 elif ( 968 isinstance(transition, tuple) 969 and len(transition) == 2 970 and isinstance(transition[0], base.Transition) 971 and isinstance(transition[1], list) 972 and all(isinstance(sfi, bool) for sfi in transition[1]) 973 ): 974 if len(transition[1]) == 0: 975 return transition[0] 976 else: 977 result = transition[0] + self.formatDict[Lexeme.withDetails] 978 for outcome in transition[1]: 979 if outcome: 980 result += self.successIndicator 981 else: 982 result += self.failureIndicator 983 return result 984 else: 985 raise TypeError( 986 f"Invalid AnyTransition: neither a string, nor a" 987 f" length-2 tuple consisting of a string followed by a" 988 f" list of booleans. Got: {transition!r}" 989 ) 990 991 def parseSpecificTransition( 992 self, 993 content: str 994 ) -> Tuple[base.DecisionName, base.Transition]: 995 """ 996 Splits a decision:transition pair to the decision and transition 997 part, using a custom separator if one is defined. 998 """ 999 sep = self.formatDict[Lexeme.withDetails] 1000 n = content.count(sep) 1001 if n == 0: 1002 raise ParseError( 1003 f"Cannot split '{content}' into a decision name and a" 1004 f" transition name (no separator '{sep}' found)." 1005 ) 1006 elif n > 1: 1007 raise ParseError( 1008 f"Cannot split '{content}' into a decision name and a" 1009 f" transition name (too many ({n}) '{sep}' separators" 1010 f" found)." 1011 ) 1012 else: 1013 return cast( 1014 Tuple[base.DecisionName, base.Transition], 1015 tuple(content.split(sep)) 1016 ) 1017 1018 def splitDirections( 1019 self, 1020 content: str 1021 ) -> Tuple[Optional[str], Optional[str]]: 1022 """ 1023 Splits a piece of text using the 'Lexeme.reciprocalSeparator' 1024 into two pieces. If there is no separator, the second piece will 1025 be `None`; if either side of the separator is blank, that side 1026 will be `None`, and if there is more than one separator, a 1027 `ParseError` will be raised. Whitespace will be stripped from 1028 both sides of each result. 1029 1030 Examples: 1031 1032 >>> pf = ParseFormat() 1033 >>> pf.splitDirections('abc / def') 1034 ('abc', 'def') 1035 >>> pf.splitDirections('abc def ') 1036 ('abc def', None) 1037 >>> pf.splitDirections('abc def /') 1038 ('abc def', None) 1039 >>> pf.splitDirections('/abc def') 1040 (None, 'abc def') 1041 >>> pf.splitDirections('a/b/c') # doctest: +IGNORE_EXCEPTION_DETAIL 1042 Traceback (most recent call last): 1043 ... 1044 ParseError: ... 1045 """ 1046 sep = self.formatDict[Lexeme.reciprocalSeparator] 1047 count = content.count(sep) 1048 if count > 1: 1049 raise ParseError( 1050 f"Too many split points ('{sep}') in content:" 1051 f" '{content}' (only one is allowed)." 1052 ) 1053 1054 elif count == 1: 1055 before, after = content.split(sep) 1056 before = before.strip() 1057 after = after.strip() 1058 return (before or None, after or None) 1059 1060 else: # no split points 1061 stripped = content.strip() 1062 if stripped: 1063 return stripped, None 1064 else: 1065 return None, None 1066 1067 def parseItem( 1068 self, 1069 item: str 1070 ) -> Union[ 1071 base.Capability, 1072 Tuple[base.Token, int], 1073 Tuple[base.MechanismName, base.MechanismState] 1074 ]: 1075 """ 1076 Parses an item, which is a capability (just a string), a 1077 token-type*number pair (returned as a tuple with the number 1078 converted to an integer), or a mechanism-name:state pair 1079 (returned as a tuple with the state as a string). The 1080 'Lexeme.tokenCount' and `Lexeme.mechanismSeparator` format 1081 values determine the separators that this looks for. 1082 """ 1083 tsep = self.formatDict[Lexeme.tokenCount] 1084 msep = self.formatDict[Lexeme.mechanismSeparator] 1085 if tsep in item: 1086 # It's a token w/ an associated count 1087 parts = item.split(tsep) 1088 if len(parts) != 2: 1089 raise ParseError( 1090 f"Item '{item}' has a '{tsep}' but doesn't separate" 1091 f" into a token type and a count." 1092 ) 1093 typ, count = parts 1094 try: 1095 num = int(count) 1096 except ValueError: 1097 raise ParseError( 1098 f"Item '{item}' has invalid token count '{count}'." 1099 ) 1100 1101 return (typ, num) 1102 elif msep in item: 1103 parts = item.split(msep) 1104 mechanism = msep.join(parts[:-1]) 1105 state = parts[-1] 1106 if mechanism.endswith(':'): 1107 # Just a zone-qualified name... 1108 return item 1109 else: 1110 return (mechanism, state) 1111 else: 1112 # It's just a capability 1113 return item 1114 1115 def unparseAnyDecision(self, decision: base.AnyDecisionSpecifier) -> str: 1116 """ 1117 Turns any kind of decision specifier (ID, 1118 `base.DecisionSpecifier`, or name string) into a string that 1119 should parse back using `parseDecisionSpecifier`. 1120 1121 Raises a `TypeError` if given something that isn't a decision 1122 identifier. 1123 1124 For example: 1125 1126 >>> pf = ParseFormat() 1127 >>> pf.unparseAnyDecision( 1128 ... base.DecisionSpecifier("domain", "zone", "D") 1129 ... ) 1130 'domain//zone::D' 1131 >>> pf.unparseAnyDecision(3) 1132 '3' 1133 >>> pf.unparseAnyDecision('D') 1134 'D' 1135 >>> pf.unparseAnyDecision('domain//zone::D') 1136 'domain//zone::D' 1137 >>> pf.unparseAnyDecision([1, 2]) 1138 Traceback (most recent call last): 1139 ... 1140 TypeError... 1141 """ 1142 if isinstance(decision, base.DecisionSpecifier): 1143 return self.unparseDecisionSpecifier(decision) 1144 elif isinstance(decision, (base.DecisionID, base.DecisionName)): 1145 # leave as-is OR convert integer ID to string 1146 return str(decision) 1147 else: 1148 raise TypeError( 1149 "Unrecognized decision identifier type " + type(decision) 1150 ) 1151 1152 def unparseDecisionSpecifier(self, spec: base.DecisionSpecifier) -> str: 1153 """ 1154 Turns a decision specifier back into a string, which would be 1155 parsed as a decision specifier as part of various different 1156 things. 1157 1158 For example: 1159 1160 >>> pf = ParseFormat() 1161 >>> pf.unparseDecisionSpecifier( 1162 ... base.DecisionSpecifier(None, None, 'where') 1163 ... ) 1164 'where' 1165 >>> pf.unparseDecisionSpecifier( 1166 ... base.DecisionSpecifier(None, 'zone', 'where') 1167 ... ) 1168 'zone::where' 1169 >>> pf.unparseDecisionSpecifier( 1170 ... base.DecisionSpecifier('domain', 'zone', 'where') 1171 ... ) 1172 'domain//zone::where' 1173 >>> pf.unparseDecisionSpecifier( 1174 ... base.DecisionSpecifier('domain', None, 'where') 1175 ... ) 1176 'domain//where' 1177 """ 1178 result = spec.name 1179 if spec.zone is not None: 1180 result = ( 1181 spec.zone 1182 + self.formatDict[Lexeme.zoneSeparator] 1183 + result 1184 ) 1185 if spec.domain is not None: 1186 result = ( 1187 spec.domain 1188 + self.formatDict[Lexeme.domainSeparator] 1189 + result 1190 ) 1191 return result 1192 1193 def unparseMechanismSpecifier( 1194 self, 1195 spec: base.MechanismSpecifier 1196 ) -> str: 1197 """ 1198 Turns a mechanism specifier back into a string, which would be 1199 parsed as a mechanism specifier as part of various different 1200 things. Note that a mechanism specifier with a zone part but no 1201 decision part is not valid, since it would parse as a decision 1202 part instead. 1203 1204 For example: 1205 1206 >>> pf = ParseFormat() 1207 >>> pf.unparseMechanismSpecifier( 1208 ... base.MechanismSpecifier(None, None, None, 'lever') 1209 ... ) 1210 'lever' 1211 >>> pf.unparseMechanismSpecifier( 1212 ... base.MechanismSpecifier('domain', 'zone', 'decision', 'door') 1213 ... ) 1214 'domain//zone::decision::door' 1215 >>> pf.unparseMechanismSpecifier( 1216 ... base.MechanismSpecifier('domain', None, None, 'door') 1217 ... ) 1218 'domain//door' 1219 >>> pf.unparseMechanismSpecifier( 1220 ... base.MechanismSpecifier(None, 'a', 'b', 'door') 1221 ... ) 1222 'a::b::door' 1223 >>> pf.unparseMechanismSpecifier( 1224 ... base.MechanismSpecifier(None, 'a', None, 'door') 1225 ... ) 1226 Traceback (most recent call last): 1227 ... 1228 exploration.base.InvalidMechanismSpecifierError... 1229 >>> pf.unparseMechanismSpecifier( 1230 ... base.MechanismSpecifier(None, None, 'a', 'door') 1231 ... ) 1232 'a::door' 1233 >>> pf.unparseMechanismSpecifier( 1234 ... base.MechanismSpecifier(None, None, 37, 'door') 1235 ... ) 1236 '37::door' 1237 """ 1238 if spec.decision is None and spec.zone is not None: 1239 raise base.InvalidMechanismSpecifierError( 1240 f"Mechanism specifier has a zone part but no decision" 1241 f" part; it cannot be unparsed since it would parse" 1242 f" differently:\n{spec}" 1243 ) 1244 result = spec.name 1245 if spec.decision is not None: 1246 result = ( 1247 str(spec.decision) 1248 + self.formatDict[Lexeme.zoneSeparator] 1249 + result 1250 ) 1251 if spec.zone is not None: 1252 result = ( 1253 spec.zone 1254 + self.formatDict[Lexeme.zoneSeparator] 1255 + result 1256 ) 1257 if spec.domain is not None: 1258 result = ( 1259 spec.domain 1260 + self.formatDict[Lexeme.domainSeparator] 1261 + result 1262 ) 1263 return result 1264 1265 def effectType(self, effectMarker: str) -> Optional[base.EffectType]: 1266 """ 1267 Returns the `base.EffectType` string corresponding to the 1268 given effect marker string. Returns `None` for an unrecognized 1269 marker. 1270 """ 1271 return self.effectNames.get(effectMarker) 1272 1273 def parseCommandFromTokens( 1274 self, 1275 tokens: LexedTokens, 1276 start: int = 0, 1277 end: int = -1 1278 ) -> commands.Command: 1279 """ 1280 Given tokens that specify a `commands.Command`, parses that 1281 command and returns it. Really just turns the tokens back into 1282 strings and calls `commands.command`. 1283 1284 For example: 1285 1286 >>> pf = ParseFormat() 1287 >>> t = ['val', '5'] 1288 >>> c = commands.command(*t) 1289 >>> pf.parseCommandFromTokens(t) == c 1290 True 1291 >>> t = ['op', Lexeme.tokenCount, '$val', '$val'] 1292 >>> c = commands.command('op', '*', '$val', '$val') 1293 >>> pf.parseCommandFromTokens(t) == c 1294 True 1295 """ 1296 start, end, nTokens = normalizeEnds(tokens, start, end) 1297 args: List[str] = [] 1298 for token in tokens[start:end + 1]: 1299 if isinstance(token, Lexeme): 1300 args.append(self.formatDict[token]) 1301 else: 1302 args.append(token) 1303 1304 if len(args) == 0: 1305 raise ParseError( 1306 f"No arguments for command:\n{tokens[start:end + 1]}" 1307 ) 1308 return commands.command(*args) 1309 1310 def unparseCommand(self, command: commands.Command) -> str: 1311 """ 1312 Turns a `Command` back into the string that would produce that 1313 command when parsed using `parseCommandList`. 1314 1315 Note that the results will be more explicit in some cases than what 1316 `parseCommandList` would accept as input. 1317 1318 For example: 1319 1320 >>> pf = ParseFormat() 1321 >>> pf.unparseCommand( 1322 ... commands.LiteralValue(command='val', value='5') 1323 ... ) 1324 'val 5' 1325 >>> pf.unparseCommand( 1326 ... commands.LiteralValue(command='val', value='"5"') 1327 ... ) 1328 'val "5"' 1329 >>> pf.unparseCommand( 1330 ... commands.EstablishCollection( 1331 ... command='empty', 1332 ... collection='list' 1333 ... ) 1334 ... ) 1335 'empty list' 1336 >>> pf.unparseCommand( 1337 ... commands.AppendValue(command='append', value='$_') 1338 ... ) 1339 'append $_' 1340 """ 1341 candidate = None 1342 for k, v in commands.COMMAND_SETUP.items(): 1343 if v[0] == type(command): 1344 if candidate is None: 1345 candidate = k 1346 else: 1347 raise ValueError( 1348 f"COMMAND_SETUP includes multiple keys with" 1349 f" {type(command)} as their value type:" 1350 f" '{candidate}' and '{k}'." 1351 ) 1352 1353 if candidate is None: 1354 raise ValueError( 1355 f"COMMAND_SETUP has no key with {type(command)} as its" 1356 f" value type." 1357 ) 1358 1359 result = candidate 1360 for x in command[1:]: 1361 # TODO: Is this hack good enough? 1362 result += ' ' + str(x) 1363 return result 1364 1365 def unparseCommandList(self, commands: List[commands.Command]) -> str: 1366 """ 1367 Takes a list of commands and returns a string that would parse 1368 into them using `parseOneEffectArg`. The result contains 1369 newlines and indentation to make it easier to read. 1370 1371 For example: 1372 1373 >>> pf = ParseFormat() 1374 >>> pf.unparseCommandList( 1375 ... [commands.command('val', '5'), commands.command('pop')] 1376 ... ) 1377 '{\\n val 5;\\n pop;\\n}' 1378 """ 1379 result = self.formatDict[Lexeme.openCurly] 1380 for cmd in commands: 1381 result += f'\n {self.unparseCommand(cmd)};' 1382 if len(commands) > 0: 1383 result += '\n' 1384 return result + self.formatDict[Lexeme.closeCurly] 1385 1386 def parseCommandListFromTokens( 1387 self, 1388 tokens: LexedTokens, 1389 start: int = 0 1390 ) -> Tuple[List[commands.Command], int]: 1391 """ 1392 Parses a command list from a list of lexed tokens, which must 1393 start with `Lexeme.openCurly`. Returns the parsed command list 1394 as a list of `commands.Command` objects, along with the end 1395 index of that command list (which will be the matching curly 1396 brace. 1397 """ 1398 end = self.matchingBrace( 1399 tokens, 1400 start, 1401 Lexeme.openCurly, 1402 Lexeme.closeCurly 1403 ) 1404 parts = list( 1405 findSeparatedParts( 1406 tokens, 1407 Lexeme.consequenceSeparator, 1408 start + 1, 1409 end - 1, 1410 Lexeme.openCurly, 1411 Lexeme.closeCurly, 1412 ) 1413 ) 1414 return ( 1415 [ 1416 self.parseCommandFromTokens(tokens, fromIndex, toIndex) 1417 for fromIndex, toIndex in parts 1418 if fromIndex <= toIndex # ignore empty parts 1419 ], 1420 end 1421 ) 1422 1423 def parseOneEffectArg( 1424 self, 1425 tokens: LexedTokens, 1426 start: int = 0, 1427 limit: Optional[int] = None 1428 ) -> Tuple[ 1429 Union[ 1430 base.Capability, # covers 'str' possibility 1431 Tuple[base.Token, base.TokenCount], 1432 Tuple[Literal['skill'], base.Skill, base.Level], 1433 Tuple[base.MechanismSpecifier, base.MechanismState], 1434 base.DecisionSpecifier, 1435 base.DecisionID, 1436 Literal[Lexeme.inCommon, Lexeme.isHidden], 1437 Tuple[Literal[Lexeme.sepOrDelay, Lexeme.effectCharges], int], 1438 List[commands.Command] 1439 ], 1440 int 1441 ]: 1442 """ 1443 Looks at tokens starting at the specified position and parses 1444 one or more of them as an effect argument (an argument that 1445 could be given to `base.effect`). Looks at various key `Lexeme`s 1446 to determine which type to use. 1447 1448 Items in the tokens list beyond the specified limit will not be 1449 considered, even when they in theory could be grouped with items 1450 up to the limit into a more complex argument. 1451 1452 For example: 1453 1454 >>> pf = ParseFormat() 1455 >>> pf.parseOneEffectArg(['hi']) 1456 ('hi', 0) 1457 >>> pf.parseOneEffectArg(['hi'], 1) 1458 Traceback (most recent call last): 1459 ... 1460 IndexError... 1461 >>> pf.parseOneEffectArg(['hi', 'bye']) 1462 ('hi', 0) 1463 >>> pf.parseOneEffectArg(['hi', 'bye'], 1) 1464 ('bye', 1) 1465 >>> pf.parseOneEffectArg( 1466 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1467 ... 0 1468 ... ) 1469 ((MechanismSpecifier(domain=None, zone=None, decision=None,\ 1470 name='gate'), 'open'), 2) 1471 >>> pf.parseOneEffectArg( 1472 ... ['set', 'gate', Lexeme.mechanismSeparator, 'open'], 1473 ... 1 1474 ... ) 1475 ((MechanismSpecifier(domain=None, zone=None, decision=None,\ 1476 name='gate'), 'open'), 3) 1477 >>> pf.parseOneEffectArg( 1478 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1479 ... 1 1480 ... ) 1481 Traceback (most recent call last): 1482 ... 1483 exploration.parsing.ParseError... 1484 >>> pf.parseOneEffectArg( 1485 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1486 ... 2 1487 ... ) 1488 ('open', 2) 1489 >>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, '10'], 0) 1490 (('gold', 10), 2) 1491 >>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, 'ten'], 0) 1492 Traceback (most recent call last): 1493 ... 1494 exploration.parsing.ParseError... 1495 >>> pf.parseOneEffectArg([Lexeme.inCommon], 0) 1496 (<Lexeme.inCommon: ...>, 0) 1497 >>> pf.parseOneEffectArg([Lexeme.isHidden], 0) 1498 (<Lexeme.isHidden: ...>, 0) 1499 >>> pf.parseOneEffectArg([Lexeme.tokenCount, '3'], 0) 1500 Traceback (most recent call last): 1501 ... 1502 exploration.parsing.ParseError... 1503 >>> pf.parseOneEffectArg([Lexeme.effectCharges, '3'], 0) 1504 ((<Lexeme.effectCharges: ...>, 3), 1) 1505 >>> pf.parseOneEffectArg([Lexeme.tokenCount, 3], 0) # int is a lexeme 1506 Traceback (most recent call last): 1507 ... 1508 exploration.parsing.ParseError... 1509 >>> pf.parseOneEffectArg([Lexeme.sepOrDelay, '-2'], 0) 1510 ((<Lexeme.sepOrDelay: ...>, -2), 1) 1511 >>> pf.parseOneEffectArg(['agility', Lexeme.skillLevel, '3'], 0) 1512 (('skill', 'agility', 3), 2) 1513 >>> pf.parseOneEffectArg( 1514 ... [ 1515 ... 'main', 1516 ... Lexeme.domainSeparator, 1517 ... 'zone', 1518 ... Lexeme.zoneSeparator, 1519 ... 'decision', 1520 ... Lexeme.zoneSeparator, 1521 ... 'compass', 1522 ... Lexeme.mechanismSeparator, 1523 ... 'north', 1524 ... 'south', 1525 ... 'east', 1526 ... 'west' 1527 ... ], 1528 ... 0 1529 ... ) 1530 ((MechanismSpecifier(domain='main', zone='zone',\ 1531 decision='decision', name='compass'), 'north'), 8) 1532 >>> pf.parseOneEffectArg( 1533 ... [ 1534 ... 'before', 1535 ... 'main', 1536 ... Lexeme.domainSeparator, 1537 ... 'zone', 1538 ... Lexeme.zoneSeparator, 1539 ... 'decision', 1540 ... Lexeme.zoneSeparator, 1541 ... 'compass', 1542 ... 'north', 1543 ... 'south', 1544 ... 'east', 1545 ... 'west' 1546 ... ], 1547 ... 1 1548 ... ) # a mechanism specifier without a state will become a 1549 ... # decision specifier 1550 (DecisionSpecifier(domain='main', zone='zone',\ 1551 name='decision'), 5) 1552 >>> tokens = [ 1553 ... 'set', 1554 ... 'main', 1555 ... Lexeme.domainSeparator, 1556 ... 'zone', 1557 ... Lexeme.zoneSeparator, 1558 ... 'compass', 1559 ... 'north', 1560 ... 'bounce', 1561 ... ] 1562 >>> pf.parseOneEffectArg(tokens, 0) 1563 ('set', 0) 1564 >>> pf.parseDecisionSpecifierFromTokens(tokens, 1) 1565 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 1566 >>> pf.parseOneEffectArg(tokens, 1) 1567 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 1568 >>> pf.parseOneEffectArg(tokens, 6) 1569 ('north', 6) 1570 >>> pf.parseOneEffectArg(tokens, 7) 1571 ('bounce', 7) 1572 >>> pf.parseOneEffectArg( 1573 ... [ 1574 ... "fort", Lexeme.zoneSeparator, "gate", 1575 ... Lexeme.mechanismSeparator, "open", 1576 ... ], 1577 ... 0 1578 ... ) 1579 ((MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1580 name='gate'), 'open'), 4) 1581 >>> pf.parseOneEffectArg( 1582 ... [Lexeme.openCurly, 'val', '5', Lexeme.closeCurly], 1583 ... 0 1584 ... ) == ([commands.command('val', '5')], 3) 1585 True 1586 >>> a = [ 1587 ... Lexeme.openCurly, 'val', '5', Lexeme.closeCurly, 1588 ... Lexeme.openCurly, 'append', Lexeme.consequenceSeparator, 1589 ... 'pop', Lexeme.closeCurly 1590 ... ] 1591 >>> cl = [ 1592 ... [commands.command('val', '5')], 1593 ... [commands.command('append'), commands.command('pop')] 1594 ... ] 1595 >>> pf.parseOneEffectArg(a, 0) == (cl[0], 3) 1596 True 1597 >>> pf.parseOneEffectArg(a, 4) == (cl[1], 8) 1598 True 1599 >>> pf.parseOneEffectArg(a, 1) 1600 ('val', 1) 1601 >>> pf.parseOneEffectArg(a, 2) 1602 ('5', 2) 1603 >>> pf.parseOneEffectArg(a, 3) 1604 Traceback (most recent call last): 1605 ... 1606 exploration.parsing.ParseError... 1607 """ 1608 start, limit, nTokens = normalizeEnds( 1609 tokens, 1610 start, 1611 limit if limit is not None else -1 1612 ) 1613 if nTokens == 0: 1614 raise ParseError("No effect arguments available.") 1615 1616 first = tokens[start] 1617 1618 if nTokens == 1: 1619 if first in (Lexeme.inCommon, Lexeme.isHidden): 1620 return (first, start) 1621 elif not isinstance(first, str): 1622 raise ParseError( 1623 f"Only one token and it's a special character" 1624 f" ({first} = {repr(self.formatDict[first])})" 1625 ) 1626 else: 1627 return (cast(base.Capability, first), start) 1628 1629 assert (nTokens > 1) 1630 1631 second = tokens[start + 1] 1632 1633 # Command lists start with an open curly brace and effect 1634 # modifiers start with a Lexme, but nothing else may 1635 if first == Lexeme.openCurly: 1636 return self.parseCommandListFromTokens(tokens, start) 1637 elif first in (Lexeme.inCommon, Lexeme.isHidden): 1638 return (first, start) 1639 elif first in (Lexeme.sepOrDelay, Lexeme.effectCharges): 1640 if not isinstance(second, str): 1641 raise ParseError( 1642 f"Token following a modifier that needs a count" 1643 f" must be a string in tokens:" 1644 f"\n{tokens[start:limit or len(tokens)]}" 1645 ) 1646 try: 1647 val = int(second) 1648 except ValueError: 1649 raise ParseError( 1650 f"Token following a modifier that needs a count" 1651 f" must be convertible to an int:" 1652 f"\n{tokens[start:limit or len(tokens)]}" 1653 ) 1654 1655 first = cast( 1656 Literal[Lexeme.sepOrDelay, Lexeme.effectCharges], 1657 first 1658 ) 1659 return ((first, val), start + 1) 1660 elif not isinstance(first, str): 1661 raise ParseError( 1662 f"First token must be a string unless it's a modifier" 1663 f" lexeme or command/reversion-set opener. Got:" 1664 f"\n{tokens[start:limit or len(tokens)]}" 1665 ) 1666 1667 # If we have two strings in a row, then the first is our parsed 1668 # value alone and we'll parse the second separately. 1669 if isinstance(second, str): 1670 return (first, start) 1671 elif second in (Lexeme.inCommon, Lexeme.isHidden): 1672 return (first, start) 1673 1674 # Must have at least 3 tokens at this point, or else we need to 1675 # have the inCommon or isHidden lexeme second. 1676 if nTokens < 3: 1677 return (first, start) 1678 1679 third = tokens[start + 2] 1680 if not isinstance(third, str): 1681 return (first, start) 1682 1683 second = cast(Lexeme, second) 1684 third = cast(str, third) 1685 1686 if second in (Lexeme.tokenCount, Lexeme.skillLevel): 1687 try: 1688 num = int(third) 1689 except ValueError: 1690 raise ParseError( 1691 f"Invalid effect tokens: count for Tokens or level" 1692 f" for Skill must be convertible to an integer." 1693 f"\n{tokens[start:limit + 1]}" 1694 ) 1695 if second == Lexeme.tokenCount: 1696 return ((first, num), start + 2) # token/count pair 1697 else: 1698 return (('skill', first, num), start + 2) # token/count pair 1699 1700 elif second == Lexeme.mechanismSeparator: # bare mechanism 1701 return ( 1702 ( 1703 base.MechanismSpecifier( 1704 domain=None, 1705 zone=None, 1706 decision=None, 1707 name=first 1708 ), 1709 third 1710 ), 1711 start + 2 1712 ) 1713 1714 elif second in (Lexeme.domainSeparator, Lexeme.zoneSeparator): 1715 try: 1716 mSpec, mEnd = self.parseMechanismSpecifierFromTokens( 1717 tokens, 1718 start 1719 ) # works whether it's a mechanism or decision specifier... 1720 except ParseError: 1721 return self.parseDecisionSpecifierFromTokens(tokens, start) 1722 if mEnd + 2 > limit: 1723 # No room for following mechanism separator + state 1724 return self.parseDecisionSpecifierFromTokens(tokens, start) 1725 sep = tokens[mEnd + 1] 1726 after = tokens[mEnd + 2] 1727 if sep == Lexeme.mechanismSeparator: 1728 if not isinstance(after, str): 1729 raise ParseError( 1730 f"Mechanism separator not followed by state:" 1731 f"\n{tokens[start]}" 1732 ) 1733 return ((mSpec, after), mEnd + 2) 1734 else: 1735 # No mechanism separator afterwards 1736 return self.parseDecisionSpecifierFromTokens(tokens, start) 1737 1738 else: # unrecognized as a longer combo 1739 return (first, start) 1740 1741 def coalesceEffectArgs( 1742 self, 1743 tokens: LexedTokens, 1744 start: int = 0, 1745 end: int = -1 1746 ) -> Tuple[ 1747 List[ # List of effect args 1748 Union[ 1749 base.Capability, # covers 'str' possibility 1750 Tuple[base.Token, base.TokenCount], 1751 Tuple[Literal['skill'], base.Skill, base.Level], 1752 Tuple[base.MechanismSpecifier, base.MechanismState], 1753 base.DecisionSpecifier, 1754 List[commands.Command], 1755 Set[str] 1756 ] 1757 ], 1758 Tuple[ # Slots for modifiers: common/hidden/charges/delay 1759 Optional[bool], 1760 Optional[bool], 1761 Optional[int], 1762 Optional[int], 1763 ] 1764 ]: 1765 """ 1766 Given a region of a lexed tokens list which contains one or more 1767 effect arguments, combines token sequences representing things 1768 like capabilities, mechanism states, token counts, and skill 1769 levels, representing these using the tuples that would be passed 1770 to `base.effect`. Returns a tuple with two elements: 1771 1772 - First, a list that contains several different kinds of 1773 objects, each of which is distinguishable by its type or 1774 part of its value. 1775 - Next, a tuple with four entires for common, hidden, charges, 1776 and/or delay values based on the presence of modifier 1777 sequences. Any or all of these may be `None` if the relevant 1778 modifier was not present (the usual case). 1779 1780 For example: 1781 1782 >>> pf = ParseFormat() 1783 >>> pf.coalesceEffectArgs(["jump"]) 1784 (['jump'], (None, None, None, None)) 1785 >>> pf.coalesceEffectArgs(["coin", Lexeme.tokenCount, "3", "fly"]) 1786 ([('coin', 3), 'fly'], (None, None, None, None)) 1787 >>> pf.coalesceEffectArgs( 1788 ... [ 1789 ... "fort", Lexeme.zoneSeparator, "gate", 1790 ... Lexeme.mechanismSeparator, "open" 1791 ... ] 1792 ... ) 1793 ([(MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1794 name='gate'), 'open')], (None, None, None, None)) 1795 >>> pf.coalesceEffectArgs( 1796 ... [ 1797 ... "main", Lexeme.domainSeparator, "cliff" 1798 ... ] 1799 ... ) 1800 ([DecisionSpecifier(domain='main', zone=None, name='cliff')],\ 1801 (None, None, None, None)) 1802 >>> pf.coalesceEffectArgs( 1803 ... [ 1804 ... "door", Lexeme.mechanismSeparator, "open" 1805 ... ] 1806 ... ) 1807 ([(MechanismSpecifier(domain=None, zone=None, decision=None,\ 1808 name='door'), 'open')], (None, None, None, None)) 1809 >>> pf.coalesceEffectArgs( 1810 ... [ 1811 ... "fort", Lexeme.zoneSeparator, "gate", 1812 ... Lexeme.mechanismSeparator, "open", 1813 ... "canJump", 1814 ... "coins", Lexeme.tokenCount, "3", 1815 ... Lexeme.inCommon, 1816 ... "agility", Lexeme.skillLevel, "-1", 1817 ... Lexeme.sepOrDelay, "0", 1818 ... "main", Lexeme.domainSeparator, "cliff" 1819 ... ] 1820 ... ) 1821 ([(MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1822 name='gate'), 'open'), 'canJump', ('coins', 3), ('skill', 'agility', -1),\ 1823 DecisionSpecifier(domain='main', zone=None, name='cliff')],\ 1824 (True, None, None, 0)) 1825 >>> pf.coalesceEffectArgs(["bounce", Lexeme.isHidden]) 1826 (['bounce'], (None, True, None, None)) 1827 >>> pf.coalesceEffectArgs( 1828 ... ["goto", "3", Lexeme.inCommon, Lexeme.isHidden] 1829 ... ) 1830 (['goto', '3'], (True, True, None, None)) 1831 """ 1832 start, end, nTokens = normalizeEnds(tokens, start, end) 1833 where = start 1834 result: List[ # List of effect args 1835 Union[ 1836 base.Capability, # covers 'str' possibility 1837 Tuple[base.Token, base.TokenCount], 1838 Tuple[Literal['skill'], base.Skill, base.Level], 1839 Tuple[base.MechanismSpecifier, base.MechanismState], 1840 base.DecisionSpecifier, 1841 List[commands.Command], 1842 Set[str] 1843 ] 1844 ] = [] 1845 inCommon: Optional[bool] = None 1846 isHidden: Optional[bool] = None 1847 charges: Optional[int] = None 1848 delay: Optional[int] = None 1849 while where <= end: 1850 following, thisEnd = self.parseOneEffectArg(tokens, where, end) 1851 if following == Lexeme.inCommon: 1852 if inCommon is not None: 1853 raise ParseError( 1854 f"In-common effect modifier specified more than" 1855 f" once in effect args:" 1856 f"\n{tokens[start:end + 1]}" 1857 ) 1858 inCommon = True 1859 elif following == Lexeme.isHidden: 1860 if isHidden is not None: 1861 raise ParseError( 1862 f"Is-hidden effect modifier specified more than" 1863 f" once in effect args:" 1864 f"\n{tokens[start:end + 1]}" 1865 ) 1866 isHidden = True 1867 elif ( 1868 isinstance(following, tuple) 1869 and len(following) == 2 1870 and following[0] in (Lexeme.effectCharges, Lexeme.sepOrDelay) 1871 and isinstance(following[1], int) 1872 ): 1873 if following[0] == Lexeme.effectCharges: 1874 if charges is not None: 1875 raise ParseError( 1876 f"Charges effect modifier specified more than" 1877 f" once in effect args:" 1878 f"\n{tokens[start:end + 1]}" 1879 ) 1880 charges = following[1] 1881 else: 1882 if delay is not None: 1883 raise ParseError( 1884 f"Delay effect modifier specified more than" 1885 f" once in effect args:" 1886 f"\n{tokens[start:end + 1]}" 1887 ) 1888 delay = following[1] 1889 elif ( 1890 isinstance(following, base.Capability) 1891 or ( 1892 isinstance(following, tuple) 1893 and len(following) == 2 1894 and isinstance(following[0], base.Token) 1895 and isinstance(following[1], base.TokenCount) 1896 ) or ( 1897 isinstance(following, tuple) 1898 and len(following) == 3 1899 and following[0] == 'skill' 1900 and isinstance(following[1], base.Skill) 1901 and isinstance(following[2], base.Level) 1902 ) or ( 1903 isinstance(following, tuple) 1904 and len(following) == 2 1905 and isinstance(following[0], base.MechanismSpecifier) 1906 and isinstance(following[1], base.MechanismState) 1907 ) or ( 1908 isinstance(following, base.DecisionSpecifier) 1909 ) or ( 1910 isinstance(following, list) 1911 and all(isinstance(item, tuple) for item in following) 1912 # TODO: Stricter command list check here? 1913 ) or ( 1914 isinstance(following, set) 1915 and all(isinstance(item, str) for item in following) 1916 ) 1917 ): 1918 result.append(following) 1919 else: 1920 raise ParseError(f"Invalid coalesced argument: {following}") 1921 where = thisEnd + 1 1922 1923 return (result, (inCommon, isHidden, charges, delay)) 1924 1925 def parseEffectFromTokens( 1926 self, 1927 tokens: LexedTokens, 1928 start: int = 0, 1929 end: int = -1 1930 ) -> base.Effect: 1931 """ 1932 Given a region of a list of lexed tokens specifying an effect, 1933 returns the `Effect` object that those tokens specify. 1934 """ 1935 start, end, nTokens = normalizeEnds(tokens, start, end) 1936 1937 # Check for empty list 1938 if nTokens == 0: 1939 raise ParseError( 1940 "Effect must include at least a type." 1941 ) 1942 1943 firstPart = tokens[start] 1944 1945 if isinstance(firstPart, Lexeme): 1946 raise ParseError( 1947 f"First part of effect must be an effect type. Got" 1948 f" {firstPart} ({repr(self.formatDict[firstPart])})." 1949 ) 1950 1951 firstPart = cast(str, firstPart) 1952 1953 # Get the effect type 1954 fType = self.effectType(firstPart) 1955 1956 if fType is None: 1957 raise ParseError( 1958 f"Unrecognized effect type {firstPart!r}. Check the" 1959 f" EffectType entries in the effect names dictionary." 1960 ) 1961 1962 if start + 1 > end: # No tokens left: set empty args 1963 groupedArgs: List[ 1964 Union[ 1965 base.Capability, # covers 'str' possibility 1966 Tuple[base.Token, base.TokenCount], 1967 Tuple[Literal['skill'], base.Skill, base.Level], 1968 Tuple[base.MechanismSpecifier, base.MechanismState], 1969 base.DecisionSpecifier, 1970 List[commands.Command], 1971 Set[str] 1972 ] 1973 ] = [] 1974 modifiers: Tuple[ 1975 Optional[bool], 1976 Optional[bool], 1977 Optional[int], 1978 Optional[int] 1979 ] = (None, None, None, None) 1980 else: # Coalesce remaining tokens if there are any 1981 groupedArgs, modifiers = self.coalesceEffectArgs( 1982 tokens, 1983 start + 1, 1984 end 1985 ) 1986 1987 # Set up arguments for base.effect and handle modifiers first 1988 args: Dict[ 1989 str, 1990 Union[ 1991 None, 1992 base.ContextSpecifier, 1993 base.Capability, 1994 Tuple[base.Token, base.TokenCount], 1995 Tuple[Literal['skill'], base.Skill, base.Level], 1996 Tuple[base.MechanismSpecifier, base.MechanismState], 1997 Tuple[base.MechanismSpecifier, List[base.MechanismState]], 1998 List[base.Capability], 1999 base.AnyDecisionSpecifier, 2000 Tuple[base.AnyDecisionSpecifier, base.FocalPointName], 2001 bool, 2002 int, 2003 base.SaveSlot, 2004 Tuple[base.SaveSlot, Set[str]] 2005 ] 2006 ] = {} 2007 if modifiers[0]: 2008 args['applyTo'] = 'common' 2009 if modifiers[1]: 2010 args['hidden'] = True 2011 else: 2012 args['hidden'] = False 2013 if modifiers[2] is not None: 2014 args['charges'] = modifiers[2] 2015 if modifiers[3] is not None: 2016 args['delay'] = modifiers[3] 2017 2018 # Now handle the main effect-type-based argument 2019 if fType in ("gain", "lose"): 2020 if len(groupedArgs) != 1: 2021 raise ParseError( 2022 f"'{fType}' effect must have exactly one grouped" 2023 f" argument (got {len(groupedArgs)}:\n{groupedArgs}" 2024 ) 2025 thing = groupedArgs[0] 2026 if isinstance(thing, tuple): 2027 if len(thing) == 2: 2028 if ( 2029 not isinstance(thing[0], base.Token) 2030 or not isinstance(thing[1], base.TokenCount) 2031 ): 2032 raise ParseError( 2033 f"'{fType}' effect grouped arg pair must be a" 2034 f" (token, amount) pair. Got:\n{thing}" 2035 ) 2036 elif len(thing) == 3: 2037 if ( 2038 thing[0] != 'skill' 2039 or not isinstance(thing[1], base.Skill) 2040 or not isinstance(thing[2], base.Level) 2041 ): 2042 raise ParseError( 2043 f"'{fType}' effect grouped arg pair must be a" 2044 f" (token, amount) pair. Got:\n{thing}" 2045 ) 2046 else: 2047 raise ParseError( 2048 f"'{fType}' effect grouped arg tuple must have" 2049 f" length 2 or 3. Got (length {len(thing)}):\n{thing}" 2050 ) 2051 elif not isinstance(thing, base.Capability): 2052 raise ParseError( 2053 f"'{fType}' effect grouped arg must be a capability" 2054 f" or a (token, amount) tuple. Got:\n{thing}" 2055 ) 2056 args[fType] = thing 2057 return base.effect(**args) # type:ignore 2058 2059 elif fType == "set": 2060 if len(groupedArgs) != 1: 2061 raise ParseError( 2062 f"'{fType}' effect must have exactly one grouped" 2063 f" argument (got {len(groupedArgs)}:\n{groupedArgs}" 2064 ) 2065 setVal = groupedArgs[0] 2066 if not isinstance( 2067 setVal, 2068 tuple 2069 ): 2070 raise ParseError( 2071 f"'{fType}' effect grouped arg must be a tuple. Got:" 2072 f"\n{setVal}" 2073 ) 2074 if len(setVal) == 2: 2075 setWhat, setTo = setVal 2076 if ( 2077 isinstance(setWhat, base.Token) 2078 and isinstance(setTo, base.TokenCount) 2079 ) or ( 2080 isinstance(setWhat, base.MechanismSpecifier) 2081 and isinstance(setTo, base.MechanismState) 2082 ): 2083 args[fType] = setVal 2084 return base.effect(**args) # type:ignore 2085 else: 2086 raise ParseError( 2087 f"Invalid '{fType}' effect grouped args:" 2088 f"\n{groupedArgs}" 2089 ) 2090 elif len(setVal) == 3: 2091 indicator, whichSkill, setTo = setVal 2092 if ( 2093 indicator == 'skill' 2094 and isinstance(whichSkill, base.Skill) 2095 and isinstance(setTo, base.Level) 2096 ): 2097 args[fType] = setVal 2098 return base.effect(**args) # type:ignore 2099 else: 2100 raise ParseError( 2101 f"Invalid '{fType}' effect grouped args (not a" 2102 f" skill):\n{groupedArgs}" 2103 ) 2104 else: 2105 raise ParseError( 2106 f"Invalid '{fType}' effect grouped args (wrong" 2107 f" length tuple):\n{groupedArgs}" 2108 ) 2109 2110 elif fType == "toggle": 2111 if len(groupedArgs) == 0: 2112 raise ParseError( 2113 f"'{fType}' effect must have at least one grouped" 2114 f" argument. Got:\n{groupedArgs}" 2115 ) 2116 if ( 2117 isinstance(groupedArgs[0], tuple) 2118 and len(groupedArgs[0]) == 2 2119 and isinstance(groupedArgs[0][0], base.MechanismSpecifier) 2120 and isinstance(groupedArgs[0][1], base.MechanismState) 2121 and all( 2122 isinstance(a, base.MechanismState) 2123 for a in groupedArgs[1:] 2124 ) 2125 ): # a mechanism toggle 2126 args[fType] = ( 2127 groupedArgs[0][0], 2128 cast( 2129 List[base.MechanismState], 2130 [groupedArgs[0][1]] + groupedArgs[1:] 2131 ) 2132 ) 2133 return base.effect(**args) # type:ignore 2134 elif all(isinstance(a, base.Capability) for a in groupedArgs): 2135 # a capability toggle 2136 args[fType] = cast(List[base.Capability], groupedArgs) 2137 return base.effect(**args) # type:ignore 2138 else: 2139 raise ParseError( 2140 f"Invalid arguments for '{fType}' effect. Got:" 2141 f"\n{groupedArgs}" 2142 ) 2143 2144 elif fType in ("bounce", "deactivate"): 2145 if len(groupedArgs) != 0: 2146 raise ParseError( 2147 f"'{fType}' effect may not include any" 2148 f" arguments. Got {len(groupedArgs)}):" 2149 f"\n{groupedArgs}" 2150 ) 2151 args[fType] = True 2152 return base.effect(**args) # type:ignore 2153 2154 elif fType == "follow": 2155 if len(groupedArgs) != 1: 2156 raise ParseError( 2157 f"'{fType}' effect must include exactly one" 2158 f" argument. Got {len(groupedArgs)}):" 2159 f"\n{groupedArgs}" 2160 ) 2161 2162 transition = groupedArgs[0] 2163 if not isinstance(transition, base.Transition): 2164 raise ParseError( 2165 f"Invalid argument for '{fType}' effect. Needed a" 2166 f" transition but got:\n{groupedArgs}" 2167 ) 2168 args[fType] = transition 2169 return base.effect(**args) # type:ignore 2170 2171 elif fType == "edit": 2172 if len(groupedArgs) == 0: 2173 raise ParseError( 2174 "An 'edit' effect requires at least one argument." 2175 ) 2176 for i, arg in enumerate(groupedArgs): 2177 if not isinstance(arg, list): 2178 raise ParseError( 2179 f"'edit' effect argument {i} is not a sub-list:" 2180 f"\n {arg!r}" 2181 f"\nAmong arguments:" 2182 f"\n {groupedArgs}" 2183 ) 2184 for j, cmd in enumerate(arg): 2185 if not isinstance(cmd, tuple): 2186 raise ParseError( 2187 f"'edit' effect argument {i} contains" 2188 f" non-tuple part {j}:" 2189 f"\n {cmd!r}" 2190 f"\nAmong arguments:" 2191 f"\n {groupedArgs}" 2192 ) 2193 2194 args[fType] = groupedArgs # type:ignore 2195 return base.effect(**args) # type:ignore 2196 2197 elif fType == "goto": 2198 if len(groupedArgs) not in (1, 2): 2199 raise ParseError( 2200 f"A 'goto' effect must include either one or two" 2201 f" grouped arguments. Got {len(groupedArgs)}:" 2202 f"\n{groupedArgs}" 2203 ) 2204 2205 first = groupedArgs[0] 2206 if not isinstance( 2207 first, 2208 (base.DecisionName, base.DecisionSpecifier) 2209 ): 2210 raise ParseError( 2211 f"'{fType}' effect must first specify a destination" 2212 f" decision. Got:\n{groupedArgs}" 2213 ) 2214 2215 # Check if it's really a decision ID 2216 dSpec: base.AnyDecisionSpecifier 2217 if isinstance(first, base.DecisionName): 2218 try: 2219 dSpec = int(first) 2220 except ValueError: 2221 dSpec = first 2222 else: 2223 dSpec = first 2224 2225 if len(groupedArgs) == 2: 2226 second = groupedArgs[1] 2227 if not isinstance(second, base.FocalPointName): 2228 raise ParseError( 2229 f"'{fType}' effect must have a focal point name" 2230 f" if it has a second part. Got:\n{groupedArgs}" 2231 ) 2232 args[fType] = (dSpec, second) 2233 else: 2234 args[fType] = dSpec 2235 2236 return base.effect(**args) # type:ignore 2237 2238 elif fType == "save": 2239 if len(groupedArgs) not in (0, 1): 2240 raise ParseError( 2241 f"'{fType}' effect must include exactly zero or one" 2242 f" argument(s). Got {len(groupedArgs)}):" 2243 f"\n{groupedArgs}" 2244 ) 2245 2246 if len(groupedArgs) == 1: 2247 slot = groupedArgs[0] 2248 else: 2249 slot = base.DEFAULT_SAVE_SLOT 2250 if not isinstance(slot, base.SaveSlot): 2251 raise ParseError( 2252 f"Invalid argument for '{fType}' effect. Needed a" 2253 f" save slot but got:\n{groupedArgs}" 2254 ) 2255 args[fType] = slot 2256 return base.effect(**args) # type:ignore 2257 2258 else: 2259 raise ParseError(f"Invalid effect type: '{fType}'.") 2260 2261 def parseEffect(self, effectStr: str) -> base.Effect: 2262 """ 2263 Works like `parseEffectFromTokens` but starts with a raw string. 2264 For example: 2265 2266 >>> pf = ParseFormat() 2267 >>> pf.parseEffect("gain jump") == base.effect(gain='jump') 2268 True 2269 >>> pf.parseEffect("set door:open") == base.effect( 2270 ... set=( 2271 ... base.MechanismSpecifier(None, None, None, 'door'), 2272 ... 'open' 2273 ... ) 2274 ... ) 2275 True 2276 >>> pf.parseEffect("set coins*10") == base.effect(set=('coins', 10)) 2277 True 2278 >>> pf.parseEffect("set agility^3") == base.effect( 2279 ... set=('skill', 'agility', 3) 2280 ... ) 2281 True 2282 """ 2283 return self.parseEffectFromTokens(self.lex(effectStr)) 2284 2285 def unparseEffect(self, effect: base.Effect) -> str: 2286 """ 2287 The opposite of `parseEffect`; turns an effect back into a 2288 string reprensentation. 2289 2290 For example: 2291 2292 >>> pf = ParseFormat() 2293 >>> e = { 2294 ... "type": "gain", 2295 ... "applyTo": "active", 2296 ... "value": "flight", 2297 ... "delay": None, 2298 ... "charges": None, 2299 ... "hidden": False 2300 ... } 2301 >>> pf.unparseEffect(e) 2302 'gain flight' 2303 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2304 True 2305 >>> s = 'gain flight' 2306 >>> pf.unparseEffect(pf.parseEffect(s)) == s 2307 True 2308 >>> s2 = ' gain\\nflight' 2309 >>> pf.unparseEffect(pf.parseEffect(s2)) == s 2310 True 2311 >>> e = { 2312 ... "type": "gain", 2313 ... "applyTo": "active", 2314 ... "value": ("gold", 5), 2315 ... "delay": 1, 2316 ... "charges": 2, 2317 ... "hidden": False 2318 ... } 2319 >>> pf.unparseEffect(e) 2320 'gain gold*5 ,1 =2' 2321 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2322 True 2323 >>> e = { 2324 ... "type": "set", 2325 ... "applyTo": "active", 2326 ... "value": ( 2327 ... base.MechanismSpecifier(None, None, None, "gears"), 2328 ... "on" 2329 ... ), 2330 ... "delay": None, 2331 ... "charges": 1, 2332 ... "hidden": False 2333 ... } 2334 >>> pf.unparseEffect(e) 2335 'set gears:on =1' 2336 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2337 True 2338 >>> e = { 2339 ... "type": "toggle", 2340 ... "applyTo": "active", 2341 ... "value": ["red", "blue"], 2342 ... "delay": None, 2343 ... "charges": None, 2344 ... "hidden": False 2345 ... } 2346 >>> pf.unparseEffect(e) 2347 'toggle red blue' 2348 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2349 True 2350 >>> e = { 2351 ... "type": "toggle", 2352 ... "applyTo": "active", 2353 ... "value": ( 2354 ... base.MechanismSpecifier(None, None, None, "switch"), 2355 ... ["on", "off"] 2356 ... ), 2357 ... "delay": None, 2358 ... "charges": None, 2359 ... "hidden": False 2360 ... } 2361 >>> pf.unparseEffect(e) 2362 'toggle switch:on off' 2363 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2364 True 2365 >>> e = { 2366 ... "type": "deactivate", 2367 ... "applyTo": "active", 2368 ... "value": None, 2369 ... "delay": 2, 2370 ... "charges": None, 2371 ... "hidden": False 2372 ... } 2373 >>> pf.unparseEffect(e) 2374 'deactivate ,2' 2375 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2376 True 2377 >>> e = { 2378 ... "type": "goto", 2379 ... "applyTo": "common", 2380 ... "value": 3, 2381 ... "delay": None, 2382 ... "charges": None, 2383 ... "hidden": False 2384 ... } 2385 >>> pf.unparseEffect(e) 2386 'goto 3 +c' 2387 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2388 True 2389 >>> e = { 2390 ... "type": "goto", 2391 ... "applyTo": "common", 2392 ... "value": 3, 2393 ... "delay": None, 2394 ... "charges": None, 2395 ... "hidden": True 2396 ... } 2397 >>> pf.unparseEffect(e) 2398 'goto 3 +c +h' 2399 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2400 True 2401 >>> e = { 2402 ... "type": "goto", 2403 ... "applyTo": "active", 2404 ... "value": 'home', 2405 ... "delay": None, 2406 ... "charges": None, 2407 ... "hidden": False 2408 ... } 2409 >>> pf.unparseEffect(e) 2410 'goto home' 2411 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2412 True 2413 >>> e = base.effect(edit=[ 2414 ... [ 2415 ... commands.command('val', '5'), 2416 ... commands.command('empty', 'list'), 2417 ... commands.command('append', '$_') 2418 ... ], 2419 ... [ 2420 ... commands.command('val', '11'), 2421 ... commands.command('assign', 'var', '$_'), 2422 ... commands.command('op', '+', '$var', '$var') 2423 ... ], 2424 ... ]) 2425 >>> pf.unparseEffect(e) 2426 'edit {\\n val 5;\\n empty list;\\n append $_;\\n}\ 2427 {\\n val 11;\\n assign var $_;\\n op + $var $var;\\n}' 2428 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2429 True 2430 >>> e = base.effect(set=('coins', 3)) 2431 >>> pf.unparseEffect(e) 2432 'set coins*3' 2433 >>> e = base.effect(set=('skill', 'mashing', 3)) 2434 >>> pf.unparseEffect(e) 2435 'set mashing^3' 2436 """ 2437 result: List[str] = [] 2438 2439 # Reverse the effect type into a marker 2440 eType = effect['type'] 2441 for key, val in self.effectNames.items(): 2442 if val == eType: 2443 if len(result) != 0: 2444 raise ParseError( 2445 f"Effect map contains multiple matching entries" 2446 f"for effect type '{effect['type']}':" 2447 f" '{result[0]}' and '{key}'" 2448 ) 2449 result.append(key) 2450 # Don't break 'cause we'd like to check uniqueness 2451 2452 eVal = effect['value'] 2453 if eType in ('gain', 'lose'): 2454 eVal = cast(Union[base.Capability, Tuple[base.Token, int]], eVal) 2455 if isinstance(eVal, str): # a capability 2456 result.append(eVal) 2457 else: # a token 2458 result.append( 2459 eVal[0] 2460 + self.formatDict[Lexeme.tokenCount] 2461 + str(eVal[1]) 2462 ) 2463 elif eType == 'set': 2464 eVal = cast( 2465 # TODO: Add skill level setting here & elsewhere 2466 Union[ 2467 Tuple[base.Token, base.TokenCount], 2468 Tuple[base.MechanismSpecifier, base.MechanismState] 2469 ], 2470 eVal 2471 ) 2472 if len(eVal) not in (2, 3): 2473 raise ValueError( 2474 f"'set' effect has value with length other than 2" 2475 f" or 3:\n {repr(effect)}" 2476 ) 2477 if len(eVal) == 3: 2478 if eVal[0] != "skill": 2479 raise ValueError( 2480 f"'set' effect with length-3 value doesn't" 2481 f" start with string 'skill':\n {repr(effect)}" 2482 ) 2483 result.append( 2484 eVal[1] 2485 + self.formatDict[Lexeme.skillLevel] 2486 + str(eVal[2]) 2487 ) 2488 elif isinstance(eVal[1], int): # a token count 2489 result.append( 2490 eVal[0] 2491 + self.formatDict[Lexeme.tokenCount] 2492 + str(eVal[1]) 2493 ) 2494 else: # a mechanism 2495 if isinstance(eVal[0], base.MechanismSpecifier): 2496 mSpec = self.unparseMechanismSpecifier(eVal[0]) 2497 elif isinstance(eVal[0], base.MechanismID): 2498 # TODO: Specify mechanism by decision name + 2499 # mechanism name? Would require threading through a 2500 # DecisionGraph and using mechanismDetails 2501 mSpec = "" + eVal[0] 2502 else: 2503 assert isinstance(eVal[0], base.MechanismName) 2504 mSpec = eVal[0] 2505 result.append( 2506 mSpec 2507 + self.formatDict[Lexeme.mechanismSeparator] 2508 + eVal[1] 2509 ) 2510 elif eType == 'toggle': 2511 if isinstance(eVal, tuple): # mechanism states 2512 tSpec, states = cast( 2513 Tuple[ 2514 base.AnyMechanismSpecifier, 2515 List[base.MechanismState] 2516 ], 2517 eVal 2518 ) 2519 firstState = states[0] 2520 restStates = states[1:] 2521 if isinstance(tSpec, base.MechanismSpecifier): 2522 mStr = self.unparseMechanismSpecifier(tSpec) 2523 else: 2524 # Could be ID or name 2525 mStr = str(tSpec) 2526 result.append( 2527 mStr 2528 + self.formatDict[Lexeme.mechanismSeparator] 2529 + firstState 2530 ) 2531 result.extend(restStates) 2532 else: # capabilities 2533 assert isinstance(eVal, list) 2534 eVal = cast(List[base.Capability], eVal) 2535 result.extend(eVal) 2536 elif eType in ('deactivate', 'bounce'): 2537 if eVal is not None: 2538 raise ValueError( 2539 f"'{eType}' effect has non-None value:" 2540 f"\n {repr(effect)}" 2541 ) 2542 elif eType == 'follow': 2543 eVal = cast(base.Token, eVal) 2544 result.append(eVal) 2545 elif eType == 'edit': 2546 eVal = cast(List[List[commands.Command]], eVal) 2547 if len(eVal) == 0: 2548 result[-1] = '{}' 2549 else: 2550 for cmdList in eVal: 2551 result.append( 2552 self.unparseCommandList(cmdList) 2553 ) 2554 elif eType == 'goto': 2555 if ( 2556 isinstance(eVal, tuple) 2557 and len(eVal) == 2 2558 and isinstance(eVal[1], base.FocalPointName) 2559 ): 2560 result.append( 2561 self.unparseAnyDecision( 2562 cast(base.AnyDecisionSpecifier, eVal[0]) 2563 ) 2564 ) 2565 result.append(eVal[1]) 2566 else: 2567 assert isinstance( 2568 eVal, 2569 (base.DecisionID, base.DecisionSpecifier, str) 2570 ) 2571 result.append(self.unparseAnyDecision(eVal)) 2572 elif eType == 'save': 2573 # It's just a string naming the save slot 2574 eVal = cast(str, eVal) 2575 result.append(eVal) 2576 else: 2577 raise ValueError( 2578 f"Unrecognized effect type '{eType}' in effect:" 2579 f"\n {repr(effect)}" 2580 ) 2581 2582 # Add modifier strings 2583 if effect['applyTo'] == 'common': 2584 result.append(self.formatDict[Lexeme.inCommon]) 2585 2586 if effect['hidden']: 2587 result.append(self.formatDict[Lexeme.isHidden]) 2588 2589 dVal = effect['delay'] 2590 if dVal is not None: 2591 result.append( 2592 self.formatDict[Lexeme.sepOrDelay] + str(dVal) 2593 ) 2594 2595 cVal = effect['charges'] 2596 if cVal is not None: 2597 result.append( 2598 self.formatDict[Lexeme.effectCharges] + str(cVal) 2599 ) 2600 2601 joined = '' 2602 before = False 2603 for r in result: 2604 if ( 2605 r.startswith(' ') 2606 or r.startswith('\n') 2607 or r.endswith(' ') 2608 or r.endswith('\n') 2609 ): 2610 joined += r 2611 before = False 2612 else: 2613 joined += (' ' if before else '') + r 2614 before = True 2615 return joined 2616 2617 def parseDecisionSpecifierFromTokens( 2618 self, 2619 tokens: LexedTokens, 2620 start: int = 0 2621 ) -> Tuple[Union[base.DecisionSpecifier, int], int]: 2622 """ 2623 Parses a decision specifier starting at the specified position 2624 in the given tokens list. No ending position is specified, but 2625 instead this function returns a tuple containing the parsed 2626 `base.DecisionSpecifier` along with an index in the tokens list 2627 where the end of the specifier was found. 2628 2629 For example: 2630 2631 >>> pf = ParseFormat() 2632 >>> pf.parseDecisionSpecifierFromTokens(['m']) 2633 (DecisionSpecifier(domain=None, zone=None, name='m'), 0) 2634 >>> pf.parseDecisionSpecifierFromTokens(['12']) # ID specifier 2635 (12, 0) 2636 >>> pf.parseDecisionSpecifierFromTokens(['a', 'm']) 2637 (DecisionSpecifier(domain=None, zone=None, name='a'), 0) 2638 >>> pf.parseDecisionSpecifierFromTokens(['a', 'm'], 1) 2639 (DecisionSpecifier(domain=None, zone=None, name='m'), 1) 2640 >>> pf.parseDecisionSpecifierFromTokens( 2641 ... ['a', Lexeme.domainSeparator, 'm'] 2642 ... ) 2643 (DecisionSpecifier(domain='a', zone=None, name='m'), 2) 2644 >>> pf.parseDecisionSpecifierFromTokens( 2645 ... ['a', Lexeme.zoneSeparator, 'm'] 2646 ... ) 2647 (DecisionSpecifier(domain=None, zone='a', name='m'), 2) 2648 >>> pf.parseDecisionSpecifierFromTokens( 2649 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm'] 2650 ... ) 2651 (DecisionSpecifier(domain=None, zone='a', name='b'), 2) 2652 >>> pf.parseDecisionSpecifierFromTokens( 2653 ... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm'] 2654 ... ) 2655 (DecisionSpecifier(domain='a', zone='b', name='m'), 4) 2656 >>> pf.parseDecisionSpecifierFromTokens( 2657 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'] 2658 ... ) 2659 (DecisionSpecifier(domain=None, zone='a', name='b'), 2) 2660 >>> pf.parseDecisionSpecifierFromTokens( # ID-style name w/ zone 2661 ... ['a', Lexeme.zoneSeparator, '5'], 2662 ... ) 2663 Traceback (most recent call last): 2664 ... 2665 exploration.base.InvalidDecisionSpecifierError... 2666 >>> pf.parseDecisionSpecifierFromTokens( 2667 ... ['d', Lexeme.domainSeparator, '123'] 2668 ... ) 2669 Traceback (most recent call last): 2670 ... 2671 exploration.base.InvalidDecisionSpecifierError... 2672 >>> pf.parseDecisionSpecifierFromTokens( 2673 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 2674 ... 1 2675 ... ) 2676 Traceback (most recent call last): 2677 ... 2678 exploration.parsing.ParseError... 2679 >>> pf.parseDecisionSpecifierFromTokens( 2680 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 2681 ... 2 2682 ... ) 2683 (DecisionSpecifier(domain='b', zone=None, name='m'), 4) 2684 >>> pf.parseDecisionSpecifierFromTokens( 2685 ... [ 2686 ... 'a', 2687 ... Lexeme.domainSeparator, 2688 ... 'b', 2689 ... Lexeme.zoneSeparator, 2690 ... 'c', 2691 ... Lexeme.zoneSeparator, 2692 ... 'm' 2693 ... ] 2694 ... ) 2695 (DecisionSpecifier(domain='a', zone='b', name='c'), 4) 2696 >>> pf.parseDecisionSpecifierFromTokens( 2697 ... [ 2698 ... 'a', 2699 ... Lexeme.domainSeparator, 2700 ... 'b', 2701 ... Lexeme.zoneSeparator, 2702 ... 'c', 2703 ... Lexeme.zoneSeparator, 2704 ... 'm' 2705 ... ], 2706 ... 2 2707 ... ) 2708 (DecisionSpecifier(domain=None, zone='b', name='c'), 4) 2709 >>> pf.parseDecisionSpecifierFromTokens( 2710 ... [ 2711 ... 'a', 2712 ... Lexeme.domainSeparator, 2713 ... 'b', 2714 ... Lexeme.zoneSeparator, 2715 ... 'c', 2716 ... Lexeme.zoneSeparator, 2717 ... 'm' 2718 ... ], 2719 ... 4 2720 ... ) 2721 (DecisionSpecifier(domain=None, zone='c', name='m'), 6) 2722 >>> pf.parseDecisionSpecifierFromTokens( 2723 ... [ 2724 ... 'set', 2725 ... 'main', 2726 ... Lexeme.domainSeparator, 2727 ... 'zone', 2728 ... Lexeme.zoneSeparator, 2729 ... 'compass', 2730 ... 'north', 2731 ... 'bounce', 2732 ... ], 2733 ... 1 2734 ... ) 2735 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 2736 """ 2737 # Check bounds & normalize start index 2738 nTokens = len(tokens) 2739 if start < -nTokens: 2740 raise IndexError( 2741 f"Invalid start index {start} for {nTokens} tokens (too" 2742 f" negative)." 2743 ) 2744 elif start >= nTokens: 2745 raise IndexError( 2746 f"Invalid start index {start} for {nTokens} tokens (too" 2747 f" big)." 2748 ) 2749 elif start < 0: 2750 start = nTokens + start 2751 2752 assert (start < nTokens) 2753 2754 first = tokens[start] 2755 if not isinstance(first, str): 2756 raise ParseError( 2757 f"Invalid domain specifier (must start with a name or" 2758 f" id; got: {first} = {self.formatDict[first]})." 2759 ) 2760 2761 ds = base.DecisionSpecifier(None, None, first) 2762 result = (base.idOrDecisionSpecifier(ds), start) 2763 2764 domain = None 2765 zoneOrDecision = None 2766 2767 if start + 1 >= nTokens: # at end of tokens 2768 return result 2769 2770 firstSep = tokens[start + 1] 2771 if firstSep == Lexeme.domainSeparator: 2772 domain = first 2773 elif firstSep == Lexeme.zoneSeparator: 2774 zoneOrDecision = first 2775 else: 2776 return result 2777 2778 if start + 2 >= nTokens: 2779 return result 2780 2781 second = tokens[start + 2] 2782 if isinstance(second, Lexeme): 2783 return result 2784 2785 ds = base.DecisionSpecifier(domain, zoneOrDecision, second) 2786 result = (base.idOrDecisionSpecifier(ds), start + 2) 2787 2788 if start + 3 >= nTokens: 2789 return result 2790 2791 secondSep = tokens[start + 3] 2792 if start + 4 >= nTokens: 2793 return result 2794 2795 third = tokens[start + 4] 2796 if secondSep == Lexeme.zoneSeparator: 2797 if zoneOrDecision is not None: # two in a row 2798 return result 2799 else: 2800 if not isinstance(third, base.DecisionName): 2801 return result 2802 else: 2803 zoneOrDecision = second 2804 else: 2805 return result 2806 2807 if isinstance(third, Lexeme): 2808 return result 2809 2810 ds = base.DecisionSpecifier(domain, zoneOrDecision, third) 2811 return (base.idOrDecisionSpecifier(ds), start + 4) 2812 2813 def parseDecisionSpecifier( 2814 self, 2815 specString: str 2816 ) -> Union[base.DecisionID, base.DecisionSpecifier]: 2817 """ 2818 Parses a full `DecisionSpecifier` from a single string. Can 2819 parse integer decision IDs in string form, and returns a 2820 `DecisionID` in that case, otherwise returns a 2821 `DecisionSpecifier`. Assumes that all int-convertible strings 2822 are decision IDs, so it cannot deal with feature names which are 2823 just numbers. 2824 2825 For example: 2826 2827 >>> pf = ParseFormat() 2828 >>> pf.parseDecisionSpecifier('example') 2829 DecisionSpecifier(domain=None, zone=None, name='example') 2830 >>> pf.parseDecisionSpecifier('outer::example') 2831 DecisionSpecifier(domain=None, zone='outer', name='example') 2832 >>> pf.parseDecisionSpecifier('domain//region::feature') 2833 DecisionSpecifier(domain='domain', zone='region', name='feature') 2834 >>> pf.parseDecisionSpecifier('123') 2835 123 2836 >>> pf.parseDecisionSpecifier('region::domain//feature') 2837 Traceback (most recent call last): 2838 ... 2839 exploration.base.InvalidDecisionSpecifierError... 2840 >>> pf.parseDecisionSpecifier('domain1//domain2//feature') 2841 Traceback (most recent call last): 2842 ... 2843 exploration.base.InvalidDecisionSpecifierError... 2844 >>> pf.parseDecisionSpecifier('domain//123') 2845 Traceback (most recent call last): 2846 ... 2847 exploration.base.InvalidDecisionSpecifierError... 2848 >>> pf.parseDecisionSpecifier('region::123') 2849 Traceback (most recent call last): 2850 ... 2851 exploration.base.InvalidDecisionSpecifierError... 2852 """ 2853 try: 2854 return int(specString) 2855 except ValueError: 2856 tokens = self.lex(specString) 2857 result, end = self.parseDecisionSpecifierFromTokens(tokens) 2858 if end != len(tokens) - 1: 2859 raise base.InvalidDecisionSpecifierError( 2860 f"Junk after end of decision specifier:" 2861 f"\n{tokens[end + 1:]}" 2862 ) 2863 return result 2864 2865 def parseFeatureSpecifierFromTokens( 2866 self, 2867 tokens: LexedTokens, 2868 start: int = 0, 2869 limit: int = -1 2870 ) -> Tuple[base.FeatureSpecifier, int]: 2871 """ 2872 Parses a `FeatureSpecifier` starting from the specified part of 2873 a tokens list. Returns a tuple containing the feature specifier 2874 and the end position of the end of the feature specifier. 2875 2876 Can parse integer feature IDs in string form, as well as nested 2877 feature specifiers and plain feature specifiers. Assumes that 2878 all int-convertible strings are feature IDs, so it cannot deal 2879 with feature names which are just numbers. 2880 2881 For example: 2882 2883 >>> pf = ParseFormat() 2884 >>> pf.parseFeatureSpecifierFromTokens(['example']) 2885 (FeatureSpecifier(domain=None, within=[], feature='example',\ 2886 part=None), 0) 2887 >>> pf.parseFeatureSpecifierFromTokens(['example1', 'example2'], 1) 2888 (FeatureSpecifier(domain=None, within=[], feature='example2',\ 2889 part=None), 1) 2890 >>> pf.parseFeatureSpecifierFromTokens( 2891 ... [ 2892 ... 'domain', 2893 ... Lexeme.domainSeparator, 2894 ... 'region', 2895 ... Lexeme.zoneSeparator, 2896 ... 'feature', 2897 ... Lexeme.partSeparator, 2898 ... 'part' 2899 ... ] 2900 ... ) 2901 (FeatureSpecifier(domain='domain', within=['region'],\ 2902 feature='feature', part='part'), 6) 2903 >>> pf.parseFeatureSpecifierFromTokens( 2904 ... [ 2905 ... 'outerRegion', 2906 ... Lexeme.zoneSeparator, 2907 ... 'midRegion', 2908 ... Lexeme.zoneSeparator, 2909 ... 'innerRegion', 2910 ... Lexeme.zoneSeparator, 2911 ... 'feature' 2912 ... ] 2913 ... ) 2914 (FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion',\ 2915 'innerRegion'], feature='feature', part=None), 6) 2916 >>> pf.parseFeatureSpecifierFromTokens( 2917 ... [ 2918 ... 'outerRegion', 2919 ... Lexeme.zoneSeparator, 2920 ... 'midRegion', 2921 ... Lexeme.zoneSeparator, 2922 ... 'innerRegion', 2923 ... Lexeme.zoneSeparator, 2924 ... 'feature' 2925 ... ], 2926 ... 1 2927 ... ) 2928 Traceback (most recent call last): 2929 ... 2930 exploration.parsing.InvalidFeatureSpecifierError... 2931 >>> pf.parseFeatureSpecifierFromTokens( 2932 ... [ 2933 ... 'outerRegion', 2934 ... Lexeme.zoneSeparator, 2935 ... 'midRegion', 2936 ... Lexeme.zoneSeparator, 2937 ... 'innerRegion', 2938 ... Lexeme.zoneSeparator, 2939 ... 'feature' 2940 ... ], 2941 ... 2 2942 ... ) 2943 (FeatureSpecifier(domain=None, within=['midRegion', 'innerRegion'],\ 2944 feature='feature', part=None), 6) 2945 >>> pf.parseFeatureSpecifierFromTokens( 2946 ... [ 2947 ... 'outerRegion', 2948 ... Lexeme.zoneSeparator, 2949 ... 'feature', 2950 ... Lexeme.domainSeparator, 2951 ... 'after', 2952 ... ] 2953 ... ) 2954 (FeatureSpecifier(domain=None, within=['outerRegion'],\ 2955 feature='feature', part=None), 2) 2956 >>> pf.parseFeatureSpecifierFromTokens( 2957 ... [ 2958 ... 'outerRegion', 2959 ... Lexeme.zoneSeparator, 2960 ... 'feature', 2961 ... Lexeme.domainSeparator, 2962 ... 'after', 2963 ... ], 2964 ... 2 2965 ... ) 2966 (FeatureSpecifier(domain='feature', within=[], feature='after',\ 2967 part=None), 4) 2968 >>> # Including a limit: 2969 >>> pf.parseFeatureSpecifierFromTokens( 2970 ... [ 2971 ... 'outerRegion', 2972 ... Lexeme.zoneSeparator, 2973 ... 'midRegion', 2974 ... Lexeme.zoneSeparator, 2975 ... 'feature', 2976 ... ], 2977 ... 0, 2978 ... 2 2979 ... ) 2980 (FeatureSpecifier(domain=None, within=['outerRegion'],\ 2981 feature='midRegion', part=None), 2) 2982 >>> pf.parseFeatureSpecifierFromTokens( 2983 ... [ 2984 ... 'outerRegion', 2985 ... Lexeme.zoneSeparator, 2986 ... 'midRegion', 2987 ... Lexeme.zoneSeparator, 2988 ... 'feature', 2989 ... ], 2990 ... 0, 2991 ... 0 2992 ... ) 2993 (FeatureSpecifier(domain=None, within=[], feature='outerRegion',\ 2994 part=None), 0) 2995 >>> pf.parseFeatureSpecifierFromTokens( 2996 ... [ 2997 ... 'region', 2998 ... Lexeme.zoneSeparator, 2999 ... Lexeme.zoneSeparator, 3000 ... 'feature', 3001 ... ] 3002 ... ) 3003 (FeatureSpecifier(domain=None, within=[], feature='region',\ 3004 part=None), 0) 3005 """ 3006 start, limit, nTokens = normalizeEnds(tokens, start, limit) 3007 3008 if nTokens == 0: 3009 raise InvalidFeatureSpecifierError( 3010 "Can't parse a feature specifier from 0 tokens." 3011 ) 3012 first = tokens[start] 3013 if isinstance(first, Lexeme): 3014 raise InvalidFeatureSpecifierError( 3015 f"Feature specifier can't begin with a special token." 3016 f"Got:\n{tokens[start:limit + 1]}" 3017 ) 3018 3019 if nTokens in (1, 2): 3020 # 2 tokens isn't enough for a second part 3021 fs = base.FeatureSpecifier( 3022 domain=None, 3023 within=[], 3024 feature=first, 3025 part=None 3026 ) 3027 return (base.normalizeFeatureSpecifier(fs), start) 3028 3029 firstSep = tokens[start + 1] 3030 secondPart = tokens[start + 2] 3031 3032 if ( 3033 firstSep not in ( 3034 Lexeme.domainSeparator, 3035 Lexeme.zoneSeparator, 3036 Lexeme.partSeparator 3037 ) 3038 or not isinstance(secondPart, str) 3039 ): 3040 # Following tokens won't work out 3041 fs = base.FeatureSpecifier( 3042 domain=None, 3043 within=[], 3044 feature=first, 3045 part=None 3046 ) 3047 return (base.normalizeFeatureSpecifier(fs), start) 3048 3049 if firstSep == Lexeme.domainSeparator: 3050 if start + 2 > limit: 3051 return ( 3052 base.FeatureSpecifier( 3053 domain=first, 3054 within=[], 3055 feature=secondPart, 3056 part=None 3057 ), 3058 start + 2 3059 ) 3060 else: 3061 rest, restEnd = self.parseFeatureSpecifierFromTokens( 3062 tokens, 3063 start + 2, 3064 limit 3065 ) 3066 if rest.domain is not None: # two domainSeparators in a row 3067 fs = base.FeatureSpecifier( 3068 domain=first, 3069 within=[], 3070 feature=rest.domain, 3071 part=None 3072 ) 3073 return (base.normalizeFeatureSpecifier(fs), start + 2) 3074 else: 3075 fs = base.FeatureSpecifier( 3076 domain=first, 3077 within=rest.within, 3078 feature=rest.feature, 3079 part=rest.part 3080 ) 3081 return (base.normalizeFeatureSpecifier(fs), restEnd) 3082 3083 elif firstSep == Lexeme.zoneSeparator: 3084 if start + 2 > limit: 3085 fs = base.FeatureSpecifier( 3086 domain=None, 3087 within=[first], 3088 feature=secondPart, 3089 part=None 3090 ) 3091 return (base.normalizeFeatureSpecifier(fs), start + 2) 3092 else: 3093 rest, restEnd = self.parseFeatureSpecifierFromTokens( 3094 tokens, 3095 start + 2, 3096 limit 3097 ) 3098 if rest.domain is not None: # domain sep after zone sep 3099 fs = base.FeatureSpecifier( 3100 domain=None, 3101 within=[first], 3102 feature=rest.domain, 3103 part=None 3104 ) 3105 return (base.normalizeFeatureSpecifier(fs), start + 2) 3106 else: 3107 within = [first] 3108 within.extend(rest.within) 3109 fs = base.FeatureSpecifier( 3110 domain=None, 3111 within=within, 3112 feature=rest.feature, 3113 part=rest.part 3114 ) 3115 return (base.normalizeFeatureSpecifier(fs), restEnd) 3116 3117 else: # must be partSeparator 3118 fs = base.FeatureSpecifier( 3119 domain=None, 3120 within=[], 3121 feature=first, 3122 part=secondPart 3123 ) 3124 return (base.normalizeFeatureSpecifier(fs), start + 2) 3125 3126 def parseFeatureSpecifier(self, specString: str) -> base.FeatureSpecifier: 3127 """ 3128 Parses a full `FeatureSpecifier` from a single string. See 3129 `parseFeatureSpecifierFromTokens`. 3130 3131 >>> pf = ParseFormat() 3132 >>> pf.parseFeatureSpecifier('example') 3133 FeatureSpecifier(domain=None, within=[], feature='example', part=None) 3134 >>> pf.parseFeatureSpecifier('outer::example') 3135 FeatureSpecifier(domain=None, within=['outer'], feature='example',\ 3136 part=None) 3137 >>> pf.parseFeatureSpecifier('example%%middle') 3138 FeatureSpecifier(domain=None, within=[], feature='example',\ 3139 part='middle') 3140 >>> pf.parseFeatureSpecifier('domain//region::feature%%part') 3141 FeatureSpecifier(domain='domain', within=['region'],\ 3142 feature='feature', part='part') 3143 >>> pf.parseFeatureSpecifier( 3144 ... 'outerRegion::midRegion::innerRegion::feature' 3145 ... ) 3146 FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion',\ 3147 'innerRegion'], feature='feature', part=None) 3148 >>> pf.parseFeatureSpecifier('region::domain//feature') 3149 Traceback (most recent call last): 3150 ... 3151 exploration.parsing.InvalidFeatureSpecifierError... 3152 >>> pf.parseFeatureSpecifier('feature%%part1%%part2') 3153 Traceback (most recent call last): 3154 ... 3155 exploration.parsing.InvalidFeatureSpecifierError... 3156 >>> pf.parseFeatureSpecifier('domain1//domain2//feature') 3157 Traceback (most recent call last): 3158 ... 3159 exploration.parsing.InvalidFeatureSpecifierError... 3160 >>> # TODO: Issue warnings for these... 3161 >>> pf.parseFeatureSpecifier('domain//123') # domain discarded 3162 FeatureSpecifier(domain=None, within=[], feature=123, part=None) 3163 >>> pf.parseFeatureSpecifier('region::123') # zone discarded 3164 FeatureSpecifier(domain=None, within=[], feature=123, part=None) 3165 >>> pf.parseFeatureSpecifier('123%%part') 3166 FeatureSpecifier(domain=None, within=[], feature=123, part='part') 3167 """ 3168 tokens = self.lex(specString) 3169 result, rEnd = self.parseFeatureSpecifierFromTokens(tokens) 3170 if rEnd != len(tokens) - 1: 3171 raise InvalidFeatureSpecifierError( 3172 f"Feature specifier has extra stuff at end:" 3173 f" {tokens[rEnd + 1:]}" 3174 ) 3175 else: 3176 return result 3177 3178 def normalizeFeatureSpecifier( 3179 self, 3180 spec: base.AnyFeatureSpecifier 3181 ) -> base.FeatureSpecifier: 3182 """ 3183 Normalizes any kind of feature specifier into an official 3184 `FeatureSpecifier` tuple. 3185 3186 For example: 3187 3188 >>> pf = ParseFormat() 3189 >>> pf.normalizeFeatureSpecifier('town') 3190 FeatureSpecifier(domain=None, within=[], feature='town', part=None) 3191 >>> pf.normalizeFeatureSpecifier(5) 3192 FeatureSpecifier(domain=None, within=[], feature=5, part=None) 3193 >>> pf.parseFeatureSpecifierFromTokens( 3194 ... [ 3195 ... 'domain', 3196 ... Lexeme.domainSeparator, 3197 ... 'region', 3198 ... Lexeme.zoneSeparator, 3199 ... 'feature', 3200 ... Lexeme.partSeparator, 3201 ... 'part' 3202 ... ] 3203 ... ) 3204 (FeatureSpecifier(domain='domain', within=['region'],\ 3205 feature='feature', part='part'), 6) 3206 >>> pf.normalizeFeatureSpecifier('dom//one::two::three%%middle') 3207 FeatureSpecifier(domain='dom', within=['one', 'two'],\ 3208 feature='three', part='middle') 3209 >>> pf.normalizeFeatureSpecifier( 3210 ... base.FeatureSpecifier(None, ['region'], 'place', None) 3211 ... ) 3212 FeatureSpecifier(domain=None, within=['region'], feature='place',\ 3213 part=None) 3214 >>> fs = base.FeatureSpecifier(None, [], 'place', None) 3215 >>> ns = pf.normalizeFeatureSpecifier(fs) 3216 >>> ns is fs # Doesn't create unnecessary clones 3217 True 3218 """ 3219 if isinstance(spec, base.FeatureSpecifier): 3220 return spec 3221 elif isinstance(spec, base.FeatureID): 3222 return base.FeatureSpecifier(None, [], spec, None) 3223 elif isinstance(spec, str): 3224 return self.parseFeatureSpecifier(spec) 3225 else: 3226 raise TypeError(f"Invalid feature specifier type: '{type(spec)}'") 3227 3228 def unparseChallenge(self, challenge: base.Challenge) -> str: 3229 """ 3230 Turns a `base.Challenge` into a string that can be turned back 3231 into an equivalent challenge by `parseChallenge`. For example: 3232 3233 >>> pf = ParseFormat() 3234 >>> c = base.challenge( 3235 ... skills=base.BestSkill('brains', 'brawn'), 3236 ... level=2, 3237 ... success=[base.effect(set=('switch', 'on'))], 3238 ... failure=[ 3239 ... base.effect(deactivate=True, delay=1), 3240 ... base.effect(bounce=True) 3241 ... ], 3242 ... outcome=True 3243 ... ) 3244 >>> r = pf.unparseChallenge(c) 3245 >>> r 3246 '<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}' 3247 >>> pf.parseChallenge(r) == c 3248 True 3249 >>> c2 = base.challenge( 3250 ... skills=base.CombinedSkill( 3251 ... -2, 3252 ... base.ConditionalSkill( 3253 ... base.ReqCapability('tough'), 3254 ... base.BestSkill(1), 3255 ... base.BestSkill(-1) 3256 ... ) 3257 ... ), 3258 ... level=-2, 3259 ... success=[base.effect(gain='orb')], 3260 ... failure=[], 3261 ... outcome=None 3262 ... ) 3263 >>> r2 = pf.unparseChallenge(c2) 3264 >>> r2 3265 '<-2>sum(-2, if(tough, best(1), best(-1))){gain orb}{}' 3266 >>> # TODO: let this parse through without BestSkills... 3267 >>> pf.parseChallenge(r2) == c2 3268 True 3269 """ 3270 lt = self.formatDict[Lexeme.angleLeft] 3271 gt = self.formatDict[Lexeme.angleRight] 3272 result = ( 3273 lt + str(challenge['level']) + gt 3274 + challenge['skills'].unparse() 3275 ) 3276 if challenge['outcome'] is True: 3277 result += gt 3278 result += self.unparseConsequence(challenge['success']) 3279 if challenge['outcome'] is False: 3280 result += gt 3281 result += self.unparseConsequence(challenge['failure']) 3282 return result 3283 3284 def unparseCondition(self, condition: base.Condition) -> str: 3285 """ 3286 Given a `base.Condition` returns a string that would result in 3287 that condition if given to `parseCondition`. For example: 3288 3289 >>> pf = ParseFormat() 3290 >>> c = base.condition( 3291 ... condition=base.ReqAny([ 3292 ... base.ReqCapability('brawny'), 3293 ... base.ReqNot(base.ReqTokens('weights', 3)) 3294 ... ]), 3295 ... consequence=[base.effect(gain='power')] 3296 ... ) 3297 >>> r = pf.unparseCondition(c) 3298 >>> r 3299 '??((brawny|!(weights*3))){gain power}{}' 3300 >>> pf.parseCondition(r) == c 3301 True 3302 """ 3303 return ( 3304 self.formatDict[Lexeme.doubleQuestionmark] 3305 + self.formatDict[Lexeme.openParen] 3306 + condition['condition'].unparse() 3307 + self.formatDict[Lexeme.closeParen] 3308 + self.unparseConsequence(condition['consequence']) 3309 + self.unparseConsequence(condition['alternative']) 3310 ) 3311 3312 def unparseConsequence(self, consequence: base.Consequence) -> str: 3313 """ 3314 Given a `base.Consequence`, returns a string encoding of it, 3315 using the same format that `parseConsequence` will parse. Uses 3316 function-call-like syntax and curly braces to denote different 3317 sub-consequences. See also `SkillCombination.unparse` and 3318 `Requirement.unparse` For example: 3319 3320 >>> pf = ParseFormat() 3321 >>> c = [base.effect(gain='one'), base.effect(lose='one')] 3322 >>> pf.unparseConsequence(c) 3323 '{gain one; lose one}' 3324 >>> c = [ 3325 ... base.challenge( 3326 ... skills=base.BestSkill('brains', 'brawn'), 3327 ... level=2, 3328 ... success=[base.effect(set=('switch', 'on'))], 3329 ... failure=[ 3330 ... base.effect(deactivate=True, delay=1), 3331 ... base.effect(bounce=True) 3332 ... ], 3333 ... outcome=True 3334 ... ) 3335 ... ] 3336 >>> pf.unparseConsequence(c) 3337 '{<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}}' 3338 >>> c[0]['outcome'] = False 3339 >>> pf.unparseConsequence(c) 3340 '{<2>best(brains, brawn){set switch:on}>{deactivate ,1; bounce}}' 3341 >>> c[0]['outcome'] = None 3342 >>> pf.unparseConsequence(c) 3343 '{<2>best(brains, brawn){set switch:on}{deactivate ,1; bounce}}' 3344 >>> c = [ 3345 ... base.condition( 3346 ... condition=base.ReqAny([ 3347 ... base.ReqCapability('brawny'), 3348 ... base.ReqNot(base.ReqTokens('weights', 3)) 3349 ... ]), 3350 ... consequence=[ 3351 ... base.challenge( 3352 ... skills=base.CombinedSkill('brains', 'brawn'), 3353 ... level=3, 3354 ... success=[base.effect(goto='home')], 3355 ... failure=[base.effect(bounce=True)], 3356 ... outcome=None 3357 ... ) 3358 ... ] # no alternative -> empty list 3359 ... ) 3360 ... ] 3361 >>> pf.unparseConsequence(c) 3362 '{??((brawny|!(weights*3))){\ 3363<3>sum(brains, brawn){goto home}{bounce}}{}}' 3364 >>> c = [base.effect(gain='if(power){gain "mimic"}')] 3365 >>> # TODO: Make this work! 3366 >>> # pf.unparseConsequence(c) 3367 3368 '{gain "if(power){gain \\\\"mimic\\\\"}"}' 3369 """ 3370 result = self.formatDict[Lexeme.openCurly] 3371 for item in consequence: 3372 if 'skills' in item: # a Challenge 3373 item = cast(base.Challenge, item) 3374 result += self.unparseChallenge(item) 3375 3376 elif 'value' in item: # an Effect 3377 item = cast(base.Effect, item) 3378 result += self.unparseEffect(item) 3379 3380 elif 'condition' in item: # a Condition 3381 item = cast(base.Condition, item) 3382 result += self.unparseCondition(item) 3383 3384 else: # bad dict 3385 raise TypeError( 3386 f"Invalid consequence: items in the list must be" 3387 f" Effects, Challenges, or Conditions (got a dictionary" 3388 f" without 'skills', 'value', or 'condition' keys)." 3389 f"\nGot item: {repr(item)}" 3390 ) 3391 result += '; ' 3392 3393 if result.endswith('; '): 3394 result = result[:-2] 3395 3396 return result + self.formatDict[Lexeme.closeCurly] 3397 3398 def parseMechanismSpecifierFromTokens( 3399 self, 3400 tokens: LexedTokens, 3401 start: int = 0 3402 ) -> Tuple[base.MechanismSpecifier, int]: 3403 """ 3404 Parses a mechanism specifier starting at the specified position 3405 in the given tokens list. No ending position is specified, but 3406 instead this function returns a tuple containing the parsed 3407 `base.MechanismSpecifier` along with an index in the tokens list 3408 where the end of the specifier was found. 3409 3410 For example: 3411 3412 >>> pf = ParseFormat() 3413 >>> pf.parseMechanismSpecifierFromTokens(['m']) 3414 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3415 name='m'), 0) 3416 >>> pf.parseMechanismSpecifierFromTokens(['a', 'm']) 3417 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3418 name='a'), 0) 3419 >>> pf.parseMechanismSpecifierFromTokens(['a', 'm'], 1) 3420 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3421 name='m'), 1) 3422 >>> pf.parseMechanismSpecifierFromTokens( 3423 ... ['a', Lexeme.domainSeparator, 'm'] 3424 ... ) 3425 (MechanismSpecifier(domain='a', zone=None, decision=None,\ 3426 name='m'), 2) 3427 >>> pf.parseMechanismSpecifierFromTokens( 3428 ... ['a', Lexeme.zoneSeparator, 'm'] 3429 ... ) 3430 (MechanismSpecifier(domain=None, zone=None, decision='a',\ 3431 name='m'), 2) 3432 >>> pf.parseMechanismSpecifierFromTokens( 3433 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm'] 3434 ... ) 3435 (MechanismSpecifier(domain=None, zone='a', decision='b',\ 3436 name='m'), 4) 3437 >>> pf.parseMechanismSpecifierFromTokens( 3438 ... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm'] 3439 ... ) 3440 (MechanismSpecifier(domain='a', zone=None, decision='b',\ 3441 name='m'), 4) 3442 >>> pf.parseMechanismSpecifierFromTokens( 3443 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'] 3444 ... ) 3445 (MechanismSpecifier(domain=None, zone=None, decision='a',\ 3446 name='b'), 2) 3447 >>> pf.parseMechanismSpecifierFromTokens( 3448 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 3449 ... 1 3450 ... ) 3451 Traceback (most recent call last): 3452 ... 3453 exploration.parsing.ParseError... 3454 >>> pf.parseMechanismSpecifierFromTokens( 3455 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 3456 ... 2 3457 ... ) 3458 (MechanismSpecifier(domain='b', zone=None, decision=None,\ 3459 name='m'), 4) 3460 >>> pf.parseMechanismSpecifierFromTokens( 3461 ... [ 3462 ... 'a', 3463 ... Lexeme.domainSeparator, 3464 ... 'b', 3465 ... Lexeme.zoneSeparator, 3466 ... 'c', 3467 ... Lexeme.zoneSeparator, 3468 ... 'm' 3469 ... ] 3470 ... ) 3471 (MechanismSpecifier(domain='a', zone='b', decision='c', name='m'), 6) 3472 >>> pf.parseMechanismSpecifierFromTokens( 3473 ... [ 3474 ... 'a', 3475 ... Lexeme.domainSeparator, 3476 ... 'b', 3477 ... Lexeme.zoneSeparator, 3478 ... 'c', 3479 ... Lexeme.zoneSeparator, 3480 ... 'm' 3481 ... ], 3482 ... 2 3483 ... ) 3484 (MechanismSpecifier(domain=None, zone='b', decision='c',\ 3485 name='m'), 6) 3486 >>> pf.parseMechanismSpecifierFromTokens( 3487 ... [ 3488 ... 'a', 3489 ... Lexeme.domainSeparator, 3490 ... 'b', 3491 ... Lexeme.zoneSeparator, 3492 ... 'c', 3493 ... Lexeme.zoneSeparator, 3494 ... 'm' 3495 ... ], 3496 ... 4 3497 ... ) 3498 (MechanismSpecifier(domain=None, zone=None, decision='c',\ 3499 name='m'), 6) 3500 >>> pf.parseMechanismSpecifierFromTokens( 3501 ... [ 3502 ... 'roomB', 3503 ... Lexeme.zoneSeparator, 3504 ... 'switch', 3505 ... Lexeme.mechanismSeparator, 3506 ... 'on' 3507 ... ] 3508 ... ) 3509 (MechanismSpecifier(domain=None, zone=None, decision='roomB',\ 3510 name='switch'), 2) 3511 >>> pf.parseMechanismSpecifierFromTokens( 3512 ... [ 3513 ... '500', 3514 ... Lexeme.zoneSeparator, 3515 ... 'm' 3516 ... ], 3517 ... 0 3518 ... ) 3519 (MechanismSpecifier(domain=None, zone=None, decision=500,\ 3520 name='m'), 2) 3521 >>> pf.parseMechanismSpecifierFromTokens( 3522 ... [ 3523 ... '500', 3524 ... Lexeme.zoneSeparator, 3525 ... Lexeme.zoneSeparator, 3526 ... 'm' 3527 ... ], 3528 ... 0 3529 ... ) 3530 Traceback (most recent call last): 3531 ... 3532 exploration.parsing.ParseError... 3533 >>> pf.parseMechanismSpecifierFromTokens( 3534 ... [ 3535 ... '500', 3536 ... Lexeme.zoneSeparator, 3537 ... 'm', 3538 ... Lexeme.mechanismSeparator, 3539 ... 'on' 3540 ... ], 3541 ... 0 3542 ... ) 3543 (MechanismSpecifier(domain=None, zone=None, decision=500,\ 3544 name='m'), 2) 3545 >>> pf.parseMechanismSpecifierFromTokens( 3546 ... [ 3547 ... '500', 3548 ... Lexeme.zoneSeparator, 3549 ... 'd', 3550 ... Lexeme.zoneSeparator, 3551 ... 'm' 3552 ... ], 3553 ... 0 3554 ... ) 3555 (MechanismSpecifier(domain=None, zone='500', decision='d',\ 3556 name='m'), 4) 3557 """ 3558 start, tEnd, nLeft = normalizeEnds(tokens, start, -1) 3559 3560 try: 3561 dSpec, dEnd = self.parseDecisionSpecifierFromTokens( 3562 tokens, 3563 start 3564 ) 3565 except ParseError: 3566 raise ParseError( 3567 "Failed to parse mechanism specifier couldn't parse" 3568 " initial mechanism name." 3569 ) 3570 3571 # Note: This doesn't normally happen because the mechanism name 3572 # makes it seem like the integer decision ID is really a zone 3573 # name. 3574 if isinstance(dSpec, int): 3575 sep = tokens[dEnd + 1] 3576 after = tokens[dEnd + 2] 3577 3578 if sep == Lexeme.zoneSeparator and not isinstance(after, Lexeme): 3579 return ( 3580 base.MechanismSpecifier( 3581 domain=None, 3582 zone=None, 3583 decision=dSpec, 3584 name=after 3585 ), 3586 dEnd + 2 3587 ) 3588 else: 3589 raise ParseError( 3590 f"Invalid mechanism specifier: got a decision ID" 3591 f" NOT followed by a zone separator and mechanism" 3592 f" name. Got: {tokens[start:]}" 3593 ) 3594 3595 mDomain = dSpec.domain 3596 if dEnd == tEnd or dEnd == tEnd - 1: 3597 if dSpec.zone is not None: 3598 try: 3599 # Case for integer "zone" -> integer decision ID 3600 zID = int(dSpec.zone) 3601 return ( 3602 base.MechanismSpecifier( 3603 domain=None, 3604 zone=None, 3605 decision=zID, 3606 name=dSpec.name 3607 ), 3608 dEnd 3609 ) 3610 except ValueError: 3611 pass 3612 return ( 3613 base.MechanismSpecifier( 3614 domain=mDomain, 3615 zone=None, 3616 decision=dSpec.zone, 3617 name=dSpec.name 3618 ), 3619 dEnd 3620 ) 3621 3622 sep = tokens[dEnd + 1] 3623 after = tokens[dEnd + 2] 3624 3625 mDec: Optional[Union[base.DecisionName, base.DecisionID]] 3626 if sep == Lexeme.zoneSeparator: 3627 if isinstance(after, Lexeme): 3628 mZone = None 3629 mDec = dSpec.zone 3630 mName = dSpec.name 3631 mEnd = dEnd 3632 else: 3633 mZone = dSpec.zone 3634 mDec = dSpec.name 3635 mName = after 3636 mEnd = dEnd + 2 3637 else: 3638 mZone = None 3639 mDec = dSpec.zone 3640 mName = dSpec.name 3641 mEnd = dEnd 3642 3643 # Treat numerical decision "names" as decision IDs 3644 if mDec is not None: 3645 try: 3646 mDec = int(mDec) 3647 if mDomain is not None or mZone is not None: 3648 raise ParseError( 3649 f"Invalid mechanism specifier: got a numerical" 3650 f" decision ID but also a domain and/or zone." 3651 f" Got: {tokens[start:]}" 3652 ) 3653 except ValueError: 3654 pass 3655 3656 return ( 3657 base.MechanismSpecifier( 3658 domain=mDomain, 3659 zone=mZone, 3660 decision=mDec, 3661 name=mName 3662 ), 3663 mEnd 3664 ) 3665 3666 def groupReqTokens( 3667 self, 3668 tokens: LexedTokens, 3669 start: int = 0, 3670 end: int = -1 3671 ) -> GroupedTokens: 3672 """ 3673 Groups tokens for a requirement, stripping out all parentheses 3674 but replacing parenthesized expressions with sub-lists of tokens. 3675 3676 For example: 3677 3678 >>> pf = ParseFormat() 3679 >>> pf.groupReqTokens(['jump']) 3680 ['jump'] 3681 >>> pf.groupReqTokens([Lexeme.openParen, 'jump']) 3682 Traceback (most recent call last): 3683 ... 3684 exploration.parsing.ParseError... 3685 >>> pf.groupReqTokens([Lexeme.closeParen, 'jump']) 3686 Traceback (most recent call last): 3687 ... 3688 exploration.parsing.ParseError... 3689 >>> pf.groupReqTokens(['jump', Lexeme.closeParen]) 3690 Traceback (most recent call last): 3691 ... 3692 exploration.parsing.ParseError... 3693 >>> pf.groupReqTokens([Lexeme.openParen, 'jump', Lexeme.closeParen]) 3694 [['jump']] 3695 >>> pf.groupReqTokens( 3696 ... [ 3697 ... Lexeme.openParen, 3698 ... 'jump', 3699 ... Lexeme.orBar, 3700 ... 'climb', 3701 ... Lexeme.closeParen, 3702 ... Lexeme.ampersand, 3703 ... 'crawl', 3704 ... ] 3705 ... ) 3706 [['jump', <Lexeme.orBar: ...>, 'climb'], <Lexeme.ampersand: ...>,\ 3707 'crawl'] 3708 """ 3709 start, end, nTokens = normalizeEnds(tokens, start, end) 3710 if nTokens == 0: 3711 raise ParseError("Ran out of tokens.") 3712 3713 resultsStack: List[GroupedTokens] = [[]] 3714 here = start 3715 while here <= end: 3716 token = tokens[here] 3717 here += 1 3718 if token == Lexeme.closeParen: 3719 if len(resultsStack) == 1: 3720 raise ParseError( 3721 f"Too many closing parens at index {here - 1}" 3722 f" in:\n{tokens[start:end + 1]}" 3723 ) 3724 else: 3725 closed = resultsStack.pop() 3726 resultsStack[-1].append(closed) 3727 elif token == Lexeme.openParen: 3728 resultsStack.append([]) 3729 else: 3730 resultsStack[-1].append(token) 3731 if len(resultsStack) != 1: 3732 raise ParseError( 3733 f"Mismatched parentheses in tokens:" 3734 f"\n{tokens[start:end + 1]}" 3735 ) 3736 return resultsStack[0] 3737 3738 def groupReqTokensByPrecedence( 3739 self, 3740 tokenGroups: GroupedTokens 3741 ) -> GroupedRequirementParts: 3742 """ 3743 Re-groups requirement tokens that have been grouped using 3744 `groupReqTokens` according to operator precedence, effectively 3745 creating an equivalent result which would have been obtained by 3746 `groupReqTokens` if all possible non-redundant explicit 3747 parentheses had been included. 3748 3749 Also turns each leaf part into a `Requirement`. 3750 3751 TODO: Make this actually reasonably efficient T_T 3752 3753 Examples: 3754 3755 >>> pf = ParseFormat() 3756 >>> r = pf.parseRequirement('capability&roomB::switch:on') 3757 >>> pf.groupReqTokensByPrecedence( 3758 ... [ 3759 ... ['jump', Lexeme.orBar, 'climb'], 3760 ... Lexeme.ampersand, 3761 ... Lexeme.notMarker, 3762 ... 'coin', 3763 ... Lexeme.tokenCount, 3764 ... '3' 3765 ... ] 3766 ... ) 3767 [\ 3768[\ 3769[[ReqCapability('jump'), <Lexeme.orBar: ...>, ReqCapability('climb')]],\ 3770 <Lexeme.ampersand: ...>,\ 3771 [<Lexeme.notMarker: ...>, ReqTokens('coin', 3)]\ 3772]\ 3773] 3774 """ 3775 subgrouped: List[Union[Lexeme, str, GroupedRequirementParts]] = [] 3776 # First recursively group all parenthesized expressions 3777 for i, item in enumerate(tokenGroups): 3778 if isinstance(item, list): 3779 subgrouped.append(self.groupReqTokensByPrecedence(item)) 3780 else: 3781 subgrouped.append(item) 3782 3783 # Now process all leaf requirements 3784 leavesConverted: GroupedRequirementParts = [] 3785 i = 0 3786 while i < len(subgrouped): 3787 gItem = subgrouped[i] 3788 3789 if isinstance(gItem, list): 3790 leavesConverted.append(gItem) 3791 elif isinstance(gItem, Lexeme): 3792 leavesConverted.append(gItem) 3793 elif i == len(subgrouped) - 1: 3794 if isinstance(gItem, Lexeme): 3795 raise ParseError( 3796 f"Lexeme at end of requirement. Grouped tokens:" 3797 f"\n{tokenGroups}" 3798 ) 3799 else: 3800 assert isinstance(gItem, str) 3801 if gItem == 'X': 3802 leavesConverted.append(base.ReqImpossible()) 3803 elif gItem == 'O': 3804 leavesConverted.append(base.ReqNothing()) 3805 else: 3806 leavesConverted.append(base.ReqCapability(gItem)) 3807 else: 3808 assert isinstance(gItem, str) 3809 try: 3810 # TODO: Avoid list copy here... 3811 couldBeMechanismSpecifier: LexedTokens = [] 3812 for ii in range(i, len(subgrouped)): 3813 lexemeOrStr = subgrouped[ii] 3814 if isinstance(lexemeOrStr, (Lexeme, str)): 3815 couldBeMechanismSpecifier.append(lexemeOrStr) 3816 else: 3817 break 3818 mSpec, mEnd = self.parseMechanismSpecifierFromTokens( 3819 couldBeMechanismSpecifier 3820 ) 3821 mEnd += i 3822 if ( 3823 mEnd >= len(subgrouped) - 2 3824 or subgrouped[mEnd + 1] != Lexeme.mechanismSeparator 3825 ): 3826 raise ParseError("Not a mechanism requirement.") 3827 3828 mState = subgrouped[mEnd + 2] 3829 if not isinstance(mState, base.MechanismState): 3830 raise ParseError("Not a mechanism requirement.") 3831 leavesConverted.append(base.ReqMechanism(mSpec, mState)) 3832 i = mEnd + 2 # + 1 will happen automatically below 3833 except ParseError: 3834 following = subgrouped[i + 1] 3835 if following in ( 3836 Lexeme.tokenCount, 3837 Lexeme.mechanismSeparator, 3838 Lexeme.wigglyLine, 3839 Lexeme.skillLevel 3840 ): 3841 if ( 3842 i == len(subgrouped) - 2 3843 or isinstance(subgrouped[i + 2], Lexeme) 3844 ): 3845 if following == Lexeme.wigglyLine: 3846 # Default tag value is 1 3847 leavesConverted.append(base.ReqTag(gItem, 1)) 3848 i += 1 # another +1 automatic below 3849 else: 3850 raise ParseError( 3851 f"Lexeme at end of requirement. Grouped" 3852 f" tokens:\n{tokenGroups}" 3853 ) 3854 else: 3855 afterwards = subgrouped[i + 2] 3856 if not isinstance(afterwards, str): 3857 raise ParseError( 3858 f"Lexeme after token/mechanism/tag/skill" 3859 f" separator at index {i}." 3860 f" Grouped tokens:\n{tokenGroups}" 3861 ) 3862 i += 2 # another +1 automatic below 3863 if following == Lexeme.tokenCount: 3864 try: 3865 tCount = int(afterwards) 3866 except ValueError: 3867 raise ParseError( 3868 f"Token count could not be" 3869 f" parsed as an integer:" 3870 f" {afterwards!r}. Grouped" 3871 f" tokens:\n{tokenGroups}" 3872 ) 3873 leavesConverted.append( 3874 base.ReqTokens(gItem, tCount) 3875 ) 3876 elif following == Lexeme.mechanismSeparator: 3877 leavesConverted.append( 3878 base.ReqMechanism(gItem, afterwards) 3879 ) 3880 elif following == Lexeme.wigglyLine: 3881 tVal = self.parseTagValue(afterwards) 3882 leavesConverted.append( 3883 base.ReqTag(gItem, tVal) 3884 ) 3885 else: 3886 assert following == Lexeme.skillLevel 3887 try: 3888 sLevel = int(afterwards) 3889 except ValueError: 3890 raise ParseError( 3891 f"Skill level could not be" 3892 f" parsed as an integer:" 3893 f" {afterwards!r}. Grouped" 3894 f" tokens:\n{tokenGroups}" 3895 ) 3896 leavesConverted.append( 3897 base.ReqLevel(gItem, sLevel) 3898 ) 3899 else: 3900 if gItem == 'X': 3901 leavesConverted.append(base.ReqImpossible()) 3902 elif gItem == 'O': 3903 leavesConverted.append(base.ReqNothing()) 3904 else: 3905 leavesConverted.append( 3906 base.ReqCapability(gItem) 3907 ) 3908 3909 # Finally, increment our index: 3910 i += 1 3911 3912 # Now group all NOT operators 3913 i = 0 3914 notsGrouped: GroupedRequirementParts = [] 3915 while i < len(leavesConverted): 3916 leafItem = leavesConverted[i] 3917 group = [] 3918 while leafItem == Lexeme.notMarker: 3919 group.append(leafItem) 3920 i += 1 3921 if i >= len(leavesConverted): 3922 raise ParseError( 3923 f"NOT at end of tokens:\n{leavesConverted}" 3924 ) 3925 leafItem = leavesConverted[i] 3926 if group == []: 3927 notsGrouped.append(leafItem) 3928 i += 1 3929 else: 3930 group.append(leafItem) 3931 i += 1 3932 notsGrouped.append(group) 3933 3934 # Next group all AND operators 3935 i = 0 3936 andsGrouped: GroupedRequirementParts = [] 3937 while i < len(notsGrouped): 3938 notGroupItem = notsGrouped[i] 3939 if notGroupItem == Lexeme.ampersand: 3940 if i == len(notsGrouped) - 1: 3941 raise ParseError( 3942 f"AND at end of group in tokens:" 3943 f"\n{tokenGroups}" 3944 f"Which had been grouped into:" 3945 f"\n{notsGrouped}" 3946 ) 3947 itemAfter = notsGrouped[i + 1] 3948 if isinstance(itemAfter, Lexeme): 3949 raise ParseError( 3950 f"Lexeme after AND in of group in tokens:" 3951 f"\n{tokenGroups}" 3952 f"Which had been grouped into:" 3953 f"\n{notsGrouped}" 3954 ) 3955 assert isinstance(itemAfter, (base.Requirement, list)) 3956 prev = andsGrouped[-1] 3957 if ( 3958 isinstance(prev, list) 3959 and len(prev) > 2 3960 and prev[1] == Lexeme.ampersand 3961 ): 3962 prev.extend(notsGrouped[i:i + 2]) 3963 i += 1 # with an extra +1 below 3964 else: 3965 andsGrouped.append( 3966 [andsGrouped.pop()] + notsGrouped[i:i + 2] 3967 ) 3968 i += 1 # extra +1 below 3969 else: 3970 andsGrouped.append(notGroupItem) 3971 i += 1 3972 3973 # Finally check that we only have OR operators left over 3974 i = 0 3975 finalResult: GroupedRequirementParts = [] 3976 while i < len(andsGrouped): 3977 andGroupItem = andsGrouped[i] 3978 if andGroupItem == Lexeme.orBar: 3979 if i == len(andsGrouped) - 1: 3980 raise ParseError( 3981 f"OR at end of group in tokens:" 3982 f"\n{tokenGroups}" 3983 f"Which had been grouped into:" 3984 f"\n{andsGrouped}" 3985 ) 3986 itemAfter = andsGrouped[i + 1] 3987 if isinstance(itemAfter, Lexeme): 3988 raise ParseError( 3989 f"Lexeme after OR in of group in tokens:" 3990 f"\n{tokenGroups}" 3991 f"Which had been grouped into:" 3992 f"\n{andsGrouped}" 3993 ) 3994 assert isinstance(itemAfter, (base.Requirement, list)) 3995 prev = finalResult[-1] 3996 if ( 3997 isinstance(prev, list) 3998 and len(prev) > 2 3999 and prev[1] == Lexeme.orBar 4000 ): 4001 prev.extend(andsGrouped[i:i + 2]) 4002 i += 1 # with an extra +1 below 4003 else: 4004 finalResult.append( 4005 [finalResult.pop()] + andsGrouped[i:i + 2] 4006 ) 4007 i += 1 # extra +1 below 4008 elif isinstance(andGroupItem, Lexeme): 4009 raise ParseError( 4010 f"Leftover lexeme when grouping ORs at index {i}" 4011 f" in grouped tokens:\n{andsGrouped}" 4012 f"\nOriginal tokens were:\n{tokenGroups}" 4013 ) 4014 else: 4015 finalResult.append(andGroupItem) 4016 i += 1 4017 4018 return finalResult 4019 4020 def parseRequirementFromRegroupedTokens( 4021 self, 4022 reqGroups: GroupedRequirementParts 4023 ) -> base.Requirement: 4024 """ 4025 Recursive parser that works once tokens have been turned into 4026 requirements at the leaves and grouped by operator precedence 4027 otherwise (see `groupReqTokensByPrecedence`). 4028 4029 TODO: Simplify by just doing this while grouping... ? 4030 """ 4031 if len(reqGroups) == 0: 4032 raise ParseError("Ran out of tokens.") 4033 4034 elif len(reqGroups) == 1: 4035 only = reqGroups[0] 4036 if isinstance(only, list): 4037 return self.parseRequirementFromRegroupedTokens(only) 4038 elif isinstance(only, base.Requirement): 4039 return only 4040 else: 4041 raise ParseError(f"Invalid singleton group:\n{only}") 4042 elif reqGroups[0] == Lexeme.notMarker: 4043 if ( 4044 not all(x == Lexeme.notMarker for x in reqGroups[:-1]) 4045 or not isinstance(reqGroups[-1], (list, base.Requirement)) 4046 ): 4047 raise ParseError(f"Invalid negation group:\n{reqGroups}") 4048 result = reqGroups[-1] 4049 if isinstance(result, list): 4050 result = self.parseRequirementFromRegroupedTokens(result) 4051 assert isinstance(result, base.Requirement) 4052 for i in range(len(reqGroups) - 1): 4053 result = base.ReqNot(result) 4054 return result 4055 elif len(reqGroups) % 2 == 0: 4056 raise ParseError(f"Even-length non-negation group:\n{reqGroups}") 4057 else: 4058 if ( 4059 reqGroups[1] not in (Lexeme.ampersand, Lexeme.orBar) 4060 or not all( 4061 reqGroups[i] == reqGroups[1] 4062 for i in range(1, len(reqGroups), 2) 4063 ) 4064 ): 4065 raise ParseError( 4066 f"Inconsistent operator(s) in group:\n{reqGroups}" 4067 ) 4068 op = reqGroups[1] 4069 operands = [ 4070 ( 4071 self.parseRequirementFromRegroupedTokens(x) 4072 if isinstance(x, list) 4073 else x 4074 ) 4075 for x in reqGroups[::2] 4076 ] 4077 if not all(isinstance(x, base.Requirement) for x in operands): 4078 raise ParseError( 4079 f"Item not reducible to Requirement in AND group:" 4080 f"\n{reqGroups}" 4081 ) 4082 reqSequence = cast(Sequence[base.Requirement], operands) 4083 if op == Lexeme.ampersand: 4084 return base.ReqAll(reqSequence).flatten() 4085 else: 4086 assert op == Lexeme.orBar 4087 return base.ReqAny(reqSequence).flatten() 4088 4089 def parseRequirementFromGroupedTokens( 4090 self, 4091 tokenGroups: GroupedTokens 4092 ) -> base.Requirement: 4093 """ 4094 Parses a `base.Requirement` from a pre-grouped tokens list (see 4095 `groupReqTokens`). Uses the 'orBar', 'ampersand', 'notMarker', 4096 'tokenCount', and 'mechanismSeparator' `Lexeme`s to provide 4097 'or', 'and', and 'not' operators along with distinguishing 4098 between capabilities, tokens, and mechanisms. 4099 4100 Precedence ordering is not, then and, then or, but you are 4101 encouraged to use parentheses for explicit grouping (the 4102 'openParen' and 'closeParen' `Lexeme`s, although these must be 4103 handled by `groupReqTokens` so this function won't see them 4104 directly). 4105 4106 You can also use 'X' (without quotes) for a never-satisfied 4107 requirement, and 'O' (without quotes) for an always-satisfied 4108 requirement. 4109 4110 Note that when '!' is applied to a token requirement it flips 4111 the sense of the integer from 'must have at least this many' to 4112 'must have strictly less than this many'. 4113 4114 Raises a `ParseError` if the grouped tokens it is given cannot 4115 be parsed as a `Requirement`. 4116 4117 Examples: 4118 4119 >>> pf = ParseFormat() 4120 >>> pf.parseRequirementFromGroupedTokens(['capability']) 4121 ReqCapability('capability') 4122 >>> pf.parseRequirementFromGroupedTokens( 4123 ... ['token', Lexeme.tokenCount, '3'] 4124 ... ) 4125 ReqTokens('token', 3) 4126 >>> pf.parseRequirementFromGroupedTokens( 4127 ... ['mechanism', Lexeme.mechanismSeparator, 'state'] 4128 ... ) 4129 ReqMechanism('mechanism', 'state') 4130 >>> pf.parseRequirementFromGroupedTokens( 4131 ... ['capability', Lexeme.orBar, 'token', 4132 ... Lexeme.tokenCount, '3'] 4133 ... ) 4134 ReqAny([ReqCapability('capability'), ReqTokens('token', 3)]) 4135 >>> pf.parseRequirementFromGroupedTokens( 4136 ... ['one', Lexeme.ampersand, 'two', Lexeme.orBar, 'three'] 4137 ... ) 4138 ReqAny([ReqAll([ReqCapability('one'), ReqCapability('two')]),\ 4139 ReqCapability('three')]) 4140 >>> pf.parseRequirementFromGroupedTokens( 4141 ... [ 4142 ... 'one', 4143 ... Lexeme.ampersand, 4144 ... [ 4145 ... 'two', 4146 ... Lexeme.orBar, 4147 ... 'three' 4148 ... ] 4149 ... ] 4150 ... ) 4151 ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'),\ 4152 ReqCapability('three')])]) 4153 >>> pf.parseRequirementFromTokens(['X']) 4154 ReqImpossible() 4155 >>> pf.parseRequirementFromTokens(['O']) 4156 ReqNothing() 4157 >>> pf.parseRequirementFromTokens( 4158 ... [Lexeme.openParen, 'O', Lexeme.closeParen] 4159 ... ) 4160 ReqNothing() 4161 """ 4162 if len(tokenGroups) == 0: 4163 raise ParseError("Ran out of tokens.") 4164 4165 reGrouped = self.groupReqTokensByPrecedence(tokenGroups) 4166 4167 return self.parseRequirementFromRegroupedTokens(reGrouped) 4168 4169 def parseRequirementFromTokens( 4170 self, 4171 tokens: LexedTokens, 4172 start: int = 0, 4173 end: int = -1 4174 ) -> base.Requirement: 4175 """ 4176 Parses a requirement from `LexedTokens` by grouping them first 4177 and then using `parseRequirementFromGroupedTokens`. 4178 4179 For example: 4180 4181 >>> pf = ParseFormat() 4182 >>> pf.parseRequirementFromTokens( 4183 ... [ 4184 ... 'one', 4185 ... Lexeme.ampersand, 4186 ... Lexeme.openParen, 4187 ... 'two', 4188 ... Lexeme.orBar, 4189 ... 'three', 4190 ... Lexeme.closeParen 4191 ... ] 4192 ... ) 4193 ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'),\ 4194 ReqCapability('three')])]) 4195 """ 4196 grouped = self.groupReqTokens(tokens, start, end) 4197 return self.parseRequirementFromGroupedTokens(grouped) 4198 4199 def parseRequirement(self, encoded: str) -> base.Requirement: 4200 """ 4201 Parses a `base.Requirement` from a string by calling `lex` and 4202 then feeding it into `ParseFormat.parseRequirementFromTokens`. 4203 As stated in `parseRequirementFromTokens`, the precedence 4204 binding order is NOT, then AND, then OR. 4205 4206 For example: 4207 4208 >>> pf = ParseFormat() 4209 >>> pf.parseRequirement('! coin * 3') 4210 ReqNot(ReqTokens('coin', 3)) 4211 >>> pf.parseRequirement( 4212 ... ' oneWord | "two words"|"three words words" ' 4213 ... ) 4214 ReqAny([ReqCapability('oneWord'), ReqCapability('"two words"'),\ 4215 ReqCapability('"three words words"')]) 4216 >>> pf.parseRequirement('words-with-dashes') 4217 ReqCapability('words-with-dashes') 4218 >>> r = pf.parseRequirement('capability&roomB::switch:on') 4219 >>> r 4220 ReqAll([ReqCapability('capability'),\ 4221 ReqMechanism(MechanismSpecifier(domain=None, zone=None, decision='roomB',\ 4222 name='switch'), 'on')]) 4223 >>> r.unparse() 4224 '(capability&roomB::switch:on)' 4225 >>> pf.parseRequirement('!!!one') 4226 ReqNot(ReqNot(ReqNot(ReqCapability('one')))) 4227 >>> pf.parseRequirement('domain//zone::where::mechanism:state') 4228 ReqMechanism(MechanismSpecifier(domain='domain', zone='zone',\ 4229 decision='where', name='mechanism'), 'state') 4230 >>> pf.parseRequirement('domain//mechanism:state') 4231 ReqMechanism(MechanismSpecifier(domain='domain', zone=None,\ 4232 decision=None, name='mechanism'), 'state') 4233 >>> pf.parseRequirement('where::mechanism:state') 4234 ReqMechanism(MechanismSpecifier(domain=None, zone=None,\ 4235 decision='where', name='mechanism'), 'state') 4236 >>> pf.parseRequirement('zone::where::mechanism:state') 4237 ReqMechanism(MechanismSpecifier(domain=None, zone='zone',\ 4238 decision='where', name='mechanism'), 'state') 4239 >>> pf.parseRequirement('tag~') 4240 ReqTag('tag', 1) 4241 >>> pf.parseRequirement('tag~&tag2~') 4242 ReqAll([ReqTag('tag', 1), ReqTag('tag2', 1)]) 4243 >>> pf.parseRequirement('tag~value|tag~3|tag~3.5|skill^3') 4244 ReqAny([ReqTag('tag', 'value'), ReqTag('tag', 3),\ 4245 ReqTag('tag', 3.5), ReqLevel('skill', 3)]) 4246 >>> pf.parseRequirement('tag~True|tag~False|tag~None') 4247 ReqAny([ReqTag('tag', True), ReqTag('tag', False), ReqTag('tag', None)]) 4248 4249 Precedence examples: 4250 4251 >>> pf.parseRequirement('A|B&C') 4252 ReqAny([ReqCapability('A'), ReqAll([ReqCapability('B'),\ 4253 ReqCapability('C')])]) 4254 >>> pf.parseRequirement('A&B|C') 4255 ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4256 ReqCapability('C')]) 4257 >>> pf.parseRequirement('(A&B)|C') 4258 ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4259 ReqCapability('C')]) 4260 >>> pf.parseRequirement('(A&B|C)&D') 4261 ReqAll([ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4262 ReqCapability('C')]), ReqCapability('D')]) 4263 4264 Error examples: 4265 4266 >>> pf.parseRequirement('one ! Word') 4267 Traceback (most recent call last): 4268 ... 4269 exploration.parsing.ParseError... 4270 >>> pf.parseRequirement('a|') 4271 Traceback (most recent call last): 4272 ... 4273 exploration.parsing.ParseError... 4274 >>> pf.parseRequirement('b!') 4275 Traceback (most recent call last): 4276 ... 4277 exploration.parsing.ParseError... 4278 >>> pf.parseRequirement('*emph*') 4279 Traceback (most recent call last): 4280 ... 4281 exploration.parsing.ParseError... 4282 >>> pf.parseRequirement('one&&two') 4283 Traceback (most recent call last): 4284 ... 4285 exploration.parsing.ParseError... 4286 >>> pf.parseRequirement('one!|two') 4287 Traceback (most recent call last): 4288 ... 4289 exploration.parsing.ParseError... 4290 >>> pf.parseRequirement('one*two') 4291 Traceback (most recent call last): 4292 ... 4293 exploration.parsing.ParseError... 4294 >>> pf.parseRequirement('one*') 4295 Traceback (most recent call last): 4296 ... 4297 exploration.parsing.ParseError... 4298 >>> pf.parseRequirement('()') 4299 Traceback (most recent call last): 4300 ... 4301 exploration.parsing.ParseError... 4302 >>> pf.parseRequirement('(one)*3') 4303 Traceback (most recent call last): 4304 ... 4305 exploration.parsing.ParseError... 4306 >>> pf.parseRequirement('a:') 4307 Traceback (most recent call last): 4308 ... 4309 exploration.parsing.ParseError... 4310 >>> pf.parseRequirement('a:b:c') 4311 Traceback (most recent call last): 4312 ... 4313 exploration.parsing.ParseError... 4314 >>> pf.parseRequirement('where::capability') 4315 Traceback (most recent call last): 4316 ... 4317 exploration.parsing.ParseError... 4318 """ 4319 return self.parseRequirementFromTokens( 4320 lex(encoded, self.reverseFormat) 4321 ) 4322 4323 def parseSkillCombinationFromTokens( 4324 self, 4325 tokens: LexedTokens, 4326 start: int = 0, 4327 end: int = -1 4328 ) -> Union[base.Skill, base.SkillCombination]: 4329 """ 4330 Parses a skill combination from the specified range within the 4331 given tokens list. If just a single string token is selected, it 4332 will be returned as a `base.BestSkill` with just that skill 4333 inside. 4334 4335 For example: 4336 4337 >>> pf = ParseFormat() 4338 >>> pf.parseSkillCombinationFromTokens(['climbing']) 4339 BestSkill('climbing') 4340 >>> tokens = [ 4341 ... 'best', 4342 ... Lexeme.openParen, 4343 ... 'brains', 4344 ... Lexeme.sepOrDelay, 4345 ... 'brawn', 4346 ... Lexeme.closeParen, 4347 ... ] 4348 >>> pf.parseSkillCombinationFromTokens(tokens) 4349 BestSkill('brains', 'brawn') 4350 >>> tokens[2] = '3' # not a lexeme so it's a string 4351 >>> pf.parseSkillCombinationFromTokens(tokens) 4352 BestSkill(3, 'brawn') 4353 >>> tokens = [ 4354 ... Lexeme.wigglyLine, 4355 ... Lexeme.wigglyLine, 4356 ... 'yes', 4357 ... ] 4358 >>> pf.parseSkillCombinationFromTokens(tokens) 4359 InverseSkill(InverseSkill('yes')) 4360 """ 4361 start, end, nTokens = normalizeEnds(tokens, start, end) 4362 4363 first = tokens[start] 4364 if nTokens == 1: 4365 if isinstance(first, base.Skill): 4366 try: 4367 level = int(first) 4368 return base.BestSkill(level) 4369 except ValueError: 4370 return base.BestSkill(first) 4371 else: 4372 raise ParseError( 4373 "Invalid SkillCombination:\n{tokens[start:end + 1]" 4374 ) 4375 4376 if first == Lexeme.wigglyLine: 4377 inv = self.parseSkillCombinationFromTokens( 4378 tokens, 4379 start + 1, 4380 end 4381 ) 4382 if isinstance(inv, base.BestSkill) and len(inv.skills) == 1: 4383 return base.InverseSkill(inv.skills[0]) 4384 else: 4385 return base.InverseSkill(inv) 4386 4387 second = tokens[start + 1] 4388 if second != Lexeme.openParen: 4389 raise ParseError( 4390 f"Invalid SkillCombination (missing paren):" 4391 f"\n{tokens[start:end + 1]}" 4392 ) 4393 4394 parenEnd = self.matchingBrace( 4395 tokens, 4396 start + 1, 4397 Lexeme.openParen, 4398 Lexeme.closeParen 4399 ) 4400 if parenEnd != end: 4401 raise ParseError( 4402 f"Extra junk after SkillCombination:" 4403 f"\n{tokens[parenEnd + 1:end + 1]}" 4404 ) 4405 4406 if first == 'if': 4407 parts = list( 4408 findSeparatedParts( 4409 tokens, 4410 Lexeme.sepOrDelay, 4411 start + 2, 4412 end - 1, 4413 Lexeme.openParen, 4414 Lexeme.closeParen 4415 ) 4416 ) 4417 if len(parts) != 3: 4418 raise ParseError( 4419 f"Wrong number of parts for ConditionalSkill (needs" 4420 f" 3, got {len(parts)}:" 4421 f"\n{tokens[start + 2:end]}" 4422 ) 4423 reqStart, reqEnd = parts[0] 4424 ifStart, ifEnd = parts[1] 4425 elseStart, elseEnd = parts[2] 4426 return base.ConditionalSkill( 4427 self.parseRequirementFromTokens(tokens, reqStart, reqEnd), 4428 self.parseSkillCombinationFromTokens(tokens, ifStart, ifEnd), 4429 self.parseSkillCombinationFromTokens( 4430 tokens, 4431 elseStart, 4432 elseEnd 4433 ), 4434 ) 4435 elif first in ('sum', 'best', 'worst'): 4436 make: type[base.SkillCombination] 4437 if first == 'sum': 4438 make = base.CombinedSkill 4439 elif first == 'best': 4440 make = base.BestSkill 4441 else: 4442 make = base.WorstSkill 4443 4444 subs = [] 4445 for partStart, partEnd in findSeparatedParts( 4446 tokens, 4447 Lexeme.sepOrDelay, 4448 start + 2, 4449 end - 1, 4450 Lexeme.openParen, 4451 Lexeme.closeParen 4452 ): 4453 sub = self.parseSkillCombinationFromTokens( 4454 tokens, 4455 partStart, 4456 partEnd 4457 ) 4458 if ( 4459 isinstance(sub, base.BestSkill) 4460 and len(sub.skills) == 1 4461 ): 4462 subs.append(sub.skills[0]) 4463 else: 4464 subs.append(sub) 4465 4466 return make(*subs) 4467 else: 4468 raise ParseError( 4469 "Invalid SkillCombination:\n{tokens[start:end + 1]" 4470 ) 4471 4472 def parseSkillCombination( 4473 self, 4474 encoded: str 4475 ) -> base.SkillCombination: 4476 """ 4477 Parses a `SkillCombination` from a string. Calls `lex` and then 4478 `parseSkillCombinationFromTokens`. 4479 """ 4480 result = self.parseSkillCombinationFromTokens( 4481 lex(encoded, self.reverseFormat) 4482 ) 4483 if not isinstance(result, base.SkillCombination): 4484 return base.BestSkill(result) 4485 else: 4486 return result 4487 4488 def parseConditionFromTokens( 4489 self, 4490 tokens: LexedTokens, 4491 start: int = 0, 4492 end: int = -1 4493 ) -> base.Condition: 4494 """ 4495 Parses a `base.Condition` from a lexed tokens list. For example: 4496 4497 >>> pf = ParseFormat() 4498 >>> tokens = [ 4499 ... Lexeme.doubleQuestionmark, 4500 ... Lexeme.openParen, 4501 ... "fire", 4502 ... Lexeme.ampersand, 4503 ... "water", 4504 ... Lexeme.closeParen, 4505 ... Lexeme.openCurly, 4506 ... "gain", 4507 ... "wind", 4508 ... Lexeme.closeCurly, 4509 ... Lexeme.openCurly, 4510 ... Lexeme.closeCurly, 4511 ... ] 4512 >>> pf.parseConditionFromTokens(tokens) == base.condition( 4513 ... condition=base.ReqAll([ 4514 ... base.ReqCapability('fire'), 4515 ... base.ReqCapability('water') 4516 ... ]), 4517 ... consequence=[base.effect(gain='wind')] 4518 ... ) 4519 True 4520 """ 4521 start, end, nTokens = normalizeEnds(tokens, start, end) 4522 if nTokens < 8: 4523 raise ParseError( 4524 f"A Condition requires at least 8 tokens (got {nTokens})." 4525 ) 4526 if tokens[start] != Lexeme.doubleQuestionmark: 4527 raise ParseError( 4528 f"A Condition must start with" 4529 f" {repr(self.formatDict[Lexeme.doubleQuestionmark])}" 4530 ) 4531 try: 4532 consequenceStart = tokens.index(Lexeme.openCurly, start) 4533 except ValueError: 4534 raise ParseError("A condition must include a consequence block.") 4535 consequenceEnd = self.matchingBrace(tokens, consequenceStart) 4536 altStart = consequenceEnd + 1 4537 altEnd = self.matchingBrace(tokens, altStart) 4538 4539 if altEnd != end: 4540 raise ParseError( 4541 f"Junk after condition:\n{tokens[altEnd + 1: end + 1]}" 4542 ) 4543 4544 return base.condition( 4545 condition=self.parseRequirementFromTokens( 4546 tokens, 4547 start + 1, 4548 consequenceStart - 1 4549 ), 4550 consequence=self.parseConsequenceFromTokens( 4551 tokens, 4552 consequenceStart, 4553 consequenceEnd 4554 ), 4555 alternative=self.parseConsequenceFromTokens( 4556 tokens, 4557 altStart, 4558 altEnd 4559 ) 4560 ) 4561 4562 def parseCondition( 4563 self, 4564 encoded: str 4565 ) -> base.Condition: 4566 """ 4567 Lexes the given string and then calls `parseConditionFromTokens` 4568 to return a `base.Condition`. 4569 """ 4570 return self.parseConditionFromTokens( 4571 lex(encoded, self.reverseFormat) 4572 ) 4573 4574 def parseChallengeFromTokens( 4575 self, 4576 tokens: LexedTokens, 4577 start: int = 0, 4578 end: int = -1 4579 ) -> base.Challenge: 4580 """ 4581 Parses a `base.Challenge` from a lexed tokens list. 4582 4583 For example: 4584 4585 >>> pf = ParseFormat() 4586 >>> tokens = [ 4587 ... Lexeme.angleLeft, 4588 ... '2', 4589 ... Lexeme.angleRight, 4590 ... 'best', 4591 ... Lexeme.openParen, 4592 ... "chess", 4593 ... Lexeme.sepOrDelay, 4594 ... "checkers", 4595 ... Lexeme.closeParen, 4596 ... Lexeme.openCurly, 4597 ... "gain", 4598 ... "coin", 4599 ... Lexeme.tokenCount, 4600 ... "5", 4601 ... Lexeme.closeCurly, 4602 ... Lexeme.angleRight, 4603 ... Lexeme.openCurly, 4604 ... "lose", 4605 ... "coin", 4606 ... Lexeme.tokenCount, 4607 ... "5", 4608 ... Lexeme.closeCurly, 4609 ... ] 4610 >>> c = pf.parseChallengeFromTokens(tokens) 4611 >>> c['skills'] == base.BestSkill('chess', 'checkers') 4612 True 4613 >>> c['level'] 4614 2 4615 >>> c['success'] == [base.effect(gain=('coin', 5))] 4616 True 4617 >>> c['failure'] == [base.effect(lose=('coin', 5))] 4618 True 4619 >>> c['outcome'] 4620 False 4621 >>> c == base.challenge( 4622 ... skills=base.BestSkill('chess', 'checkers'), 4623 ... level=2, 4624 ... success=[base.effect(gain=('coin', 5))], 4625 ... failure=[base.effect(lose=('coin', 5))], 4626 ... outcome=False 4627 ... ) 4628 True 4629 >>> t2 = ['hi'] + tokens + ['bye'] # parsing only part of the list 4630 >>> c == pf.parseChallengeFromTokens(t2, 1, -2) 4631 True 4632 """ 4633 start, end, nTokens = normalizeEnds(tokens, start, end) 4634 if nTokens < 8: 4635 raise ParseError( 4636 f"Not enough tokens for a challenge: {nTokens}" 4637 ) 4638 if tokens[start] != Lexeme.angleLeft: 4639 raise ParseError( 4640 f"Challenge must start with" 4641 f" {repr(self.formatDict[Lexeme.angleLeft])}" 4642 ) 4643 levelStr = tokens[start + 1] 4644 if isinstance(levelStr, Lexeme): 4645 raise ParseError( 4646 f"Challenge must start with a level in angle brackets" 4647 f" (got {repr(self.formatDict[levelStr])})." 4648 ) 4649 if tokens[start + 2] != Lexeme.angleRight: 4650 raise ParseError( 4651 f"Challenge must include" 4652 f" {repr(self.formatDict[Lexeme.angleRight])} after" 4653 f" the level." 4654 ) 4655 try: 4656 level = int(levelStr) 4657 except ValueError: 4658 raise ParseError( 4659 f"Challenge level must be an integer (got" 4660 f" {repr(tokens[start + 1])}." 4661 ) 4662 try: 4663 successStart = tokens.index(Lexeme.openCurly, start) 4664 skillsEnd = successStart - 1 4665 except ValueError: 4666 raise ParseError("A challenge must include a consequence block.") 4667 4668 outcome: Optional[bool] = None 4669 if tokens[skillsEnd] == Lexeme.angleRight: 4670 skillsEnd -= 1 4671 outcome = True 4672 successEnd = self.matchingBrace(tokens, successStart) 4673 failStart = successEnd + 1 4674 if tokens[failStart] == Lexeme.angleRight: 4675 failStart += 1 4676 if outcome is not None: 4677 raise ParseError( 4678 "Cannot indicate both success and failure as" 4679 " outcomes in a challenge." 4680 ) 4681 outcome = False 4682 failEnd = self.matchingBrace(tokens, failStart) 4683 4684 if failEnd != end: 4685 raise ParseError( 4686 f"Junk after condition:\n{tokens[failEnd + 1:end + 1]}" 4687 ) 4688 4689 skills = self.parseSkillCombinationFromTokens( 4690 tokens, 4691 start + 3, 4692 skillsEnd 4693 ) 4694 if isinstance(skills, base.Skill): 4695 skills = base.BestSkill(skills) 4696 4697 return base.challenge( 4698 level=level, 4699 outcome=outcome, 4700 skills=skills, 4701 success=self.parseConsequenceFromTokens( 4702 tokens[successStart:successEnd + 1] 4703 ), 4704 failure=self.parseConsequenceFromTokens( 4705 tokens[failStart:failEnd + 1] 4706 ) 4707 ) 4708 4709 def parseChallenge( 4710 self, 4711 encoded: str 4712 ) -> base.Challenge: 4713 """ 4714 Lexes the given string and then calls `parseChallengeFromTokens` 4715 to return a `base.Challenge`. 4716 """ 4717 return self.parseChallengeFromTokens( 4718 lex(encoded, self.reverseFormat) 4719 ) 4720 4721 def parseConsequenceFromTokens( 4722 self, 4723 tokens: LexedTokens, 4724 start: int = 0, 4725 end: int = -1 4726 ) -> base.Consequence: 4727 """ 4728 Parses a consequence from a lexed token list. If start and/or end 4729 are specified, only processes the part of the list between those 4730 two indices (inclusive). Use `lex` to turn a string into a 4731 `LexedTokens` list (or use `ParseFormat.parseConsequence` which 4732 does that for you). 4733 4734 An example: 4735 4736 >>> pf = ParseFormat() 4737 >>> tokens = [ 4738 ... Lexeme.openCurly, 4739 ... 'gain', 4740 ... 'power', 4741 ... Lexeme.closeCurly 4742 ... ] 4743 >>> c = pf.parseConsequenceFromTokens(tokens) 4744 >>> c == [base.effect(gain='power')] 4745 True 4746 >>> tokens.append('hi') 4747 >>> c == pf.parseConsequenceFromTokens(tokens, end=-2) 4748 True 4749 >>> c == pf.parseConsequenceFromTokens(tokens, end=3) 4750 True 4751 """ 4752 start, end, nTokens = normalizeEnds(tokens, start, end) 4753 4754 if nTokens < 2: 4755 raise ParseError("Consequence must have at least two tokens.") 4756 4757 if tokens[start] != Lexeme.openCurly: 4758 raise ParseError( 4759 f"Consequence must start with an open curly brace:" 4760 f" {repr(self.formatDict[Lexeme.openCurly])}." 4761 ) 4762 4763 if tokens[end] != Lexeme.closeCurly: 4764 raise ParseError( 4765 f"Consequence must end with a closing curly brace:" 4766 f" {repr(self.formatDict[Lexeme.closeCurly])}." 4767 ) 4768 4769 if nTokens == 2: 4770 return [] 4771 4772 result: base.Consequence = [] 4773 for partStart, partEnd in findSeparatedParts( 4774 tokens, 4775 Lexeme.consequenceSeparator, 4776 start + 1, 4777 end - 1, 4778 Lexeme.openCurly, 4779 Lexeme.closeCurly 4780 ): 4781 if partEnd - partStart < 0: 4782 raise ParseError("Empty consequence part.") 4783 if tokens[partStart] == Lexeme.angleLeft: # a challenge 4784 result.append( 4785 self.parseChallengeFromTokens( 4786 tokens, 4787 partStart, 4788 partEnd 4789 ) 4790 ) 4791 elif tokens[partStart] == Lexeme.doubleQuestionmark: # condition 4792 result.append( 4793 self.parseConditionFromTokens( 4794 tokens, 4795 partStart, 4796 partEnd 4797 ) 4798 ) 4799 else: # Must be an effect 4800 result.append( 4801 self.parseEffectFromTokens( 4802 tokens, 4803 partStart, 4804 partEnd 4805 ) 4806 ) 4807 4808 return result 4809 4810 def parseConsequence(self, encoded: str) -> base.Consequence: 4811 """ 4812 Parses a consequence from a string. Uses `lex` and 4813 `ParseFormat.parseConsequenceFromTokens`. For example: 4814 4815 >>> pf = ParseFormat() 4816 >>> c = pf.parseConsequence( 4817 ... '{gain power}' 4818 ... ) 4819 >>> c == [base.effect(gain='power')] 4820 True 4821 >>> pf.unparseConsequence(c) 4822 '{gain power}' 4823 >>> c = pf.parseConsequence( 4824 ... '{\\n' 4825 ... ' ??(brawny|!weights*3){\\n' 4826 ... ' <3>sum(brains, brawn){goto home}>{bounce}\\n' 4827 ... ' }{};\\n' 4828 ... ' lose coin*1\\n' 4829 ... '}' 4830 ... ) 4831 >>> len(c) 4832 2 4833 >>> c[0]['condition'] == base.ReqAny([ 4834 ... base.ReqCapability('brawny'), 4835 ... base.ReqNot(base.ReqTokens('weights', 3)) 4836 ... ]) 4837 True 4838 >>> len(c[0]['consequence']) 4839 1 4840 >>> len(c[0]['alternative']) 4841 0 4842 >>> cons = c[0]['consequence'][0] 4843 >>> cons['skills'] == base.CombinedSkill('brains', 'brawn') 4844 True 4845 >>> cons['level'] 4846 3 4847 >>> len(cons['success']) 4848 1 4849 >>> len(cons['failure']) 4850 1 4851 >>> cons['success'][0] == base.effect(goto='home') 4852 True 4853 >>> cons['failure'][0] == base.effect(bounce=True) 4854 True 4855 >>> cons['outcome'] = False 4856 >>> c[0] == base.condition( 4857 ... condition=base.ReqAny([ 4858 ... base.ReqCapability('brawny'), 4859 ... base.ReqNot(base.ReqTokens('weights', 3)) 4860 ... ]), 4861 ... consequence=[ 4862 ... base.challenge( 4863 ... skills=base.CombinedSkill('brains', 'brawn'), 4864 ... level=3, 4865 ... success=[base.effect(goto='home')], 4866 ... failure=[base.effect(bounce=True)], 4867 ... outcome=False 4868 ... ) 4869 ... ] 4870 ... ) 4871 True 4872 >>> c[1] == base.effect(lose=('coin', 1)) 4873 True 4874 """ 4875 return self.parseConsequenceFromTokens( 4876 lex(encoded, self.reverseFormat) 4877 ) 4878 4879 4880#---------------------# 4881# Graphviz dot format # 4882#---------------------# 4883 4884class ParsedDotGraph(TypedDict): 4885 """ 4886 Represents a parsed `graphviz` dot-format graph consisting of nodes, 4887 edges, and subgraphs, with attributes attached to nodes and/or 4888 edges. An intermediate format during conversion to a full 4889 `DecisionGraph`. Includes the following slots: 4890 4891 - `'nodes'`: A list of tuples each holding a node ID followed by a 4892 list of name/value attribute pairs. 4893 - `'edges'`: A list of tuples each holding a from-ID, a to-ID, 4894 and then a list of name/value attribute pairs. 4895 - `'attrs'`: A list of tuples each holding a name/value attribute 4896 pair for graph-level attributes. 4897 - `'subgraphs'`: A list of subgraphs (each a tuple with a subgraph 4898 name and then another dictionary in the same format as this 4899 one). 4900 """ 4901 nodes: List[Tuple[int, List[Tuple[str, str]]]] 4902 edges: List[Tuple[int, int, List[Tuple[str, str]]]] 4903 attrs: List[Tuple[str, str]] 4904 subgraphs: List[Tuple[str, 'ParsedDotGraph']] 4905 4906 4907def parseSimpleDotAttrs(fragment: str) -> List[Tuple[str, str]]: 4908 """ 4909 Given a string fragment that starts with '[' and ends with ']', 4910 parses a simple attribute list in `graphviz` dot format from that 4911 fragment, returning a list of name/value attribute tuples. Raises a 4912 `DotParseError` if the fragment doesn't have the right format. 4913 4914 Examples: 4915 4916 >>> parseSimpleDotAttrs('[ name=value ]') 4917 [('name', 'value')] 4918 >>> parseSimpleDotAttrs('[ a=b c=d e=f ]') 4919 [('a', 'b'), ('c', 'd'), ('e', 'f')] 4920 >>> parseSimpleDotAttrs('[ a=b "c d"="e f" ]') 4921 [('a', 'b'), ('c d', 'e f')] 4922 >>> parseSimpleDotAttrs('[a=b "c d"="e f"]') 4923 [('a', 'b'), ('c d', 'e f')] 4924 >>> parseSimpleDotAttrs('[ a=b "c d"="e f"') 4925 Traceback (most recent call last): 4926 ... 4927 exploration.parsing.DotParseError... 4928 >>> parseSimpleDotAttrs('a=b "c d"="e f" ]') 4929 Traceback (most recent call last): 4930 ... 4931 exploration.parsing.DotParseError... 4932 >>> parseSimpleDotAttrs('[ a b=c ]') 4933 Traceback (most recent call last): 4934 ... 4935 exploration.parsing.DotParseError... 4936 >>> parseSimpleDotAttrs('[ a=b c ]') 4937 Traceback (most recent call last): 4938 ... 4939 exploration.parsing.DotParseError... 4940 >>> parseSimpleDotAttrs('[ name="value" ]') 4941 [('name', 'value')] 4942 >>> parseSimpleDotAttrs('[ name="\\\\"value\\\\"" ]') 4943 [('name', '"value"')] 4944 """ 4945 if not fragment.startswith('[') or not fragment.endswith(']'): 4946 raise DotParseError( 4947 f"Simple attrs fragment missing delimiters:" 4948 f"\n {repr(fragment)}" 4949 ) 4950 result = [] 4951 rest = fragment[1:-1].strip() 4952 while rest: 4953 # Get possibly-quoted attribute name: 4954 if rest.startswith('"'): 4955 try: 4956 aName, rest = utils.unquoted(rest) 4957 except ValueError: 4958 raise DotParseError( 4959 f"Malformed quoted attribute name in" 4960 f" fragment:\n {repr(fragment)}" 4961 ) 4962 rest = rest.lstrip() 4963 if not rest.startswith('='): 4964 raise DotParseError( 4965 f"Missing '=' in attribute block in" 4966 f" fragment:\n {repr(fragment)}" 4967 ) 4968 rest = rest[1:].lstrip() 4969 else: 4970 try: 4971 eqInd = rest.index('=') 4972 except ValueError: 4973 raise DotParseError( 4974 f"Missing '=' in attribute block in" 4975 f" fragment:\n {repr(fragment)}" 4976 ) 4977 aName = rest[:eqInd] 4978 if ' ' in aName: 4979 raise DotParseError( 4980 f"Malformed unquoted attribute name" 4981 f" {repr(aName)} in fragment:" 4982 f"\n {repr(fragment)}" 4983 ) 4984 rest = rest[eqInd + 1:].lstrip() 4985 4986 # Get possibly-quoted attribute value: 4987 if rest.startswith('"'): 4988 try: 4989 aVal, rest = utils.unquoted(rest) 4990 except ValueError: 4991 raise DotParseError( 4992 f"Malformed quoted attribute value in" 4993 f" fragment:\n {repr(fragment)}" 4994 ) 4995 rest = rest.lstrip() 4996 else: 4997 try: 4998 spInd = rest.index(' ') 4999 except ValueError: 5000 spInd = len(rest) 5001 aVal = rest[:spInd] 5002 rest = rest[spInd:].lstrip() 5003 5004 # Append this attribute pair and continue parsing 5005 result.append((aName, aVal)) 5006 5007 return result 5008 5009 5010def parseDotNode( 5011 nodeLine: str 5012) -> Tuple[int, Union[bool, List[Tuple[str, str]]]]: 5013 """ 5014 Given a line of text from a `graphviz` dot-format graph 5015 (possibly ending in an '[' to indicate attributes to follow, or 5016 possible including a '[ ... ]' block with attributes in-line), 5017 parses it as a node declaration, returning the ID of the node, 5018 along with a boolean indicating whether attributes follow or 5019 not. If an inline attribute block is present, the second member 5020 of the tuple will be a list of attribute name/value pairs. In 5021 that case, all attribute names and values must either be quoted 5022 or not include spaces. 5023 Examples: 5024 5025 >>> parseDotNode('1') 5026 (1, False) 5027 >>> parseDotNode(' 1 [ ') 5028 (1, True) 5029 >>> parseDotNode(' 1 [ a=b "c d"="e f" ] ') 5030 (1, [('a', 'b'), ('c d', 'e f')]) 5031 >>> parseDotNode(' 3 [ name="A = \\\\"grate:open\\\\"" ]') 5032 (3, [('name', 'A = "grate:open"')]) 5033 >>> parseDotNode(' "1"[') 5034 (1, True) 5035 >>> parseDotNode(' 100[') 5036 (100, True) 5037 >>> parseDotNode(' 1 2') 5038 Traceback (most recent call last): 5039 ... 5040 exploration.parsing.DotParseError... 5041 >>> parseDotNode(' 1 [ 2') 5042 Traceback (most recent call last): 5043 ... 5044 exploration.parsing.DotParseError... 5045 >>> parseDotNode(' 1 2') 5046 Traceback (most recent call last): 5047 ... 5048 exploration.parsing.DotParseError... 5049 >>> parseDotNode(' 1 [ junk not=attrs ]') 5050 Traceback (most recent call last): 5051 ... 5052 exploration.parsing.DotParseError... 5053 >>> parseDotNode(' \\n') 5054 Traceback (most recent call last): 5055 ... 5056 exploration.parsing.DotParseError... 5057 """ 5058 stripped = nodeLine.strip() 5059 if len(stripped) == 0: 5060 raise DotParseError( 5061 "Empty node in dot graph on line:\n {repr(nodeLine)}" 5062 ) 5063 hasAttrs: Union[bool, List[Tuple[str, str]]] = False 5064 if stripped.startswith('"'): 5065 nodeName, rest = utils.unquoted(stripped) 5066 rest = rest.strip() 5067 if rest == '[': 5068 hasAttrs = True 5069 elif rest.startswith('[') and rest.endswith(']'): 5070 hasAttrs = parseSimpleDotAttrs(rest) 5071 elif rest: 5072 raise DotParseError( 5073 f"Extra junk {repr(rest)} after node on line:" 5074 f"\n {repr(nodeLine)}" 5075 ) 5076 5077 else: 5078 if stripped.endswith('['): 5079 hasAttrs = True 5080 stripped = stripped[:-1].rstrip() 5081 elif stripped.endswith(']'): 5082 try: 5083 # TODO: Why did this used to be rindex? Was that 5084 # important in some case? (That doesn't work since the 5085 # value may contain a quoted open bracket). 5086 attrStart = stripped.index('[') 5087 except ValueError: 5088 raise DotParseError( 5089 f"Unmatched ']' on line:\n {repr(nodeLine)}" 5090 ) 5091 hasAttrs = parseSimpleDotAttrs( 5092 stripped[attrStart:] 5093 ) 5094 stripped = stripped[:attrStart].rstrip() 5095 5096 if ' ' in stripped: 5097 raise DotParseError( 5098 f"Unquoted multi-word node on line:\n {repr(nodeLine)}" 5099 ) 5100 else: 5101 nodeName = stripped 5102 5103 try: 5104 nodeID = int(nodeName) 5105 except ValueError: 5106 raise DotParseError( 5107 f"Node name f{repr(nodeName)} is not an integer on" 5108 f" line:\n {repr(nodeLine)}" 5109 ) 5110 5111 return (nodeID, hasAttrs) 5112 5113 5114def parseDotAttr(attrLine: str) -> Tuple[str, str]: 5115 """ 5116 Given a line of text from a `graphviz` dot-format graph, parses 5117 it as an attribute (maybe-quoted-attr-name = 5118 maybe-quoted-attr-value). Returns the (maybe-unquoted) attr-name 5119 and the (maybe-unquoted) attr-value as a pair of strings. Raises 5120 a `DotParseError` if the line cannot be parsed as an attribute. 5121 Examples: 5122 5123 >>> parseDotAttr("a=b") 5124 ('a', 'b') 5125 >>> parseDotAttr(" a = b ") 5126 ('a', 'b') 5127 >>> parseDotAttr('"a" = "b"') 5128 ('a', 'b') 5129 >>> parseDotAttr('"a" -> "b"') 5130 Traceback (most recent call last): 5131 ... 5132 exploration.parsing.DotParseError... 5133 >>> parseDotAttr('"a" = "b" c') 5134 Traceback (most recent call last): 5135 ... 5136 exploration.parsing.DotParseError... 5137 >>> parseDotAttr('a') 5138 Traceback (most recent call last): 5139 ... 5140 exploration.parsing.DotParseError... 5141 >>> parseDotAttr('') 5142 Traceback (most recent call last): 5143 ... 5144 exploration.parsing.DotParseError... 5145 >>> parseDotAttr('0 [ name="A" ]') 5146 Traceback (most recent call last): 5147 ... 5148 exploration.parsing.DotParseError... 5149 """ 5150 stripped = attrLine.lstrip() 5151 if len(stripped) == 0: 5152 raise DotParseError( 5153 "Empty attribute in dot graph on line:\n {repr(attrLine)}" 5154 ) 5155 if stripped.endswith(']') or stripped.endswith('['): 5156 raise DotParseError( 5157 f"Node attribute ends in '[' or ']' on line:" 5158 f"\n {repr(attrLine)}" 5159 ) 5160 if stripped.startswith('"'): 5161 try: 5162 attrName, rest = utils.unquoted(stripped) 5163 except ValueError: 5164 raise DotParseError( 5165 f"Unmatched quotes in line:\n {repr(attrLine)}" 5166 ) 5167 rest = rest.lstrip() 5168 if len(rest) == 0 or rest[0] != '=': 5169 raise DotParseError( 5170 f"No equals sign following attribute name on" 5171 f" line:\n {repr(attrLine)}" 5172 ) 5173 rest = rest[1:].lstrip() 5174 else: 5175 try: 5176 eqInd = stripped.index('=') 5177 except ValueError: 5178 raise DotParseError( 5179 f"No equals sign in attribute line:" 5180 f"\n {repr(attrLine)}" 5181 ) 5182 attrName = stripped[:eqInd].rstrip() 5183 rest = stripped[eqInd + 1:].lstrip() 5184 5185 if rest[0] == '"': 5186 try: 5187 attrVal, rest = utils.unquoted(rest) 5188 except ValueError: 5189 raise DotParseError( 5190 f"Unmatched quotes in line:\n {repr(attrLine)}" 5191 ) 5192 if rest.strip(): 5193 raise DotParseError( 5194 f"Junk after attribute on line:" 5195 f"\n {repr(attrLine)}" 5196 ) 5197 else: 5198 attrVal = rest.rstrip() 5199 5200 return attrName, attrVal 5201 5202 5203def parseDotEdge(edgeLine: str) -> Tuple[int, int, bool]: 5204 """ 5205 Given a line of text from a `graphviz` dot-format graph, parses 5206 it as an edge (fromID -> toID). Returns a tuple containing the 5207 from ID, the to ID, and a boolean indicating whether attributes 5208 follow the edge on subsequent lines (true if the line ends with 5209 '['). Raises a `DotParseError` if the line cannot be parsed as 5210 an edge pair. Examples: 5211 5212 >>> parseDotEdge("1 -> 2") 5213 (1, 2, False) 5214 >>> parseDotEdge(" 1 -> 2 ") 5215 (1, 2, False) 5216 >>> parseDotEdge('"1" -> "2"') 5217 (1, 2, False) 5218 >>> parseDotEdge('"1" -> "2" [') 5219 (1, 2, True) 5220 >>> parseDotEdge("a -> b") 5221 Traceback (most recent call last): 5222 ... 5223 exploration.parsing.DotParseError... 5224 >>> parseDotEdge('"1" = "1"') 5225 Traceback (most recent call last): 5226 ... 5227 exploration.parsing.DotParseError... 5228 >>> parseDotEdge('"1" -> "2" c') 5229 Traceback (most recent call last): 5230 ... 5231 exploration.parsing.DotParseError... 5232 >>> parseDotEdge('1') 5233 Traceback (most recent call last): 5234 ... 5235 exploration.parsing.DotParseError... 5236 >>> parseDotEdge('') 5237 Traceback (most recent call last): 5238 ... 5239 exploration.parsing.DotParseError... 5240 """ 5241 stripped = edgeLine.lstrip() 5242 if len(stripped) == 0: 5243 raise DotParseError( 5244 "Empty edge in dot graph on line:\n {repr(edgeLine)}" 5245 ) 5246 if stripped.startswith('"'): 5247 try: 5248 fromStr, rest = utils.unquoted(stripped) 5249 except ValueError: 5250 raise DotParseError( 5251 f"Unmatched quotes in line:\n {repr(edgeLine)}" 5252 ) 5253 rest = rest.lstrip() 5254 if rest[:2] != '->': 5255 raise DotParseError( 5256 f"No arrow sign following source name on" 5257 f" line:\n {repr(edgeLine)}" 5258 ) 5259 rest = rest[2:].lstrip() 5260 else: 5261 try: 5262 arrowInd = stripped.index('->') 5263 except ValueError: 5264 raise DotParseError( 5265 f"No arrow in edge line:" 5266 f"\n {repr(edgeLine)}" 5267 ) 5268 fromStr = stripped[:arrowInd].rstrip() 5269 rest = stripped[arrowInd + 2:].lstrip() 5270 if ' ' in fromStr: 5271 raise DotParseError( 5272 f"Unquoted multi-word edge source on line:" 5273 f"\n {repr(edgeLine)}" 5274 ) 5275 5276 hasAttrs = False 5277 if rest[0] == '"': 5278 try: 5279 toStr, rest = utils.unquoted(rest) 5280 except ValueError: 5281 raise DotParseError( 5282 f"Unmatched quotes in line:\n {repr(edgeLine)}" 5283 ) 5284 stripped = rest.strip() 5285 if stripped == '[': 5286 hasAttrs = True 5287 elif stripped: 5288 raise DotParseError( 5289 f"Junk after edge on line:" 5290 f"\n {repr(edgeLine)}" 5291 ) 5292 else: 5293 toStr = rest.rstrip() 5294 if toStr.endswith('['): 5295 toStr = toStr[:-1].rstrip() 5296 hasAttrs = True 5297 if ' ' in toStr: 5298 raise DotParseError( 5299 f"Unquoted multi-word edge destination on line:" 5300 f"\n {repr(edgeLine)}" 5301 ) 5302 5303 try: 5304 fromID = int(fromStr) 5305 except ValueError: 5306 raise DotParseError( 5307 f"Invalid 'from' ID: {repr(fromStr)} on line:" 5308 f"\n {repr(edgeLine)}" 5309 ) 5310 5311 try: 5312 toID = int(toStr) 5313 except ValueError: 5314 raise DotParseError( 5315 f"Invalid 'to' ID: {repr(toStr)} on line:" 5316 f"\n {repr(edgeLine)}" 5317 ) 5318 5319 return (fromID, toID, hasAttrs) 5320 5321 5322def parseDotAttrList( 5323 lines: List[str] 5324) -> Tuple[List[Tuple[str, str]], List[str]]: 5325 """ 5326 Given a list of lines of text from a `graphviz` dot-format 5327 graph which starts with an attribute line, parses multiple 5328 attribute lines until a line containing just ']' is found. 5329 Returns a list of the parsed name/value attribute pair tuples, 5330 along with a list of remaining unparsed strings (not counting 5331 the closing ']' line). Raises a `DotParseError` if it finds a 5332 non-attribute line or if it fails to find a closing ']' line. 5333 Examples: 5334 5335 >>> parseDotAttrList([ 5336 ... 'a=b\\n', 5337 ... 'c=d\\n', 5338 ... ']\\n', 5339 ... ]) 5340 ([('a', 'b'), ('c', 'd')], []) 5341 >>> parseDotAttrList([ 5342 ... 'a=b', 5343 ... 'c=d', 5344 ... ' ]', 5345 ... 'more', 5346 ... 'lines', 5347 ... ]) 5348 ([('a', 'b'), ('c', 'd')], ['more', 'lines']) 5349 >>> parseDotAttrList([ 5350 ... 'a=b', 5351 ... 'c=d', 5352 ... ]) 5353 Traceback (most recent call last): 5354 ... 5355 exploration.parsing.DotParseError... 5356 """ 5357 index = 0 5358 found = [] 5359 while index < len(lines): 5360 thisLine = lines[index] 5361 try: 5362 found.append(parseDotAttr(thisLine)) 5363 except DotParseError: 5364 if thisLine.strip() == ']': 5365 return (found, lines[index + 1:]) 5366 else: 5367 raise DotParseError( 5368 f"Could not parse attribute from line:" 5369 f"\n {repr(thisLine)}" 5370 f"\nAttributes block starts on line:" 5371 f"\n {repr(lines[0])}" 5372 ) 5373 index += 1 5374 5375 raise DotParseError( 5376 f"No list terminator (']') for attributes starting on line:" 5377 f"\n {repr(lines[0])}" 5378 ) 5379 5380 5381def parseDotSubgraphStart(line: str) -> str: 5382 """ 5383 Parses the start of a subgraph from a line of a graph file. The 5384 line must start with the word 'subgraph' and then have a name, 5385 followed by a '{' at the end of the line. Raises a 5386 `DotParseError` if this format doesn't match. Examples: 5387 5388 >>> parseDotSubgraphStart('subgraph A {') 5389 'A' 5390 >>> parseDotSubgraphStart('subgraph A B {') 5391 Traceback (most recent call last): 5392 ... 5393 exploration.parsing.DotParseError... 5394 >>> parseDotSubgraphStart('subgraph "A B" {') 5395 'A B' 5396 >>> parseDotSubgraphStart('subgraph A') 5397 Traceback (most recent call last): 5398 ... 5399 exploration.parsing.DotParseError... 5400 """ 5401 stripped = line.strip() 5402 if len(stripped) == 0: 5403 raise DotParseError( 5404 f"Empty line where subgraph was expected:" 5405 f"\n {repr(line)}" 5406 ) 5407 5408 if not stripped.startswith('subgraph '): 5409 raise DotParseError( 5410 f"Subgraph doesn't start with 'subgraph' on line:" 5411 f"\n {repr(line)}" 5412 ) 5413 5414 stripped = stripped[9:] 5415 if stripped.startswith('"'): 5416 try: 5417 name, rest = utils.unquoted(stripped) 5418 except ValueError: 5419 raise DotParseError( 5420 f"Malformed quotes on subgraph line:\n {repr(line)}" 5421 ) 5422 if rest.strip() != '{': 5423 raise DotParseError( 5424 f"Junk or missing '{{' on subgraph line:\n {repr(line)}" 5425 ) 5426 else: 5427 parts = stripped.split() 5428 if len(parts) != 2 or parts[1] != '{': 5429 raise DotParseError( 5430 f"Junk or missing '{{' on subgraph line:\n {repr(line)}" 5431 ) 5432 name, _ = parts 5433 5434 return name 5435 5436 5437def parseDotGraphContents( 5438 lines: List[str] 5439) -> Tuple[ParsedDotGraph, List[str]]: 5440 """ 5441 Given a list of lines from a `graphviz` dot-format string, 5442 parses the list as the contents of a graph (or subgraph), 5443 stopping when it reaches a line that just contains '}'. Raises a 5444 `DotParseError` if it cannot do so or if the terminator is 5445 missing. Returns a tuple containing the parsed graph data (see 5446 `ParsedDotGraph` and the list of remaining lines after the 5447 terminator. Recursively parses subgraphs. Example: 5448 5449 >>> bits = parseDotGraphContents([ 5450 ... '"graph attr"=1', 5451 ... '1 [', 5452 ... ' attr=value', 5453 ... ']', 5454 ... '1 -> 2 [', 5455 ... ' fullLabel="to_B"', 5456 ... ' quality=number', 5457 ... ']', 5458 ... 'subgraph name {', 5459 ... ' 300', 5460 ... ' 400', 5461 ... ' 300 -> 400 [', 5462 ... ' fullLabel=forward', 5463 ... ' ]', 5464 ... '}', 5465 ... '}', 5466 ... ]) 5467 >>> len(bits) 5468 2 5469 >>> g = bits[0] 5470 >>> bits[1] 5471 [] 5472 >>> sorted(g.keys()) 5473 ['attrs', 'edges', 'nodes', 'subgraphs'] 5474 >>> g['nodes'] 5475 [(1, [('attr', 'value')])] 5476 >>> g['edges'] 5477 [(1, 2, [('fullLabel', 'to_B'), ('quality', 'number')])] 5478 >>> g['attrs'] 5479 [('graph attr', '1')] 5480 >>> sgs = g['subgraphs'] 5481 >>> len(sgs) 5482 1 5483 >>> len(sgs[0]) 5484 2 5485 >>> sgs[0][0] 5486 'name' 5487 >>> sg = sgs[0][1] 5488 >>> sorted(sg.keys()) 5489 ['attrs', 'edges', 'nodes', 'subgraphs'] 5490 >>> sg["nodes"] 5491 [(300, []), (400, [])] 5492 >>> sg["edges"] 5493 [(300, 400, [('fullLabel', 'forward')])] 5494 >>> sg["attrs"] 5495 [] 5496 >>> sg["subgraphs"] 5497 [] 5498 """ 5499 result: ParsedDotGraph = { 5500 'nodes': [], 5501 'edges': [], 5502 'attrs': [], 5503 'subgraphs': [], 5504 } 5505 index = 0 5506 remainder = None 5507 # Consider each line: 5508 while index < len(lines): 5509 # Grab line and pre-increment index 5510 thisLine = lines[index] 5511 index += 1 5512 5513 # Check for } first because it could be parsed as a node 5514 stripped = thisLine.strip() 5515 if stripped == '}': 5516 remainder = lines[index:] 5517 break 5518 elif stripped == '': # ignore blank lines 5519 continue 5520 5521 # Cascading parsing attempts, since the possibilities are 5522 # mostly mutually exclusive. 5523 # TODO: Node/attr confusion with = in a node name? 5524 try: 5525 attrName, attrVal = parseDotAttr(thisLine) 5526 result['attrs'].append((attrName, attrVal)) 5527 except DotParseError: 5528 try: 5529 fromNode, toNode, hasEAttrs = parseDotEdge( 5530 thisLine 5531 ) 5532 if hasEAttrs: 5533 attrs, rest = parseDotAttrList( 5534 lines[index:] 5535 ) 5536 # Restart to process rest 5537 lines = rest 5538 index = 0 5539 else: 5540 attrs = [] 5541 result['edges'].append((fromNode, toNode, attrs)) 5542 except DotParseError: 5543 try: 5544 nodeName, hasNAttrs = parseDotNode( 5545 thisLine 5546 ) 5547 if hasNAttrs is True: 5548 attrs, rest = parseDotAttrList( 5549 lines[index:] 5550 ) 5551 # Restart to process rest 5552 lines = rest 5553 index = 0 5554 elif hasNAttrs: 5555 attrs = hasNAttrs 5556 else: 5557 attrs = [] 5558 result['nodes'].append((nodeName, attrs)) 5559 except DotParseError: 5560 try: 5561 subName = parseDotSubgraphStart( 5562 thisLine 5563 ) 5564 subStuff, rest = \ 5565 parseDotGraphContents( 5566 lines[index:] 5567 ) 5568 result['subgraphs'].append((subName, subStuff)) 5569 # Restart to process rest 5570 lines = rest 5571 index = 0 5572 except DotParseError: 5573 raise DotParseError( 5574 f"Unrecognizable graph line (possibly" 5575 f" beginning of unfinished structure):" 5576 f"\n {repr(thisLine)}" 5577 ) 5578 if remainder is None: 5579 raise DotParseError( 5580 f"Graph (or subgraph) is missing closing '}}'. Starts" 5581 f" on line:\n {repr(lines[0])}" 5582 ) 5583 else: 5584 return (result, remainder) 5585 5586 5587def parseDot( 5588 dotStr: str, 5589 parseFormat: ParseFormat = ParseFormat() 5590) -> core.DecisionGraph: 5591 """ 5592 Converts a `graphviz` dot-format string into a `core.DecisionGraph`. 5593 A custom `ParseFormat` may be specified if desired; the default 5594 `ParseFormat` is used if not. Note that this relies on specific 5595 indentation schemes used by `toDot` so a hand-edited dot-format 5596 graph will probably not work. A `DotParseError` is raised if the 5597 provided string can't be parsed. Example 5598 5599 >>> parseDotNode(' 3 [ label="A = \\\\"grate:open\\\\"" ]') 5600 (3, [('label', 'A = "grate:open"')]) 5601 >>> sg = '''\ 5602 ... subgraph __requirements__ { 5603 ... 3 [ label="A = \\\\"grate:open\\\\"" ] 5604 ... 4 [ label="B = \\\\"!(helmet)\\\\"" ] 5605 ... 5 [ label="C = \\\\"helmet\\\\"" ] 5606 ... }''' 5607 >>> parseDotGraphContents(sg.splitlines()[1:]) 5608 ({'nodes': [(3, [('label', 'A = "grate:open"')]),\ 5609 (4, [('label', 'B = "!(helmet)"')]), (5, [('label', 'C = "helmet"')])],\ 5610 'edges': [], 'attrs': [], 'subgraphs': []}, []) 5611 >>> from . import core 5612 >>> dg = core.DecisionGraph.example('simple') 5613 >>> encoded = toDot(dg) 5614 >>> reconstructed = parseDot(encoded) 5615 >>> for diff in dg.listDifferences(reconstructed): 5616 ... print(diff) 5617 >>> reconstructed == dg 5618 True 5619 >>> dg = core.DecisionGraph.example('abc') 5620 >>> encoded = toDot(dg) 5621 >>> reconstructed = parseDot(encoded) 5622 >>> for diff in dg.listDifferences(reconstructed): 5623 ... print(diff) 5624 >>> reconstructed == dg 5625 True 5626 >>> tg = core.DecisionGraph() 5627 >>> tg.addDecision('A') 5628 0 5629 >>> tg.addDecision('B') 5630 1 5631 >>> tg.addTransition('A', 'up', 'B', 'down') 5632 >>> same = parseDot(''' 5633 ... digraph { 5634 ... 0 [ name=A label=A ] 5635 ... 0 -> 1 [ 5636 ... label=up 5637 ... fullLabel=up 5638 ... reciprocal=down 5639 ... ] 5640 ... 1 [ name=B label=B ] 5641 ... 1 -> 0 [ 5642 ... label=down 5643 ... fullLabel=down 5644 ... reciprocal=up 5645 ... ] 5646 ... }''') 5647 >>> for diff in tg.listDifferences(same): 5648 ... print(diff) 5649 >>> same == tg 5650 True 5651 >>> pf = ParseFormat() 5652 >>> tg.setTransitionRequirement('A', 'up', pf.parseRequirement('one|two')) 5653 >>> tg.setConsequence( 5654 ... 'B', 5655 ... 'down', 5656 ... [base.effect(gain="one")] 5657 ... ) 5658 >>> test = parseDot(''' 5659 ... digraph { 5660 ... 0 [ name="A = \\\\"one|two\\\\"" label="A = \\\\"one|two\\\\"" ] 5661 ... } 5662 ... ''') 5663 >>> list(test.nodes) 5664 [0] 5665 >>> test.nodes[0]['name'] 5666 'A = "one|two"' 5667 >>> eff = ( 5668 ... r'"A = \\"[{\\\\\\"type\\\\\\": \\\\\\"gain\\\\\\",' 5669 ... r' \\\\\\"applyTo\\\\\\": \\\\\\"active\\\\\\",' 5670 ... r' \\\\\\"value\\\\\\": \\\\\\"one\\\\\\",' 5671 ... r' \\\\\\"charges\\\\\\": null, \\\\\\"hidden\\\\\\": false,' 5672 ... r' \\\\\\"delay\\\\\\": null}]\\""' 5673 ... ) 5674 >>> utils.unquoted(eff)[1] 5675 '' 5676 >>> test2 = parseDot( 5677 ... 'digraph {\\n 0 [ name=' + eff + ' label=' + eff + ' ]\\n}' 5678 ... ) 5679 >>> s = test2.nodes[0]['name'] 5680 >>> s[:25] 5681 'A = "[{\\\\"type\\\\": \\\\"gain\\\\"' 5682 >>> s[25:50] 5683 ', \\\\"applyTo\\\\": \\\\"active\\\\"' 5684 >>> s[50:70] 5685 ', \\\\"value\\\\": \\\\"one\\\\"' 5686 >>> s[70:89] 5687 ', \\\\"charges\\\\": null' 5688 >>> s[89:108] 5689 ', \\\\"hidden\\\\": false' 5690 >>> s[108:] 5691 ', \\\\"delay\\\\": null}]"' 5692 >>> ae = s[s.index('=') + 1:].strip() 5693 >>> uq, after = utils.unquoted(ae) 5694 >>> after 5695 '' 5696 >>> fromJSON(uq) == [base.effect(gain="one")] 5697 True 5698 >>> same = parseDot(''' 5699 ... digraph { 5700 ... 0 [ name=A label=A ] 5701 ... 0 -> 1 [ 5702 ... label=up 5703 ... fullLabel=up 5704 ... reciprocal=down 5705 ... req=A 5706 ... ] 5707 ... 1 [ name=B label=B ] 5708 ... 1 -> 0 [ 5709 ... label=down 5710 ... fullLabel=down 5711 ... reciprocal=up 5712 ... consequence=A 5713 ... ] 5714 ... subgraph __requirements__ { 5715 ... 2 [ label="A = \\\\"one|two\\\\"" ] 5716 ... } 5717 ... subgraph __consequences__ { 5718 ... 3 [ label=''' + eff + ''' ] 5719 ... } 5720 ... }''') 5721 >>> c = {'tags': {}, 'annotations': [], 'reciprocal': 'up', 'consequence': [{'type': 'gain', 'applyTo': 'active', 'value': 'one', 'delay': None, 'charges': None}]}['consequence'] # noqa 5722 5723 >>> for diff in tg.listDifferences(same): 5724 ... print(diff) 5725 >>> same == tg 5726 True 5727 """ 5728 lines = dotStr.splitlines() 5729 while lines[0].strip() == '': 5730 lines.pop(0) 5731 if lines.pop(0).strip() != "digraph {": 5732 raise DotParseError("Input doesn't begin with 'digraph {'.") 5733 5734 # Create our result 5735 result = core.DecisionGraph() 5736 5737 # Parse to intermediate graph data structure 5738 graphStuff, remaining = parseDotGraphContents(lines) 5739 if remaining: 5740 if len(remaining) <= 4: 5741 junk = '\n '.join(repr(line) for line in remaining) 5742 else: 5743 junk = '\n '.join(repr(line) for line in remaining[:4]) 5744 junk += '\n ...' 5745 raise DotParseError("Extra junk after graph:\n {junk}") 5746 5747 # Sort out subgraphs to find legends 5748 zoneSubs = [] 5749 reqLegend = None 5750 consequenceLegend = None 5751 mechanismLegend = None 5752 for sub in graphStuff['subgraphs']: 5753 if sub[0] == '__requirements__': 5754 reqLegend = sub[1] 5755 elif sub[0] == '__consequences__': 5756 consequenceLegend = sub[1] 5757 elif sub[0] == '__mechanisms__': 5758 mechanismLegend = sub[1] 5759 else: 5760 zoneSubs.append(sub) 5761 5762 # Build out our mapping from requirement abbreviations to actual 5763 # requirement objects 5764 reqMap: Dict[str, base.Requirement] = {} 5765 if reqLegend is not None: 5766 if reqLegend['edges']: 5767 raise DotParseError( 5768 f"Requirements legend subgraph has edges:" 5769 f"\n {repr(reqLegend['edges'])}" 5770 f"\n(It should only have nodes.)" 5771 ) 5772 if reqLegend['attrs']: 5773 raise DotParseError( 5774 f"Requirements legend subgraph has attributes:" 5775 f"\n {repr(reqLegend['attrs'])}" 5776 f"\n(It should only have nodes.)" 5777 ) 5778 if reqLegend['subgraphs']: 5779 raise DotParseError( 5780 f"Requirements legend subgraph has subgraphs:" 5781 f"\n {repr(reqLegend['subgraphs'])}" 5782 f"\n(It should only have nodes.)" 5783 ) 5784 for node, attrs in reqLegend['nodes']: 5785 if not attrs: 5786 raise DotParseError( 5787 f"Node in requirements legend missing attributes:" 5788 f"\n {repr(attrs)}" 5789 ) 5790 if len(attrs) != 1: 5791 raise DotParseError( 5792 f"Node in requirements legend has multiple" 5793 f" attributes:\n {repr(attrs)}" 5794 ) 5795 reqStr = attrs[0][1] 5796 try: 5797 eqInd = reqStr.index('=') 5798 except ValueError: 5799 raise DotParseError( 5800 f"Missing '=' in requirement specifier:" 5801 f"\n {repr(reqStr)}" 5802 ) 5803 ab = reqStr[:eqInd].rstrip() 5804 encoded = reqStr[eqInd + 1:].lstrip() 5805 try: 5806 encVal, empty = utils.unquoted(encoded) 5807 except ValueError: 5808 raise DotParseError( 5809 f"Invalid quoted requirement value:" 5810 f"\n {repr(encoded)}" 5811 ) 5812 if empty.strip(): 5813 raise DotParseError( 5814 f"Extra junk after requirement value:" 5815 f"\n {repr(empty)}" 5816 ) 5817 try: 5818 req = parseFormat.parseRequirement(encVal) 5819 except ValueError: 5820 raise DotParseError( 5821 f"Invalid encoded requirement in requirements" 5822 f" legend:\n {repr(encVal)}" 5823 ) 5824 if ab in reqMap: 5825 raise DotParseError( 5826 f"Abbreviation '{ab}' was defined multiple" 5827 f" times in requirements legend." 5828 ) 5829 reqMap[ab] = req 5830 5831 # Build out our mapping from consequence abbreviations to actual 5832 # consequence lists 5833 consequenceMap: Dict[str, base.Consequence] = {} 5834 if consequenceLegend is not None: 5835 if consequenceLegend['edges']: 5836 raise DotParseError( 5837 f"Consequences legend subgraph has edges:" 5838 f"\n {repr(consequenceLegend['edges'])}" 5839 f"\n(It should only have nodes.)" 5840 ) 5841 if consequenceLegend['attrs']: 5842 raise DotParseError( 5843 f"Consequences legend subgraph has attributes:" 5844 f"\n {repr(consequenceLegend['attrs'])}" 5845 f"\n(It should only have nodes.)" 5846 ) 5847 if consequenceLegend['subgraphs']: 5848 raise DotParseError( 5849 f"Consequences legend subgraph has subgraphs:" 5850 f"\n {repr(consequenceLegend['subgraphs'])}" 5851 f"\n(It should only have nodes.)" 5852 ) 5853 for node, attrs in consequenceLegend['nodes']: 5854 if not attrs: 5855 raise DotParseError( 5856 f"Node in consequence legend missing attributes:" 5857 f"\n {repr(attrs)}" 5858 ) 5859 if len(attrs) != 1: 5860 raise DotParseError( 5861 f"Node in consequences legend has multiple" 5862 f" attributes:\n {repr(attrs)}" 5863 ) 5864 consStr = attrs[0][1] 5865 try: 5866 eqInd = consStr.index('=') 5867 except ValueError: 5868 raise DotParseError( 5869 f"Missing '=' in consequence string:" 5870 f"\n {repr(consStr)}" 5871 ) 5872 ab = consStr[:eqInd].rstrip() 5873 encoded = consStr[eqInd + 1:].lstrip() 5874 try: 5875 encVal, empty = utils.unquoted(encoded) 5876 except ValueError: 5877 raise DotParseError( 5878 f"Invalid quoted consequence value:" 5879 f"\n {repr(encoded)}" 5880 ) 5881 if empty.strip(): 5882 raise DotParseError( 5883 f"Extra junk after consequence value:" 5884 f"\n {repr(empty)}" 5885 ) 5886 try: 5887 consequences = fromJSON(encVal) 5888 except json.decoder.JSONDecodeError: 5889 raise DotParseError( 5890 f"Invalid encoded consequence in requirements" 5891 f" legend:\n {repr(encVal)}" 5892 ) 5893 if ab in consequenceMap: 5894 raise DotParseError( 5895 f"Abbreviation '{ab}' was defined multiple" 5896 f" times in effects legend." 5897 ) 5898 consequenceMap[ab] = consequences 5899 5900 # Reconstruct mechanisms 5901 if mechanismLegend is not None: 5902 if mechanismLegend['edges']: 5903 raise DotParseError( 5904 f"Mechanisms legend subgraph has edges:" 5905 f"\n {repr(mechanismLegend['edges'])}" 5906 f"\n(It should only have nodes.)" 5907 ) 5908 if mechanismLegend['attrs']: 5909 raise DotParseError( 5910 f"Mechanisms legend subgraph has attributes:" 5911 f"\n {repr(mechanismLegend['attrs'])}" 5912 f"\n(It should only have nodes.)" 5913 ) 5914 if mechanismLegend['subgraphs']: 5915 raise DotParseError( 5916 f"Mechanisms legend subgraph has subgraphs:" 5917 f"\n {repr(mechanismLegend['subgraphs'])}" 5918 f"\n(It should only have nodes.)" 5919 ) 5920 for node, attrs in mechanismLegend['nodes']: 5921 if not attrs: 5922 raise DotParseError( 5923 f"Node in mechanisms legend missing attributes:" 5924 f"\n {repr(attrs)}" 5925 ) 5926 if len(attrs) != 1: 5927 raise DotParseError( 5928 f"Node in mechanisms legend has multiple" 5929 f" attributes:\n {repr(attrs)}" 5930 ) 5931 mechStr = attrs[0][1] 5932 try: 5933 atInd = mechStr.index('@') 5934 colonInd = mechStr.index(':') 5935 except ValueError: 5936 raise DotParseError( 5937 f"Missing '@' or ':' in mechanism string:" 5938 f"\n {repr(mechStr)}" 5939 ) 5940 if atInd > colonInd: 5941 raise DotParseError( 5942 f"':' after '@' in mechanism string:" 5943 f"\n {repr(mechStr)}" 5944 ) 5945 mID: base.MechanismID 5946 where: Optional[base.DecisionID] 5947 mName: base.MechanismName 5948 try: 5949 mID = int(mechStr[:atInd].rstrip()) 5950 except ValueError: 5951 raise DotParseError( 5952 f"Invalid mechanism ID in mechanism string:" 5953 f"\n {repr(mechStr)}" 5954 ) 5955 try: 5956 whereStr = mechStr[atInd + 1:colonInd].strip() 5957 if whereStr == "None": 5958 where = None 5959 else: 5960 where = int(whereStr) 5961 except ValueError: 5962 raise DotParseError( 5963 f"Invalid mechanism location in mechanism string:" 5964 f"\n {repr(mechStr)}" 5965 ) 5966 mName, rest = utils.unquoted(mechStr[colonInd + 1:].lstrip()) 5967 if rest.strip(): 5968 raise DotParseError( 5969 f"Junk after mechanism name in mechanism string:" 5970 f"\n {repr(mechStr)}" 5971 ) 5972 result.mechanisms[mID] = (where, mName) 5973 if where is None: 5974 result.globalMechanisms[mName] = mID 5975 5976 # Add zones to the graph based on parent info 5977 # Map from zones to children we should add to them once all 5978 # zones are created: 5979 zoneChildMap: Dict[str, List[str]] = {} 5980 for prefixedName, graphData in zoneSubs: 5981 # Chop off cluster_ or _ prefix: 5982 zoneName = prefixedName[prefixedName.index('_') + 1:] 5983 if graphData['edges']: 5984 raise DotParseError( 5985 f"Zone subgraph for zone {repr(zoneName)} has edges:" 5986 f"\n {repr(graphData['edges'])}" 5987 f"\n(It should only have nodes and attributes.)" 5988 ) 5989 if graphData['subgraphs']: 5990 raise DotParseError( 5991 f"Zone subgraph for zone {repr(zoneName)} has" 5992 f" subgraphs:" 5993 f"\n {repr(graphData['subgraphs'])}" 5994 f"\n(It should only have nodes and attributes.)" 5995 ) 5996 # Note: we ignore nodes as that info is used for 5997 # visualization but is redundant with the zone parent info 5998 # stored in nodes, and it would be tricky to tease apart 5999 # direct vs. indirect relationships from merged info. 6000 parents = None 6001 level = None 6002 for attr, aVal in graphData['attrs']: 6003 if attr == 'parents': 6004 try: 6005 parents = set(fromJSON(aVal)) 6006 except json.decoder.JSONDecodeError: 6007 raise DotParseError( 6008 f"Invalid parents JSON in zone subgraph for" 6009 f" zone '{zoneName}':\n {repr(aVal)}" 6010 ) 6011 elif attr == 'level': 6012 try: 6013 level = int(aVal) 6014 except ValueError: 6015 raise DotParseError( 6016 f"Invalid level in zone subgraph for" 6017 f" zone '{zoneName}':\n {repr(aVal)}" 6018 ) 6019 elif attr == 'label': 6020 pass # name already extracted from the subgraph name 6021 6022 else: 6023 raise DotParseError( 6024 f"Unexpected attribute '{attr}' in zone" 6025 f" subgraph for zone '{zoneName}'" 6026 ) 6027 if parents is None: 6028 raise DotParseError( 6029 f"No parents attribute for zone '{zoneName}'." 6030 f" Graph is:\n {repr(graphData)}" 6031 ) 6032 if level is None: 6033 raise DotParseError( 6034 f"No level attribute for zone '{zoneName}'." 6035 f" Graph is:\n {repr(graphData)}" 6036 ) 6037 6038 # Add ourself to our parents in the child map 6039 for parent in parents: 6040 zoneChildMap.setdefault(parent, []).append(zoneName) 6041 6042 # Create this zone 6043 result.createZone(zoneName, level) 6044 6045 # Add zone parent/child relationships 6046 for parent, children in zoneChildMap.items(): 6047 for child in children: 6048 result.addZoneToZone(child, parent) 6049 6050 # Add nodes to the graph 6051 for (node, attrs) in graphStuff['nodes']: 6052 name: Optional[str] = None 6053 annotations = [] 6054 tags: Dict[base.Tag, base.TagValue] = {} 6055 zones = [] 6056 for attr, aVal in attrs: 6057 if attr == 'name': # it's the name 6058 name = aVal 6059 elif attr == 'label': # zone + name; redundant 6060 pass 6061 elif attr.startswith('t_'): # it's a tag 6062 tagName = attr[2:] 6063 try: 6064 tagAny = fromJSON(aVal) 6065 except json.decoder.JSONDecodeError: 6066 raise DotParseError( 6067 f"Error in JSON for tag attr '{attr}' of node" 6068 f" '{node}'" 6069 ) 6070 if isinstance(tagAny, base.TagValueTypes): 6071 tagVal: base.TagValue = cast(base.TagValue, tagAny) 6072 else: 6073 raise DotParseError( 6074 f"JSON for tag value encodes disallowed tag" 6075 f" value of type {type(tagAny)}. Value is:" 6076 f"\n {repr(tagAny)}" 6077 ) 6078 tags[tagName] = tagVal 6079 elif attr.startswith('z_'): # it's a zone 6080 zones.append(attr[2:]) 6081 elif attr == 'annotations': # It's the annotations 6082 try: 6083 annotations = fromJSON(aVal) 6084 except json.decoder.JSONDecodeError: 6085 raise DotParseError( 6086 f"Bad JSON in attribute '{attr}' of node" 6087 f" '{node}'" 6088 ) 6089 else: 6090 raise DotParseError( 6091 f"Unrecognized node attribute '{attr}' for node" 6092 f" '{node}'" 6093 ) 6094 6095 # TODO: Domains here? 6096 if name is None: 6097 raise DotParseError(f"Node '{node}' does not have a name.") 6098 6099 result.addIdentifiedDecision( 6100 node, 6101 name, 6102 tags=tags, 6103 annotations=annotations 6104 ) 6105 for zone in zones: 6106 try: 6107 result.addDecisionToZone(node, zone) 6108 except core.MissingZoneError: 6109 raise DotParseError( 6110 f"Zone '{zone}' for node {node} does not" 6111 f" exist." 6112 ) 6113 6114 # Add mechanisms to each node: 6115 for (mID, (where, mName)) in result.mechanisms.items(): 6116 mPool = result.nodes[where].setdefault('mechanisms', {}) 6117 if mName in mPool: 6118 raise DotParseError( 6119 f"Multiple mechanisms named {mName!r} at" 6120 f" decision {where}." 6121 ) 6122 mPool[mName] = mID 6123 6124 # Reciprocals to double-check once all edges are added 6125 recipChecks: Dict[ 6126 Tuple[base.DecisionID, base.Transition], 6127 base.Transition 6128 ] = {} 6129 6130 # Add each edge 6131 for (source, dest, attrs) in graphStuff['edges']: 6132 annotations = [] 6133 tags = {} 6134 label = None 6135 requirements = None 6136 consequence = None 6137 reciprocal = None 6138 for attr, aVal in attrs: 6139 if attr.startswith('t_'): 6140 try: 6141 tags[attr[2:]] = fromJSON(aVal) 6142 except json.decoder.JSONDecodeError: 6143 raise DotParseError( 6144 f"Invalid JSON in edge tag '{attr}' for edge" 6145 f"from '{source}' to '{dest}':" 6146 f"\n {repr(aVal)}" 6147 ) 6148 elif attr == "label": # We ignore the short-label 6149 pass 6150 elif attr == "fullLabel": # This is our transition name 6151 label = aVal 6152 elif attr == "reciprocal": 6153 reciprocal = aVal 6154 elif attr == "req": 6155 reqAbbr = aVal 6156 if reqAbbr not in reqMap: 6157 raise DotParseError( 6158 f"Edge from '{source}' to '{dest}' has" 6159 f" requirement abbreviation '{reqAbbr}'" 6160 f" but that abbreviation was not listed" 6161 f" in the '__requirements__' subgraph." 6162 ) 6163 requirements = reqMap[reqAbbr] 6164 elif attr == "consequence": 6165 consequenceAbbr = aVal 6166 if consequenceAbbr not in reqMap: 6167 raise DotParseError( 6168 f"Edge from '{source}' to '{dest}' has" 6169 f" consequence abbreviation" 6170 f" '{consequenceAbbr}' but that" 6171 f" abbreviation was not listed in the" 6172 f" '__consequences__' subgraph." 6173 ) 6174 consequence = consequenceMap[consequenceAbbr] 6175 elif attr == "annotations": 6176 try: 6177 annotations = fromJSON(aVal) 6178 except json.decoder.JSONDecodeError: 6179 raise DotParseError( 6180 f"Invalid JSON in edge annotations for" 6181 f" edge from '{source}' to '{dest}':" 6182 f"\n {repr(aVal)}" 6183 ) 6184 else: 6185 raise DotParseError( 6186 f"Unrecognized edge attribute '{attr}' for edge" 6187 f" from '{source}' to '{dest}'" 6188 ) 6189 6190 if label is None: 6191 raise DotParseError( 6192 f"Edge from '{source}' to '{dest}' is missing" 6193 f" a 'fullLabel' attribute." 6194 ) 6195 6196 # Add the requested transition 6197 result.addTransition( 6198 source, 6199 label, 6200 dest, 6201 tags=tags, 6202 annotations=annotations, 6203 requires=requirements, # None works here 6204 consequence=consequence # None works here 6205 ) 6206 # Either we're first or our reciprocal is, so this will only 6207 # trigger for one of the pair 6208 if reciprocal is not None: 6209 recipDest = result.getDestination(dest, reciprocal) 6210 if recipDest is None: 6211 recipChecks[(source, label)] = reciprocal 6212 # we'll get set as a reciprocal when that edge is 6213 # instantiated, we hope, but let's check that later 6214 elif recipDest != source: 6215 raise DotParseError( 6216 f"Transition '{label}' from '{source}' to" 6217 f" '{dest}' lists reciprocal '{reciprocal}'" 6218 f" but that transition from '{dest}' goes to" 6219 f" '{recipDest}', not '{source}'." 6220 ) 6221 else: 6222 # At this point we know the reciprocal edge exists 6223 # and has the appropriate destination (our source). 6224 # No need to check for a pre-existing reciprocal as 6225 # this edge is newly created and cannot already have 6226 # a reciprocal assigned. 6227 result.setReciprocal(source, label, reciprocal) 6228 6229 # Double-check skipped reciprocals 6230 for ((source, transition), reciprocal) in recipChecks.items(): 6231 actual = result.getReciprocal(source, transition) 6232 if actual != reciprocal: 6233 raise DotParseError( 6234 f"Transition '{transition}' from '{source}' was" 6235 f" expecting to have reciprocal '{reciprocal}' but" 6236 f" all edges have been processed and its reciprocal" 6237 f" is {repr(actual)}." 6238 ) 6239 6240 # Finally get graph-level attribute values 6241 for (name, value) in graphStuff['attrs']: 6242 if name == "unknownCount": 6243 try: 6244 result.unknownCount = int(value) 6245 except ValueError: 6246 raise DotParseError( 6247 f"Invalid 'unknownCount' value {repr(value)}." 6248 ) 6249 elif name == "nextID": 6250 try: 6251 result.nextID = int(value) 6252 except ValueError: 6253 raise DotParseError( 6254 f"Invalid 'nextID' value:" 6255 f"\n {repr(value)}" 6256 ) 6257 collisionCourse = [x for x in result if x >= result.nextID] 6258 if len(collisionCourse) > 0: 6259 raise DotParseError( 6260 f"Next ID {value} is wrong because the graph" 6261 f" already contains one or more node(s) with" 6262 f" ID(s) that is/are at least that large:" 6263 f" {collisionCourse}" 6264 ) 6265 elif name == "nextMechanismID": 6266 try: 6267 result.nextMechanismID = int(value) 6268 except ValueError: 6269 raise DotParseError( 6270 f"Invalid 'nextMechanismID' value:" 6271 f"\n {repr(value)}" 6272 ) 6273 elif name in ( 6274 "equivalences", 6275 "reversionTypes", 6276 "mechanisms", 6277 "globalMechanisms", 6278 "nameLookup" 6279 ): 6280 try: 6281 setattr(result, name, fromJSON(value)) 6282 except json.decoder.JSONDecodeError: 6283 raise DotParseError( 6284 f"Invalid JSON in '{name}' attribute:" 6285 f"\n {repr(value)}" 6286 ) 6287 else: 6288 raise DotParseError( 6289 f"Graph has unexpected attribute '{name}'." 6290 ) 6291 6292 # Final check for mechanism ID value after both mechanism ID and 6293 # mechanisms dictionary have been parsed: 6294 leftBehind = [ 6295 x 6296 for x in result.mechanisms 6297 if x >= result.nextMechanismID 6298 ] 6299 if len(leftBehind) > 0: 6300 raise DotParseError( 6301 f"Next mechanism ID {value} is wrong because" 6302 f" the graph already contains one or more" 6303 f" node(s) with ID(s) that is/are at least that" 6304 f" large: {leftBehind}" 6305 ) 6306 6307 # And we're done! 6308 return result 6309 6310 6311def toDot( 6312 graph: core.DecisionGraph, 6313 clusterLevels: Union[str, List[int]] = [0] 6314) -> str: 6315 """ 6316 Converts the decision graph into a "dot"-format string suitable 6317 for processing by `graphviz`. 6318 6319 See [the dot language 6320 specification](https://graphviz.org/doc/info/lang.html) for more 6321 detail on the syntax we convert to. 6322 6323 If `clusterLevels` is given, it should be either the string '*', 6324 or a list of integers. '*' means that all zone levels should be 6325 cluster-style subgraphs, while a list of integers specifies that 6326 zones at those levels should be cluster-style subgraphs. This 6327 will prefix the subgraph names with 'cluster_' instead of just 6328 '_'. 6329 6330 TODO: Check edge cases for quotes in capability names, tag names, 6331 transition names, annotations, etc. 6332 6333 TODO: At least colons not allowed in tag names! 6334 6335 TODO: Spaces in decision/transition names? Other special 6336 characters in those names? 6337 """ 6338 # Set up result including unknownCount and nextID 6339 result = ( 6340 f"digraph {{" 6341 f"\n unknownCount={graph.unknownCount}" 6342 f"\n nextID={graph.nextID}" 6343 f"\n nextMechanismID={graph.nextMechanismID}" 6344 f"\n" 6345 ) 6346 6347 # Dictionaries for using letters to substitute for unique 6348 # requirements/consequences found throughout the graph. Keys are 6349 # quoted requirement or consequence reprs, and values are 6350 # abbreviation strings for them. 6351 currentReqKey = utils.nextAbbrKey(None) 6352 currentEffectKey = utils.nextAbbrKey(None) 6353 reqKeys: Dict[str, str] = {} 6354 consequenceKeys: Dict[str, str] = {} 6355 6356 # Add all decision and transition info 6357 decision: base.DecisionID # TODO: Fix Multidigraph type stubs 6358 for decision in graph.nodes: 6359 nodeInfo = graph.nodes[decision] 6360 tags = nodeInfo.get('tags', {}) 6361 annotations = toJSON(nodeInfo.get('annotations', [])) 6362 zones = nodeInfo.get('zones', set()) 6363 nodeAttrs = f"\n name={utils.quoted(nodeInfo['name'])}" 6364 immediateZones = [z for z in zones if graph.zoneHierarchyLevel(z) == 0] 6365 if len(immediateZones) > 0: 6366 useZone = sorted(immediateZones)[0] 6367 # TODO: Don't hardcode :: here? 6368 withZone = useZone + "::" + nodeInfo['name'] 6369 nodeAttrs += f"\n label={utils.quoted(withZone)}" 6370 else: 6371 nodeAttrs += f"\n label={utils.quoted(nodeInfo['name'])}" 6372 for tag, value in tags.items(): 6373 rep = utils.quoted(toJSON(value)) 6374 nodeAttrs += f"\n t_{tag}={rep}" 6375 for z in sorted(zones): 6376 nodeAttrs += f"\n z_{z}=1" 6377 if annotations: 6378 nodeAttrs += '\n annotations=' + utils.quoted(annotations) 6379 6380 result += f'\n {decision} [{nodeAttrs}\n ]' 6381 6382 for (transition, destination) in graph._byEdge[decision].items(): 6383 edgeAttrs = ( 6384 '\n label=' 6385 + utils.quoted(utils.abbr(transition)) 6386 ) 6387 edgeAttrs += ( 6388 '\n fullLabel=' 6389 + utils.quoted(transition) 6390 ) 6391 reciprocal = graph.getReciprocal(decision, transition) 6392 if reciprocal is not None: 6393 edgeAttrs += ( 6394 '\n reciprocal=' 6395 + utils.quoted(reciprocal) 6396 ) 6397 info = graph.edges[ 6398 decision, # type:ignore 6399 destination, 6400 transition 6401 ] 6402 if 'requirement' in info: 6403 # Get string rep for requirement 6404 rep = utils.quoted(info['requirement'].unparse()) 6405 # Get assigned abbreviation or assign one 6406 if rep in reqKeys: 6407 ab = reqKeys[rep] 6408 else: 6409 ab = currentReqKey 6410 reqKeys[rep] = ab 6411 currentReqKey = utils.nextAbbrKey(currentReqKey) 6412 # Add abbreviation as edge attribute 6413 edgeAttrs += f'\n req={ab}' 6414 if 'consequence' in info: 6415 # Get string representation of consequences 6416 rep = utils.quoted( 6417 toJSON(info['consequence']) 6418 ) 6419 # Get abbreviation for that or assign one: 6420 if rep in consequenceKeys: 6421 ab = consequenceKeys[rep] 6422 else: 6423 ab = currentEffectKey 6424 consequenceKeys[rep] = ab 6425 currentEffectKey = utils.nextAbbrKey( 6426 currentEffectKey 6427 ) 6428 # Add abbreviation as an edge attribute 6429 edgeAttrs += f'\n consequence={ab}' 6430 for (tag, value) in info["tags"].items(): 6431 # Get string representation of tag value 6432 rep = utils.quoted(toJSON(value)) 6433 # Add edge attribute for tag 6434 edgeAttrs += f'\n t_{tag}={rep}' 6435 if 'annotations' in info: 6436 edgeAttrs += ( 6437 '\n annotations=' 6438 + utils.quoted(toJSON(info['annotations'])) 6439 ) 6440 result += f'\n {decision} -> {destination}' 6441 result += f' [{edgeAttrs}\n ]' 6442 6443 # Add zone info as subgraph structure 6444 for z, zinfo in graph.zones.items(): 6445 parents = utils.quoted(toJSON(sorted(zinfo.parents))) 6446 if clusterLevels == '*' or zinfo.level in clusterLevels: 6447 zName = "cluster_" + z 6448 else: 6449 zName = '_' + z 6450 zoneSubgraph = f'\n subgraph {utils.quoted(zName)} {{' 6451 zoneSubgraph += f'\n label={z}' 6452 zoneSubgraph += f'\n level={zinfo.level}' 6453 zoneSubgraph += f'\n parents={parents}' 6454 for decision in sorted(graph.allDecisionsInZone(z)): 6455 zoneSubgraph += f'\n {decision}' 6456 zoneSubgraph += '\n }' 6457 result += zoneSubgraph 6458 6459 # Add equivalences, mechanisms, etc. 6460 for attr in [ 6461 "equivalences", 6462 "reversionTypes", 6463 "mechanisms", 6464 "globalMechanisms", 6465 "nameLookup" 6466 ]: 6467 aRep = utils.quoted(toJSON(getattr(graph, attr))) 6468 result += f'\n {attr}={aRep}' 6469 6470 # Add legend subgraphs to represent abbreviations 6471 useID = graph.nextID 6472 if reqKeys: 6473 result += '\n subgraph __requirements__ {' 6474 for rrepr, ab in reqKeys.items(): 6475 nStr = utils.quoted(ab + ' = ' + rrepr) 6476 result += ( 6477 f"\n {useID} [ label={nStr} ]" 6478 ) 6479 useID += 1 6480 result += '\n }' 6481 6482 if consequenceKeys: 6483 result += '\n subgraph __consequences__ {' 6484 for erepr, ab in consequenceKeys.items(): 6485 nStr = utils.quoted(ab + ' = ' + erepr) 6486 result += ( 6487 f"\n {useID} [ label={nStr} ]" 6488 ) 6489 useID += 1 6490 result += '\n }' 6491 6492 if graph.mechanisms: 6493 result += '\n subgraph __mechanisms__ {' 6494 mID: base.MechanismID 6495 mWhere: Optional[base.DecisionID] 6496 mName: base.MechanismName 6497 for (mID, (mWhere, mName)) in graph.mechanisms.items(): 6498 qName = utils.quoted(mName) 6499 nStr = utils.quoted(f"{mID}@{mWhere}:{qName}") 6500 result += ( 6501 f"\n {useID} [ label={nStr} ]" 6502 ) 6503 useID += 1 6504 result += '\n }' 6505 6506 result += "\n}\n" 6507 return result 6508 6509 6510#------# 6511# JSON # 6512#------# 6513 6514T = TypeVar("T") 6515"Type var for `loadCustom`." 6516 6517 6518def loadCustom(stream: TextIO, loadAs: Type[T]) -> T: 6519 """ 6520 Loads a new JSON-encodable object from the JSON data in the 6521 given text stream (e.g., a file open in read mode). See 6522 `CustomJSONDecoder` for details on the format and which object types 6523 are supported. 6524 6525 This casts the result to the specified type, but errors out with a 6526 `TypeError` if it doesn't match. 6527 """ 6528 result = json.load(stream, cls=CustomJSONDecoder) 6529 if isinstance(result, loadAs): 6530 return result 6531 else: 6532 raise TypeError( 6533 f"Expected to load a {loadAs} but got a {type(result)}." 6534 ) 6535 6536 6537def saveCustom( 6538 toSave: Union[ # TODO: More in this union? 6539 base.MetricSpace, 6540 core.DecisionGraph, 6541 core.DiscreteExploration, 6542 ], 6543 stream: TextIO 6544) -> None: 6545 """ 6546 Saves a JSON-encodable object as JSON into the given text stream 6547 (e.g., a file open in writing mode). See `CustomJSONEncoder` for 6548 details on the format and which types are supported.. 6549 """ 6550 json.dump(toSave, stream, cls=CustomJSONEncoder) 6551 6552 6553def toJSON(obj: Any) -> str: 6554 """ 6555 Defines the standard object -> JSON operation using the 6556 `CustomJSONEncoder` as well as not using `sort_keys`. 6557 """ 6558 return CustomJSONEncoder(sort_keys=False).encode(obj) 6559 6560 6561def fromJSON(encoded: str) -> Any: 6562 """ 6563 Defines the standard JSON -> object operation using 6564 `CustomJSONDecoder`. 6565 """ 6566 return json.loads(encoded, cls=CustomJSONDecoder) 6567 6568 6569class CustomJSONEncoder(json.JSONEncoder): 6570 """ 6571 A custom JSON encoder that has special protocols for handling the 6572 smae objects that `CustomJSONDecoder` decodes. It handles these 6573 objects specially so that they can be decoded back to their original 6574 form. 6575 6576 Examples: 6577 6578 >>> from . import core 6579 >>> tupList = [(1, 1), (2, 2)] 6580 >>> encTup = toJSON(tupList) 6581 >>> encTup 6582 '[{"^^d": "t", "values": [1, 1]}, {"^^d": "t", "values": [2, 2]}]' 6583 >>> fromJSON(encTup) == tupList 6584 True 6585 >>> dg = core.DecisionGraph.example('simple') 6586 >>> fromJSON(toJSON(dg)) == dg 6587 True 6588 >>> dg = core.DecisionGraph.example('abc') 6589 >>> zi = dg.getZoneInfo('upZone') 6590 >>> zi 6591 ZoneInfo(level=1, parents=set(), contents={'zoneA'}, tags={},\ 6592 annotations=[]) 6593 >>> zj = toJSON(zi) 6594 >>> zj 6595 '{"^^d": "nt", "name": "ZoneInfo", "values":\ 6596 {"level": 1, "parents": {"^^d": "s", "values": []},\ 6597 "contents": {"^^d": "s", "values": ["zoneA"]}, "tags": {},\ 6598 "annotations": []}}' 6599 >>> fromJSON(toJSON(zi)) 6600 ZoneInfo(level=1, parents=set(), contents={'zoneA'}, tags={},\ 6601 annotations=[]) 6602 >>> fromJSON(toJSON(zi)) == zi 6603 True 6604 >>> toJSON({'a': 'b', 1: 2}) 6605 '{"^^d": "d", "items": [["a", "b"], [1, 2]]}' 6606 >>> toJSON(((1, 2), (3, 4))) 6607 '{"^^d": "t", "values": [{"^^d": "t", "values": [1, 2]},\ 6608 {"^^d": "t", "values": [3, 4]}]}' 6609 >>> toJSON(base.effect(set=('grate', 'open'))) 6610 '{"type": "set", "applyTo": "active",\ 6611 "value": {"^^d": "t",\ 6612 "values": [{"^^d": "nt", "name": "MechanismSpecifier",\ 6613 "values": {"domain": null, "zone": null, "decision": null, "name": "grate"}},\ 6614 "open"]}, "delay": null, "charges": null, "hidden": false}' 6615 >>> j = toJSON(dg) 6616 >>> expected = ( 6617 ... '{"^^d": "DG",' 6618 ... ' "props": {},' 6619 ... ' "node_links": {"directed": true,' 6620 ... ' "multigraph": true,' 6621 ... ' "graph": {},' 6622 ... ' "nodes": [' 6623 ... '{"name": "A", "domain": "main", "tags": {},' 6624 ... ' "annotations": ["This is a multi-word \\\\"annotation.\\\\""],' 6625 ... ' "zones": {"^^d": "s", "values": ["zoneA"]},' 6626 ... ' "mechanisms": {"grate": 0},' 6627 ... ' "id": 0' 6628 ... '},' 6629 ... ' {' 6630 ... '"name": "B",' 6631 ... ' "domain": "main",' 6632 ... ' "tags": {"b": 1, "tag2": "\\\\"value\\\\""},' 6633 ... ' "annotations": [],' 6634 ... ' "zones": {"^^d": "s", "values": ["zoneB"]},' 6635 ... ' "id": 1' 6636 ... '},' 6637 ... ' {' 6638 ... '"name": "C",' 6639 ... ' "domain": "main",' 6640 ... ' "tags": {"aw\\\\"ful": "ha\\'ha"},' 6641 ... ' "annotations": [],' 6642 ... ' "zones": {"^^d": "s", "values": ["zoneA"]},' 6643 ... ' "id": 2' 6644 ... '}' 6645 ... '],' 6646 ... ' "links": [' 6647 ... '{' 6648 ... '"tags": {},' 6649 ... ' "annotations": [],' 6650 ... ' "reciprocal": "right",' 6651 ... ' "source": 0,' 6652 ... ' "target": 1,' 6653 ... ' "key": "left"' 6654 ... '},' 6655 ... ' {' 6656 ... '"tags": {},' 6657 ... ' "annotations": [],' 6658 ... ' "reciprocal": "up_right",' 6659 ... ' "requirement": {"^^d": "R", "value": "grate:open"},' 6660 ... ' "source": 0,' 6661 ... ' "target": 1,' 6662 ... ' "key": "up_left"' 6663 ... '},' 6664 ... ' {' 6665 ... '"tags": {},' 6666 ... ' "annotations": ["Transition \\'annotation.\\'"],' 6667 ... ' "reciprocal": "up",' 6668 ... ' "source": 0,' 6669 ... ' "target": 2,' 6670 ... ' "key": "down"' 6671 ... '},' 6672 ... ' {' 6673 ... '"tags": {},' 6674 ... ' "annotations": [],' 6675 ... ' "reciprocal": "left",' 6676 ... ' "source": 1,' 6677 ... ' "target": 0,' 6678 ... ' "key": "right"' 6679 ... '},' 6680 ... ' {' 6681 ... '"tags": {},' 6682 ... ' "annotations": [],' 6683 ... ' "reciprocal": "up_left",' 6684 ... ' "requirement": {"^^d": "R", "value": "grate:open"},' 6685 ... ' "source": 1,' 6686 ... ' "target": 0,' 6687 ... ' "key": "up_right"' 6688 ... '},' 6689 ... ' {' 6690 ... '"tags": {"fast": 1},' 6691 ... ' "annotations": [],' 6692 ... ' "reciprocal": "down",' 6693 ... ' "source": 2,' 6694 ... ' "target": 0,' 6695 ... ' "key": "up"' 6696 ... '},' 6697 ... ' {' 6698 ... '"tags": {},' 6699 ... ' "annotations": [],' 6700 ... ' "requirement": {"^^d": "R", "value": "!(helmet)"},' 6701 ... ' "consequence": [' 6702 ... '{' 6703 ... '"type": "gain", "applyTo": "active", "value": "helmet",' 6704 ... ' "delay": null, "charges": null, "hidden": false' 6705 ... '},' 6706 ... ' {' 6707 ... '"type": "deactivate",' 6708 ... ' "applyTo": "active", "value": null,' 6709 ... ' "delay": 3, "charges": null, "hidden": false' 6710 ... '}' 6711 ... '],' 6712 ... ' "source": 2,' 6713 ... ' "target": 2,' 6714 ... ' "key": "grab_helmet"' 6715 ... '},' 6716 ... ' {' 6717 ... '"tags": {},' 6718 ... ' "annotations": [],' 6719 ... ' "requirement": {"^^d": "R", "value": "helmet"},' 6720 ... ' "consequence": [' 6721 ... '{"type": "lose", "applyTo": "active", "value": "helmet",' 6722 ... ' "delay": null, "charges": null, "hidden": false},' 6723 ... ' {"type": "gain", "applyTo": "active",' 6724 ... ' "value": {"^^d": "t", "values": ["token", 1]},' 6725 ... ' "delay": null, "charges": null, "hidden": false' 6726 ... '},' 6727 ... ' {"condition":' 6728 ... ' {"^^d": "R", "value": "token*2"},' 6729 ... ' "consequence": [' 6730 ... '{"type": "set", "applyTo": "active",' 6731 ... ' "value": {"^^d": "t", "values": [' 6732 ... '{"^^d": "nt", "name": "MechanismSpecifier",' 6733 ... ' "values": {"domain": null, "zone": null, "decision": null,' 6734 ... ' "name": "grate"}}, "open"]},' 6735 ... ' "delay": null, "charges": null, "hidden": false' 6736 ... '},' 6737 ... ' {"type": "deactivate", "applyTo": "active", "value": null,' 6738 ... ' "delay": null, "charges": null, "hidden": false' 6739 ... '}' 6740 ... '],' 6741 ... ' "alternative": []' 6742 ... '}' 6743 ... '],' 6744 ... ' "source": 2,' 6745 ... ' "target": 2,' 6746 ... ' "key": "pull_lever"' 6747 ... '}' 6748 ... ']' 6749 ... '},' 6750 ... ' "_byEdge": {"^^d": "d", "items":' 6751 ... ' [[0, {"left": 1, "up_left": 1, "down": 2}],' 6752 ... ' [1, {"right": 0, "up_right": 0}],' 6753 ... ' [2, {"up": 0, "grab_helmet": 2, "pull_lever": 2}]]},' 6754 ... ' "zones": {"zoneA":' 6755 ... ' {"^^d": "nt", "name": "ZoneInfo",' 6756 ... ' "values": {' 6757 ... '"level": 0,' 6758 ... ' "parents": {"^^d": "s", "values": ["upZone"]},' 6759 ... ' "contents": {"^^d": "s", "values": [0, 2]},' 6760 ... ' "tags": {},' 6761 ... ' "annotations": []' 6762 ... '}' 6763 ... '},' 6764 ... ' "zoneB":' 6765 ... ' {"^^d": "nt", "name": "ZoneInfo",' 6766 ... ' "values": {' 6767 ... '"level": 0,' 6768 ... ' "parents": {"^^d": "s", "values": []},' 6769 ... ' "contents": {"^^d": "s", "values": [1]},' 6770 ... ' "tags": {},' 6771 ... ' "annotations": []' 6772 ... '}' 6773 ... '},' 6774 ... ' "upZone":' 6775 ... ' {"^^d": "nt", "name": "ZoneInfo",' 6776 ... ' "values": {' 6777 ... '"level": 1,' 6778 ... ' "parents": {"^^d": "s", "values": []},' 6779 ... ' "contents": {"^^d": "s", "values": ["zoneA"]},' 6780 ... ' "tags": {},' 6781 ... ' "annotations": []' 6782 ... '}' 6783 ... '}' 6784 ... '},' 6785 ... ' "unknownCount": 0,' 6786 ... ' "equivalences": {"^^d": "d", "items": [' 6787 ... '[{"^^d": "t", "values": [0, "open"]},' 6788 ... ' {"^^d": "s", "values": [' 6789 ... '{"^^d": "R", "value": "helmet"}]}]' 6790 ... ']},' 6791 ... ' "reversionTypes": {},' 6792 ... ' "nextID": 3,' 6793 ... ' "nextMechanismID": 1,' 6794 ... ' "mechanisms": {"^^d": "d", "items": [' 6795 ... '[0, {"^^d": "t", "values": [0, "grate"]}]]},' 6796 ... ' "globalMechanisms": {},' 6797 ... ' "nameLookup": {"A": [0], "B": [1], "C": [2]}' 6798 ... '}' 6799 ... ) 6800 >>> for i in range(len(j)): 6801 ... if j[i] != expected[i:i+1]: 6802 ... print( 6803 ... 'exp: ' + expected[i-10:i+50] + '\\ngot: ' + j[i-10:i+50] 6804 ... ) 6805 ... break 6806 >>> j == expected 6807 True 6808 >>> rec = fromJSON(j) 6809 >>> rec.nodes == dg.nodes 6810 True 6811 >>> rec.edges == dg.edges 6812 True 6813 >>> rec.unknownCount == dg.unknownCount 6814 True 6815 >>> rec.equivalences == dg.equivalences 6816 True 6817 >>> rec.reversionTypes == dg.reversionTypes 6818 True 6819 >>> rec._byEdge == dg._byEdge 6820 True 6821 >>> rec.zones == dg.zones 6822 True 6823 >>> for diff in dg.listDifferences(rec): 6824 ... print(diff) 6825 >>> rec == dg 6826 True 6827 6828 `base.MetricSpace` example: 6829 6830 >>> ms = base.MetricSpace("test") 6831 >>> ms.addPoint([2, 3]) 6832 0 6833 >>> ms.addPoint([2, 7, 0]) 6834 1 6835 >>> ms.addPoint([2, 7]) 6836 2 6837 >>> toJSON(ms) 6838 '{"^^d": "MS", "name": "test",\ 6839 "points": {"^^d": "d", "items": [[0, [2, 3]], [1, [2, 7,\ 6840 0]], [2, [2, 7]]]}, "lastID": 2}' 6841 >>> ms.removePoint(0) 6842 >>> ms.removePoint(1) 6843 >>> ms.removePoint(2) 6844 >>> toJSON(ms) 6845 '{"^^d": "MS", "name": "test", "points": {}, "lastID": 2}' 6846 >>> ms.addPoint([5, 6]) 6847 3 6848 >>> ms.addPoint([7, 8]) 6849 4 6850 >>> toJSON(ms) 6851 '{"^^d": "MS", "name": "test",\ 6852 "points": {"^^d": "d", "items": [[3, [5, 6]], [4, [7, 8]]]}, "lastID": 4}' 6853 6854 # TODO: more examples, including one for a DiscreteExploration 6855 """ 6856 6857 def default(self, o: Any) -> Any: 6858 """ 6859 Re-writes objects for encoding. We re-write the following 6860 objects: 6861 6862 - `set` 6863 - `dict` (if the keys aren't all strings) 6864 - `tuple`/`namedtuple` 6865 - `ZoneInfo` 6866 - `Requirement` 6867 - `SkillCombination` 6868 - `DecisionGraph` 6869 - `DiscreteExploration` 6870 - `MetricSpace` 6871 6872 TODO: FeatureGraph... 6873 """ 6874 if isinstance(o, list): 6875 return [self.default(x) for x in o] 6876 6877 elif isinstance(o, set): 6878 return { 6879 '^^d': 's', 6880 'values': sorted( 6881 [self.default(e) for e in o], 6882 key=lambda x: str(x) 6883 ) 6884 } 6885 6886 elif isinstance(o, dict): 6887 if all(isinstance(k, str) for k in o): 6888 return { 6889 k: self.default(v) 6890 for k, v in o.items() 6891 } 6892 else: 6893 return { 6894 '^^d': 'd', 6895 'items': [ 6896 [self.default(k), self.default(v)] 6897 for (k, v) in o.items() 6898 ] 6899 } 6900 6901 elif isinstance(o, tuple): 6902 if hasattr(o, '_fields') and hasattr(o, '_asdict'): 6903 # Named tuple 6904 return { 6905 '^^d': 'nt', 6906 'name': o.__class__.__name__, 6907 'values': { 6908 k: self.default(v) 6909 for k, v in o._asdict().items() 6910 } 6911 } 6912 else: 6913 # Normal tuple 6914 return { 6915 '^^d': 't', 6916 "values": [self.default(e) for e in o] 6917 } 6918 6919 elif isinstance(o, base.Requirement): 6920 return { 6921 '^^d': 'R', 6922 'value': o.unparse() 6923 } 6924 6925 elif isinstance(o, base.SkillCombination): 6926 return { 6927 '^^d': 'SC', 6928 'value': o.unparse() 6929 } 6930 # TODO: Consequence, Condition, Challenge, and Effect here? 6931 6932 elif isinstance(o, core.DecisionGraph): 6933 return { 6934 '^^d': 'DG', 6935 'props': self.default(o.graph), # type:ignore [attr-defined] 6936 'node_links': self.default( 6937 networkx.node_link_data(o, edges="links") # type: ignore 6938 # TODO: Fix networkx stubs 6939 ), 6940 '_byEdge': self.default(o._byEdge), 6941 'zones': self.default(o.zones), 6942 'unknownCount': o.unknownCount, 6943 'equivalences': self.default(o.equivalences), 6944 'reversionTypes': self.default(o.reversionTypes), 6945 'nextID': o.nextID, 6946 'nextMechanismID': o.nextMechanismID, 6947 'mechanisms': self.default(o.mechanisms), 6948 'globalMechanisms': self.default(o.globalMechanisms), 6949 'nameLookup': self.default(o.nameLookup) 6950 } 6951 6952 elif isinstance(o, core.DiscreteExploration): 6953 return { 6954 '^^d': 'DE', 6955 'situations': self.default(o.situations) 6956 } 6957 6958 elif isinstance(o, base.MetricSpace): 6959 return { 6960 '^^d': 'MS', 6961 'name': o.name, 6962 'points': self.default(o.points), 6963 'lastID': o.lastID() 6964 } 6965 6966 else: 6967 return o 6968 6969 def encode(self, o: Any) -> str: 6970 """ 6971 Custom encode function since we need to override behavior for 6972 tuples and dicts. 6973 """ 6974 if isinstance(o, (tuple, dict, set)): 6975 o = self.default(o) 6976 elif isinstance(o, list): 6977 o = [self.default(x) for x in o] 6978 6979 try: 6980 return super().encode(o) 6981 except TypeError: 6982 return super().encode(self.default(o)) 6983 6984 def iterencode( 6985 self, 6986 o: Any, 6987 _one_shot: bool = False 6988 ) -> Generator[str, None, None]: 6989 """ 6990 Custom iterencode function since we need to override behavior for 6991 tuples and dicts. 6992 """ 6993 if isinstance(o, (tuple, dict)): 6994 o = self.default(o) 6995 6996 yield from super().iterencode(o, _one_shot=_one_shot) 6997 6998 6999class CustomJSONDecoder(json.JSONDecoder): 7000 """ 7001 A custom JSON decoder that has special protocols for handling 7002 several types, including: 7003 7004 - `set` 7005 - `tuple` & `namedtuple` 7006 - `dict` (where keys aren't all strings) 7007 - `Requirement` 7008 - `SkillCombination` 7009 - `DecisionGraph` 7010 - `DiscreteExploration` 7011 - `MetricSpace` 7012 7013 Used by `toJSON` 7014 7015 When initializing it, you can st a custom parse format by supplying 7016 a 'parseFormat' keyword argument; by default a standard 7017 `ParseFormat` will be used. 7018 7019 Examples: 7020 7021 >>> r = base.ReqAny([ 7022 ... base.ReqCapability('power'), 7023 ... base.ReqTokens('money', 5) 7024 ... ]) 7025 >>> s = toJSON(r) 7026 >>> s 7027 '{"^^d": "R", "value": "(power|money*5)"}' 7028 >>> l = fromJSON(s) 7029 >>> r == l 7030 True 7031 >>> o = {1, 2, 'hi'} 7032 >>> s = toJSON(o) 7033 >>> s 7034 '{"^^d": "s", "values": [1, 2, "hi"]}' 7035 >>> l = fromJSON(s) 7036 >>> o == l 7037 True 7038 >>> zi = base.ZoneInfo(1, set(), set(), {}, []) 7039 >>> s = toJSON(zi) 7040 >>> c = ( 7041 ... '{"^^d": "nt", "name": "ZoneInfo", "values": {' 7042 ... '"level": 1,' 7043 ... ' "parents": {"^^d": "s", "values": []},' 7044 ... ' "contents": {"^^d": "s", "values": []},' 7045 ... ' "tags": {},' 7046 ... ' "annotations": []' 7047 ... '}}' 7048 ... ) 7049 >>> s == c 7050 True 7051 >>> setm = base.effect(set=("door", "open")) 7052 >>> s = toJSON(setm) 7053 >>> f = fromJSON(s) 7054 >>> f == setm 7055 True 7056 >>> pf = ParseFormat() 7057 >>> pf.unparseEffect(f) 7058 'set door:open' 7059 >>> pf.unparseEffect(f) == pf.unparseEffect(setm) 7060 True 7061 >>> g = core.DecisionGraph() 7062 >>> g.addDecision('A') 7063 0 7064 >>> g.addDecision('B') 7065 1 7066 >>> g.addTransition('A', 'up', 'B', 'down') 7067 >>> g2 = fromJSON(toJSON(g)) 7068 >>> g2.destinationsFrom('A') 7069 {'up': 1} 7070 >>> g2.destinationsFrom('B') 7071 {'down': 0} 7072 >>> g2.addDecision('C') 7073 2 7074 >>> g2.addTransition('A', 'right', 'C', 'left') 7075 >>> g2.destinationsFrom('A') 7076 {'up': 1, 'right': 2} 7077 >>> g2.destinationsFrom('C') 7078 {'left': 0} 7079 7080 TODO: SkillCombination example 7081 """ 7082 def __init__(self, *args, **kwargs): 7083 if 'object_hook' in kwargs: 7084 outerHook = kwargs['object_hook'] 7085 kwargs['object_hook'] = ( 7086 lambda o: outerHook(self.unpack(o)) 7087 ) 7088 # TODO: What if it's a positional argument? :( 7089 else: 7090 kwargs['object_hook'] = lambda o: self.unpack(o) 7091 7092 if 'parseFormat' in kwargs: 7093 self.parseFormat = kwargs['parseFormat'] 7094 del kwargs['parseFormat'] 7095 else: 7096 self.parseFormat = ParseFormat() 7097 7098 super().__init__(*args, **kwargs) 7099 7100 def unpack(self, obj: Any) -> Any: 7101 """ 7102 Unpacks an object; used as the `object_hook` for decoding. 7103 """ 7104 if '^^d' in obj: 7105 asType = obj['^^d'] 7106 if asType == 't': 7107 return tuple(obj['values']) 7108 7109 elif asType == 'nt': 7110 g = globals() 7111 name = obj['name'] 7112 values = obj['values'] 7113 # Use an existing global namedtuple class if there is 7114 # one that goes by the specified name, so that we don't 7115 # create too many spurious equivalent namedtuple 7116 # classes. But fall back on creating a new namedtuple 7117 # class if we need to: 7118 ntClass = g.get(name) 7119 if ( 7120 ntClass is None 7121 or not issubclass(ntClass, tuple) 7122 or not hasattr(ntClass, '_asdict') 7123 ): 7124 # Now try again specifically in the base module where 7125 # most of our nametuples are defined (TODO: NOT this 7126 # hack..., but it does make isinstance work...) 7127 ntClass = getattr(base, name, None) 7128 if ( 7129 ntClass is None 7130 or not issubclass(ntClass, tuple) 7131 or not hasattr(ntClass, '_asdict') 7132 ): 7133 # TODO: cache these... 7134 ntClass = collections.namedtuple( # type: ignore 7135 name, 7136 values.keys() 7137 ) 7138 ntClass = cast(Callable, ntClass) 7139 return ntClass(**values) 7140 7141 elif asType == 's': 7142 return set(obj['values']) 7143 7144 elif asType == 'd': 7145 return dict(obj['items']) 7146 7147 elif asType == 'R': 7148 return self.parseFormat.parseRequirement(obj['value']) 7149 7150 elif asType == 'SC': 7151 return self.parseFormat.parseSkillCombination(obj['value']) 7152 7153 elif asType == 'E': 7154 return self.parseFormat.parseEffect(obj['value']) 7155 7156 elif asType == 'Ch': 7157 return self.parseFormat.parseChallenge(obj['value']) 7158 7159 elif asType == 'Cd': 7160 return self.parseFormat.parseCondition(obj['value']) 7161 7162 elif asType == 'Cq': 7163 return self.parseFormat.parseConsequence(obj['value']) 7164 7165 elif asType == 'DG': 7166 baseGraph: networkx.MultiDiGraph = networkx.node_link_graph( 7167 obj['node_links'], 7168 edges="links" 7169 ) # type: ignore 7170 # TODO: Fix networkx stubs 7171 graphResult = core.DecisionGraph() 7172 # Copy over instance attributes minus internals 7173 # TODO: Do we need internals? 7174 for (attr, val) in baseGraph.__dict__.items(): 7175 # Note: __dict__ over dir() here to avoid attributes 7176 # of type and get just attributes of instance 7177 if attr == "name": # name will get copied below 7178 continue 7179 if not attr.startswith('__') or not attr.endswith('__'): 7180 setattr( 7181 graphResult, 7182 attr, 7183 copy.deepcopy(val) 7184 # TODO: Does this copying disentangle too 7185 # much? Which values even get copied this 7186 # way? 7187 ) 7188 7189 if baseGraph.name != '': 7190 graphResult.name = baseGraph.name 7191 graphResult.graph.update(obj['props']) # type:ignore [attr-defined] # noqa 7192 storedByEdge = obj['_byEdge'] 7193 graphResult._byEdge = { 7194 int(k): storedByEdge[k] 7195 for k in storedByEdge 7196 } 7197 graphResult.zones = obj['zones'] 7198 graphResult.unknownCount = obj['unknownCount'] 7199 graphResult.equivalences = obj['equivalences'] 7200 graphResult.reversionTypes = obj['reversionTypes'] 7201 graphResult.nextID = obj.get('nextID') 7202 # Old code didn't store nextID; we extrapolate if necessary 7203 if graphResult.nextID is None: 7204 graphResult.nextID = max(graphResult.nodes) + 1 7205 graphResult.nextMechanismID = obj['nextMechanismID'] 7206 graphResult.mechanisms = { 7207 int(k): v 7208 for k, v in 7209 obj['mechanisms'].items() 7210 } 7211 graphResult.globalMechanisms = obj['globalMechanisms'] 7212 graphResult.nameLookup = obj['nameLookup'] 7213 return graphResult 7214 7215 elif asType == 'DE': 7216 exResult = core.DiscreteExploration() 7217 exResult.situations = obj['situations'] 7218 return exResult 7219 7220 elif asType == 'MS': 7221 msResult = base.MetricSpace(obj['name']) 7222 msResult.points = obj['points'] 7223 msResult.nextID = obj['lastID'] + 1 7224 return msResult 7225 7226 else: 7227 raise NotImplementedError( 7228 f"No special handling has been defined for" 7229 f" decoding type '{asType}'." 7230 ) 7231 7232 else: 7233 return obj
These are the different separators, grouping characters, and keywords
used as part of parsing. The characters that are actually recognized are
defined as part of a Format.
Inherited Members
- enum.Enum
- name
- value
- builtins.int
- conjugate
- bit_length
- bit_count
- to_bytes
- from_bytes
- as_integer_ratio
- real
- imag
- numerator
- denominator
A journal format is specified using a dictionary with keys that denote journal marker types and values which are one-to-several-character strings indicating the markup used for that entry/info type.
The default parsing format.
Default names for each effect type. Maps names to canonical effect type strings. A different mapping could be used to allow for writing effect names in another language, for example.
Default names for each domain focalization type. Maps each focalization type string to itself.
Default characters used to indicate success/failure when transcribing a
TransitionWithOutcomes.
140class ParseWarning(Warning): 141 """ 142 Represents a warning encountered when parsing something. 143 """ 144 pass
Represents a warning encountered when parsing something.
Inherited Members
- builtins.Warning
- Warning
- builtins.BaseException
- with_traceback
- add_note
- args
147class ParseError(ValueError): 148 """ 149 Represents a error encountered when parsing. 150 """ 151 pass
Represents a error encountered when parsing.
Inherited Members
- builtins.ValueError
- ValueError
- builtins.BaseException
- with_traceback
- add_note
- args
154class DotParseError(ParseError): 155 """ 156 An error raised during parsing when incorrectly-formatted graphviz 157 "dot" data is provided. See `parseDot`. 158 """ 159 pass
An error raised during parsing when incorrectly-formatted graphviz
"dot" data is provided. See parseDot.
Inherited Members
- builtins.ValueError
- ValueError
- builtins.BaseException
- with_traceback
- add_note
- args
162class InvalidFeatureSpecifierError(ParseError): 163 """ 164 An error used when a feature specifier is in the wrong format. 165 Errors with part specifiers also use this. 166 """
An error used when a feature specifier is in the wrong format. Errors with part specifiers also use this.
Inherited Members
- builtins.ValueError
- ValueError
- builtins.BaseException
- with_traceback
- add_note
- args
When lexing, we pull apart a string into pieces, but when we recognize lexemes, we use their integer IDs in the list instead of strings, so we get a list that's a mix of ints and strings.
Some parsing processes group tokens into sub-lists. This type represents
LexedTokens which might also contain sub-lists, to arbitrary depth.
Another intermediate parsing result during requirement parsing: a list
of base.Requirements possibly with some sub-lists and/or Lexemes
mixed in.
196def lex( 197 characters: str, 198 tokenMap: Optional[Dict[str, Lexeme]] = None 199) -> LexedTokens: 200 """ 201 Lexes a list of tokens from a characters string. Recognizes any 202 special characters you provide in the token map, as well as 203 collections of non-mapped characters. Recognizes double-quoted 204 strings which can contain any of those (and which use 205 backslash-escapes for internal double quotes) and includes quoted 206 versions of those strings as tokens (any token string starting with a 207 double quote will be such a string). Breaks tokens on whitespace 208 outside of quotation marks, and ignores that whitespace. 209 210 Examples: 211 212 >>> lex('abc') 213 ['abc'] 214 >>> lex('(abc)', {'(': 0, ')': 1}) 215 [0, 'abc', 1] 216 >>> lex('{(abc)}', {'(': 0, ')': 1, '{': 2, '}': 3}) 217 [2, 0, 'abc', 1, 3] 218 >>> lex('abc def') 219 ['abc', 'def'] 220 >>> lex('abc def') 221 ['abc', 'def'] 222 >>> lex('abc \\n def') 223 ['abc', 'def'] 224 >>> lex ('"quoted"') 225 ['"quoted"'] 226 >>> lex ('"quoted pair"') 227 ['"quoted pair"'] 228 >>> lex (' oneWord | "two words"|"three words words" ', {'|': 0}) 229 ['oneWord', 0, '"two words"', 0, '"three words words"'] 230 >>> tokenMap = { c: i for (i, c) in enumerate("(){}~:;>,") } 231 >>> tokenMap['::'] = 9 232 >>> tokenMap['~~'] = 10 233 >>> lex( 234 ... '{~~2:best(brains, brawn)>{set switch on}' 235 ... '{deactivate ,1; bounce}}', 236 ... tokenMap 237 ... ) 238 [2, 10, '2', 5, 'best', 0, 'brains', 8, 'brawn', 1, 7, 2, 'set',\ 239 'switch', 'on', 3, 2, 'deactivate', 8, '1', 6, 'bounce', 3, 3] 240 >>> lex('set where::mechanism state', tokenMap) 241 ['set', 'where', 9, 'mechanism', 'state'] 242 >>> # Note r' doesn't take full effect 'cause we're in triple quotes 243 >>> esc = r'"escape \\\\a"' 244 >>> result = [ r'"escape \\\\a"' ] # 'quoted' doubles the backslash 245 >>> len(esc) 246 12 247 >>> len(result[0]) 248 12 249 >>> lex(esc) == result 250 True 251 >>> quoteInQuote = r'before "hello \\\\ \\" goodbye"after' 252 >>> # Note r' doesn't take full effect 'cause we're in triple quotes 253 >>> expect = ['before', r'"hello \\\\ \\" goodbye"', 'after'] 254 >>> lex(quoteInQuote) == expect 255 True 256 >>> lex('O\\'Neill') 257 ["O'Neill"] 258 >>> lex('one "quote ') 259 ['one', '"quote "'] 260 >>> lex('geo*15', {'*': 0}) 261 ['geo', 0, '15'] 262 """ 263 if tokenMap is None: 264 tokenMap = {} 265 tokenStarts: Dict[str, List[str]] = {} 266 for key in sorted(tokenMap.keys(), key=lambda x: -len(x)): 267 tokenStarts.setdefault(key[:1], []).append(key) 268 tokens: LexedTokens = [] 269 sofar = '' 270 inQuote = False 271 escaped = False 272 skip = 0 273 for i in range(len(characters)): 274 if skip > 0: 275 skip -= 1 276 continue 277 278 char = characters[i] 279 if escaped: 280 # TODO: Escape sequences? 281 sofar += char 282 escaped = False 283 284 elif char == '\\': 285 if inQuote: 286 escaped = True 287 else: 288 sofar += char 289 290 elif char == '"': 291 if sofar != '': 292 if inQuote: 293 tokens.append(utils.quoted(sofar)) 294 else: 295 tokens.append(sofar) 296 sofar = '' 297 inQuote = not inQuote 298 299 elif inQuote: 300 sofar += char 301 302 elif char in tokenStarts: 303 options = tokenStarts[char] 304 hit: Optional[str] = None 305 for possibility in options: 306 lp = len(possibility) 307 if ( 308 (lp == 1 and char == possibility) 309 or characters[i:i + lp] == possibility 310 ): 311 hit = possibility 312 break 313 314 if hit is not None: 315 if sofar != '': 316 tokens.append(sofar) 317 tokens.append(tokenMap[possibility]) 318 sofar = '' 319 skip = len(hit) - 1 320 else: # Not actually a recognized token 321 sofar += char 322 323 elif char.isspace(): 324 if sofar != '': 325 tokens.append(sofar) 326 sofar = '' 327 328 else: 329 sofar += char 330 331 if sofar != '': 332 if inQuote: 333 tokens.append(utils.quoted(sofar)) 334 else: 335 tokens.append(sofar) 336 337 return tokens
Lexes a list of tokens from a characters string. Recognizes any special characters you provide in the token map, as well as collections of non-mapped characters. Recognizes double-quoted strings which can contain any of those (and which use backslash-escapes for internal double quotes) and includes quoted versions of those strings as tokens (any token string starting with a double quote will be such a string). Breaks tokens on whitespace outside of quotation marks, and ignores that whitespace.
Examples:
>>> lex('abc')
['abc']
>>> lex('(abc)', {'(': 0, ')': 1})
[0, 'abc', 1]
>>> lex('{(abc)}', {'(': 0, ')': 1, '{': 2, '}': 3})
[2, 0, 'abc', 1, 3]
>>> lex('abc def')
['abc', 'def']
>>> lex('abc def')
['abc', 'def']
>>> lex('abc \n def')
['abc', 'def']
>>> lex ('"quoted"')
['"quoted"']
>>> lex ('"quoted pair"')
['"quoted pair"']
>>> lex (' oneWord | "two words"|"three words words" ', {'|': 0})
['oneWord', 0, '"two words"', 0, '"three words words"']
>>> tokenMap = { c: i for (i, c) in enumerate("(){}~:;>,") }
>>> tokenMap['::'] = 9
>>> tokenMap['~~'] = 10
>>> lex(
... '{~~2:best(brains, brawn)>{set switch on}'
... '{deactivate ,1; bounce}}',
... tokenMap
... )
[2, 10, '2', 5, 'best', 0, 'brains', 8, 'brawn', 1, 7, 2, 'set', 'switch', 'on', 3, 2, 'deactivate', 8, '1', 6, 'bounce', 3, 3]
>>> lex('set where::mechanism state', tokenMap)
['set', 'where', 9, 'mechanism', 'state']
>>> # Note r' doesn't take full effect 'cause we're in triple quotes
>>> esc = r'"escape \\a"'
>>> result = [ r'"escape \\a"' ] # 'quoted' doubles the backslash
>>> len(esc)
12
>>> len(result[0])
12
>>> lex(esc) == result
True
>>> quoteInQuote = r'before "hello \\ \" goodbye"after'
>>> # Note r' doesn't take full effect 'cause we're in triple quotes
>>> expect = ['before', r'"hello \\ \" goodbye"', 'after']
>>> lex(quoteInQuote) == expect
True
>>> lex('O\'Neill')
["O'Neill"]
>>> lex('one "quote ')
['one', '"quote "']
>>> lex('geo*15', {'*': 0})
['geo', 0, '15']
340def unLex( 341 tokens: LexedTokens, 342 tokenMap: Optional[Dict[str, Lexeme]] = None 343) -> str: 344 """ 345 Turns lexed stuff back into a string, substituting strings back into 346 token spots by reversing the given token map. Adds quotation marks to 347 complex tokens where necessary to prevent them from re-lexing into 348 multiple tokens (but `lex` doesn't remove those, so in some cases 349 there's not a perfect round-trip unLex -> lex). 350 351 For example: 352 353 >>> unLex(['a', 'b']) 354 'a b' 355 >>> tokens = {'(': 0, ')': 1, '{': 2, '}': 3, '::': 4} 356 >>> unLex([0, 'hi', 1], tokens) 357 '(hi)' 358 >>> unLex([0, 'visit', 'zone', 4, 'decision', 1], tokens) 359 '(visit zone::decision)' 360 >>> q = unLex(['a complex token', '\\'single\\' and "double" quotes']) 361 >>> q # unLex adds quotes 362 '"a complex token" "\\'single\\' and \\\\"double\\\\" quotes"' 363 >>> lex(q) # Not the same as the original list 364 ['"a complex token"', '"\\'single\\' and \\\\"double\\\\" quotes"'] 365 >>> lex(unLex(lex(q))) # But further round-trips work 366 ['"a complex token"', '"\\'single\\' and \\\\"double\\\\" quotes"'] 367 368 TODO: Fix this: 369 For now, it generates incorrect results when token combinations can 370 be ambiguous. These ambiguous token combinations should not ever be 371 generated by `lex` at least. For example: 372 373 >>> ambiguous = {':': 0, '::': 1} 374 >>> u = unLex(['a', 0, 0, 'b'], ambiguous) 375 >>> u 376 'a::b' 377 >>> l = lex(u, ambiguous) 378 >>> l 379 ['a', 1, 'b'] 380 >>> l == u 381 False 382 """ 383 if tokenMap is None: 384 nTokens = 0 385 revMap = {} 386 else: 387 nTokens = len(tokenMap) 388 revMap = {y: x for (x, y) in tokenMap.items()} 389 390 prevRaw = False 391 # TODO: add spaces where necessary to disambiguate token sequences... 392 if len(revMap) != nTokens: 393 warnings.warn( 394 ( 395 "Irreversible token map! Two or more tokens have the same" 396 " integer value." 397 ), 398 ParseWarning 399 ) 400 401 result = "" 402 for item in tokens: 403 if isinstance(item, int): 404 try: 405 result += revMap[item] 406 except KeyError: 407 raise ValueError( 408 f"Tokens list contains {item} but the token map" 409 f" does not have any entry which maps to {item}." 410 ) 411 prevRaw = False 412 elif isinstance(item, str): 413 if prevRaw: 414 result += ' ' 415 if len(lex(item)) > 1: 416 result += utils.quoted(item) 417 else: 418 result += item 419 prevRaw = True 420 else: 421 raise TypeError( 422 f"Token list contained non-int non-str item:" 423 f" {repr(item)}" 424 ) 425 426 return result
Turns lexed stuff back into a string, substituting strings back into
token spots by reversing the given token map. Adds quotation marks to
complex tokens where necessary to prevent them from re-lexing into
multiple tokens (but lex doesn't remove those, so in some cases
there's not a perfect round-trip unLex -> lex).
For example:
>>> unLex(['a', 'b'])
'a b'
>>> tokens = {'(': 0, ')': 1, '{': 2, '}': 3, '::': 4}
>>> unLex([0, 'hi', 1], tokens)
'(hi)'
>>> unLex([0, 'visit', 'zone', 4, 'decision', 1], tokens)
'(visit zone::decision)'
>>> q = unLex(['a complex token', '\'single\' and "double" quotes'])
>>> q # unLex adds quotes
'"a complex token" "\'single\' and \\"double\\" quotes"'
>>> lex(q) # Not the same as the original list
['"a complex token"', '"\'single\' and \\"double\\" quotes"']
>>> lex(unLex(lex(q))) # But further round-trips work
['"a complex token"', '"\'single\' and \\"double\\" quotes"']
TODO: Fix this:
For now, it generates incorrect results when token combinations can
be ambiguous. These ambiguous token combinations should not ever be
generated by lex at least. For example:
>>> ambiguous = {':': 0, '::': 1}
>>> u = unLex(['a', 0, 0, 'b'], ambiguous)
>>> u
'a::b'
>>> l = lex(u, ambiguous)
>>> l
['a', 1, 'b']
>>> l == u
False
433def normalizeEnds( 434 tokens: List, 435 start: int, 436 end: int 437) -> Tuple[int, int, int]: 438 """ 439 Given a tokens list and start & end integers, does some bounds 440 checking and normalization on the integers: converts negative 441 indices to positive indices, and raises an `IndexError` if they're 442 out-of-bounds after conversion. Returns a tuple containing the 443 normalized start & end indices, along with the number of tokens they 444 cover. 445 """ 446 totalTokens = len(tokens) 447 if start < -len(tokens): 448 raise IndexError( 449 f"Negative start index out of bounds (got {start} for" 450 f" {totalTokens} tokens)." 451 ) 452 elif start >= totalTokens: 453 raise IndexError( 454 f"Start index out of bounds (got {start} for" 455 f" {totalTokens} tokens)." 456 ) 457 elif start < 0: 458 start = totalTokens + start 459 460 if end < -len(tokens): 461 raise IndexError( 462 f"Negative end index out of bounds (got {end} for" 463 f" {totalTokens} tokens)." 464 ) 465 elif end >= totalTokens: 466 raise IndexError( 467 f"Start index out of bounds (got {end} for" 468 f" {totalTokens} tokens)." 469 ) 470 elif end < 0: 471 end = totalTokens + end 472 473 if end >= len(tokens): 474 end = len(tokens) - 1 475 476 return (start, end, (end - start) + 1)
Given a tokens list and start & end integers, does some bounds
checking and normalization on the integers: converts negative
indices to positive indices, and raises an IndexError if they're
out-of-bounds after conversion. Returns a tuple containing the
normalized start & end indices, along with the number of tokens they
cover.
479def findSeparatedParts( 480 tokens: LexedTokens, 481 sep: Union[str, int], 482 start: int = 0, 483 end: int = -1, 484 groupStart: Union[str, int, None] = None, 485 groupEnd: Union[str, int, None] = None 486) -> Generator[Tuple[int, int], None, None]: 487 """ 488 Finds parts separated by a separator lexeme, such as ';' or ',', but 489 ignoring separators nested within groupStart/groupEnd pairs (if 490 those arguments are supplied). For each token sequence found, yields 491 a tuple containing the start index and end index for that part, with 492 separators not included in the parts. 493 494 If two separators appear in a row, the start/end pair will have a 495 start index one after the end index. 496 497 If there are no separators, yields one pair containing the start and 498 end of the entire tokens sequence. 499 500 Raises a `ParseError` if there are unbalanced grouping elements. 501 502 For example: 503 504 >>> list(findSeparatedParts( 505 ... [ 'one' ], 506 ... Lexeme.sepOrDelay, 507 ... 0, 508 ... 0, 509 ... Lexeme.openParen, 510 ... Lexeme.closeParen 511 ... )) 512 [(0, 0)] 513 >>> list(findSeparatedParts( 514 ... [ 515 ... 'best', 516 ... Lexeme.openParen, 517 ... 'chess', 518 ... Lexeme.sepOrDelay, 519 ... 'checkers', 520 ... Lexeme.closeParen 521 ... ], 522 ... Lexeme.sepOrDelay, 523 ... 2, 524 ... 4, 525 ... Lexeme.openParen, 526 ... Lexeme.closeParen 527 ... )) 528 [(2, 2), (4, 4)] 529 """ 530 start, end, n = normalizeEnds(tokens, start, end) 531 level = 0 532 thisStart = start 533 for i in range(start, end + 1): 534 token = tokens[i] 535 if token == sep and level == 0: 536 yield (thisStart, i - 1) 537 thisStart = i + 1 538 elif token == groupStart: 539 level += 1 540 elif token == groupEnd: 541 level -= 1 542 if level < 0: 543 raise ParseError("Unbalanced grouping tokens.") 544 if level < 0: 545 raise ParseError("Unbalanced grouping tokens.") 546 yield (thisStart, end)
Finds parts separated by a separator lexeme, such as ';' or ',', but ignoring separators nested within groupStart/groupEnd pairs (if those arguments are supplied). For each token sequence found, yields a tuple containing the start index and end index for that part, with separators not included in the parts.
If two separators appear in a row, the start/end pair will have a start index one after the end index.
If there are no separators, yields one pair containing the start and end of the entire tokens sequence.
Raises a ParseError if there are unbalanced grouping elements.
For example:
>>> list(findSeparatedParts(
... [ 'one' ],
... Lexeme.sepOrDelay,
... 0,
... 0,
... Lexeme.openParen,
... Lexeme.closeParen
... ))
[(0, 0)]
>>> list(findSeparatedParts(
... [
... 'best',
... Lexeme.openParen,
... 'chess',
... Lexeme.sepOrDelay,
... 'checkers',
... Lexeme.closeParen
... ],
... Lexeme.sepOrDelay,
... 2,
... 4,
... Lexeme.openParen,
... Lexeme.closeParen
... ))
[(2, 2), (4, 4)]
Type variable for dictionary keys.
Type variable for dictionary values.
555def checkCompleteness( 556 name, 557 mapping: Dict[K, V], 558 keysSet: Optional[Set[K]] = None, 559 valuesSet: Optional[Set[V]] = None 560): 561 """ 562 Checks that a dictionary has a certain exact set of keys (or 563 values). Raises a `ValueError` if it finds an extra or missing key 564 or value. 565 """ 566 if keysSet is not None: 567 for key in mapping.keys(): 568 if key not in keysSet: 569 raise ValueError("{name} has extra key {repr(key)}.") 570 571 for key in keysSet: 572 if key not in mapping: 573 raise ValueError("{name} is missing key {repr(key)}.") 574 575 if valuesSet is not None: 576 for value in mapping.values(): 577 if value not in valuesSet: 578 raise ValueError("{name} has extra value {repr(value)}.") 579 580 checkVals = mapping.values() 581 for value in valuesSet: 582 if value not in checkVals: 583 raise ValueError("{name} is missing value {repr(value)}.")
Checks that a dictionary has a certain exact set of keys (or
values). Raises a ValueError if it finds an extra or missing key
or value.
586class ParseFormat: 587 """ 588 A ParseFormat manages the mapping from markers to entry types and 589 vice versa. 590 """ 591 def __init__( 592 self, 593 formatDict: Format = DEFAULT_FORMAT, 594 effectNames: Dict[str, base.EffectType] = DEFAULT_EFFECT_NAMES, 595 focalizationNames: Dict[ 596 str, 597 base.DomainFocalization 598 ] = DEFAULT_FOCALIZATION_NAMES, 599 successFailureIndicators: Tuple[str, str] = DEFAULT_SF_INDICATORS 600 ): 601 """ 602 Sets up the parsing format. Requires a `Format` dictionary to 603 define the specifics. Raises a `ValueError` unless the keys of 604 the `Format` dictionary exactly match the `Lexeme` values. 605 """ 606 self.formatDict = formatDict 607 self.effectNames = effectNames 608 self.focalizationNames = focalizationNames 609 if ( 610 len(successFailureIndicators) != 2 611 or any(len(i) != 1 for i in successFailureIndicators) 612 ): 613 raise ValueError( 614 f"Invalid success/failure indicators: must be a pair of" 615 f" length-1 strings. Got: {successFailureIndicators!r}" 616 ) 617 self.successIndicator, self.failureIndicator = ( 618 successFailureIndicators 619 ) 620 621 # Check completeness for each dictionary 622 checkCompleteness('formatDict', self.formatDict, set(Lexeme)) 623 checkCompleteness( 624 'effectNames', 625 self.effectNames, 626 valuesSet=set(get_args(base.EffectType)) 627 ) 628 checkCompleteness( 629 'focalizationNames', 630 self.focalizationNames, 631 valuesSet=set(get_args(base.DomainFocalization)) 632 ) 633 634 # Build some reverse lookup dictionaries for specific 635 self.reverseFormat = {y: x for (x, y) in self.formatDict.items()} 636 637 # circumstances: 638 self.effectModMap = { 639 self.formatDict[x]: x 640 for x in [ 641 Lexeme.effectCharges, 642 Lexeme.sepOrDelay, 643 Lexeme.inCommon, 644 Lexeme.isHidden 645 ] 646 } 647 648 def lex(self, content: str) -> LexedTokens: 649 """ 650 Applies `lex` using this format's lexeme mapping. 651 """ 652 return lex(content, self.reverseFormat) 653 654 def onOff(self, word: str) -> Optional[bool]: 655 """ 656 Parse an on/off indicator and returns a boolean (`True` for on 657 and `False` for off). Returns `None` if the word isn't either 658 the 'on' or the 'off' word. Generates a `ParseWarning` 659 (and still returns `None`) if the word is a case-swapped version 660 of the 'on' or 'off' word and is not equal to either of them. 661 """ 662 onWord = self.formatDict[Lexeme.stateOn] 663 offWord = self.formatDict[Lexeme.stateOff] 664 665 # Generate warning if we suspect a case error 666 if ( 667 word.casefold() in (onWord, offWord) 668 and word not in (onWord, offWord) 669 ): 670 warnings.warn( 671 ( 672 f"Word '{word}' cannot be interpreted as an on/off" 673 f" value, although it is almost one (the correct" 674 f" values are '{onWord}' and '{offWord}'." 675 ), 676 ParseWarning 677 ) 678 679 # return the appropriate value 680 if word == onWord: 681 return True 682 elif word == offWord: 683 return False 684 else: 685 return None 686 687 def matchingBrace( 688 self, 689 tokens: LexedTokens, 690 where: int, 691 opener: int = Lexeme.openCurly, 692 closer: int = Lexeme.closeCurly 693 ) -> int: 694 """ 695 Returns the index within the given tokens list of the closing 696 curly brace which matches the open brace at the specified index. 697 You can specify custom `opener` and/or `closer` lexemes to find 698 matching pairs of other things. Raises a `ParseError` if there 699 is no opening brace at the specified index, or if there isn't a 700 matching closing brace. Handles nested braces of the specified 701 type. 702 703 Examples: 704 >>> pf = ParseFormat() 705 >>> ob = Lexeme.openCurly 706 >>> cb = Lexeme.closeCurly 707 >>> pf.matchingBrace([ob, cb], 0) 708 1 709 >>> pf.matchingBrace([ob, cb], 1) 710 Traceback (most recent call last): 711 ... 712 exploration.parsing.ParseError: ... 713 >>> pf.matchingBrace(['hi', ob, cb], 0) 714 Traceback (most recent call last): 715 ... 716 exploration.parsing.ParseError: ... 717 >>> pf.matchingBrace(['hi', ob, cb], 1) 718 2 719 >>> pf.matchingBrace(['hi', ob, 'lo', cb], 1) 720 3 721 >>> pf.matchingBrace([ob, 'hi', 'lo', cb], 1) 722 Traceback (most recent call last): 723 ... 724 exploration.parsing.ParseError: ... 725 >>> pf.matchingBrace([ob, 'hi', 'lo', cb], 0) 726 3 727 >>> pf.matchingBrace([ob, ob, cb, cb], 0) 728 3 729 >>> pf.matchingBrace([ob, ob, cb, cb], 1) 730 2 731 >>> pf.matchingBrace([ob, cb, ob, cb], 0) 732 1 733 >>> pf.matchingBrace([ob, cb, ob, cb], 2) 734 3 735 >>> pf.matchingBrace([ob, cb, cb, cb], 0) 736 1 737 >>> pf.matchingBrace([ob, ob, ob, cb], 0) 738 Traceback (most recent call last): 739 ... 740 exploration.parsing.ParseError: ... 741 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 0) 742 7 743 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 1) 744 6 745 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 2) 746 Traceback (most recent call last): 747 ... 748 exploration.parsing.ParseError: ... 749 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 3) 750 4 751 >>> op = Lexeme.openParen 752 >>> cp = Lexeme.closeParen 753 >>> pf.matchingBrace([ob, op, ob, cp], 1, op, cp) 754 3 755 """ 756 if where >= len(tokens): 757 raise ParseError( 758 f"Out-of-bounds brace start: index {where} with" 759 f" {len(tokens)} tokens." 760 ) 761 if tokens[where] != opener: 762 raise ParseError( 763 f"Can't find matching brace for token" 764 f" {repr(tokens[where])} at index {where} because it's" 765 f" not an open brace." 766 ) 767 768 level = 1 769 for i in range(where + 1, len(tokens)): 770 token = tokens[i] 771 if token == opener: 772 level += 1 773 elif token == closer: 774 level -= 1 775 if level == 0: 776 return i 777 778 raise ParseError( 779 f"Failed to find matching curly brace from index {where}." 780 ) 781 782 def parseFocalization(self, word: str) -> base.DomainFocalization: 783 """ 784 Parses a focalization type for a domain, recognizing 785 'domainFocalizationSingular', 'domainFocalizationPlural', and 786 'domainFocalizationSpreading'. 787 """ 788 try: 789 return self.focalizationNames[word] 790 except KeyError: 791 raise ParseError( 792 f"Invalid domain focalization name {repr(word)}. Valid" 793 f" name are: {repr(list(self.focalizationNames))}'." 794 ) 795 796 def parseTagValue(self, value: str) -> base.TagValue: 797 """ 798 Converts a string to a tag value, following these rules: 799 800 1. If the string is exactly one of 'None', 'True', or 'False', we 801 convert it to the corresponding Python value. 802 2. If the string can be converted to an integer without raising a 803 ValueError, we use that integer. 804 3. If the string can be converted to a float without raising a 805 ValueError, we use that float. 806 4. Otherwise, it remains a string. 807 808 Note that there is currently no syntax for using list, dictionary, 809 Requirement, or Consequence tag values. 810 TODO: Support those types? 811 812 Examples: 813 814 >>> pf = ParseFormat() 815 >>> pf.parseTagValue('hi') 816 'hi' 817 >>> pf.parseTagValue('3') 818 3 819 >>> pf.parseTagValue('3.0') 820 3.0 821 >>> pf.parseTagValue('True') 822 True 823 >>> pf.parseTagValue('False') 824 False 825 >>> pf.parseTagValue('None') is None 826 True 827 >>> pf.parseTagValue('none') 828 'none' 829 """ 830 # TODO: Allow these keywords to be redefined? 831 if value == 'True': 832 return True 833 elif value == 'False': 834 return False 835 elif value == 'None': 836 return None 837 else: 838 try: 839 return int(value) 840 except ValueError: 841 try: 842 return float(value) 843 except ValueError: 844 return value 845 846 def unparseTagValue(self, value: base.TagValue) -> str: 847 """ 848 Converts a tag value into a string that would be parsed back into a 849 tag value via `parseTagValue`. Currently does not work for list, 850 dictionary, Requirement, or Consequence values. 851 TODO: Those 852 """ 853 return str(value) 854 855 def hasZoneParts(self, name: str) -> bool: 856 """ 857 Returns true if the specified name contains zone parts (using 858 the `zoneSeparator`). 859 """ 860 return self.formatDict[Lexeme.zoneSeparator] in name 861 862 def splitZone( 863 self, 864 name: str 865 ) -> Tuple[List[base.Zone], base.DecisionName]: 866 """ 867 Splits a decision name that includes zone information into the 868 list-of-zones part and the decision part. If there is no zone 869 information in the name, the list-of-zones will be an empty 870 list. 871 """ 872 sep = self.formatDict[Lexeme.zoneSeparator] 873 parts = name.split(sep) 874 return (list(parts[:-1]), parts[-1]) 875 876 def prefixWithZone( 877 self, 878 name: base.DecisionName, 879 zone: base.Zone 880 ) -> base.DecisionName: 881 """ 882 Returns the given decision name, prefixed with the given zone 883 name. Does NOT check whether the decision name already includes 884 a prefix or not. 885 """ 886 return zone + self.formatDict[Lexeme.zoneSeparator] + name 887 888 def parseAnyTransitionFromTokens( 889 self, 890 tokens: LexedTokens, 891 start: int = 0 892 ) -> Tuple[base.TransitionWithOutcomes, int]: 893 """ 894 Parses a `base.TransitionWithOutcomes` from a tokens list, 895 accepting either a transition name or a transition name followed 896 by a `Lexeme.withDetails` followed by a string of success and 897 failure indicator characters. Returns a tuple containing a 898 `base.TransitionWithOutcomes` and an integer indicating the end 899 index of the parsed item within the tokens. 900 """ 901 # Normalize start index so we can do index math 902 if start < 0: 903 useIndex = len(tokens) + start 904 else: 905 useIndex = start 906 907 try: 908 first = tokens[useIndex] 909 except IndexError: 910 raise ParseError( 911 f"Invalid token index: {start!r} among {len(tokens)}" 912 f" tokens." 913 ) 914 915 if isinstance(first, Lexeme): 916 raise ParseError( 917 f"Expecting a transition name (possibly with a" 918 f" success/failure indicator string) but first token is" 919 f" {first!r}." 920 ) 921 922 try: 923 second = tokens[useIndex + 1] 924 third = tokens[useIndex + 2] 925 except IndexError: 926 return ((first, []), useIndex) 927 928 if second != Lexeme.withDetails or isinstance(third, Lexeme): 929 return ((first, []), useIndex) 930 931 outcomes = [] 932 for char in third: 933 if char == self.successIndicator: 934 outcomes.append(True) 935 elif char == self.failureIndicator: 936 outcomes.append(False) 937 else: 938 return ((first, []), useIndex) 939 940 return ((first, outcomes), useIndex + 2) 941 942 def parseTransitionWithOutcomes( 943 self, 944 content: str 945 ) -> base.TransitionWithOutcomes: 946 """ 947 Takes a transition that may have outcomes listed as a series of 948 s/f strings after a colon and returns the corresponding 949 `TransitionWithOutcomes` tuple. Calls `lex` and then 950 `parseAnyTransitionFromTokens`. 951 """ 952 return self.parseAnyTransitionFromTokens(self.lex(content))[0] 953 954 def unparseTransitionWithOutocmes( 955 self, 956 transition: base.AnyTransition 957 ) -> str: 958 """ 959 Turns a `base.AnyTransition` back into a string that would parse 960 to an equivalent `base.TransitionWithOutcomes` via 961 `parseTransitionWithOutcomes`. If a bare `base.Transition` is 962 given, returns a string that would result in a 963 `base.TransitionWithOutcomes` that has an empty outcomes 964 sequence. 965 """ 966 if isinstance(transition, base.Transition): 967 return transition 968 elif ( 969 isinstance(transition, tuple) 970 and len(transition) == 2 971 and isinstance(transition[0], base.Transition) 972 and isinstance(transition[1], list) 973 and all(isinstance(sfi, bool) for sfi in transition[1]) 974 ): 975 if len(transition[1]) == 0: 976 return transition[0] 977 else: 978 result = transition[0] + self.formatDict[Lexeme.withDetails] 979 for outcome in transition[1]: 980 if outcome: 981 result += self.successIndicator 982 else: 983 result += self.failureIndicator 984 return result 985 else: 986 raise TypeError( 987 f"Invalid AnyTransition: neither a string, nor a" 988 f" length-2 tuple consisting of a string followed by a" 989 f" list of booleans. Got: {transition!r}" 990 ) 991 992 def parseSpecificTransition( 993 self, 994 content: str 995 ) -> Tuple[base.DecisionName, base.Transition]: 996 """ 997 Splits a decision:transition pair to the decision and transition 998 part, using a custom separator if one is defined. 999 """ 1000 sep = self.formatDict[Lexeme.withDetails] 1001 n = content.count(sep) 1002 if n == 0: 1003 raise ParseError( 1004 f"Cannot split '{content}' into a decision name and a" 1005 f" transition name (no separator '{sep}' found)." 1006 ) 1007 elif n > 1: 1008 raise ParseError( 1009 f"Cannot split '{content}' into a decision name and a" 1010 f" transition name (too many ({n}) '{sep}' separators" 1011 f" found)." 1012 ) 1013 else: 1014 return cast( 1015 Tuple[base.DecisionName, base.Transition], 1016 tuple(content.split(sep)) 1017 ) 1018 1019 def splitDirections( 1020 self, 1021 content: str 1022 ) -> Tuple[Optional[str], Optional[str]]: 1023 """ 1024 Splits a piece of text using the 'Lexeme.reciprocalSeparator' 1025 into two pieces. If there is no separator, the second piece will 1026 be `None`; if either side of the separator is blank, that side 1027 will be `None`, and if there is more than one separator, a 1028 `ParseError` will be raised. Whitespace will be stripped from 1029 both sides of each result. 1030 1031 Examples: 1032 1033 >>> pf = ParseFormat() 1034 >>> pf.splitDirections('abc / def') 1035 ('abc', 'def') 1036 >>> pf.splitDirections('abc def ') 1037 ('abc def', None) 1038 >>> pf.splitDirections('abc def /') 1039 ('abc def', None) 1040 >>> pf.splitDirections('/abc def') 1041 (None, 'abc def') 1042 >>> pf.splitDirections('a/b/c') # doctest: +IGNORE_EXCEPTION_DETAIL 1043 Traceback (most recent call last): 1044 ... 1045 ParseError: ... 1046 """ 1047 sep = self.formatDict[Lexeme.reciprocalSeparator] 1048 count = content.count(sep) 1049 if count > 1: 1050 raise ParseError( 1051 f"Too many split points ('{sep}') in content:" 1052 f" '{content}' (only one is allowed)." 1053 ) 1054 1055 elif count == 1: 1056 before, after = content.split(sep) 1057 before = before.strip() 1058 after = after.strip() 1059 return (before or None, after or None) 1060 1061 else: # no split points 1062 stripped = content.strip() 1063 if stripped: 1064 return stripped, None 1065 else: 1066 return None, None 1067 1068 def parseItem( 1069 self, 1070 item: str 1071 ) -> Union[ 1072 base.Capability, 1073 Tuple[base.Token, int], 1074 Tuple[base.MechanismName, base.MechanismState] 1075 ]: 1076 """ 1077 Parses an item, which is a capability (just a string), a 1078 token-type*number pair (returned as a tuple with the number 1079 converted to an integer), or a mechanism-name:state pair 1080 (returned as a tuple with the state as a string). The 1081 'Lexeme.tokenCount' and `Lexeme.mechanismSeparator` format 1082 values determine the separators that this looks for. 1083 """ 1084 tsep = self.formatDict[Lexeme.tokenCount] 1085 msep = self.formatDict[Lexeme.mechanismSeparator] 1086 if tsep in item: 1087 # It's a token w/ an associated count 1088 parts = item.split(tsep) 1089 if len(parts) != 2: 1090 raise ParseError( 1091 f"Item '{item}' has a '{tsep}' but doesn't separate" 1092 f" into a token type and a count." 1093 ) 1094 typ, count = parts 1095 try: 1096 num = int(count) 1097 except ValueError: 1098 raise ParseError( 1099 f"Item '{item}' has invalid token count '{count}'." 1100 ) 1101 1102 return (typ, num) 1103 elif msep in item: 1104 parts = item.split(msep) 1105 mechanism = msep.join(parts[:-1]) 1106 state = parts[-1] 1107 if mechanism.endswith(':'): 1108 # Just a zone-qualified name... 1109 return item 1110 else: 1111 return (mechanism, state) 1112 else: 1113 # It's just a capability 1114 return item 1115 1116 def unparseAnyDecision(self, decision: base.AnyDecisionSpecifier) -> str: 1117 """ 1118 Turns any kind of decision specifier (ID, 1119 `base.DecisionSpecifier`, or name string) into a string that 1120 should parse back using `parseDecisionSpecifier`. 1121 1122 Raises a `TypeError` if given something that isn't a decision 1123 identifier. 1124 1125 For example: 1126 1127 >>> pf = ParseFormat() 1128 >>> pf.unparseAnyDecision( 1129 ... base.DecisionSpecifier("domain", "zone", "D") 1130 ... ) 1131 'domain//zone::D' 1132 >>> pf.unparseAnyDecision(3) 1133 '3' 1134 >>> pf.unparseAnyDecision('D') 1135 'D' 1136 >>> pf.unparseAnyDecision('domain//zone::D') 1137 'domain//zone::D' 1138 >>> pf.unparseAnyDecision([1, 2]) 1139 Traceback (most recent call last): 1140 ... 1141 TypeError... 1142 """ 1143 if isinstance(decision, base.DecisionSpecifier): 1144 return self.unparseDecisionSpecifier(decision) 1145 elif isinstance(decision, (base.DecisionID, base.DecisionName)): 1146 # leave as-is OR convert integer ID to string 1147 return str(decision) 1148 else: 1149 raise TypeError( 1150 "Unrecognized decision identifier type " + type(decision) 1151 ) 1152 1153 def unparseDecisionSpecifier(self, spec: base.DecisionSpecifier) -> str: 1154 """ 1155 Turns a decision specifier back into a string, which would be 1156 parsed as a decision specifier as part of various different 1157 things. 1158 1159 For example: 1160 1161 >>> pf = ParseFormat() 1162 >>> pf.unparseDecisionSpecifier( 1163 ... base.DecisionSpecifier(None, None, 'where') 1164 ... ) 1165 'where' 1166 >>> pf.unparseDecisionSpecifier( 1167 ... base.DecisionSpecifier(None, 'zone', 'where') 1168 ... ) 1169 'zone::where' 1170 >>> pf.unparseDecisionSpecifier( 1171 ... base.DecisionSpecifier('domain', 'zone', 'where') 1172 ... ) 1173 'domain//zone::where' 1174 >>> pf.unparseDecisionSpecifier( 1175 ... base.DecisionSpecifier('domain', None, 'where') 1176 ... ) 1177 'domain//where' 1178 """ 1179 result = spec.name 1180 if spec.zone is not None: 1181 result = ( 1182 spec.zone 1183 + self.formatDict[Lexeme.zoneSeparator] 1184 + result 1185 ) 1186 if spec.domain is not None: 1187 result = ( 1188 spec.domain 1189 + self.formatDict[Lexeme.domainSeparator] 1190 + result 1191 ) 1192 return result 1193 1194 def unparseMechanismSpecifier( 1195 self, 1196 spec: base.MechanismSpecifier 1197 ) -> str: 1198 """ 1199 Turns a mechanism specifier back into a string, which would be 1200 parsed as a mechanism specifier as part of various different 1201 things. Note that a mechanism specifier with a zone part but no 1202 decision part is not valid, since it would parse as a decision 1203 part instead. 1204 1205 For example: 1206 1207 >>> pf = ParseFormat() 1208 >>> pf.unparseMechanismSpecifier( 1209 ... base.MechanismSpecifier(None, None, None, 'lever') 1210 ... ) 1211 'lever' 1212 >>> pf.unparseMechanismSpecifier( 1213 ... base.MechanismSpecifier('domain', 'zone', 'decision', 'door') 1214 ... ) 1215 'domain//zone::decision::door' 1216 >>> pf.unparseMechanismSpecifier( 1217 ... base.MechanismSpecifier('domain', None, None, 'door') 1218 ... ) 1219 'domain//door' 1220 >>> pf.unparseMechanismSpecifier( 1221 ... base.MechanismSpecifier(None, 'a', 'b', 'door') 1222 ... ) 1223 'a::b::door' 1224 >>> pf.unparseMechanismSpecifier( 1225 ... base.MechanismSpecifier(None, 'a', None, 'door') 1226 ... ) 1227 Traceback (most recent call last): 1228 ... 1229 exploration.base.InvalidMechanismSpecifierError... 1230 >>> pf.unparseMechanismSpecifier( 1231 ... base.MechanismSpecifier(None, None, 'a', 'door') 1232 ... ) 1233 'a::door' 1234 >>> pf.unparseMechanismSpecifier( 1235 ... base.MechanismSpecifier(None, None, 37, 'door') 1236 ... ) 1237 '37::door' 1238 """ 1239 if spec.decision is None and spec.zone is not None: 1240 raise base.InvalidMechanismSpecifierError( 1241 f"Mechanism specifier has a zone part but no decision" 1242 f" part; it cannot be unparsed since it would parse" 1243 f" differently:\n{spec}" 1244 ) 1245 result = spec.name 1246 if spec.decision is not None: 1247 result = ( 1248 str(spec.decision) 1249 + self.formatDict[Lexeme.zoneSeparator] 1250 + result 1251 ) 1252 if spec.zone is not None: 1253 result = ( 1254 spec.zone 1255 + self.formatDict[Lexeme.zoneSeparator] 1256 + result 1257 ) 1258 if spec.domain is not None: 1259 result = ( 1260 spec.domain 1261 + self.formatDict[Lexeme.domainSeparator] 1262 + result 1263 ) 1264 return result 1265 1266 def effectType(self, effectMarker: str) -> Optional[base.EffectType]: 1267 """ 1268 Returns the `base.EffectType` string corresponding to the 1269 given effect marker string. Returns `None` for an unrecognized 1270 marker. 1271 """ 1272 return self.effectNames.get(effectMarker) 1273 1274 def parseCommandFromTokens( 1275 self, 1276 tokens: LexedTokens, 1277 start: int = 0, 1278 end: int = -1 1279 ) -> commands.Command: 1280 """ 1281 Given tokens that specify a `commands.Command`, parses that 1282 command and returns it. Really just turns the tokens back into 1283 strings and calls `commands.command`. 1284 1285 For example: 1286 1287 >>> pf = ParseFormat() 1288 >>> t = ['val', '5'] 1289 >>> c = commands.command(*t) 1290 >>> pf.parseCommandFromTokens(t) == c 1291 True 1292 >>> t = ['op', Lexeme.tokenCount, '$val', '$val'] 1293 >>> c = commands.command('op', '*', '$val', '$val') 1294 >>> pf.parseCommandFromTokens(t) == c 1295 True 1296 """ 1297 start, end, nTokens = normalizeEnds(tokens, start, end) 1298 args: List[str] = [] 1299 for token in tokens[start:end + 1]: 1300 if isinstance(token, Lexeme): 1301 args.append(self.formatDict[token]) 1302 else: 1303 args.append(token) 1304 1305 if len(args) == 0: 1306 raise ParseError( 1307 f"No arguments for command:\n{tokens[start:end + 1]}" 1308 ) 1309 return commands.command(*args) 1310 1311 def unparseCommand(self, command: commands.Command) -> str: 1312 """ 1313 Turns a `Command` back into the string that would produce that 1314 command when parsed using `parseCommandList`. 1315 1316 Note that the results will be more explicit in some cases than what 1317 `parseCommandList` would accept as input. 1318 1319 For example: 1320 1321 >>> pf = ParseFormat() 1322 >>> pf.unparseCommand( 1323 ... commands.LiteralValue(command='val', value='5') 1324 ... ) 1325 'val 5' 1326 >>> pf.unparseCommand( 1327 ... commands.LiteralValue(command='val', value='"5"') 1328 ... ) 1329 'val "5"' 1330 >>> pf.unparseCommand( 1331 ... commands.EstablishCollection( 1332 ... command='empty', 1333 ... collection='list' 1334 ... ) 1335 ... ) 1336 'empty list' 1337 >>> pf.unparseCommand( 1338 ... commands.AppendValue(command='append', value='$_') 1339 ... ) 1340 'append $_' 1341 """ 1342 candidate = None 1343 for k, v in commands.COMMAND_SETUP.items(): 1344 if v[0] == type(command): 1345 if candidate is None: 1346 candidate = k 1347 else: 1348 raise ValueError( 1349 f"COMMAND_SETUP includes multiple keys with" 1350 f" {type(command)} as their value type:" 1351 f" '{candidate}' and '{k}'." 1352 ) 1353 1354 if candidate is None: 1355 raise ValueError( 1356 f"COMMAND_SETUP has no key with {type(command)} as its" 1357 f" value type." 1358 ) 1359 1360 result = candidate 1361 for x in command[1:]: 1362 # TODO: Is this hack good enough? 1363 result += ' ' + str(x) 1364 return result 1365 1366 def unparseCommandList(self, commands: List[commands.Command]) -> str: 1367 """ 1368 Takes a list of commands and returns a string that would parse 1369 into them using `parseOneEffectArg`. The result contains 1370 newlines and indentation to make it easier to read. 1371 1372 For example: 1373 1374 >>> pf = ParseFormat() 1375 >>> pf.unparseCommandList( 1376 ... [commands.command('val', '5'), commands.command('pop')] 1377 ... ) 1378 '{\\n val 5;\\n pop;\\n}' 1379 """ 1380 result = self.formatDict[Lexeme.openCurly] 1381 for cmd in commands: 1382 result += f'\n {self.unparseCommand(cmd)};' 1383 if len(commands) > 0: 1384 result += '\n' 1385 return result + self.formatDict[Lexeme.closeCurly] 1386 1387 def parseCommandListFromTokens( 1388 self, 1389 tokens: LexedTokens, 1390 start: int = 0 1391 ) -> Tuple[List[commands.Command], int]: 1392 """ 1393 Parses a command list from a list of lexed tokens, which must 1394 start with `Lexeme.openCurly`. Returns the parsed command list 1395 as a list of `commands.Command` objects, along with the end 1396 index of that command list (which will be the matching curly 1397 brace. 1398 """ 1399 end = self.matchingBrace( 1400 tokens, 1401 start, 1402 Lexeme.openCurly, 1403 Lexeme.closeCurly 1404 ) 1405 parts = list( 1406 findSeparatedParts( 1407 tokens, 1408 Lexeme.consequenceSeparator, 1409 start + 1, 1410 end - 1, 1411 Lexeme.openCurly, 1412 Lexeme.closeCurly, 1413 ) 1414 ) 1415 return ( 1416 [ 1417 self.parseCommandFromTokens(tokens, fromIndex, toIndex) 1418 for fromIndex, toIndex in parts 1419 if fromIndex <= toIndex # ignore empty parts 1420 ], 1421 end 1422 ) 1423 1424 def parseOneEffectArg( 1425 self, 1426 tokens: LexedTokens, 1427 start: int = 0, 1428 limit: Optional[int] = None 1429 ) -> Tuple[ 1430 Union[ 1431 base.Capability, # covers 'str' possibility 1432 Tuple[base.Token, base.TokenCount], 1433 Tuple[Literal['skill'], base.Skill, base.Level], 1434 Tuple[base.MechanismSpecifier, base.MechanismState], 1435 base.DecisionSpecifier, 1436 base.DecisionID, 1437 Literal[Lexeme.inCommon, Lexeme.isHidden], 1438 Tuple[Literal[Lexeme.sepOrDelay, Lexeme.effectCharges], int], 1439 List[commands.Command] 1440 ], 1441 int 1442 ]: 1443 """ 1444 Looks at tokens starting at the specified position and parses 1445 one or more of them as an effect argument (an argument that 1446 could be given to `base.effect`). Looks at various key `Lexeme`s 1447 to determine which type to use. 1448 1449 Items in the tokens list beyond the specified limit will not be 1450 considered, even when they in theory could be grouped with items 1451 up to the limit into a more complex argument. 1452 1453 For example: 1454 1455 >>> pf = ParseFormat() 1456 >>> pf.parseOneEffectArg(['hi']) 1457 ('hi', 0) 1458 >>> pf.parseOneEffectArg(['hi'], 1) 1459 Traceback (most recent call last): 1460 ... 1461 IndexError... 1462 >>> pf.parseOneEffectArg(['hi', 'bye']) 1463 ('hi', 0) 1464 >>> pf.parseOneEffectArg(['hi', 'bye'], 1) 1465 ('bye', 1) 1466 >>> pf.parseOneEffectArg( 1467 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1468 ... 0 1469 ... ) 1470 ((MechanismSpecifier(domain=None, zone=None, decision=None,\ 1471 name='gate'), 'open'), 2) 1472 >>> pf.parseOneEffectArg( 1473 ... ['set', 'gate', Lexeme.mechanismSeparator, 'open'], 1474 ... 1 1475 ... ) 1476 ((MechanismSpecifier(domain=None, zone=None, decision=None,\ 1477 name='gate'), 'open'), 3) 1478 >>> pf.parseOneEffectArg( 1479 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1480 ... 1 1481 ... ) 1482 Traceback (most recent call last): 1483 ... 1484 exploration.parsing.ParseError... 1485 >>> pf.parseOneEffectArg( 1486 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1487 ... 2 1488 ... ) 1489 ('open', 2) 1490 >>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, '10'], 0) 1491 (('gold', 10), 2) 1492 >>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, 'ten'], 0) 1493 Traceback (most recent call last): 1494 ... 1495 exploration.parsing.ParseError... 1496 >>> pf.parseOneEffectArg([Lexeme.inCommon], 0) 1497 (<Lexeme.inCommon: ...>, 0) 1498 >>> pf.parseOneEffectArg([Lexeme.isHidden], 0) 1499 (<Lexeme.isHidden: ...>, 0) 1500 >>> pf.parseOneEffectArg([Lexeme.tokenCount, '3'], 0) 1501 Traceback (most recent call last): 1502 ... 1503 exploration.parsing.ParseError... 1504 >>> pf.parseOneEffectArg([Lexeme.effectCharges, '3'], 0) 1505 ((<Lexeme.effectCharges: ...>, 3), 1) 1506 >>> pf.parseOneEffectArg([Lexeme.tokenCount, 3], 0) # int is a lexeme 1507 Traceback (most recent call last): 1508 ... 1509 exploration.parsing.ParseError... 1510 >>> pf.parseOneEffectArg([Lexeme.sepOrDelay, '-2'], 0) 1511 ((<Lexeme.sepOrDelay: ...>, -2), 1) 1512 >>> pf.parseOneEffectArg(['agility', Lexeme.skillLevel, '3'], 0) 1513 (('skill', 'agility', 3), 2) 1514 >>> pf.parseOneEffectArg( 1515 ... [ 1516 ... 'main', 1517 ... Lexeme.domainSeparator, 1518 ... 'zone', 1519 ... Lexeme.zoneSeparator, 1520 ... 'decision', 1521 ... Lexeme.zoneSeparator, 1522 ... 'compass', 1523 ... Lexeme.mechanismSeparator, 1524 ... 'north', 1525 ... 'south', 1526 ... 'east', 1527 ... 'west' 1528 ... ], 1529 ... 0 1530 ... ) 1531 ((MechanismSpecifier(domain='main', zone='zone',\ 1532 decision='decision', name='compass'), 'north'), 8) 1533 >>> pf.parseOneEffectArg( 1534 ... [ 1535 ... 'before', 1536 ... 'main', 1537 ... Lexeme.domainSeparator, 1538 ... 'zone', 1539 ... Lexeme.zoneSeparator, 1540 ... 'decision', 1541 ... Lexeme.zoneSeparator, 1542 ... 'compass', 1543 ... 'north', 1544 ... 'south', 1545 ... 'east', 1546 ... 'west' 1547 ... ], 1548 ... 1 1549 ... ) # a mechanism specifier without a state will become a 1550 ... # decision specifier 1551 (DecisionSpecifier(domain='main', zone='zone',\ 1552 name='decision'), 5) 1553 >>> tokens = [ 1554 ... 'set', 1555 ... 'main', 1556 ... Lexeme.domainSeparator, 1557 ... 'zone', 1558 ... Lexeme.zoneSeparator, 1559 ... 'compass', 1560 ... 'north', 1561 ... 'bounce', 1562 ... ] 1563 >>> pf.parseOneEffectArg(tokens, 0) 1564 ('set', 0) 1565 >>> pf.parseDecisionSpecifierFromTokens(tokens, 1) 1566 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 1567 >>> pf.parseOneEffectArg(tokens, 1) 1568 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 1569 >>> pf.parseOneEffectArg(tokens, 6) 1570 ('north', 6) 1571 >>> pf.parseOneEffectArg(tokens, 7) 1572 ('bounce', 7) 1573 >>> pf.parseOneEffectArg( 1574 ... [ 1575 ... "fort", Lexeme.zoneSeparator, "gate", 1576 ... Lexeme.mechanismSeparator, "open", 1577 ... ], 1578 ... 0 1579 ... ) 1580 ((MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1581 name='gate'), 'open'), 4) 1582 >>> pf.parseOneEffectArg( 1583 ... [Lexeme.openCurly, 'val', '5', Lexeme.closeCurly], 1584 ... 0 1585 ... ) == ([commands.command('val', '5')], 3) 1586 True 1587 >>> a = [ 1588 ... Lexeme.openCurly, 'val', '5', Lexeme.closeCurly, 1589 ... Lexeme.openCurly, 'append', Lexeme.consequenceSeparator, 1590 ... 'pop', Lexeme.closeCurly 1591 ... ] 1592 >>> cl = [ 1593 ... [commands.command('val', '5')], 1594 ... [commands.command('append'), commands.command('pop')] 1595 ... ] 1596 >>> pf.parseOneEffectArg(a, 0) == (cl[0], 3) 1597 True 1598 >>> pf.parseOneEffectArg(a, 4) == (cl[1], 8) 1599 True 1600 >>> pf.parseOneEffectArg(a, 1) 1601 ('val', 1) 1602 >>> pf.parseOneEffectArg(a, 2) 1603 ('5', 2) 1604 >>> pf.parseOneEffectArg(a, 3) 1605 Traceback (most recent call last): 1606 ... 1607 exploration.parsing.ParseError... 1608 """ 1609 start, limit, nTokens = normalizeEnds( 1610 tokens, 1611 start, 1612 limit if limit is not None else -1 1613 ) 1614 if nTokens == 0: 1615 raise ParseError("No effect arguments available.") 1616 1617 first = tokens[start] 1618 1619 if nTokens == 1: 1620 if first in (Lexeme.inCommon, Lexeme.isHidden): 1621 return (first, start) 1622 elif not isinstance(first, str): 1623 raise ParseError( 1624 f"Only one token and it's a special character" 1625 f" ({first} = {repr(self.formatDict[first])})" 1626 ) 1627 else: 1628 return (cast(base.Capability, first), start) 1629 1630 assert (nTokens > 1) 1631 1632 second = tokens[start + 1] 1633 1634 # Command lists start with an open curly brace and effect 1635 # modifiers start with a Lexme, but nothing else may 1636 if first == Lexeme.openCurly: 1637 return self.parseCommandListFromTokens(tokens, start) 1638 elif first in (Lexeme.inCommon, Lexeme.isHidden): 1639 return (first, start) 1640 elif first in (Lexeme.sepOrDelay, Lexeme.effectCharges): 1641 if not isinstance(second, str): 1642 raise ParseError( 1643 f"Token following a modifier that needs a count" 1644 f" must be a string in tokens:" 1645 f"\n{tokens[start:limit or len(tokens)]}" 1646 ) 1647 try: 1648 val = int(second) 1649 except ValueError: 1650 raise ParseError( 1651 f"Token following a modifier that needs a count" 1652 f" must be convertible to an int:" 1653 f"\n{tokens[start:limit or len(tokens)]}" 1654 ) 1655 1656 first = cast( 1657 Literal[Lexeme.sepOrDelay, Lexeme.effectCharges], 1658 first 1659 ) 1660 return ((first, val), start + 1) 1661 elif not isinstance(first, str): 1662 raise ParseError( 1663 f"First token must be a string unless it's a modifier" 1664 f" lexeme or command/reversion-set opener. Got:" 1665 f"\n{tokens[start:limit or len(tokens)]}" 1666 ) 1667 1668 # If we have two strings in a row, then the first is our parsed 1669 # value alone and we'll parse the second separately. 1670 if isinstance(second, str): 1671 return (first, start) 1672 elif second in (Lexeme.inCommon, Lexeme.isHidden): 1673 return (first, start) 1674 1675 # Must have at least 3 tokens at this point, or else we need to 1676 # have the inCommon or isHidden lexeme second. 1677 if nTokens < 3: 1678 return (first, start) 1679 1680 third = tokens[start + 2] 1681 if not isinstance(third, str): 1682 return (first, start) 1683 1684 second = cast(Lexeme, second) 1685 third = cast(str, third) 1686 1687 if second in (Lexeme.tokenCount, Lexeme.skillLevel): 1688 try: 1689 num = int(third) 1690 except ValueError: 1691 raise ParseError( 1692 f"Invalid effect tokens: count for Tokens or level" 1693 f" for Skill must be convertible to an integer." 1694 f"\n{tokens[start:limit + 1]}" 1695 ) 1696 if second == Lexeme.tokenCount: 1697 return ((first, num), start + 2) # token/count pair 1698 else: 1699 return (('skill', first, num), start + 2) # token/count pair 1700 1701 elif second == Lexeme.mechanismSeparator: # bare mechanism 1702 return ( 1703 ( 1704 base.MechanismSpecifier( 1705 domain=None, 1706 zone=None, 1707 decision=None, 1708 name=first 1709 ), 1710 third 1711 ), 1712 start + 2 1713 ) 1714 1715 elif second in (Lexeme.domainSeparator, Lexeme.zoneSeparator): 1716 try: 1717 mSpec, mEnd = self.parseMechanismSpecifierFromTokens( 1718 tokens, 1719 start 1720 ) # works whether it's a mechanism or decision specifier... 1721 except ParseError: 1722 return self.parseDecisionSpecifierFromTokens(tokens, start) 1723 if mEnd + 2 > limit: 1724 # No room for following mechanism separator + state 1725 return self.parseDecisionSpecifierFromTokens(tokens, start) 1726 sep = tokens[mEnd + 1] 1727 after = tokens[mEnd + 2] 1728 if sep == Lexeme.mechanismSeparator: 1729 if not isinstance(after, str): 1730 raise ParseError( 1731 f"Mechanism separator not followed by state:" 1732 f"\n{tokens[start]}" 1733 ) 1734 return ((mSpec, after), mEnd + 2) 1735 else: 1736 # No mechanism separator afterwards 1737 return self.parseDecisionSpecifierFromTokens(tokens, start) 1738 1739 else: # unrecognized as a longer combo 1740 return (first, start) 1741 1742 def coalesceEffectArgs( 1743 self, 1744 tokens: LexedTokens, 1745 start: int = 0, 1746 end: int = -1 1747 ) -> Tuple[ 1748 List[ # List of effect args 1749 Union[ 1750 base.Capability, # covers 'str' possibility 1751 Tuple[base.Token, base.TokenCount], 1752 Tuple[Literal['skill'], base.Skill, base.Level], 1753 Tuple[base.MechanismSpecifier, base.MechanismState], 1754 base.DecisionSpecifier, 1755 List[commands.Command], 1756 Set[str] 1757 ] 1758 ], 1759 Tuple[ # Slots for modifiers: common/hidden/charges/delay 1760 Optional[bool], 1761 Optional[bool], 1762 Optional[int], 1763 Optional[int], 1764 ] 1765 ]: 1766 """ 1767 Given a region of a lexed tokens list which contains one or more 1768 effect arguments, combines token sequences representing things 1769 like capabilities, mechanism states, token counts, and skill 1770 levels, representing these using the tuples that would be passed 1771 to `base.effect`. Returns a tuple with two elements: 1772 1773 - First, a list that contains several different kinds of 1774 objects, each of which is distinguishable by its type or 1775 part of its value. 1776 - Next, a tuple with four entires for common, hidden, charges, 1777 and/or delay values based on the presence of modifier 1778 sequences. Any or all of these may be `None` if the relevant 1779 modifier was not present (the usual case). 1780 1781 For example: 1782 1783 >>> pf = ParseFormat() 1784 >>> pf.coalesceEffectArgs(["jump"]) 1785 (['jump'], (None, None, None, None)) 1786 >>> pf.coalesceEffectArgs(["coin", Lexeme.tokenCount, "3", "fly"]) 1787 ([('coin', 3), 'fly'], (None, None, None, None)) 1788 >>> pf.coalesceEffectArgs( 1789 ... [ 1790 ... "fort", Lexeme.zoneSeparator, "gate", 1791 ... Lexeme.mechanismSeparator, "open" 1792 ... ] 1793 ... ) 1794 ([(MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1795 name='gate'), 'open')], (None, None, None, None)) 1796 >>> pf.coalesceEffectArgs( 1797 ... [ 1798 ... "main", Lexeme.domainSeparator, "cliff" 1799 ... ] 1800 ... ) 1801 ([DecisionSpecifier(domain='main', zone=None, name='cliff')],\ 1802 (None, None, None, None)) 1803 >>> pf.coalesceEffectArgs( 1804 ... [ 1805 ... "door", Lexeme.mechanismSeparator, "open" 1806 ... ] 1807 ... ) 1808 ([(MechanismSpecifier(domain=None, zone=None, decision=None,\ 1809 name='door'), 'open')], (None, None, None, None)) 1810 >>> pf.coalesceEffectArgs( 1811 ... [ 1812 ... "fort", Lexeme.zoneSeparator, "gate", 1813 ... Lexeme.mechanismSeparator, "open", 1814 ... "canJump", 1815 ... "coins", Lexeme.tokenCount, "3", 1816 ... Lexeme.inCommon, 1817 ... "agility", Lexeme.skillLevel, "-1", 1818 ... Lexeme.sepOrDelay, "0", 1819 ... "main", Lexeme.domainSeparator, "cliff" 1820 ... ] 1821 ... ) 1822 ([(MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1823 name='gate'), 'open'), 'canJump', ('coins', 3), ('skill', 'agility', -1),\ 1824 DecisionSpecifier(domain='main', zone=None, name='cliff')],\ 1825 (True, None, None, 0)) 1826 >>> pf.coalesceEffectArgs(["bounce", Lexeme.isHidden]) 1827 (['bounce'], (None, True, None, None)) 1828 >>> pf.coalesceEffectArgs( 1829 ... ["goto", "3", Lexeme.inCommon, Lexeme.isHidden] 1830 ... ) 1831 (['goto', '3'], (True, True, None, None)) 1832 """ 1833 start, end, nTokens = normalizeEnds(tokens, start, end) 1834 where = start 1835 result: List[ # List of effect args 1836 Union[ 1837 base.Capability, # covers 'str' possibility 1838 Tuple[base.Token, base.TokenCount], 1839 Tuple[Literal['skill'], base.Skill, base.Level], 1840 Tuple[base.MechanismSpecifier, base.MechanismState], 1841 base.DecisionSpecifier, 1842 List[commands.Command], 1843 Set[str] 1844 ] 1845 ] = [] 1846 inCommon: Optional[bool] = None 1847 isHidden: Optional[bool] = None 1848 charges: Optional[int] = None 1849 delay: Optional[int] = None 1850 while where <= end: 1851 following, thisEnd = self.parseOneEffectArg(tokens, where, end) 1852 if following == Lexeme.inCommon: 1853 if inCommon is not None: 1854 raise ParseError( 1855 f"In-common effect modifier specified more than" 1856 f" once in effect args:" 1857 f"\n{tokens[start:end + 1]}" 1858 ) 1859 inCommon = True 1860 elif following == Lexeme.isHidden: 1861 if isHidden is not None: 1862 raise ParseError( 1863 f"Is-hidden effect modifier specified more than" 1864 f" once in effect args:" 1865 f"\n{tokens[start:end + 1]}" 1866 ) 1867 isHidden = True 1868 elif ( 1869 isinstance(following, tuple) 1870 and len(following) == 2 1871 and following[0] in (Lexeme.effectCharges, Lexeme.sepOrDelay) 1872 and isinstance(following[1], int) 1873 ): 1874 if following[0] == Lexeme.effectCharges: 1875 if charges is not None: 1876 raise ParseError( 1877 f"Charges effect modifier specified more than" 1878 f" once in effect args:" 1879 f"\n{tokens[start:end + 1]}" 1880 ) 1881 charges = following[1] 1882 else: 1883 if delay is not None: 1884 raise ParseError( 1885 f"Delay effect modifier specified more than" 1886 f" once in effect args:" 1887 f"\n{tokens[start:end + 1]}" 1888 ) 1889 delay = following[1] 1890 elif ( 1891 isinstance(following, base.Capability) 1892 or ( 1893 isinstance(following, tuple) 1894 and len(following) == 2 1895 and isinstance(following[0], base.Token) 1896 and isinstance(following[1], base.TokenCount) 1897 ) or ( 1898 isinstance(following, tuple) 1899 and len(following) == 3 1900 and following[0] == 'skill' 1901 and isinstance(following[1], base.Skill) 1902 and isinstance(following[2], base.Level) 1903 ) or ( 1904 isinstance(following, tuple) 1905 and len(following) == 2 1906 and isinstance(following[0], base.MechanismSpecifier) 1907 and isinstance(following[1], base.MechanismState) 1908 ) or ( 1909 isinstance(following, base.DecisionSpecifier) 1910 ) or ( 1911 isinstance(following, list) 1912 and all(isinstance(item, tuple) for item in following) 1913 # TODO: Stricter command list check here? 1914 ) or ( 1915 isinstance(following, set) 1916 and all(isinstance(item, str) for item in following) 1917 ) 1918 ): 1919 result.append(following) 1920 else: 1921 raise ParseError(f"Invalid coalesced argument: {following}") 1922 where = thisEnd + 1 1923 1924 return (result, (inCommon, isHidden, charges, delay)) 1925 1926 def parseEffectFromTokens( 1927 self, 1928 tokens: LexedTokens, 1929 start: int = 0, 1930 end: int = -1 1931 ) -> base.Effect: 1932 """ 1933 Given a region of a list of lexed tokens specifying an effect, 1934 returns the `Effect` object that those tokens specify. 1935 """ 1936 start, end, nTokens = normalizeEnds(tokens, start, end) 1937 1938 # Check for empty list 1939 if nTokens == 0: 1940 raise ParseError( 1941 "Effect must include at least a type." 1942 ) 1943 1944 firstPart = tokens[start] 1945 1946 if isinstance(firstPart, Lexeme): 1947 raise ParseError( 1948 f"First part of effect must be an effect type. Got" 1949 f" {firstPart} ({repr(self.formatDict[firstPart])})." 1950 ) 1951 1952 firstPart = cast(str, firstPart) 1953 1954 # Get the effect type 1955 fType = self.effectType(firstPart) 1956 1957 if fType is None: 1958 raise ParseError( 1959 f"Unrecognized effect type {firstPart!r}. Check the" 1960 f" EffectType entries in the effect names dictionary." 1961 ) 1962 1963 if start + 1 > end: # No tokens left: set empty args 1964 groupedArgs: List[ 1965 Union[ 1966 base.Capability, # covers 'str' possibility 1967 Tuple[base.Token, base.TokenCount], 1968 Tuple[Literal['skill'], base.Skill, base.Level], 1969 Tuple[base.MechanismSpecifier, base.MechanismState], 1970 base.DecisionSpecifier, 1971 List[commands.Command], 1972 Set[str] 1973 ] 1974 ] = [] 1975 modifiers: Tuple[ 1976 Optional[bool], 1977 Optional[bool], 1978 Optional[int], 1979 Optional[int] 1980 ] = (None, None, None, None) 1981 else: # Coalesce remaining tokens if there are any 1982 groupedArgs, modifiers = self.coalesceEffectArgs( 1983 tokens, 1984 start + 1, 1985 end 1986 ) 1987 1988 # Set up arguments for base.effect and handle modifiers first 1989 args: Dict[ 1990 str, 1991 Union[ 1992 None, 1993 base.ContextSpecifier, 1994 base.Capability, 1995 Tuple[base.Token, base.TokenCount], 1996 Tuple[Literal['skill'], base.Skill, base.Level], 1997 Tuple[base.MechanismSpecifier, base.MechanismState], 1998 Tuple[base.MechanismSpecifier, List[base.MechanismState]], 1999 List[base.Capability], 2000 base.AnyDecisionSpecifier, 2001 Tuple[base.AnyDecisionSpecifier, base.FocalPointName], 2002 bool, 2003 int, 2004 base.SaveSlot, 2005 Tuple[base.SaveSlot, Set[str]] 2006 ] 2007 ] = {} 2008 if modifiers[0]: 2009 args['applyTo'] = 'common' 2010 if modifiers[1]: 2011 args['hidden'] = True 2012 else: 2013 args['hidden'] = False 2014 if modifiers[2] is not None: 2015 args['charges'] = modifiers[2] 2016 if modifiers[3] is not None: 2017 args['delay'] = modifiers[3] 2018 2019 # Now handle the main effect-type-based argument 2020 if fType in ("gain", "lose"): 2021 if len(groupedArgs) != 1: 2022 raise ParseError( 2023 f"'{fType}' effect must have exactly one grouped" 2024 f" argument (got {len(groupedArgs)}:\n{groupedArgs}" 2025 ) 2026 thing = groupedArgs[0] 2027 if isinstance(thing, tuple): 2028 if len(thing) == 2: 2029 if ( 2030 not isinstance(thing[0], base.Token) 2031 or not isinstance(thing[1], base.TokenCount) 2032 ): 2033 raise ParseError( 2034 f"'{fType}' effect grouped arg pair must be a" 2035 f" (token, amount) pair. Got:\n{thing}" 2036 ) 2037 elif len(thing) == 3: 2038 if ( 2039 thing[0] != 'skill' 2040 or not isinstance(thing[1], base.Skill) 2041 or not isinstance(thing[2], base.Level) 2042 ): 2043 raise ParseError( 2044 f"'{fType}' effect grouped arg pair must be a" 2045 f" (token, amount) pair. Got:\n{thing}" 2046 ) 2047 else: 2048 raise ParseError( 2049 f"'{fType}' effect grouped arg tuple must have" 2050 f" length 2 or 3. Got (length {len(thing)}):\n{thing}" 2051 ) 2052 elif not isinstance(thing, base.Capability): 2053 raise ParseError( 2054 f"'{fType}' effect grouped arg must be a capability" 2055 f" or a (token, amount) tuple. Got:\n{thing}" 2056 ) 2057 args[fType] = thing 2058 return base.effect(**args) # type:ignore 2059 2060 elif fType == "set": 2061 if len(groupedArgs) != 1: 2062 raise ParseError( 2063 f"'{fType}' effect must have exactly one grouped" 2064 f" argument (got {len(groupedArgs)}:\n{groupedArgs}" 2065 ) 2066 setVal = groupedArgs[0] 2067 if not isinstance( 2068 setVal, 2069 tuple 2070 ): 2071 raise ParseError( 2072 f"'{fType}' effect grouped arg must be a tuple. Got:" 2073 f"\n{setVal}" 2074 ) 2075 if len(setVal) == 2: 2076 setWhat, setTo = setVal 2077 if ( 2078 isinstance(setWhat, base.Token) 2079 and isinstance(setTo, base.TokenCount) 2080 ) or ( 2081 isinstance(setWhat, base.MechanismSpecifier) 2082 and isinstance(setTo, base.MechanismState) 2083 ): 2084 args[fType] = setVal 2085 return base.effect(**args) # type:ignore 2086 else: 2087 raise ParseError( 2088 f"Invalid '{fType}' effect grouped args:" 2089 f"\n{groupedArgs}" 2090 ) 2091 elif len(setVal) == 3: 2092 indicator, whichSkill, setTo = setVal 2093 if ( 2094 indicator == 'skill' 2095 and isinstance(whichSkill, base.Skill) 2096 and isinstance(setTo, base.Level) 2097 ): 2098 args[fType] = setVal 2099 return base.effect(**args) # type:ignore 2100 else: 2101 raise ParseError( 2102 f"Invalid '{fType}' effect grouped args (not a" 2103 f" skill):\n{groupedArgs}" 2104 ) 2105 else: 2106 raise ParseError( 2107 f"Invalid '{fType}' effect grouped args (wrong" 2108 f" length tuple):\n{groupedArgs}" 2109 ) 2110 2111 elif fType == "toggle": 2112 if len(groupedArgs) == 0: 2113 raise ParseError( 2114 f"'{fType}' effect must have at least one grouped" 2115 f" argument. Got:\n{groupedArgs}" 2116 ) 2117 if ( 2118 isinstance(groupedArgs[0], tuple) 2119 and len(groupedArgs[0]) == 2 2120 and isinstance(groupedArgs[0][0], base.MechanismSpecifier) 2121 and isinstance(groupedArgs[0][1], base.MechanismState) 2122 and all( 2123 isinstance(a, base.MechanismState) 2124 for a in groupedArgs[1:] 2125 ) 2126 ): # a mechanism toggle 2127 args[fType] = ( 2128 groupedArgs[0][0], 2129 cast( 2130 List[base.MechanismState], 2131 [groupedArgs[0][1]] + groupedArgs[1:] 2132 ) 2133 ) 2134 return base.effect(**args) # type:ignore 2135 elif all(isinstance(a, base.Capability) for a in groupedArgs): 2136 # a capability toggle 2137 args[fType] = cast(List[base.Capability], groupedArgs) 2138 return base.effect(**args) # type:ignore 2139 else: 2140 raise ParseError( 2141 f"Invalid arguments for '{fType}' effect. Got:" 2142 f"\n{groupedArgs}" 2143 ) 2144 2145 elif fType in ("bounce", "deactivate"): 2146 if len(groupedArgs) != 0: 2147 raise ParseError( 2148 f"'{fType}' effect may not include any" 2149 f" arguments. Got {len(groupedArgs)}):" 2150 f"\n{groupedArgs}" 2151 ) 2152 args[fType] = True 2153 return base.effect(**args) # type:ignore 2154 2155 elif fType == "follow": 2156 if len(groupedArgs) != 1: 2157 raise ParseError( 2158 f"'{fType}' effect must include exactly one" 2159 f" argument. Got {len(groupedArgs)}):" 2160 f"\n{groupedArgs}" 2161 ) 2162 2163 transition = groupedArgs[0] 2164 if not isinstance(transition, base.Transition): 2165 raise ParseError( 2166 f"Invalid argument for '{fType}' effect. Needed a" 2167 f" transition but got:\n{groupedArgs}" 2168 ) 2169 args[fType] = transition 2170 return base.effect(**args) # type:ignore 2171 2172 elif fType == "edit": 2173 if len(groupedArgs) == 0: 2174 raise ParseError( 2175 "An 'edit' effect requires at least one argument." 2176 ) 2177 for i, arg in enumerate(groupedArgs): 2178 if not isinstance(arg, list): 2179 raise ParseError( 2180 f"'edit' effect argument {i} is not a sub-list:" 2181 f"\n {arg!r}" 2182 f"\nAmong arguments:" 2183 f"\n {groupedArgs}" 2184 ) 2185 for j, cmd in enumerate(arg): 2186 if not isinstance(cmd, tuple): 2187 raise ParseError( 2188 f"'edit' effect argument {i} contains" 2189 f" non-tuple part {j}:" 2190 f"\n {cmd!r}" 2191 f"\nAmong arguments:" 2192 f"\n {groupedArgs}" 2193 ) 2194 2195 args[fType] = groupedArgs # type:ignore 2196 return base.effect(**args) # type:ignore 2197 2198 elif fType == "goto": 2199 if len(groupedArgs) not in (1, 2): 2200 raise ParseError( 2201 f"A 'goto' effect must include either one or two" 2202 f" grouped arguments. Got {len(groupedArgs)}:" 2203 f"\n{groupedArgs}" 2204 ) 2205 2206 first = groupedArgs[0] 2207 if not isinstance( 2208 first, 2209 (base.DecisionName, base.DecisionSpecifier) 2210 ): 2211 raise ParseError( 2212 f"'{fType}' effect must first specify a destination" 2213 f" decision. Got:\n{groupedArgs}" 2214 ) 2215 2216 # Check if it's really a decision ID 2217 dSpec: base.AnyDecisionSpecifier 2218 if isinstance(first, base.DecisionName): 2219 try: 2220 dSpec = int(first) 2221 except ValueError: 2222 dSpec = first 2223 else: 2224 dSpec = first 2225 2226 if len(groupedArgs) == 2: 2227 second = groupedArgs[1] 2228 if not isinstance(second, base.FocalPointName): 2229 raise ParseError( 2230 f"'{fType}' effect must have a focal point name" 2231 f" if it has a second part. Got:\n{groupedArgs}" 2232 ) 2233 args[fType] = (dSpec, second) 2234 else: 2235 args[fType] = dSpec 2236 2237 return base.effect(**args) # type:ignore 2238 2239 elif fType == "save": 2240 if len(groupedArgs) not in (0, 1): 2241 raise ParseError( 2242 f"'{fType}' effect must include exactly zero or one" 2243 f" argument(s). Got {len(groupedArgs)}):" 2244 f"\n{groupedArgs}" 2245 ) 2246 2247 if len(groupedArgs) == 1: 2248 slot = groupedArgs[0] 2249 else: 2250 slot = base.DEFAULT_SAVE_SLOT 2251 if not isinstance(slot, base.SaveSlot): 2252 raise ParseError( 2253 f"Invalid argument for '{fType}' effect. Needed a" 2254 f" save slot but got:\n{groupedArgs}" 2255 ) 2256 args[fType] = slot 2257 return base.effect(**args) # type:ignore 2258 2259 else: 2260 raise ParseError(f"Invalid effect type: '{fType}'.") 2261 2262 def parseEffect(self, effectStr: str) -> base.Effect: 2263 """ 2264 Works like `parseEffectFromTokens` but starts with a raw string. 2265 For example: 2266 2267 >>> pf = ParseFormat() 2268 >>> pf.parseEffect("gain jump") == base.effect(gain='jump') 2269 True 2270 >>> pf.parseEffect("set door:open") == base.effect( 2271 ... set=( 2272 ... base.MechanismSpecifier(None, None, None, 'door'), 2273 ... 'open' 2274 ... ) 2275 ... ) 2276 True 2277 >>> pf.parseEffect("set coins*10") == base.effect(set=('coins', 10)) 2278 True 2279 >>> pf.parseEffect("set agility^3") == base.effect( 2280 ... set=('skill', 'agility', 3) 2281 ... ) 2282 True 2283 """ 2284 return self.parseEffectFromTokens(self.lex(effectStr)) 2285 2286 def unparseEffect(self, effect: base.Effect) -> str: 2287 """ 2288 The opposite of `parseEffect`; turns an effect back into a 2289 string reprensentation. 2290 2291 For example: 2292 2293 >>> pf = ParseFormat() 2294 >>> e = { 2295 ... "type": "gain", 2296 ... "applyTo": "active", 2297 ... "value": "flight", 2298 ... "delay": None, 2299 ... "charges": None, 2300 ... "hidden": False 2301 ... } 2302 >>> pf.unparseEffect(e) 2303 'gain flight' 2304 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2305 True 2306 >>> s = 'gain flight' 2307 >>> pf.unparseEffect(pf.parseEffect(s)) == s 2308 True 2309 >>> s2 = ' gain\\nflight' 2310 >>> pf.unparseEffect(pf.parseEffect(s2)) == s 2311 True 2312 >>> e = { 2313 ... "type": "gain", 2314 ... "applyTo": "active", 2315 ... "value": ("gold", 5), 2316 ... "delay": 1, 2317 ... "charges": 2, 2318 ... "hidden": False 2319 ... } 2320 >>> pf.unparseEffect(e) 2321 'gain gold*5 ,1 =2' 2322 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2323 True 2324 >>> e = { 2325 ... "type": "set", 2326 ... "applyTo": "active", 2327 ... "value": ( 2328 ... base.MechanismSpecifier(None, None, None, "gears"), 2329 ... "on" 2330 ... ), 2331 ... "delay": None, 2332 ... "charges": 1, 2333 ... "hidden": False 2334 ... } 2335 >>> pf.unparseEffect(e) 2336 'set gears:on =1' 2337 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2338 True 2339 >>> e = { 2340 ... "type": "toggle", 2341 ... "applyTo": "active", 2342 ... "value": ["red", "blue"], 2343 ... "delay": None, 2344 ... "charges": None, 2345 ... "hidden": False 2346 ... } 2347 >>> pf.unparseEffect(e) 2348 'toggle red blue' 2349 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2350 True 2351 >>> e = { 2352 ... "type": "toggle", 2353 ... "applyTo": "active", 2354 ... "value": ( 2355 ... base.MechanismSpecifier(None, None, None, "switch"), 2356 ... ["on", "off"] 2357 ... ), 2358 ... "delay": None, 2359 ... "charges": None, 2360 ... "hidden": False 2361 ... } 2362 >>> pf.unparseEffect(e) 2363 'toggle switch:on off' 2364 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2365 True 2366 >>> e = { 2367 ... "type": "deactivate", 2368 ... "applyTo": "active", 2369 ... "value": None, 2370 ... "delay": 2, 2371 ... "charges": None, 2372 ... "hidden": False 2373 ... } 2374 >>> pf.unparseEffect(e) 2375 'deactivate ,2' 2376 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2377 True 2378 >>> e = { 2379 ... "type": "goto", 2380 ... "applyTo": "common", 2381 ... "value": 3, 2382 ... "delay": None, 2383 ... "charges": None, 2384 ... "hidden": False 2385 ... } 2386 >>> pf.unparseEffect(e) 2387 'goto 3 +c' 2388 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2389 True 2390 >>> e = { 2391 ... "type": "goto", 2392 ... "applyTo": "common", 2393 ... "value": 3, 2394 ... "delay": None, 2395 ... "charges": None, 2396 ... "hidden": True 2397 ... } 2398 >>> pf.unparseEffect(e) 2399 'goto 3 +c +h' 2400 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2401 True 2402 >>> e = { 2403 ... "type": "goto", 2404 ... "applyTo": "active", 2405 ... "value": 'home', 2406 ... "delay": None, 2407 ... "charges": None, 2408 ... "hidden": False 2409 ... } 2410 >>> pf.unparseEffect(e) 2411 'goto home' 2412 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2413 True 2414 >>> e = base.effect(edit=[ 2415 ... [ 2416 ... commands.command('val', '5'), 2417 ... commands.command('empty', 'list'), 2418 ... commands.command('append', '$_') 2419 ... ], 2420 ... [ 2421 ... commands.command('val', '11'), 2422 ... commands.command('assign', 'var', '$_'), 2423 ... commands.command('op', '+', '$var', '$var') 2424 ... ], 2425 ... ]) 2426 >>> pf.unparseEffect(e) 2427 'edit {\\n val 5;\\n empty list;\\n append $_;\\n}\ 2428 {\\n val 11;\\n assign var $_;\\n op + $var $var;\\n}' 2429 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2430 True 2431 >>> e = base.effect(set=('coins', 3)) 2432 >>> pf.unparseEffect(e) 2433 'set coins*3' 2434 >>> e = base.effect(set=('skill', 'mashing', 3)) 2435 >>> pf.unparseEffect(e) 2436 'set mashing^3' 2437 """ 2438 result: List[str] = [] 2439 2440 # Reverse the effect type into a marker 2441 eType = effect['type'] 2442 for key, val in self.effectNames.items(): 2443 if val == eType: 2444 if len(result) != 0: 2445 raise ParseError( 2446 f"Effect map contains multiple matching entries" 2447 f"for effect type '{effect['type']}':" 2448 f" '{result[0]}' and '{key}'" 2449 ) 2450 result.append(key) 2451 # Don't break 'cause we'd like to check uniqueness 2452 2453 eVal = effect['value'] 2454 if eType in ('gain', 'lose'): 2455 eVal = cast(Union[base.Capability, Tuple[base.Token, int]], eVal) 2456 if isinstance(eVal, str): # a capability 2457 result.append(eVal) 2458 else: # a token 2459 result.append( 2460 eVal[0] 2461 + self.formatDict[Lexeme.tokenCount] 2462 + str(eVal[1]) 2463 ) 2464 elif eType == 'set': 2465 eVal = cast( 2466 # TODO: Add skill level setting here & elsewhere 2467 Union[ 2468 Tuple[base.Token, base.TokenCount], 2469 Tuple[base.MechanismSpecifier, base.MechanismState] 2470 ], 2471 eVal 2472 ) 2473 if len(eVal) not in (2, 3): 2474 raise ValueError( 2475 f"'set' effect has value with length other than 2" 2476 f" or 3:\n {repr(effect)}" 2477 ) 2478 if len(eVal) == 3: 2479 if eVal[0] != "skill": 2480 raise ValueError( 2481 f"'set' effect with length-3 value doesn't" 2482 f" start with string 'skill':\n {repr(effect)}" 2483 ) 2484 result.append( 2485 eVal[1] 2486 + self.formatDict[Lexeme.skillLevel] 2487 + str(eVal[2]) 2488 ) 2489 elif isinstance(eVal[1], int): # a token count 2490 result.append( 2491 eVal[0] 2492 + self.formatDict[Lexeme.tokenCount] 2493 + str(eVal[1]) 2494 ) 2495 else: # a mechanism 2496 if isinstance(eVal[0], base.MechanismSpecifier): 2497 mSpec = self.unparseMechanismSpecifier(eVal[0]) 2498 elif isinstance(eVal[0], base.MechanismID): 2499 # TODO: Specify mechanism by decision name + 2500 # mechanism name? Would require threading through a 2501 # DecisionGraph and using mechanismDetails 2502 mSpec = "" + eVal[0] 2503 else: 2504 assert isinstance(eVal[0], base.MechanismName) 2505 mSpec = eVal[0] 2506 result.append( 2507 mSpec 2508 + self.formatDict[Lexeme.mechanismSeparator] 2509 + eVal[1] 2510 ) 2511 elif eType == 'toggle': 2512 if isinstance(eVal, tuple): # mechanism states 2513 tSpec, states = cast( 2514 Tuple[ 2515 base.AnyMechanismSpecifier, 2516 List[base.MechanismState] 2517 ], 2518 eVal 2519 ) 2520 firstState = states[0] 2521 restStates = states[1:] 2522 if isinstance(tSpec, base.MechanismSpecifier): 2523 mStr = self.unparseMechanismSpecifier(tSpec) 2524 else: 2525 # Could be ID or name 2526 mStr = str(tSpec) 2527 result.append( 2528 mStr 2529 + self.formatDict[Lexeme.mechanismSeparator] 2530 + firstState 2531 ) 2532 result.extend(restStates) 2533 else: # capabilities 2534 assert isinstance(eVal, list) 2535 eVal = cast(List[base.Capability], eVal) 2536 result.extend(eVal) 2537 elif eType in ('deactivate', 'bounce'): 2538 if eVal is not None: 2539 raise ValueError( 2540 f"'{eType}' effect has non-None value:" 2541 f"\n {repr(effect)}" 2542 ) 2543 elif eType == 'follow': 2544 eVal = cast(base.Token, eVal) 2545 result.append(eVal) 2546 elif eType == 'edit': 2547 eVal = cast(List[List[commands.Command]], eVal) 2548 if len(eVal) == 0: 2549 result[-1] = '{}' 2550 else: 2551 for cmdList in eVal: 2552 result.append( 2553 self.unparseCommandList(cmdList) 2554 ) 2555 elif eType == 'goto': 2556 if ( 2557 isinstance(eVal, tuple) 2558 and len(eVal) == 2 2559 and isinstance(eVal[1], base.FocalPointName) 2560 ): 2561 result.append( 2562 self.unparseAnyDecision( 2563 cast(base.AnyDecisionSpecifier, eVal[0]) 2564 ) 2565 ) 2566 result.append(eVal[1]) 2567 else: 2568 assert isinstance( 2569 eVal, 2570 (base.DecisionID, base.DecisionSpecifier, str) 2571 ) 2572 result.append(self.unparseAnyDecision(eVal)) 2573 elif eType == 'save': 2574 # It's just a string naming the save slot 2575 eVal = cast(str, eVal) 2576 result.append(eVal) 2577 else: 2578 raise ValueError( 2579 f"Unrecognized effect type '{eType}' in effect:" 2580 f"\n {repr(effect)}" 2581 ) 2582 2583 # Add modifier strings 2584 if effect['applyTo'] == 'common': 2585 result.append(self.formatDict[Lexeme.inCommon]) 2586 2587 if effect['hidden']: 2588 result.append(self.formatDict[Lexeme.isHidden]) 2589 2590 dVal = effect['delay'] 2591 if dVal is not None: 2592 result.append( 2593 self.formatDict[Lexeme.sepOrDelay] + str(dVal) 2594 ) 2595 2596 cVal = effect['charges'] 2597 if cVal is not None: 2598 result.append( 2599 self.formatDict[Lexeme.effectCharges] + str(cVal) 2600 ) 2601 2602 joined = '' 2603 before = False 2604 for r in result: 2605 if ( 2606 r.startswith(' ') 2607 or r.startswith('\n') 2608 or r.endswith(' ') 2609 or r.endswith('\n') 2610 ): 2611 joined += r 2612 before = False 2613 else: 2614 joined += (' ' if before else '') + r 2615 before = True 2616 return joined 2617 2618 def parseDecisionSpecifierFromTokens( 2619 self, 2620 tokens: LexedTokens, 2621 start: int = 0 2622 ) -> Tuple[Union[base.DecisionSpecifier, int], int]: 2623 """ 2624 Parses a decision specifier starting at the specified position 2625 in the given tokens list. No ending position is specified, but 2626 instead this function returns a tuple containing the parsed 2627 `base.DecisionSpecifier` along with an index in the tokens list 2628 where the end of the specifier was found. 2629 2630 For example: 2631 2632 >>> pf = ParseFormat() 2633 >>> pf.parseDecisionSpecifierFromTokens(['m']) 2634 (DecisionSpecifier(domain=None, zone=None, name='m'), 0) 2635 >>> pf.parseDecisionSpecifierFromTokens(['12']) # ID specifier 2636 (12, 0) 2637 >>> pf.parseDecisionSpecifierFromTokens(['a', 'm']) 2638 (DecisionSpecifier(domain=None, zone=None, name='a'), 0) 2639 >>> pf.parseDecisionSpecifierFromTokens(['a', 'm'], 1) 2640 (DecisionSpecifier(domain=None, zone=None, name='m'), 1) 2641 >>> pf.parseDecisionSpecifierFromTokens( 2642 ... ['a', Lexeme.domainSeparator, 'm'] 2643 ... ) 2644 (DecisionSpecifier(domain='a', zone=None, name='m'), 2) 2645 >>> pf.parseDecisionSpecifierFromTokens( 2646 ... ['a', Lexeme.zoneSeparator, 'm'] 2647 ... ) 2648 (DecisionSpecifier(domain=None, zone='a', name='m'), 2) 2649 >>> pf.parseDecisionSpecifierFromTokens( 2650 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm'] 2651 ... ) 2652 (DecisionSpecifier(domain=None, zone='a', name='b'), 2) 2653 >>> pf.parseDecisionSpecifierFromTokens( 2654 ... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm'] 2655 ... ) 2656 (DecisionSpecifier(domain='a', zone='b', name='m'), 4) 2657 >>> pf.parseDecisionSpecifierFromTokens( 2658 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'] 2659 ... ) 2660 (DecisionSpecifier(domain=None, zone='a', name='b'), 2) 2661 >>> pf.parseDecisionSpecifierFromTokens( # ID-style name w/ zone 2662 ... ['a', Lexeme.zoneSeparator, '5'], 2663 ... ) 2664 Traceback (most recent call last): 2665 ... 2666 exploration.base.InvalidDecisionSpecifierError... 2667 >>> pf.parseDecisionSpecifierFromTokens( 2668 ... ['d', Lexeme.domainSeparator, '123'] 2669 ... ) 2670 Traceback (most recent call last): 2671 ... 2672 exploration.base.InvalidDecisionSpecifierError... 2673 >>> pf.parseDecisionSpecifierFromTokens( 2674 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 2675 ... 1 2676 ... ) 2677 Traceback (most recent call last): 2678 ... 2679 exploration.parsing.ParseError... 2680 >>> pf.parseDecisionSpecifierFromTokens( 2681 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 2682 ... 2 2683 ... ) 2684 (DecisionSpecifier(domain='b', zone=None, name='m'), 4) 2685 >>> pf.parseDecisionSpecifierFromTokens( 2686 ... [ 2687 ... 'a', 2688 ... Lexeme.domainSeparator, 2689 ... 'b', 2690 ... Lexeme.zoneSeparator, 2691 ... 'c', 2692 ... Lexeme.zoneSeparator, 2693 ... 'm' 2694 ... ] 2695 ... ) 2696 (DecisionSpecifier(domain='a', zone='b', name='c'), 4) 2697 >>> pf.parseDecisionSpecifierFromTokens( 2698 ... [ 2699 ... 'a', 2700 ... Lexeme.domainSeparator, 2701 ... 'b', 2702 ... Lexeme.zoneSeparator, 2703 ... 'c', 2704 ... Lexeme.zoneSeparator, 2705 ... 'm' 2706 ... ], 2707 ... 2 2708 ... ) 2709 (DecisionSpecifier(domain=None, zone='b', name='c'), 4) 2710 >>> pf.parseDecisionSpecifierFromTokens( 2711 ... [ 2712 ... 'a', 2713 ... Lexeme.domainSeparator, 2714 ... 'b', 2715 ... Lexeme.zoneSeparator, 2716 ... 'c', 2717 ... Lexeme.zoneSeparator, 2718 ... 'm' 2719 ... ], 2720 ... 4 2721 ... ) 2722 (DecisionSpecifier(domain=None, zone='c', name='m'), 6) 2723 >>> pf.parseDecisionSpecifierFromTokens( 2724 ... [ 2725 ... 'set', 2726 ... 'main', 2727 ... Lexeme.domainSeparator, 2728 ... 'zone', 2729 ... Lexeme.zoneSeparator, 2730 ... 'compass', 2731 ... 'north', 2732 ... 'bounce', 2733 ... ], 2734 ... 1 2735 ... ) 2736 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 2737 """ 2738 # Check bounds & normalize start index 2739 nTokens = len(tokens) 2740 if start < -nTokens: 2741 raise IndexError( 2742 f"Invalid start index {start} for {nTokens} tokens (too" 2743 f" negative)." 2744 ) 2745 elif start >= nTokens: 2746 raise IndexError( 2747 f"Invalid start index {start} for {nTokens} tokens (too" 2748 f" big)." 2749 ) 2750 elif start < 0: 2751 start = nTokens + start 2752 2753 assert (start < nTokens) 2754 2755 first = tokens[start] 2756 if not isinstance(first, str): 2757 raise ParseError( 2758 f"Invalid domain specifier (must start with a name or" 2759 f" id; got: {first} = {self.formatDict[first]})." 2760 ) 2761 2762 ds = base.DecisionSpecifier(None, None, first) 2763 result = (base.idOrDecisionSpecifier(ds), start) 2764 2765 domain = None 2766 zoneOrDecision = None 2767 2768 if start + 1 >= nTokens: # at end of tokens 2769 return result 2770 2771 firstSep = tokens[start + 1] 2772 if firstSep == Lexeme.domainSeparator: 2773 domain = first 2774 elif firstSep == Lexeme.zoneSeparator: 2775 zoneOrDecision = first 2776 else: 2777 return result 2778 2779 if start + 2 >= nTokens: 2780 return result 2781 2782 second = tokens[start + 2] 2783 if isinstance(second, Lexeme): 2784 return result 2785 2786 ds = base.DecisionSpecifier(domain, zoneOrDecision, second) 2787 result = (base.idOrDecisionSpecifier(ds), start + 2) 2788 2789 if start + 3 >= nTokens: 2790 return result 2791 2792 secondSep = tokens[start + 3] 2793 if start + 4 >= nTokens: 2794 return result 2795 2796 third = tokens[start + 4] 2797 if secondSep == Lexeme.zoneSeparator: 2798 if zoneOrDecision is not None: # two in a row 2799 return result 2800 else: 2801 if not isinstance(third, base.DecisionName): 2802 return result 2803 else: 2804 zoneOrDecision = second 2805 else: 2806 return result 2807 2808 if isinstance(third, Lexeme): 2809 return result 2810 2811 ds = base.DecisionSpecifier(domain, zoneOrDecision, third) 2812 return (base.idOrDecisionSpecifier(ds), start + 4) 2813 2814 def parseDecisionSpecifier( 2815 self, 2816 specString: str 2817 ) -> Union[base.DecisionID, base.DecisionSpecifier]: 2818 """ 2819 Parses a full `DecisionSpecifier` from a single string. Can 2820 parse integer decision IDs in string form, and returns a 2821 `DecisionID` in that case, otherwise returns a 2822 `DecisionSpecifier`. Assumes that all int-convertible strings 2823 are decision IDs, so it cannot deal with feature names which are 2824 just numbers. 2825 2826 For example: 2827 2828 >>> pf = ParseFormat() 2829 >>> pf.parseDecisionSpecifier('example') 2830 DecisionSpecifier(domain=None, zone=None, name='example') 2831 >>> pf.parseDecisionSpecifier('outer::example') 2832 DecisionSpecifier(domain=None, zone='outer', name='example') 2833 >>> pf.parseDecisionSpecifier('domain//region::feature') 2834 DecisionSpecifier(domain='domain', zone='region', name='feature') 2835 >>> pf.parseDecisionSpecifier('123') 2836 123 2837 >>> pf.parseDecisionSpecifier('region::domain//feature') 2838 Traceback (most recent call last): 2839 ... 2840 exploration.base.InvalidDecisionSpecifierError... 2841 >>> pf.parseDecisionSpecifier('domain1//domain2//feature') 2842 Traceback (most recent call last): 2843 ... 2844 exploration.base.InvalidDecisionSpecifierError... 2845 >>> pf.parseDecisionSpecifier('domain//123') 2846 Traceback (most recent call last): 2847 ... 2848 exploration.base.InvalidDecisionSpecifierError... 2849 >>> pf.parseDecisionSpecifier('region::123') 2850 Traceback (most recent call last): 2851 ... 2852 exploration.base.InvalidDecisionSpecifierError... 2853 """ 2854 try: 2855 return int(specString) 2856 except ValueError: 2857 tokens = self.lex(specString) 2858 result, end = self.parseDecisionSpecifierFromTokens(tokens) 2859 if end != len(tokens) - 1: 2860 raise base.InvalidDecisionSpecifierError( 2861 f"Junk after end of decision specifier:" 2862 f"\n{tokens[end + 1:]}" 2863 ) 2864 return result 2865 2866 def parseFeatureSpecifierFromTokens( 2867 self, 2868 tokens: LexedTokens, 2869 start: int = 0, 2870 limit: int = -1 2871 ) -> Tuple[base.FeatureSpecifier, int]: 2872 """ 2873 Parses a `FeatureSpecifier` starting from the specified part of 2874 a tokens list. Returns a tuple containing the feature specifier 2875 and the end position of the end of the feature specifier. 2876 2877 Can parse integer feature IDs in string form, as well as nested 2878 feature specifiers and plain feature specifiers. Assumes that 2879 all int-convertible strings are feature IDs, so it cannot deal 2880 with feature names which are just numbers. 2881 2882 For example: 2883 2884 >>> pf = ParseFormat() 2885 >>> pf.parseFeatureSpecifierFromTokens(['example']) 2886 (FeatureSpecifier(domain=None, within=[], feature='example',\ 2887 part=None), 0) 2888 >>> pf.parseFeatureSpecifierFromTokens(['example1', 'example2'], 1) 2889 (FeatureSpecifier(domain=None, within=[], feature='example2',\ 2890 part=None), 1) 2891 >>> pf.parseFeatureSpecifierFromTokens( 2892 ... [ 2893 ... 'domain', 2894 ... Lexeme.domainSeparator, 2895 ... 'region', 2896 ... Lexeme.zoneSeparator, 2897 ... 'feature', 2898 ... Lexeme.partSeparator, 2899 ... 'part' 2900 ... ] 2901 ... ) 2902 (FeatureSpecifier(domain='domain', within=['region'],\ 2903 feature='feature', part='part'), 6) 2904 >>> pf.parseFeatureSpecifierFromTokens( 2905 ... [ 2906 ... 'outerRegion', 2907 ... Lexeme.zoneSeparator, 2908 ... 'midRegion', 2909 ... Lexeme.zoneSeparator, 2910 ... 'innerRegion', 2911 ... Lexeme.zoneSeparator, 2912 ... 'feature' 2913 ... ] 2914 ... ) 2915 (FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion',\ 2916 'innerRegion'], feature='feature', part=None), 6) 2917 >>> pf.parseFeatureSpecifierFromTokens( 2918 ... [ 2919 ... 'outerRegion', 2920 ... Lexeme.zoneSeparator, 2921 ... 'midRegion', 2922 ... Lexeme.zoneSeparator, 2923 ... 'innerRegion', 2924 ... Lexeme.zoneSeparator, 2925 ... 'feature' 2926 ... ], 2927 ... 1 2928 ... ) 2929 Traceback (most recent call last): 2930 ... 2931 exploration.parsing.InvalidFeatureSpecifierError... 2932 >>> pf.parseFeatureSpecifierFromTokens( 2933 ... [ 2934 ... 'outerRegion', 2935 ... Lexeme.zoneSeparator, 2936 ... 'midRegion', 2937 ... Lexeme.zoneSeparator, 2938 ... 'innerRegion', 2939 ... Lexeme.zoneSeparator, 2940 ... 'feature' 2941 ... ], 2942 ... 2 2943 ... ) 2944 (FeatureSpecifier(domain=None, within=['midRegion', 'innerRegion'],\ 2945 feature='feature', part=None), 6) 2946 >>> pf.parseFeatureSpecifierFromTokens( 2947 ... [ 2948 ... 'outerRegion', 2949 ... Lexeme.zoneSeparator, 2950 ... 'feature', 2951 ... Lexeme.domainSeparator, 2952 ... 'after', 2953 ... ] 2954 ... ) 2955 (FeatureSpecifier(domain=None, within=['outerRegion'],\ 2956 feature='feature', part=None), 2) 2957 >>> pf.parseFeatureSpecifierFromTokens( 2958 ... [ 2959 ... 'outerRegion', 2960 ... Lexeme.zoneSeparator, 2961 ... 'feature', 2962 ... Lexeme.domainSeparator, 2963 ... 'after', 2964 ... ], 2965 ... 2 2966 ... ) 2967 (FeatureSpecifier(domain='feature', within=[], feature='after',\ 2968 part=None), 4) 2969 >>> # Including a limit: 2970 >>> pf.parseFeatureSpecifierFromTokens( 2971 ... [ 2972 ... 'outerRegion', 2973 ... Lexeme.zoneSeparator, 2974 ... 'midRegion', 2975 ... Lexeme.zoneSeparator, 2976 ... 'feature', 2977 ... ], 2978 ... 0, 2979 ... 2 2980 ... ) 2981 (FeatureSpecifier(domain=None, within=['outerRegion'],\ 2982 feature='midRegion', part=None), 2) 2983 >>> pf.parseFeatureSpecifierFromTokens( 2984 ... [ 2985 ... 'outerRegion', 2986 ... Lexeme.zoneSeparator, 2987 ... 'midRegion', 2988 ... Lexeme.zoneSeparator, 2989 ... 'feature', 2990 ... ], 2991 ... 0, 2992 ... 0 2993 ... ) 2994 (FeatureSpecifier(domain=None, within=[], feature='outerRegion',\ 2995 part=None), 0) 2996 >>> pf.parseFeatureSpecifierFromTokens( 2997 ... [ 2998 ... 'region', 2999 ... Lexeme.zoneSeparator, 3000 ... Lexeme.zoneSeparator, 3001 ... 'feature', 3002 ... ] 3003 ... ) 3004 (FeatureSpecifier(domain=None, within=[], feature='region',\ 3005 part=None), 0) 3006 """ 3007 start, limit, nTokens = normalizeEnds(tokens, start, limit) 3008 3009 if nTokens == 0: 3010 raise InvalidFeatureSpecifierError( 3011 "Can't parse a feature specifier from 0 tokens." 3012 ) 3013 first = tokens[start] 3014 if isinstance(first, Lexeme): 3015 raise InvalidFeatureSpecifierError( 3016 f"Feature specifier can't begin with a special token." 3017 f"Got:\n{tokens[start:limit + 1]}" 3018 ) 3019 3020 if nTokens in (1, 2): 3021 # 2 tokens isn't enough for a second part 3022 fs = base.FeatureSpecifier( 3023 domain=None, 3024 within=[], 3025 feature=first, 3026 part=None 3027 ) 3028 return (base.normalizeFeatureSpecifier(fs), start) 3029 3030 firstSep = tokens[start + 1] 3031 secondPart = tokens[start + 2] 3032 3033 if ( 3034 firstSep not in ( 3035 Lexeme.domainSeparator, 3036 Lexeme.zoneSeparator, 3037 Lexeme.partSeparator 3038 ) 3039 or not isinstance(secondPart, str) 3040 ): 3041 # Following tokens won't work out 3042 fs = base.FeatureSpecifier( 3043 domain=None, 3044 within=[], 3045 feature=first, 3046 part=None 3047 ) 3048 return (base.normalizeFeatureSpecifier(fs), start) 3049 3050 if firstSep == Lexeme.domainSeparator: 3051 if start + 2 > limit: 3052 return ( 3053 base.FeatureSpecifier( 3054 domain=first, 3055 within=[], 3056 feature=secondPart, 3057 part=None 3058 ), 3059 start + 2 3060 ) 3061 else: 3062 rest, restEnd = self.parseFeatureSpecifierFromTokens( 3063 tokens, 3064 start + 2, 3065 limit 3066 ) 3067 if rest.domain is not None: # two domainSeparators in a row 3068 fs = base.FeatureSpecifier( 3069 domain=first, 3070 within=[], 3071 feature=rest.domain, 3072 part=None 3073 ) 3074 return (base.normalizeFeatureSpecifier(fs), start + 2) 3075 else: 3076 fs = base.FeatureSpecifier( 3077 domain=first, 3078 within=rest.within, 3079 feature=rest.feature, 3080 part=rest.part 3081 ) 3082 return (base.normalizeFeatureSpecifier(fs), restEnd) 3083 3084 elif firstSep == Lexeme.zoneSeparator: 3085 if start + 2 > limit: 3086 fs = base.FeatureSpecifier( 3087 domain=None, 3088 within=[first], 3089 feature=secondPart, 3090 part=None 3091 ) 3092 return (base.normalizeFeatureSpecifier(fs), start + 2) 3093 else: 3094 rest, restEnd = self.parseFeatureSpecifierFromTokens( 3095 tokens, 3096 start + 2, 3097 limit 3098 ) 3099 if rest.domain is not None: # domain sep after zone sep 3100 fs = base.FeatureSpecifier( 3101 domain=None, 3102 within=[first], 3103 feature=rest.domain, 3104 part=None 3105 ) 3106 return (base.normalizeFeatureSpecifier(fs), start + 2) 3107 else: 3108 within = [first] 3109 within.extend(rest.within) 3110 fs = base.FeatureSpecifier( 3111 domain=None, 3112 within=within, 3113 feature=rest.feature, 3114 part=rest.part 3115 ) 3116 return (base.normalizeFeatureSpecifier(fs), restEnd) 3117 3118 else: # must be partSeparator 3119 fs = base.FeatureSpecifier( 3120 domain=None, 3121 within=[], 3122 feature=first, 3123 part=secondPart 3124 ) 3125 return (base.normalizeFeatureSpecifier(fs), start + 2) 3126 3127 def parseFeatureSpecifier(self, specString: str) -> base.FeatureSpecifier: 3128 """ 3129 Parses a full `FeatureSpecifier` from a single string. See 3130 `parseFeatureSpecifierFromTokens`. 3131 3132 >>> pf = ParseFormat() 3133 >>> pf.parseFeatureSpecifier('example') 3134 FeatureSpecifier(domain=None, within=[], feature='example', part=None) 3135 >>> pf.parseFeatureSpecifier('outer::example') 3136 FeatureSpecifier(domain=None, within=['outer'], feature='example',\ 3137 part=None) 3138 >>> pf.parseFeatureSpecifier('example%%middle') 3139 FeatureSpecifier(domain=None, within=[], feature='example',\ 3140 part='middle') 3141 >>> pf.parseFeatureSpecifier('domain//region::feature%%part') 3142 FeatureSpecifier(domain='domain', within=['region'],\ 3143 feature='feature', part='part') 3144 >>> pf.parseFeatureSpecifier( 3145 ... 'outerRegion::midRegion::innerRegion::feature' 3146 ... ) 3147 FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion',\ 3148 'innerRegion'], feature='feature', part=None) 3149 >>> pf.parseFeatureSpecifier('region::domain//feature') 3150 Traceback (most recent call last): 3151 ... 3152 exploration.parsing.InvalidFeatureSpecifierError... 3153 >>> pf.parseFeatureSpecifier('feature%%part1%%part2') 3154 Traceback (most recent call last): 3155 ... 3156 exploration.parsing.InvalidFeatureSpecifierError... 3157 >>> pf.parseFeatureSpecifier('domain1//domain2//feature') 3158 Traceback (most recent call last): 3159 ... 3160 exploration.parsing.InvalidFeatureSpecifierError... 3161 >>> # TODO: Issue warnings for these... 3162 >>> pf.parseFeatureSpecifier('domain//123') # domain discarded 3163 FeatureSpecifier(domain=None, within=[], feature=123, part=None) 3164 >>> pf.parseFeatureSpecifier('region::123') # zone discarded 3165 FeatureSpecifier(domain=None, within=[], feature=123, part=None) 3166 >>> pf.parseFeatureSpecifier('123%%part') 3167 FeatureSpecifier(domain=None, within=[], feature=123, part='part') 3168 """ 3169 tokens = self.lex(specString) 3170 result, rEnd = self.parseFeatureSpecifierFromTokens(tokens) 3171 if rEnd != len(tokens) - 1: 3172 raise InvalidFeatureSpecifierError( 3173 f"Feature specifier has extra stuff at end:" 3174 f" {tokens[rEnd + 1:]}" 3175 ) 3176 else: 3177 return result 3178 3179 def normalizeFeatureSpecifier( 3180 self, 3181 spec: base.AnyFeatureSpecifier 3182 ) -> base.FeatureSpecifier: 3183 """ 3184 Normalizes any kind of feature specifier into an official 3185 `FeatureSpecifier` tuple. 3186 3187 For example: 3188 3189 >>> pf = ParseFormat() 3190 >>> pf.normalizeFeatureSpecifier('town') 3191 FeatureSpecifier(domain=None, within=[], feature='town', part=None) 3192 >>> pf.normalizeFeatureSpecifier(5) 3193 FeatureSpecifier(domain=None, within=[], feature=5, part=None) 3194 >>> pf.parseFeatureSpecifierFromTokens( 3195 ... [ 3196 ... 'domain', 3197 ... Lexeme.domainSeparator, 3198 ... 'region', 3199 ... Lexeme.zoneSeparator, 3200 ... 'feature', 3201 ... Lexeme.partSeparator, 3202 ... 'part' 3203 ... ] 3204 ... ) 3205 (FeatureSpecifier(domain='domain', within=['region'],\ 3206 feature='feature', part='part'), 6) 3207 >>> pf.normalizeFeatureSpecifier('dom//one::two::three%%middle') 3208 FeatureSpecifier(domain='dom', within=['one', 'two'],\ 3209 feature='three', part='middle') 3210 >>> pf.normalizeFeatureSpecifier( 3211 ... base.FeatureSpecifier(None, ['region'], 'place', None) 3212 ... ) 3213 FeatureSpecifier(domain=None, within=['region'], feature='place',\ 3214 part=None) 3215 >>> fs = base.FeatureSpecifier(None, [], 'place', None) 3216 >>> ns = pf.normalizeFeatureSpecifier(fs) 3217 >>> ns is fs # Doesn't create unnecessary clones 3218 True 3219 """ 3220 if isinstance(spec, base.FeatureSpecifier): 3221 return spec 3222 elif isinstance(spec, base.FeatureID): 3223 return base.FeatureSpecifier(None, [], spec, None) 3224 elif isinstance(spec, str): 3225 return self.parseFeatureSpecifier(spec) 3226 else: 3227 raise TypeError(f"Invalid feature specifier type: '{type(spec)}'") 3228 3229 def unparseChallenge(self, challenge: base.Challenge) -> str: 3230 """ 3231 Turns a `base.Challenge` into a string that can be turned back 3232 into an equivalent challenge by `parseChallenge`. For example: 3233 3234 >>> pf = ParseFormat() 3235 >>> c = base.challenge( 3236 ... skills=base.BestSkill('brains', 'brawn'), 3237 ... level=2, 3238 ... success=[base.effect(set=('switch', 'on'))], 3239 ... failure=[ 3240 ... base.effect(deactivate=True, delay=1), 3241 ... base.effect(bounce=True) 3242 ... ], 3243 ... outcome=True 3244 ... ) 3245 >>> r = pf.unparseChallenge(c) 3246 >>> r 3247 '<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}' 3248 >>> pf.parseChallenge(r) == c 3249 True 3250 >>> c2 = base.challenge( 3251 ... skills=base.CombinedSkill( 3252 ... -2, 3253 ... base.ConditionalSkill( 3254 ... base.ReqCapability('tough'), 3255 ... base.BestSkill(1), 3256 ... base.BestSkill(-1) 3257 ... ) 3258 ... ), 3259 ... level=-2, 3260 ... success=[base.effect(gain='orb')], 3261 ... failure=[], 3262 ... outcome=None 3263 ... ) 3264 >>> r2 = pf.unparseChallenge(c2) 3265 >>> r2 3266 '<-2>sum(-2, if(tough, best(1), best(-1))){gain orb}{}' 3267 >>> # TODO: let this parse through without BestSkills... 3268 >>> pf.parseChallenge(r2) == c2 3269 True 3270 """ 3271 lt = self.formatDict[Lexeme.angleLeft] 3272 gt = self.formatDict[Lexeme.angleRight] 3273 result = ( 3274 lt + str(challenge['level']) + gt 3275 + challenge['skills'].unparse() 3276 ) 3277 if challenge['outcome'] is True: 3278 result += gt 3279 result += self.unparseConsequence(challenge['success']) 3280 if challenge['outcome'] is False: 3281 result += gt 3282 result += self.unparseConsequence(challenge['failure']) 3283 return result 3284 3285 def unparseCondition(self, condition: base.Condition) -> str: 3286 """ 3287 Given a `base.Condition` returns a string that would result in 3288 that condition if given to `parseCondition`. For example: 3289 3290 >>> pf = ParseFormat() 3291 >>> c = base.condition( 3292 ... condition=base.ReqAny([ 3293 ... base.ReqCapability('brawny'), 3294 ... base.ReqNot(base.ReqTokens('weights', 3)) 3295 ... ]), 3296 ... consequence=[base.effect(gain='power')] 3297 ... ) 3298 >>> r = pf.unparseCondition(c) 3299 >>> r 3300 '??((brawny|!(weights*3))){gain power}{}' 3301 >>> pf.parseCondition(r) == c 3302 True 3303 """ 3304 return ( 3305 self.formatDict[Lexeme.doubleQuestionmark] 3306 + self.formatDict[Lexeme.openParen] 3307 + condition['condition'].unparse() 3308 + self.formatDict[Lexeme.closeParen] 3309 + self.unparseConsequence(condition['consequence']) 3310 + self.unparseConsequence(condition['alternative']) 3311 ) 3312 3313 def unparseConsequence(self, consequence: base.Consequence) -> str: 3314 """ 3315 Given a `base.Consequence`, returns a string encoding of it, 3316 using the same format that `parseConsequence` will parse. Uses 3317 function-call-like syntax and curly braces to denote different 3318 sub-consequences. See also `SkillCombination.unparse` and 3319 `Requirement.unparse` For example: 3320 3321 >>> pf = ParseFormat() 3322 >>> c = [base.effect(gain='one'), base.effect(lose='one')] 3323 >>> pf.unparseConsequence(c) 3324 '{gain one; lose one}' 3325 >>> c = [ 3326 ... base.challenge( 3327 ... skills=base.BestSkill('brains', 'brawn'), 3328 ... level=2, 3329 ... success=[base.effect(set=('switch', 'on'))], 3330 ... failure=[ 3331 ... base.effect(deactivate=True, delay=1), 3332 ... base.effect(bounce=True) 3333 ... ], 3334 ... outcome=True 3335 ... ) 3336 ... ] 3337 >>> pf.unparseConsequence(c) 3338 '{<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}}' 3339 >>> c[0]['outcome'] = False 3340 >>> pf.unparseConsequence(c) 3341 '{<2>best(brains, brawn){set switch:on}>{deactivate ,1; bounce}}' 3342 >>> c[0]['outcome'] = None 3343 >>> pf.unparseConsequence(c) 3344 '{<2>best(brains, brawn){set switch:on}{deactivate ,1; bounce}}' 3345 >>> c = [ 3346 ... base.condition( 3347 ... condition=base.ReqAny([ 3348 ... base.ReqCapability('brawny'), 3349 ... base.ReqNot(base.ReqTokens('weights', 3)) 3350 ... ]), 3351 ... consequence=[ 3352 ... base.challenge( 3353 ... skills=base.CombinedSkill('brains', 'brawn'), 3354 ... level=3, 3355 ... success=[base.effect(goto='home')], 3356 ... failure=[base.effect(bounce=True)], 3357 ... outcome=None 3358 ... ) 3359 ... ] # no alternative -> empty list 3360 ... ) 3361 ... ] 3362 >>> pf.unparseConsequence(c) 3363 '{??((brawny|!(weights*3))){\ 3364<3>sum(brains, brawn){goto home}{bounce}}{}}' 3365 >>> c = [base.effect(gain='if(power){gain "mimic"}')] 3366 >>> # TODO: Make this work! 3367 >>> # pf.unparseConsequence(c) 3368 3369 '{gain "if(power){gain \\\\"mimic\\\\"}"}' 3370 """ 3371 result = self.formatDict[Lexeme.openCurly] 3372 for item in consequence: 3373 if 'skills' in item: # a Challenge 3374 item = cast(base.Challenge, item) 3375 result += self.unparseChallenge(item) 3376 3377 elif 'value' in item: # an Effect 3378 item = cast(base.Effect, item) 3379 result += self.unparseEffect(item) 3380 3381 elif 'condition' in item: # a Condition 3382 item = cast(base.Condition, item) 3383 result += self.unparseCondition(item) 3384 3385 else: # bad dict 3386 raise TypeError( 3387 f"Invalid consequence: items in the list must be" 3388 f" Effects, Challenges, or Conditions (got a dictionary" 3389 f" without 'skills', 'value', or 'condition' keys)." 3390 f"\nGot item: {repr(item)}" 3391 ) 3392 result += '; ' 3393 3394 if result.endswith('; '): 3395 result = result[:-2] 3396 3397 return result + self.formatDict[Lexeme.closeCurly] 3398 3399 def parseMechanismSpecifierFromTokens( 3400 self, 3401 tokens: LexedTokens, 3402 start: int = 0 3403 ) -> Tuple[base.MechanismSpecifier, int]: 3404 """ 3405 Parses a mechanism specifier starting at the specified position 3406 in the given tokens list. No ending position is specified, but 3407 instead this function returns a tuple containing the parsed 3408 `base.MechanismSpecifier` along with an index in the tokens list 3409 where the end of the specifier was found. 3410 3411 For example: 3412 3413 >>> pf = ParseFormat() 3414 >>> pf.parseMechanismSpecifierFromTokens(['m']) 3415 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3416 name='m'), 0) 3417 >>> pf.parseMechanismSpecifierFromTokens(['a', 'm']) 3418 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3419 name='a'), 0) 3420 >>> pf.parseMechanismSpecifierFromTokens(['a', 'm'], 1) 3421 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3422 name='m'), 1) 3423 >>> pf.parseMechanismSpecifierFromTokens( 3424 ... ['a', Lexeme.domainSeparator, 'm'] 3425 ... ) 3426 (MechanismSpecifier(domain='a', zone=None, decision=None,\ 3427 name='m'), 2) 3428 >>> pf.parseMechanismSpecifierFromTokens( 3429 ... ['a', Lexeme.zoneSeparator, 'm'] 3430 ... ) 3431 (MechanismSpecifier(domain=None, zone=None, decision='a',\ 3432 name='m'), 2) 3433 >>> pf.parseMechanismSpecifierFromTokens( 3434 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm'] 3435 ... ) 3436 (MechanismSpecifier(domain=None, zone='a', decision='b',\ 3437 name='m'), 4) 3438 >>> pf.parseMechanismSpecifierFromTokens( 3439 ... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm'] 3440 ... ) 3441 (MechanismSpecifier(domain='a', zone=None, decision='b',\ 3442 name='m'), 4) 3443 >>> pf.parseMechanismSpecifierFromTokens( 3444 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'] 3445 ... ) 3446 (MechanismSpecifier(domain=None, zone=None, decision='a',\ 3447 name='b'), 2) 3448 >>> pf.parseMechanismSpecifierFromTokens( 3449 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 3450 ... 1 3451 ... ) 3452 Traceback (most recent call last): 3453 ... 3454 exploration.parsing.ParseError... 3455 >>> pf.parseMechanismSpecifierFromTokens( 3456 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 3457 ... 2 3458 ... ) 3459 (MechanismSpecifier(domain='b', zone=None, decision=None,\ 3460 name='m'), 4) 3461 >>> pf.parseMechanismSpecifierFromTokens( 3462 ... [ 3463 ... 'a', 3464 ... Lexeme.domainSeparator, 3465 ... 'b', 3466 ... Lexeme.zoneSeparator, 3467 ... 'c', 3468 ... Lexeme.zoneSeparator, 3469 ... 'm' 3470 ... ] 3471 ... ) 3472 (MechanismSpecifier(domain='a', zone='b', decision='c', name='m'), 6) 3473 >>> pf.parseMechanismSpecifierFromTokens( 3474 ... [ 3475 ... 'a', 3476 ... Lexeme.domainSeparator, 3477 ... 'b', 3478 ... Lexeme.zoneSeparator, 3479 ... 'c', 3480 ... Lexeme.zoneSeparator, 3481 ... 'm' 3482 ... ], 3483 ... 2 3484 ... ) 3485 (MechanismSpecifier(domain=None, zone='b', decision='c',\ 3486 name='m'), 6) 3487 >>> pf.parseMechanismSpecifierFromTokens( 3488 ... [ 3489 ... 'a', 3490 ... Lexeme.domainSeparator, 3491 ... 'b', 3492 ... Lexeme.zoneSeparator, 3493 ... 'c', 3494 ... Lexeme.zoneSeparator, 3495 ... 'm' 3496 ... ], 3497 ... 4 3498 ... ) 3499 (MechanismSpecifier(domain=None, zone=None, decision='c',\ 3500 name='m'), 6) 3501 >>> pf.parseMechanismSpecifierFromTokens( 3502 ... [ 3503 ... 'roomB', 3504 ... Lexeme.zoneSeparator, 3505 ... 'switch', 3506 ... Lexeme.mechanismSeparator, 3507 ... 'on' 3508 ... ] 3509 ... ) 3510 (MechanismSpecifier(domain=None, zone=None, decision='roomB',\ 3511 name='switch'), 2) 3512 >>> pf.parseMechanismSpecifierFromTokens( 3513 ... [ 3514 ... '500', 3515 ... Lexeme.zoneSeparator, 3516 ... 'm' 3517 ... ], 3518 ... 0 3519 ... ) 3520 (MechanismSpecifier(domain=None, zone=None, decision=500,\ 3521 name='m'), 2) 3522 >>> pf.parseMechanismSpecifierFromTokens( 3523 ... [ 3524 ... '500', 3525 ... Lexeme.zoneSeparator, 3526 ... Lexeme.zoneSeparator, 3527 ... 'm' 3528 ... ], 3529 ... 0 3530 ... ) 3531 Traceback (most recent call last): 3532 ... 3533 exploration.parsing.ParseError... 3534 >>> pf.parseMechanismSpecifierFromTokens( 3535 ... [ 3536 ... '500', 3537 ... Lexeme.zoneSeparator, 3538 ... 'm', 3539 ... Lexeme.mechanismSeparator, 3540 ... 'on' 3541 ... ], 3542 ... 0 3543 ... ) 3544 (MechanismSpecifier(domain=None, zone=None, decision=500,\ 3545 name='m'), 2) 3546 >>> pf.parseMechanismSpecifierFromTokens( 3547 ... [ 3548 ... '500', 3549 ... Lexeme.zoneSeparator, 3550 ... 'd', 3551 ... Lexeme.zoneSeparator, 3552 ... 'm' 3553 ... ], 3554 ... 0 3555 ... ) 3556 (MechanismSpecifier(domain=None, zone='500', decision='d',\ 3557 name='m'), 4) 3558 """ 3559 start, tEnd, nLeft = normalizeEnds(tokens, start, -1) 3560 3561 try: 3562 dSpec, dEnd = self.parseDecisionSpecifierFromTokens( 3563 tokens, 3564 start 3565 ) 3566 except ParseError: 3567 raise ParseError( 3568 "Failed to parse mechanism specifier couldn't parse" 3569 " initial mechanism name." 3570 ) 3571 3572 # Note: This doesn't normally happen because the mechanism name 3573 # makes it seem like the integer decision ID is really a zone 3574 # name. 3575 if isinstance(dSpec, int): 3576 sep = tokens[dEnd + 1] 3577 after = tokens[dEnd + 2] 3578 3579 if sep == Lexeme.zoneSeparator and not isinstance(after, Lexeme): 3580 return ( 3581 base.MechanismSpecifier( 3582 domain=None, 3583 zone=None, 3584 decision=dSpec, 3585 name=after 3586 ), 3587 dEnd + 2 3588 ) 3589 else: 3590 raise ParseError( 3591 f"Invalid mechanism specifier: got a decision ID" 3592 f" NOT followed by a zone separator and mechanism" 3593 f" name. Got: {tokens[start:]}" 3594 ) 3595 3596 mDomain = dSpec.domain 3597 if dEnd == tEnd or dEnd == tEnd - 1: 3598 if dSpec.zone is not None: 3599 try: 3600 # Case for integer "zone" -> integer decision ID 3601 zID = int(dSpec.zone) 3602 return ( 3603 base.MechanismSpecifier( 3604 domain=None, 3605 zone=None, 3606 decision=zID, 3607 name=dSpec.name 3608 ), 3609 dEnd 3610 ) 3611 except ValueError: 3612 pass 3613 return ( 3614 base.MechanismSpecifier( 3615 domain=mDomain, 3616 zone=None, 3617 decision=dSpec.zone, 3618 name=dSpec.name 3619 ), 3620 dEnd 3621 ) 3622 3623 sep = tokens[dEnd + 1] 3624 after = tokens[dEnd + 2] 3625 3626 mDec: Optional[Union[base.DecisionName, base.DecisionID]] 3627 if sep == Lexeme.zoneSeparator: 3628 if isinstance(after, Lexeme): 3629 mZone = None 3630 mDec = dSpec.zone 3631 mName = dSpec.name 3632 mEnd = dEnd 3633 else: 3634 mZone = dSpec.zone 3635 mDec = dSpec.name 3636 mName = after 3637 mEnd = dEnd + 2 3638 else: 3639 mZone = None 3640 mDec = dSpec.zone 3641 mName = dSpec.name 3642 mEnd = dEnd 3643 3644 # Treat numerical decision "names" as decision IDs 3645 if mDec is not None: 3646 try: 3647 mDec = int(mDec) 3648 if mDomain is not None or mZone is not None: 3649 raise ParseError( 3650 f"Invalid mechanism specifier: got a numerical" 3651 f" decision ID but also a domain and/or zone." 3652 f" Got: {tokens[start:]}" 3653 ) 3654 except ValueError: 3655 pass 3656 3657 return ( 3658 base.MechanismSpecifier( 3659 domain=mDomain, 3660 zone=mZone, 3661 decision=mDec, 3662 name=mName 3663 ), 3664 mEnd 3665 ) 3666 3667 def groupReqTokens( 3668 self, 3669 tokens: LexedTokens, 3670 start: int = 0, 3671 end: int = -1 3672 ) -> GroupedTokens: 3673 """ 3674 Groups tokens for a requirement, stripping out all parentheses 3675 but replacing parenthesized expressions with sub-lists of tokens. 3676 3677 For example: 3678 3679 >>> pf = ParseFormat() 3680 >>> pf.groupReqTokens(['jump']) 3681 ['jump'] 3682 >>> pf.groupReqTokens([Lexeme.openParen, 'jump']) 3683 Traceback (most recent call last): 3684 ... 3685 exploration.parsing.ParseError... 3686 >>> pf.groupReqTokens([Lexeme.closeParen, 'jump']) 3687 Traceback (most recent call last): 3688 ... 3689 exploration.parsing.ParseError... 3690 >>> pf.groupReqTokens(['jump', Lexeme.closeParen]) 3691 Traceback (most recent call last): 3692 ... 3693 exploration.parsing.ParseError... 3694 >>> pf.groupReqTokens([Lexeme.openParen, 'jump', Lexeme.closeParen]) 3695 [['jump']] 3696 >>> pf.groupReqTokens( 3697 ... [ 3698 ... Lexeme.openParen, 3699 ... 'jump', 3700 ... Lexeme.orBar, 3701 ... 'climb', 3702 ... Lexeme.closeParen, 3703 ... Lexeme.ampersand, 3704 ... 'crawl', 3705 ... ] 3706 ... ) 3707 [['jump', <Lexeme.orBar: ...>, 'climb'], <Lexeme.ampersand: ...>,\ 3708 'crawl'] 3709 """ 3710 start, end, nTokens = normalizeEnds(tokens, start, end) 3711 if nTokens == 0: 3712 raise ParseError("Ran out of tokens.") 3713 3714 resultsStack: List[GroupedTokens] = [[]] 3715 here = start 3716 while here <= end: 3717 token = tokens[here] 3718 here += 1 3719 if token == Lexeme.closeParen: 3720 if len(resultsStack) == 1: 3721 raise ParseError( 3722 f"Too many closing parens at index {here - 1}" 3723 f" in:\n{tokens[start:end + 1]}" 3724 ) 3725 else: 3726 closed = resultsStack.pop() 3727 resultsStack[-1].append(closed) 3728 elif token == Lexeme.openParen: 3729 resultsStack.append([]) 3730 else: 3731 resultsStack[-1].append(token) 3732 if len(resultsStack) != 1: 3733 raise ParseError( 3734 f"Mismatched parentheses in tokens:" 3735 f"\n{tokens[start:end + 1]}" 3736 ) 3737 return resultsStack[0] 3738 3739 def groupReqTokensByPrecedence( 3740 self, 3741 tokenGroups: GroupedTokens 3742 ) -> GroupedRequirementParts: 3743 """ 3744 Re-groups requirement tokens that have been grouped using 3745 `groupReqTokens` according to operator precedence, effectively 3746 creating an equivalent result which would have been obtained by 3747 `groupReqTokens` if all possible non-redundant explicit 3748 parentheses had been included. 3749 3750 Also turns each leaf part into a `Requirement`. 3751 3752 TODO: Make this actually reasonably efficient T_T 3753 3754 Examples: 3755 3756 >>> pf = ParseFormat() 3757 >>> r = pf.parseRequirement('capability&roomB::switch:on') 3758 >>> pf.groupReqTokensByPrecedence( 3759 ... [ 3760 ... ['jump', Lexeme.orBar, 'climb'], 3761 ... Lexeme.ampersand, 3762 ... Lexeme.notMarker, 3763 ... 'coin', 3764 ... Lexeme.tokenCount, 3765 ... '3' 3766 ... ] 3767 ... ) 3768 [\ 3769[\ 3770[[ReqCapability('jump'), <Lexeme.orBar: ...>, ReqCapability('climb')]],\ 3771 <Lexeme.ampersand: ...>,\ 3772 [<Lexeme.notMarker: ...>, ReqTokens('coin', 3)]\ 3773]\ 3774] 3775 """ 3776 subgrouped: List[Union[Lexeme, str, GroupedRequirementParts]] = [] 3777 # First recursively group all parenthesized expressions 3778 for i, item in enumerate(tokenGroups): 3779 if isinstance(item, list): 3780 subgrouped.append(self.groupReqTokensByPrecedence(item)) 3781 else: 3782 subgrouped.append(item) 3783 3784 # Now process all leaf requirements 3785 leavesConverted: GroupedRequirementParts = [] 3786 i = 0 3787 while i < len(subgrouped): 3788 gItem = subgrouped[i] 3789 3790 if isinstance(gItem, list): 3791 leavesConverted.append(gItem) 3792 elif isinstance(gItem, Lexeme): 3793 leavesConverted.append(gItem) 3794 elif i == len(subgrouped) - 1: 3795 if isinstance(gItem, Lexeme): 3796 raise ParseError( 3797 f"Lexeme at end of requirement. Grouped tokens:" 3798 f"\n{tokenGroups}" 3799 ) 3800 else: 3801 assert isinstance(gItem, str) 3802 if gItem == 'X': 3803 leavesConverted.append(base.ReqImpossible()) 3804 elif gItem == 'O': 3805 leavesConverted.append(base.ReqNothing()) 3806 else: 3807 leavesConverted.append(base.ReqCapability(gItem)) 3808 else: 3809 assert isinstance(gItem, str) 3810 try: 3811 # TODO: Avoid list copy here... 3812 couldBeMechanismSpecifier: LexedTokens = [] 3813 for ii in range(i, len(subgrouped)): 3814 lexemeOrStr = subgrouped[ii] 3815 if isinstance(lexemeOrStr, (Lexeme, str)): 3816 couldBeMechanismSpecifier.append(lexemeOrStr) 3817 else: 3818 break 3819 mSpec, mEnd = self.parseMechanismSpecifierFromTokens( 3820 couldBeMechanismSpecifier 3821 ) 3822 mEnd += i 3823 if ( 3824 mEnd >= len(subgrouped) - 2 3825 or subgrouped[mEnd + 1] != Lexeme.mechanismSeparator 3826 ): 3827 raise ParseError("Not a mechanism requirement.") 3828 3829 mState = subgrouped[mEnd + 2] 3830 if not isinstance(mState, base.MechanismState): 3831 raise ParseError("Not a mechanism requirement.") 3832 leavesConverted.append(base.ReqMechanism(mSpec, mState)) 3833 i = mEnd + 2 # + 1 will happen automatically below 3834 except ParseError: 3835 following = subgrouped[i + 1] 3836 if following in ( 3837 Lexeme.tokenCount, 3838 Lexeme.mechanismSeparator, 3839 Lexeme.wigglyLine, 3840 Lexeme.skillLevel 3841 ): 3842 if ( 3843 i == len(subgrouped) - 2 3844 or isinstance(subgrouped[i + 2], Lexeme) 3845 ): 3846 if following == Lexeme.wigglyLine: 3847 # Default tag value is 1 3848 leavesConverted.append(base.ReqTag(gItem, 1)) 3849 i += 1 # another +1 automatic below 3850 else: 3851 raise ParseError( 3852 f"Lexeme at end of requirement. Grouped" 3853 f" tokens:\n{tokenGroups}" 3854 ) 3855 else: 3856 afterwards = subgrouped[i + 2] 3857 if not isinstance(afterwards, str): 3858 raise ParseError( 3859 f"Lexeme after token/mechanism/tag/skill" 3860 f" separator at index {i}." 3861 f" Grouped tokens:\n{tokenGroups}" 3862 ) 3863 i += 2 # another +1 automatic below 3864 if following == Lexeme.tokenCount: 3865 try: 3866 tCount = int(afterwards) 3867 except ValueError: 3868 raise ParseError( 3869 f"Token count could not be" 3870 f" parsed as an integer:" 3871 f" {afterwards!r}. Grouped" 3872 f" tokens:\n{tokenGroups}" 3873 ) 3874 leavesConverted.append( 3875 base.ReqTokens(gItem, tCount) 3876 ) 3877 elif following == Lexeme.mechanismSeparator: 3878 leavesConverted.append( 3879 base.ReqMechanism(gItem, afterwards) 3880 ) 3881 elif following == Lexeme.wigglyLine: 3882 tVal = self.parseTagValue(afterwards) 3883 leavesConverted.append( 3884 base.ReqTag(gItem, tVal) 3885 ) 3886 else: 3887 assert following == Lexeme.skillLevel 3888 try: 3889 sLevel = int(afterwards) 3890 except ValueError: 3891 raise ParseError( 3892 f"Skill level could not be" 3893 f" parsed as an integer:" 3894 f" {afterwards!r}. Grouped" 3895 f" tokens:\n{tokenGroups}" 3896 ) 3897 leavesConverted.append( 3898 base.ReqLevel(gItem, sLevel) 3899 ) 3900 else: 3901 if gItem == 'X': 3902 leavesConverted.append(base.ReqImpossible()) 3903 elif gItem == 'O': 3904 leavesConverted.append(base.ReqNothing()) 3905 else: 3906 leavesConverted.append( 3907 base.ReqCapability(gItem) 3908 ) 3909 3910 # Finally, increment our index: 3911 i += 1 3912 3913 # Now group all NOT operators 3914 i = 0 3915 notsGrouped: GroupedRequirementParts = [] 3916 while i < len(leavesConverted): 3917 leafItem = leavesConverted[i] 3918 group = [] 3919 while leafItem == Lexeme.notMarker: 3920 group.append(leafItem) 3921 i += 1 3922 if i >= len(leavesConverted): 3923 raise ParseError( 3924 f"NOT at end of tokens:\n{leavesConverted}" 3925 ) 3926 leafItem = leavesConverted[i] 3927 if group == []: 3928 notsGrouped.append(leafItem) 3929 i += 1 3930 else: 3931 group.append(leafItem) 3932 i += 1 3933 notsGrouped.append(group) 3934 3935 # Next group all AND operators 3936 i = 0 3937 andsGrouped: GroupedRequirementParts = [] 3938 while i < len(notsGrouped): 3939 notGroupItem = notsGrouped[i] 3940 if notGroupItem == Lexeme.ampersand: 3941 if i == len(notsGrouped) - 1: 3942 raise ParseError( 3943 f"AND at end of group in tokens:" 3944 f"\n{tokenGroups}" 3945 f"Which had been grouped into:" 3946 f"\n{notsGrouped}" 3947 ) 3948 itemAfter = notsGrouped[i + 1] 3949 if isinstance(itemAfter, Lexeme): 3950 raise ParseError( 3951 f"Lexeme after AND in of group in tokens:" 3952 f"\n{tokenGroups}" 3953 f"Which had been grouped into:" 3954 f"\n{notsGrouped}" 3955 ) 3956 assert isinstance(itemAfter, (base.Requirement, list)) 3957 prev = andsGrouped[-1] 3958 if ( 3959 isinstance(prev, list) 3960 and len(prev) > 2 3961 and prev[1] == Lexeme.ampersand 3962 ): 3963 prev.extend(notsGrouped[i:i + 2]) 3964 i += 1 # with an extra +1 below 3965 else: 3966 andsGrouped.append( 3967 [andsGrouped.pop()] + notsGrouped[i:i + 2] 3968 ) 3969 i += 1 # extra +1 below 3970 else: 3971 andsGrouped.append(notGroupItem) 3972 i += 1 3973 3974 # Finally check that we only have OR operators left over 3975 i = 0 3976 finalResult: GroupedRequirementParts = [] 3977 while i < len(andsGrouped): 3978 andGroupItem = andsGrouped[i] 3979 if andGroupItem == Lexeme.orBar: 3980 if i == len(andsGrouped) - 1: 3981 raise ParseError( 3982 f"OR at end of group in tokens:" 3983 f"\n{tokenGroups}" 3984 f"Which had been grouped into:" 3985 f"\n{andsGrouped}" 3986 ) 3987 itemAfter = andsGrouped[i + 1] 3988 if isinstance(itemAfter, Lexeme): 3989 raise ParseError( 3990 f"Lexeme after OR in of group in tokens:" 3991 f"\n{tokenGroups}" 3992 f"Which had been grouped into:" 3993 f"\n{andsGrouped}" 3994 ) 3995 assert isinstance(itemAfter, (base.Requirement, list)) 3996 prev = finalResult[-1] 3997 if ( 3998 isinstance(prev, list) 3999 and len(prev) > 2 4000 and prev[1] == Lexeme.orBar 4001 ): 4002 prev.extend(andsGrouped[i:i + 2]) 4003 i += 1 # with an extra +1 below 4004 else: 4005 finalResult.append( 4006 [finalResult.pop()] + andsGrouped[i:i + 2] 4007 ) 4008 i += 1 # extra +1 below 4009 elif isinstance(andGroupItem, Lexeme): 4010 raise ParseError( 4011 f"Leftover lexeme when grouping ORs at index {i}" 4012 f" in grouped tokens:\n{andsGrouped}" 4013 f"\nOriginal tokens were:\n{tokenGroups}" 4014 ) 4015 else: 4016 finalResult.append(andGroupItem) 4017 i += 1 4018 4019 return finalResult 4020 4021 def parseRequirementFromRegroupedTokens( 4022 self, 4023 reqGroups: GroupedRequirementParts 4024 ) -> base.Requirement: 4025 """ 4026 Recursive parser that works once tokens have been turned into 4027 requirements at the leaves and grouped by operator precedence 4028 otherwise (see `groupReqTokensByPrecedence`). 4029 4030 TODO: Simplify by just doing this while grouping... ? 4031 """ 4032 if len(reqGroups) == 0: 4033 raise ParseError("Ran out of tokens.") 4034 4035 elif len(reqGroups) == 1: 4036 only = reqGroups[0] 4037 if isinstance(only, list): 4038 return self.parseRequirementFromRegroupedTokens(only) 4039 elif isinstance(only, base.Requirement): 4040 return only 4041 else: 4042 raise ParseError(f"Invalid singleton group:\n{only}") 4043 elif reqGroups[0] == Lexeme.notMarker: 4044 if ( 4045 not all(x == Lexeme.notMarker for x in reqGroups[:-1]) 4046 or not isinstance(reqGroups[-1], (list, base.Requirement)) 4047 ): 4048 raise ParseError(f"Invalid negation group:\n{reqGroups}") 4049 result = reqGroups[-1] 4050 if isinstance(result, list): 4051 result = self.parseRequirementFromRegroupedTokens(result) 4052 assert isinstance(result, base.Requirement) 4053 for i in range(len(reqGroups) - 1): 4054 result = base.ReqNot(result) 4055 return result 4056 elif len(reqGroups) % 2 == 0: 4057 raise ParseError(f"Even-length non-negation group:\n{reqGroups}") 4058 else: 4059 if ( 4060 reqGroups[1] not in (Lexeme.ampersand, Lexeme.orBar) 4061 or not all( 4062 reqGroups[i] == reqGroups[1] 4063 for i in range(1, len(reqGroups), 2) 4064 ) 4065 ): 4066 raise ParseError( 4067 f"Inconsistent operator(s) in group:\n{reqGroups}" 4068 ) 4069 op = reqGroups[1] 4070 operands = [ 4071 ( 4072 self.parseRequirementFromRegroupedTokens(x) 4073 if isinstance(x, list) 4074 else x 4075 ) 4076 for x in reqGroups[::2] 4077 ] 4078 if not all(isinstance(x, base.Requirement) for x in operands): 4079 raise ParseError( 4080 f"Item not reducible to Requirement in AND group:" 4081 f"\n{reqGroups}" 4082 ) 4083 reqSequence = cast(Sequence[base.Requirement], operands) 4084 if op == Lexeme.ampersand: 4085 return base.ReqAll(reqSequence).flatten() 4086 else: 4087 assert op == Lexeme.orBar 4088 return base.ReqAny(reqSequence).flatten() 4089 4090 def parseRequirementFromGroupedTokens( 4091 self, 4092 tokenGroups: GroupedTokens 4093 ) -> base.Requirement: 4094 """ 4095 Parses a `base.Requirement` from a pre-grouped tokens list (see 4096 `groupReqTokens`). Uses the 'orBar', 'ampersand', 'notMarker', 4097 'tokenCount', and 'mechanismSeparator' `Lexeme`s to provide 4098 'or', 'and', and 'not' operators along with distinguishing 4099 between capabilities, tokens, and mechanisms. 4100 4101 Precedence ordering is not, then and, then or, but you are 4102 encouraged to use parentheses for explicit grouping (the 4103 'openParen' and 'closeParen' `Lexeme`s, although these must be 4104 handled by `groupReqTokens` so this function won't see them 4105 directly). 4106 4107 You can also use 'X' (without quotes) for a never-satisfied 4108 requirement, and 'O' (without quotes) for an always-satisfied 4109 requirement. 4110 4111 Note that when '!' is applied to a token requirement it flips 4112 the sense of the integer from 'must have at least this many' to 4113 'must have strictly less than this many'. 4114 4115 Raises a `ParseError` if the grouped tokens it is given cannot 4116 be parsed as a `Requirement`. 4117 4118 Examples: 4119 4120 >>> pf = ParseFormat() 4121 >>> pf.parseRequirementFromGroupedTokens(['capability']) 4122 ReqCapability('capability') 4123 >>> pf.parseRequirementFromGroupedTokens( 4124 ... ['token', Lexeme.tokenCount, '3'] 4125 ... ) 4126 ReqTokens('token', 3) 4127 >>> pf.parseRequirementFromGroupedTokens( 4128 ... ['mechanism', Lexeme.mechanismSeparator, 'state'] 4129 ... ) 4130 ReqMechanism('mechanism', 'state') 4131 >>> pf.parseRequirementFromGroupedTokens( 4132 ... ['capability', Lexeme.orBar, 'token', 4133 ... Lexeme.tokenCount, '3'] 4134 ... ) 4135 ReqAny([ReqCapability('capability'), ReqTokens('token', 3)]) 4136 >>> pf.parseRequirementFromGroupedTokens( 4137 ... ['one', Lexeme.ampersand, 'two', Lexeme.orBar, 'three'] 4138 ... ) 4139 ReqAny([ReqAll([ReqCapability('one'), ReqCapability('two')]),\ 4140 ReqCapability('three')]) 4141 >>> pf.parseRequirementFromGroupedTokens( 4142 ... [ 4143 ... 'one', 4144 ... Lexeme.ampersand, 4145 ... [ 4146 ... 'two', 4147 ... Lexeme.orBar, 4148 ... 'three' 4149 ... ] 4150 ... ] 4151 ... ) 4152 ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'),\ 4153 ReqCapability('three')])]) 4154 >>> pf.parseRequirementFromTokens(['X']) 4155 ReqImpossible() 4156 >>> pf.parseRequirementFromTokens(['O']) 4157 ReqNothing() 4158 >>> pf.parseRequirementFromTokens( 4159 ... [Lexeme.openParen, 'O', Lexeme.closeParen] 4160 ... ) 4161 ReqNothing() 4162 """ 4163 if len(tokenGroups) == 0: 4164 raise ParseError("Ran out of tokens.") 4165 4166 reGrouped = self.groupReqTokensByPrecedence(tokenGroups) 4167 4168 return self.parseRequirementFromRegroupedTokens(reGrouped) 4169 4170 def parseRequirementFromTokens( 4171 self, 4172 tokens: LexedTokens, 4173 start: int = 0, 4174 end: int = -1 4175 ) -> base.Requirement: 4176 """ 4177 Parses a requirement from `LexedTokens` by grouping them first 4178 and then using `parseRequirementFromGroupedTokens`. 4179 4180 For example: 4181 4182 >>> pf = ParseFormat() 4183 >>> pf.parseRequirementFromTokens( 4184 ... [ 4185 ... 'one', 4186 ... Lexeme.ampersand, 4187 ... Lexeme.openParen, 4188 ... 'two', 4189 ... Lexeme.orBar, 4190 ... 'three', 4191 ... Lexeme.closeParen 4192 ... ] 4193 ... ) 4194 ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'),\ 4195 ReqCapability('three')])]) 4196 """ 4197 grouped = self.groupReqTokens(tokens, start, end) 4198 return self.parseRequirementFromGroupedTokens(grouped) 4199 4200 def parseRequirement(self, encoded: str) -> base.Requirement: 4201 """ 4202 Parses a `base.Requirement` from a string by calling `lex` and 4203 then feeding it into `ParseFormat.parseRequirementFromTokens`. 4204 As stated in `parseRequirementFromTokens`, the precedence 4205 binding order is NOT, then AND, then OR. 4206 4207 For example: 4208 4209 >>> pf = ParseFormat() 4210 >>> pf.parseRequirement('! coin * 3') 4211 ReqNot(ReqTokens('coin', 3)) 4212 >>> pf.parseRequirement( 4213 ... ' oneWord | "two words"|"three words words" ' 4214 ... ) 4215 ReqAny([ReqCapability('oneWord'), ReqCapability('"two words"'),\ 4216 ReqCapability('"three words words"')]) 4217 >>> pf.parseRequirement('words-with-dashes') 4218 ReqCapability('words-with-dashes') 4219 >>> r = pf.parseRequirement('capability&roomB::switch:on') 4220 >>> r 4221 ReqAll([ReqCapability('capability'),\ 4222 ReqMechanism(MechanismSpecifier(domain=None, zone=None, decision='roomB',\ 4223 name='switch'), 'on')]) 4224 >>> r.unparse() 4225 '(capability&roomB::switch:on)' 4226 >>> pf.parseRequirement('!!!one') 4227 ReqNot(ReqNot(ReqNot(ReqCapability('one')))) 4228 >>> pf.parseRequirement('domain//zone::where::mechanism:state') 4229 ReqMechanism(MechanismSpecifier(domain='domain', zone='zone',\ 4230 decision='where', name='mechanism'), 'state') 4231 >>> pf.parseRequirement('domain//mechanism:state') 4232 ReqMechanism(MechanismSpecifier(domain='domain', zone=None,\ 4233 decision=None, name='mechanism'), 'state') 4234 >>> pf.parseRequirement('where::mechanism:state') 4235 ReqMechanism(MechanismSpecifier(domain=None, zone=None,\ 4236 decision='where', name='mechanism'), 'state') 4237 >>> pf.parseRequirement('zone::where::mechanism:state') 4238 ReqMechanism(MechanismSpecifier(domain=None, zone='zone',\ 4239 decision='where', name='mechanism'), 'state') 4240 >>> pf.parseRequirement('tag~') 4241 ReqTag('tag', 1) 4242 >>> pf.parseRequirement('tag~&tag2~') 4243 ReqAll([ReqTag('tag', 1), ReqTag('tag2', 1)]) 4244 >>> pf.parseRequirement('tag~value|tag~3|tag~3.5|skill^3') 4245 ReqAny([ReqTag('tag', 'value'), ReqTag('tag', 3),\ 4246 ReqTag('tag', 3.5), ReqLevel('skill', 3)]) 4247 >>> pf.parseRequirement('tag~True|tag~False|tag~None') 4248 ReqAny([ReqTag('tag', True), ReqTag('tag', False), ReqTag('tag', None)]) 4249 4250 Precedence examples: 4251 4252 >>> pf.parseRequirement('A|B&C') 4253 ReqAny([ReqCapability('A'), ReqAll([ReqCapability('B'),\ 4254 ReqCapability('C')])]) 4255 >>> pf.parseRequirement('A&B|C') 4256 ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4257 ReqCapability('C')]) 4258 >>> pf.parseRequirement('(A&B)|C') 4259 ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4260 ReqCapability('C')]) 4261 >>> pf.parseRequirement('(A&B|C)&D') 4262 ReqAll([ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4263 ReqCapability('C')]), ReqCapability('D')]) 4264 4265 Error examples: 4266 4267 >>> pf.parseRequirement('one ! Word') 4268 Traceback (most recent call last): 4269 ... 4270 exploration.parsing.ParseError... 4271 >>> pf.parseRequirement('a|') 4272 Traceback (most recent call last): 4273 ... 4274 exploration.parsing.ParseError... 4275 >>> pf.parseRequirement('b!') 4276 Traceback (most recent call last): 4277 ... 4278 exploration.parsing.ParseError... 4279 >>> pf.parseRequirement('*emph*') 4280 Traceback (most recent call last): 4281 ... 4282 exploration.parsing.ParseError... 4283 >>> pf.parseRequirement('one&&two') 4284 Traceback (most recent call last): 4285 ... 4286 exploration.parsing.ParseError... 4287 >>> pf.parseRequirement('one!|two') 4288 Traceback (most recent call last): 4289 ... 4290 exploration.parsing.ParseError... 4291 >>> pf.parseRequirement('one*two') 4292 Traceback (most recent call last): 4293 ... 4294 exploration.parsing.ParseError... 4295 >>> pf.parseRequirement('one*') 4296 Traceback (most recent call last): 4297 ... 4298 exploration.parsing.ParseError... 4299 >>> pf.parseRequirement('()') 4300 Traceback (most recent call last): 4301 ... 4302 exploration.parsing.ParseError... 4303 >>> pf.parseRequirement('(one)*3') 4304 Traceback (most recent call last): 4305 ... 4306 exploration.parsing.ParseError... 4307 >>> pf.parseRequirement('a:') 4308 Traceback (most recent call last): 4309 ... 4310 exploration.parsing.ParseError... 4311 >>> pf.parseRequirement('a:b:c') 4312 Traceback (most recent call last): 4313 ... 4314 exploration.parsing.ParseError... 4315 >>> pf.parseRequirement('where::capability') 4316 Traceback (most recent call last): 4317 ... 4318 exploration.parsing.ParseError... 4319 """ 4320 return self.parseRequirementFromTokens( 4321 lex(encoded, self.reverseFormat) 4322 ) 4323 4324 def parseSkillCombinationFromTokens( 4325 self, 4326 tokens: LexedTokens, 4327 start: int = 0, 4328 end: int = -1 4329 ) -> Union[base.Skill, base.SkillCombination]: 4330 """ 4331 Parses a skill combination from the specified range within the 4332 given tokens list. If just a single string token is selected, it 4333 will be returned as a `base.BestSkill` with just that skill 4334 inside. 4335 4336 For example: 4337 4338 >>> pf = ParseFormat() 4339 >>> pf.parseSkillCombinationFromTokens(['climbing']) 4340 BestSkill('climbing') 4341 >>> tokens = [ 4342 ... 'best', 4343 ... Lexeme.openParen, 4344 ... 'brains', 4345 ... Lexeme.sepOrDelay, 4346 ... 'brawn', 4347 ... Lexeme.closeParen, 4348 ... ] 4349 >>> pf.parseSkillCombinationFromTokens(tokens) 4350 BestSkill('brains', 'brawn') 4351 >>> tokens[2] = '3' # not a lexeme so it's a string 4352 >>> pf.parseSkillCombinationFromTokens(tokens) 4353 BestSkill(3, 'brawn') 4354 >>> tokens = [ 4355 ... Lexeme.wigglyLine, 4356 ... Lexeme.wigglyLine, 4357 ... 'yes', 4358 ... ] 4359 >>> pf.parseSkillCombinationFromTokens(tokens) 4360 InverseSkill(InverseSkill('yes')) 4361 """ 4362 start, end, nTokens = normalizeEnds(tokens, start, end) 4363 4364 first = tokens[start] 4365 if nTokens == 1: 4366 if isinstance(first, base.Skill): 4367 try: 4368 level = int(first) 4369 return base.BestSkill(level) 4370 except ValueError: 4371 return base.BestSkill(first) 4372 else: 4373 raise ParseError( 4374 "Invalid SkillCombination:\n{tokens[start:end + 1]" 4375 ) 4376 4377 if first == Lexeme.wigglyLine: 4378 inv = self.parseSkillCombinationFromTokens( 4379 tokens, 4380 start + 1, 4381 end 4382 ) 4383 if isinstance(inv, base.BestSkill) and len(inv.skills) == 1: 4384 return base.InverseSkill(inv.skills[0]) 4385 else: 4386 return base.InverseSkill(inv) 4387 4388 second = tokens[start + 1] 4389 if second != Lexeme.openParen: 4390 raise ParseError( 4391 f"Invalid SkillCombination (missing paren):" 4392 f"\n{tokens[start:end + 1]}" 4393 ) 4394 4395 parenEnd = self.matchingBrace( 4396 tokens, 4397 start + 1, 4398 Lexeme.openParen, 4399 Lexeme.closeParen 4400 ) 4401 if parenEnd != end: 4402 raise ParseError( 4403 f"Extra junk after SkillCombination:" 4404 f"\n{tokens[parenEnd + 1:end + 1]}" 4405 ) 4406 4407 if first == 'if': 4408 parts = list( 4409 findSeparatedParts( 4410 tokens, 4411 Lexeme.sepOrDelay, 4412 start + 2, 4413 end - 1, 4414 Lexeme.openParen, 4415 Lexeme.closeParen 4416 ) 4417 ) 4418 if len(parts) != 3: 4419 raise ParseError( 4420 f"Wrong number of parts for ConditionalSkill (needs" 4421 f" 3, got {len(parts)}:" 4422 f"\n{tokens[start + 2:end]}" 4423 ) 4424 reqStart, reqEnd = parts[0] 4425 ifStart, ifEnd = parts[1] 4426 elseStart, elseEnd = parts[2] 4427 return base.ConditionalSkill( 4428 self.parseRequirementFromTokens(tokens, reqStart, reqEnd), 4429 self.parseSkillCombinationFromTokens(tokens, ifStart, ifEnd), 4430 self.parseSkillCombinationFromTokens( 4431 tokens, 4432 elseStart, 4433 elseEnd 4434 ), 4435 ) 4436 elif first in ('sum', 'best', 'worst'): 4437 make: type[base.SkillCombination] 4438 if first == 'sum': 4439 make = base.CombinedSkill 4440 elif first == 'best': 4441 make = base.BestSkill 4442 else: 4443 make = base.WorstSkill 4444 4445 subs = [] 4446 for partStart, partEnd in findSeparatedParts( 4447 tokens, 4448 Lexeme.sepOrDelay, 4449 start + 2, 4450 end - 1, 4451 Lexeme.openParen, 4452 Lexeme.closeParen 4453 ): 4454 sub = self.parseSkillCombinationFromTokens( 4455 tokens, 4456 partStart, 4457 partEnd 4458 ) 4459 if ( 4460 isinstance(sub, base.BestSkill) 4461 and len(sub.skills) == 1 4462 ): 4463 subs.append(sub.skills[0]) 4464 else: 4465 subs.append(sub) 4466 4467 return make(*subs) 4468 else: 4469 raise ParseError( 4470 "Invalid SkillCombination:\n{tokens[start:end + 1]" 4471 ) 4472 4473 def parseSkillCombination( 4474 self, 4475 encoded: str 4476 ) -> base.SkillCombination: 4477 """ 4478 Parses a `SkillCombination` from a string. Calls `lex` and then 4479 `parseSkillCombinationFromTokens`. 4480 """ 4481 result = self.parseSkillCombinationFromTokens( 4482 lex(encoded, self.reverseFormat) 4483 ) 4484 if not isinstance(result, base.SkillCombination): 4485 return base.BestSkill(result) 4486 else: 4487 return result 4488 4489 def parseConditionFromTokens( 4490 self, 4491 tokens: LexedTokens, 4492 start: int = 0, 4493 end: int = -1 4494 ) -> base.Condition: 4495 """ 4496 Parses a `base.Condition` from a lexed tokens list. For example: 4497 4498 >>> pf = ParseFormat() 4499 >>> tokens = [ 4500 ... Lexeme.doubleQuestionmark, 4501 ... Lexeme.openParen, 4502 ... "fire", 4503 ... Lexeme.ampersand, 4504 ... "water", 4505 ... Lexeme.closeParen, 4506 ... Lexeme.openCurly, 4507 ... "gain", 4508 ... "wind", 4509 ... Lexeme.closeCurly, 4510 ... Lexeme.openCurly, 4511 ... Lexeme.closeCurly, 4512 ... ] 4513 >>> pf.parseConditionFromTokens(tokens) == base.condition( 4514 ... condition=base.ReqAll([ 4515 ... base.ReqCapability('fire'), 4516 ... base.ReqCapability('water') 4517 ... ]), 4518 ... consequence=[base.effect(gain='wind')] 4519 ... ) 4520 True 4521 """ 4522 start, end, nTokens = normalizeEnds(tokens, start, end) 4523 if nTokens < 8: 4524 raise ParseError( 4525 f"A Condition requires at least 8 tokens (got {nTokens})." 4526 ) 4527 if tokens[start] != Lexeme.doubleQuestionmark: 4528 raise ParseError( 4529 f"A Condition must start with" 4530 f" {repr(self.formatDict[Lexeme.doubleQuestionmark])}" 4531 ) 4532 try: 4533 consequenceStart = tokens.index(Lexeme.openCurly, start) 4534 except ValueError: 4535 raise ParseError("A condition must include a consequence block.") 4536 consequenceEnd = self.matchingBrace(tokens, consequenceStart) 4537 altStart = consequenceEnd + 1 4538 altEnd = self.matchingBrace(tokens, altStart) 4539 4540 if altEnd != end: 4541 raise ParseError( 4542 f"Junk after condition:\n{tokens[altEnd + 1: end + 1]}" 4543 ) 4544 4545 return base.condition( 4546 condition=self.parseRequirementFromTokens( 4547 tokens, 4548 start + 1, 4549 consequenceStart - 1 4550 ), 4551 consequence=self.parseConsequenceFromTokens( 4552 tokens, 4553 consequenceStart, 4554 consequenceEnd 4555 ), 4556 alternative=self.parseConsequenceFromTokens( 4557 tokens, 4558 altStart, 4559 altEnd 4560 ) 4561 ) 4562 4563 def parseCondition( 4564 self, 4565 encoded: str 4566 ) -> base.Condition: 4567 """ 4568 Lexes the given string and then calls `parseConditionFromTokens` 4569 to return a `base.Condition`. 4570 """ 4571 return self.parseConditionFromTokens( 4572 lex(encoded, self.reverseFormat) 4573 ) 4574 4575 def parseChallengeFromTokens( 4576 self, 4577 tokens: LexedTokens, 4578 start: int = 0, 4579 end: int = -1 4580 ) -> base.Challenge: 4581 """ 4582 Parses a `base.Challenge` from a lexed tokens list. 4583 4584 For example: 4585 4586 >>> pf = ParseFormat() 4587 >>> tokens = [ 4588 ... Lexeme.angleLeft, 4589 ... '2', 4590 ... Lexeme.angleRight, 4591 ... 'best', 4592 ... Lexeme.openParen, 4593 ... "chess", 4594 ... Lexeme.sepOrDelay, 4595 ... "checkers", 4596 ... Lexeme.closeParen, 4597 ... Lexeme.openCurly, 4598 ... "gain", 4599 ... "coin", 4600 ... Lexeme.tokenCount, 4601 ... "5", 4602 ... Lexeme.closeCurly, 4603 ... Lexeme.angleRight, 4604 ... Lexeme.openCurly, 4605 ... "lose", 4606 ... "coin", 4607 ... Lexeme.tokenCount, 4608 ... "5", 4609 ... Lexeme.closeCurly, 4610 ... ] 4611 >>> c = pf.parseChallengeFromTokens(tokens) 4612 >>> c['skills'] == base.BestSkill('chess', 'checkers') 4613 True 4614 >>> c['level'] 4615 2 4616 >>> c['success'] == [base.effect(gain=('coin', 5))] 4617 True 4618 >>> c['failure'] == [base.effect(lose=('coin', 5))] 4619 True 4620 >>> c['outcome'] 4621 False 4622 >>> c == base.challenge( 4623 ... skills=base.BestSkill('chess', 'checkers'), 4624 ... level=2, 4625 ... success=[base.effect(gain=('coin', 5))], 4626 ... failure=[base.effect(lose=('coin', 5))], 4627 ... outcome=False 4628 ... ) 4629 True 4630 >>> t2 = ['hi'] + tokens + ['bye'] # parsing only part of the list 4631 >>> c == pf.parseChallengeFromTokens(t2, 1, -2) 4632 True 4633 """ 4634 start, end, nTokens = normalizeEnds(tokens, start, end) 4635 if nTokens < 8: 4636 raise ParseError( 4637 f"Not enough tokens for a challenge: {nTokens}" 4638 ) 4639 if tokens[start] != Lexeme.angleLeft: 4640 raise ParseError( 4641 f"Challenge must start with" 4642 f" {repr(self.formatDict[Lexeme.angleLeft])}" 4643 ) 4644 levelStr = tokens[start + 1] 4645 if isinstance(levelStr, Lexeme): 4646 raise ParseError( 4647 f"Challenge must start with a level in angle brackets" 4648 f" (got {repr(self.formatDict[levelStr])})." 4649 ) 4650 if tokens[start + 2] != Lexeme.angleRight: 4651 raise ParseError( 4652 f"Challenge must include" 4653 f" {repr(self.formatDict[Lexeme.angleRight])} after" 4654 f" the level." 4655 ) 4656 try: 4657 level = int(levelStr) 4658 except ValueError: 4659 raise ParseError( 4660 f"Challenge level must be an integer (got" 4661 f" {repr(tokens[start + 1])}." 4662 ) 4663 try: 4664 successStart = tokens.index(Lexeme.openCurly, start) 4665 skillsEnd = successStart - 1 4666 except ValueError: 4667 raise ParseError("A challenge must include a consequence block.") 4668 4669 outcome: Optional[bool] = None 4670 if tokens[skillsEnd] == Lexeme.angleRight: 4671 skillsEnd -= 1 4672 outcome = True 4673 successEnd = self.matchingBrace(tokens, successStart) 4674 failStart = successEnd + 1 4675 if tokens[failStart] == Lexeme.angleRight: 4676 failStart += 1 4677 if outcome is not None: 4678 raise ParseError( 4679 "Cannot indicate both success and failure as" 4680 " outcomes in a challenge." 4681 ) 4682 outcome = False 4683 failEnd = self.matchingBrace(tokens, failStart) 4684 4685 if failEnd != end: 4686 raise ParseError( 4687 f"Junk after condition:\n{tokens[failEnd + 1:end + 1]}" 4688 ) 4689 4690 skills = self.parseSkillCombinationFromTokens( 4691 tokens, 4692 start + 3, 4693 skillsEnd 4694 ) 4695 if isinstance(skills, base.Skill): 4696 skills = base.BestSkill(skills) 4697 4698 return base.challenge( 4699 level=level, 4700 outcome=outcome, 4701 skills=skills, 4702 success=self.parseConsequenceFromTokens( 4703 tokens[successStart:successEnd + 1] 4704 ), 4705 failure=self.parseConsequenceFromTokens( 4706 tokens[failStart:failEnd + 1] 4707 ) 4708 ) 4709 4710 def parseChallenge( 4711 self, 4712 encoded: str 4713 ) -> base.Challenge: 4714 """ 4715 Lexes the given string and then calls `parseChallengeFromTokens` 4716 to return a `base.Challenge`. 4717 """ 4718 return self.parseChallengeFromTokens( 4719 lex(encoded, self.reverseFormat) 4720 ) 4721 4722 def parseConsequenceFromTokens( 4723 self, 4724 tokens: LexedTokens, 4725 start: int = 0, 4726 end: int = -1 4727 ) -> base.Consequence: 4728 """ 4729 Parses a consequence from a lexed token list. If start and/or end 4730 are specified, only processes the part of the list between those 4731 two indices (inclusive). Use `lex` to turn a string into a 4732 `LexedTokens` list (or use `ParseFormat.parseConsequence` which 4733 does that for you). 4734 4735 An example: 4736 4737 >>> pf = ParseFormat() 4738 >>> tokens = [ 4739 ... Lexeme.openCurly, 4740 ... 'gain', 4741 ... 'power', 4742 ... Lexeme.closeCurly 4743 ... ] 4744 >>> c = pf.parseConsequenceFromTokens(tokens) 4745 >>> c == [base.effect(gain='power')] 4746 True 4747 >>> tokens.append('hi') 4748 >>> c == pf.parseConsequenceFromTokens(tokens, end=-2) 4749 True 4750 >>> c == pf.parseConsequenceFromTokens(tokens, end=3) 4751 True 4752 """ 4753 start, end, nTokens = normalizeEnds(tokens, start, end) 4754 4755 if nTokens < 2: 4756 raise ParseError("Consequence must have at least two tokens.") 4757 4758 if tokens[start] != Lexeme.openCurly: 4759 raise ParseError( 4760 f"Consequence must start with an open curly brace:" 4761 f" {repr(self.formatDict[Lexeme.openCurly])}." 4762 ) 4763 4764 if tokens[end] != Lexeme.closeCurly: 4765 raise ParseError( 4766 f"Consequence must end with a closing curly brace:" 4767 f" {repr(self.formatDict[Lexeme.closeCurly])}." 4768 ) 4769 4770 if nTokens == 2: 4771 return [] 4772 4773 result: base.Consequence = [] 4774 for partStart, partEnd in findSeparatedParts( 4775 tokens, 4776 Lexeme.consequenceSeparator, 4777 start + 1, 4778 end - 1, 4779 Lexeme.openCurly, 4780 Lexeme.closeCurly 4781 ): 4782 if partEnd - partStart < 0: 4783 raise ParseError("Empty consequence part.") 4784 if tokens[partStart] == Lexeme.angleLeft: # a challenge 4785 result.append( 4786 self.parseChallengeFromTokens( 4787 tokens, 4788 partStart, 4789 partEnd 4790 ) 4791 ) 4792 elif tokens[partStart] == Lexeme.doubleQuestionmark: # condition 4793 result.append( 4794 self.parseConditionFromTokens( 4795 tokens, 4796 partStart, 4797 partEnd 4798 ) 4799 ) 4800 else: # Must be an effect 4801 result.append( 4802 self.parseEffectFromTokens( 4803 tokens, 4804 partStart, 4805 partEnd 4806 ) 4807 ) 4808 4809 return result 4810 4811 def parseConsequence(self, encoded: str) -> base.Consequence: 4812 """ 4813 Parses a consequence from a string. Uses `lex` and 4814 `ParseFormat.parseConsequenceFromTokens`. For example: 4815 4816 >>> pf = ParseFormat() 4817 >>> c = pf.parseConsequence( 4818 ... '{gain power}' 4819 ... ) 4820 >>> c == [base.effect(gain='power')] 4821 True 4822 >>> pf.unparseConsequence(c) 4823 '{gain power}' 4824 >>> c = pf.parseConsequence( 4825 ... '{\\n' 4826 ... ' ??(brawny|!weights*3){\\n' 4827 ... ' <3>sum(brains, brawn){goto home}>{bounce}\\n' 4828 ... ' }{};\\n' 4829 ... ' lose coin*1\\n' 4830 ... '}' 4831 ... ) 4832 >>> len(c) 4833 2 4834 >>> c[0]['condition'] == base.ReqAny([ 4835 ... base.ReqCapability('brawny'), 4836 ... base.ReqNot(base.ReqTokens('weights', 3)) 4837 ... ]) 4838 True 4839 >>> len(c[0]['consequence']) 4840 1 4841 >>> len(c[0]['alternative']) 4842 0 4843 >>> cons = c[0]['consequence'][0] 4844 >>> cons['skills'] == base.CombinedSkill('brains', 'brawn') 4845 True 4846 >>> cons['level'] 4847 3 4848 >>> len(cons['success']) 4849 1 4850 >>> len(cons['failure']) 4851 1 4852 >>> cons['success'][0] == base.effect(goto='home') 4853 True 4854 >>> cons['failure'][0] == base.effect(bounce=True) 4855 True 4856 >>> cons['outcome'] = False 4857 >>> c[0] == base.condition( 4858 ... condition=base.ReqAny([ 4859 ... base.ReqCapability('brawny'), 4860 ... base.ReqNot(base.ReqTokens('weights', 3)) 4861 ... ]), 4862 ... consequence=[ 4863 ... base.challenge( 4864 ... skills=base.CombinedSkill('brains', 'brawn'), 4865 ... level=3, 4866 ... success=[base.effect(goto='home')], 4867 ... failure=[base.effect(bounce=True)], 4868 ... outcome=False 4869 ... ) 4870 ... ] 4871 ... ) 4872 True 4873 >>> c[1] == base.effect(lose=('coin', 1)) 4874 True 4875 """ 4876 return self.parseConsequenceFromTokens( 4877 lex(encoded, self.reverseFormat) 4878 )
A ParseFormat manages the mapping from markers to entry types and vice versa.
591 def __init__( 592 self, 593 formatDict: Format = DEFAULT_FORMAT, 594 effectNames: Dict[str, base.EffectType] = DEFAULT_EFFECT_NAMES, 595 focalizationNames: Dict[ 596 str, 597 base.DomainFocalization 598 ] = DEFAULT_FOCALIZATION_NAMES, 599 successFailureIndicators: Tuple[str, str] = DEFAULT_SF_INDICATORS 600 ): 601 """ 602 Sets up the parsing format. Requires a `Format` dictionary to 603 define the specifics. Raises a `ValueError` unless the keys of 604 the `Format` dictionary exactly match the `Lexeme` values. 605 """ 606 self.formatDict = formatDict 607 self.effectNames = effectNames 608 self.focalizationNames = focalizationNames 609 if ( 610 len(successFailureIndicators) != 2 611 or any(len(i) != 1 for i in successFailureIndicators) 612 ): 613 raise ValueError( 614 f"Invalid success/failure indicators: must be a pair of" 615 f" length-1 strings. Got: {successFailureIndicators!r}" 616 ) 617 self.successIndicator, self.failureIndicator = ( 618 successFailureIndicators 619 ) 620 621 # Check completeness for each dictionary 622 checkCompleteness('formatDict', self.formatDict, set(Lexeme)) 623 checkCompleteness( 624 'effectNames', 625 self.effectNames, 626 valuesSet=set(get_args(base.EffectType)) 627 ) 628 checkCompleteness( 629 'focalizationNames', 630 self.focalizationNames, 631 valuesSet=set(get_args(base.DomainFocalization)) 632 ) 633 634 # Build some reverse lookup dictionaries for specific 635 self.reverseFormat = {y: x for (x, y) in self.formatDict.items()} 636 637 # circumstances: 638 self.effectModMap = { 639 self.formatDict[x]: x 640 for x in [ 641 Lexeme.effectCharges, 642 Lexeme.sepOrDelay, 643 Lexeme.inCommon, 644 Lexeme.isHidden 645 ] 646 }
648 def lex(self, content: str) -> LexedTokens: 649 """ 650 Applies `lex` using this format's lexeme mapping. 651 """ 652 return lex(content, self.reverseFormat)
Applies lex using this format's lexeme mapping.
654 def onOff(self, word: str) -> Optional[bool]: 655 """ 656 Parse an on/off indicator and returns a boolean (`True` for on 657 and `False` for off). Returns `None` if the word isn't either 658 the 'on' or the 'off' word. Generates a `ParseWarning` 659 (and still returns `None`) if the word is a case-swapped version 660 of the 'on' or 'off' word and is not equal to either of them. 661 """ 662 onWord = self.formatDict[Lexeme.stateOn] 663 offWord = self.formatDict[Lexeme.stateOff] 664 665 # Generate warning if we suspect a case error 666 if ( 667 word.casefold() in (onWord, offWord) 668 and word not in (onWord, offWord) 669 ): 670 warnings.warn( 671 ( 672 f"Word '{word}' cannot be interpreted as an on/off" 673 f" value, although it is almost one (the correct" 674 f" values are '{onWord}' and '{offWord}'." 675 ), 676 ParseWarning 677 ) 678 679 # return the appropriate value 680 if word == onWord: 681 return True 682 elif word == offWord: 683 return False 684 else: 685 return None
Parse an on/off indicator and returns a boolean (True for on
and False for off). Returns None if the word isn't either
the 'on' or the 'off' word. Generates a ParseWarning
(and still returns None) if the word is a case-swapped version
of the 'on' or 'off' word and is not equal to either of them.
687 def matchingBrace( 688 self, 689 tokens: LexedTokens, 690 where: int, 691 opener: int = Lexeme.openCurly, 692 closer: int = Lexeme.closeCurly 693 ) -> int: 694 """ 695 Returns the index within the given tokens list of the closing 696 curly brace which matches the open brace at the specified index. 697 You can specify custom `opener` and/or `closer` lexemes to find 698 matching pairs of other things. Raises a `ParseError` if there 699 is no opening brace at the specified index, or if there isn't a 700 matching closing brace. Handles nested braces of the specified 701 type. 702 703 Examples: 704 >>> pf = ParseFormat() 705 >>> ob = Lexeme.openCurly 706 >>> cb = Lexeme.closeCurly 707 >>> pf.matchingBrace([ob, cb], 0) 708 1 709 >>> pf.matchingBrace([ob, cb], 1) 710 Traceback (most recent call last): 711 ... 712 exploration.parsing.ParseError: ... 713 >>> pf.matchingBrace(['hi', ob, cb], 0) 714 Traceback (most recent call last): 715 ... 716 exploration.parsing.ParseError: ... 717 >>> pf.matchingBrace(['hi', ob, cb], 1) 718 2 719 >>> pf.matchingBrace(['hi', ob, 'lo', cb], 1) 720 3 721 >>> pf.matchingBrace([ob, 'hi', 'lo', cb], 1) 722 Traceback (most recent call last): 723 ... 724 exploration.parsing.ParseError: ... 725 >>> pf.matchingBrace([ob, 'hi', 'lo', cb], 0) 726 3 727 >>> pf.matchingBrace([ob, ob, cb, cb], 0) 728 3 729 >>> pf.matchingBrace([ob, ob, cb, cb], 1) 730 2 731 >>> pf.matchingBrace([ob, cb, ob, cb], 0) 732 1 733 >>> pf.matchingBrace([ob, cb, ob, cb], 2) 734 3 735 >>> pf.matchingBrace([ob, cb, cb, cb], 0) 736 1 737 >>> pf.matchingBrace([ob, ob, ob, cb], 0) 738 Traceback (most recent call last): 739 ... 740 exploration.parsing.ParseError: ... 741 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 0) 742 7 743 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 1) 744 6 745 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 2) 746 Traceback (most recent call last): 747 ... 748 exploration.parsing.ParseError: ... 749 >>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 3) 750 4 751 >>> op = Lexeme.openParen 752 >>> cp = Lexeme.closeParen 753 >>> pf.matchingBrace([ob, op, ob, cp], 1, op, cp) 754 3 755 """ 756 if where >= len(tokens): 757 raise ParseError( 758 f"Out-of-bounds brace start: index {where} with" 759 f" {len(tokens)} tokens." 760 ) 761 if tokens[where] != opener: 762 raise ParseError( 763 f"Can't find matching brace for token" 764 f" {repr(tokens[where])} at index {where} because it's" 765 f" not an open brace." 766 ) 767 768 level = 1 769 for i in range(where + 1, len(tokens)): 770 token = tokens[i] 771 if token == opener: 772 level += 1 773 elif token == closer: 774 level -= 1 775 if level == 0: 776 return i 777 778 raise ParseError( 779 f"Failed to find matching curly brace from index {where}." 780 )
Returns the index within the given tokens list of the closing
curly brace which matches the open brace at the specified index.
You can specify custom opener and/or closer lexemes to find
matching pairs of other things. Raises a ParseError if there
is no opening brace at the specified index, or if there isn't a
matching closing brace. Handles nested braces of the specified
type.
Examples:
>>> pf = ParseFormat()
>>> ob = Lexeme.openCurly
>>> cb = Lexeme.closeCurly
>>> pf.matchingBrace([ob, cb], 0)
1
>>> pf.matchingBrace([ob, cb], 1)
Traceback (most recent call last):
...
ParseError: ...
>>> pf.matchingBrace(['hi', ob, cb], 0)
Traceback (most recent call last):
...
ParseError: ...
>>> pf.matchingBrace(['hi', ob, cb], 1)
2
>>> pf.matchingBrace(['hi', ob, 'lo', cb], 1)
3
>>> pf.matchingBrace([ob, 'hi', 'lo', cb], 1)
Traceback (most recent call last):
...
ParseError: ...
>>> pf.matchingBrace([ob, 'hi', 'lo', cb], 0)
3
>>> pf.matchingBrace([ob, ob, cb, cb], 0)
3
>>> pf.matchingBrace([ob, ob, cb, cb], 1)
2
>>> pf.matchingBrace([ob, cb, ob, cb], 0)
1
>>> pf.matchingBrace([ob, cb, ob, cb], 2)
3
>>> pf.matchingBrace([ob, cb, cb, cb], 0)
1
>>> pf.matchingBrace([ob, ob, ob, cb], 0)
Traceback (most recent call last):
...
ParseError: ...
>>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 0)
7
>>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 1)
6
>>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 2)
Traceback (most recent call last):
...
ParseError: ...
>>> pf.matchingBrace([ob, ob, 'hi', ob, cb, 'lo', cb, cb], 3)
4
>>> op = Lexeme.openParen
>>> cp = Lexeme.closeParen
>>> pf.matchingBrace([ob, op, ob, cp], 1, op, cp)
3
782 def parseFocalization(self, word: str) -> base.DomainFocalization: 783 """ 784 Parses a focalization type for a domain, recognizing 785 'domainFocalizationSingular', 'domainFocalizationPlural', and 786 'domainFocalizationSpreading'. 787 """ 788 try: 789 return self.focalizationNames[word] 790 except KeyError: 791 raise ParseError( 792 f"Invalid domain focalization name {repr(word)}. Valid" 793 f" name are: {repr(list(self.focalizationNames))}'." 794 )
Parses a focalization type for a domain, recognizing 'domainFocalizationSingular', 'domainFocalizationPlural', and 'domainFocalizationSpreading'.
796 def parseTagValue(self, value: str) -> base.TagValue: 797 """ 798 Converts a string to a tag value, following these rules: 799 800 1. If the string is exactly one of 'None', 'True', or 'False', we 801 convert it to the corresponding Python value. 802 2. If the string can be converted to an integer without raising a 803 ValueError, we use that integer. 804 3. If the string can be converted to a float without raising a 805 ValueError, we use that float. 806 4. Otherwise, it remains a string. 807 808 Note that there is currently no syntax for using list, dictionary, 809 Requirement, or Consequence tag values. 810 TODO: Support those types? 811 812 Examples: 813 814 >>> pf = ParseFormat() 815 >>> pf.parseTagValue('hi') 816 'hi' 817 >>> pf.parseTagValue('3') 818 3 819 >>> pf.parseTagValue('3.0') 820 3.0 821 >>> pf.parseTagValue('True') 822 True 823 >>> pf.parseTagValue('False') 824 False 825 >>> pf.parseTagValue('None') is None 826 True 827 >>> pf.parseTagValue('none') 828 'none' 829 """ 830 # TODO: Allow these keywords to be redefined? 831 if value == 'True': 832 return True 833 elif value == 'False': 834 return False 835 elif value == 'None': 836 return None 837 else: 838 try: 839 return int(value) 840 except ValueError: 841 try: 842 return float(value) 843 except ValueError: 844 return value
Converts a string to a tag value, following these rules:
- If the string is exactly one of 'None', 'True', or 'False', we convert it to the corresponding Python value.
- If the string can be converted to an integer without raising a ValueError, we use that integer.
- If the string can be converted to a float without raising a ValueError, we use that float.
- Otherwise, it remains a string.
Note that there is currently no syntax for using list, dictionary, Requirement, or Consequence tag values. TODO: Support those types?
Examples:
>>> pf = ParseFormat()
>>> pf.parseTagValue('hi')
'hi'
>>> pf.parseTagValue('3')
3
>>> pf.parseTagValue('3.0')
3.0
>>> pf.parseTagValue('True')
True
>>> pf.parseTagValue('False')
False
>>> pf.parseTagValue('None') is None
True
>>> pf.parseTagValue('none')
'none'
846 def unparseTagValue(self, value: base.TagValue) -> str: 847 """ 848 Converts a tag value into a string that would be parsed back into a 849 tag value via `parseTagValue`. Currently does not work for list, 850 dictionary, Requirement, or Consequence values. 851 TODO: Those 852 """ 853 return str(value)
Converts a tag value into a string that would be parsed back into a
tag value via parseTagValue. Currently does not work for list,
dictionary, Requirement, or Consequence values.
TODO: Those
855 def hasZoneParts(self, name: str) -> bool: 856 """ 857 Returns true if the specified name contains zone parts (using 858 the `zoneSeparator`). 859 """ 860 return self.formatDict[Lexeme.zoneSeparator] in name
Returns true if the specified name contains zone parts (using
the zoneSeparator).
862 def splitZone( 863 self, 864 name: str 865 ) -> Tuple[List[base.Zone], base.DecisionName]: 866 """ 867 Splits a decision name that includes zone information into the 868 list-of-zones part and the decision part. If there is no zone 869 information in the name, the list-of-zones will be an empty 870 list. 871 """ 872 sep = self.formatDict[Lexeme.zoneSeparator] 873 parts = name.split(sep) 874 return (list(parts[:-1]), parts[-1])
Splits a decision name that includes zone information into the list-of-zones part and the decision part. If there is no zone information in the name, the list-of-zones will be an empty list.
876 def prefixWithZone( 877 self, 878 name: base.DecisionName, 879 zone: base.Zone 880 ) -> base.DecisionName: 881 """ 882 Returns the given decision name, prefixed with the given zone 883 name. Does NOT check whether the decision name already includes 884 a prefix or not. 885 """ 886 return zone + self.formatDict[Lexeme.zoneSeparator] + name
Returns the given decision name, prefixed with the given zone name. Does NOT check whether the decision name already includes a prefix or not.
888 def parseAnyTransitionFromTokens( 889 self, 890 tokens: LexedTokens, 891 start: int = 0 892 ) -> Tuple[base.TransitionWithOutcomes, int]: 893 """ 894 Parses a `base.TransitionWithOutcomes` from a tokens list, 895 accepting either a transition name or a transition name followed 896 by a `Lexeme.withDetails` followed by a string of success and 897 failure indicator characters. Returns a tuple containing a 898 `base.TransitionWithOutcomes` and an integer indicating the end 899 index of the parsed item within the tokens. 900 """ 901 # Normalize start index so we can do index math 902 if start < 0: 903 useIndex = len(tokens) + start 904 else: 905 useIndex = start 906 907 try: 908 first = tokens[useIndex] 909 except IndexError: 910 raise ParseError( 911 f"Invalid token index: {start!r} among {len(tokens)}" 912 f" tokens." 913 ) 914 915 if isinstance(first, Lexeme): 916 raise ParseError( 917 f"Expecting a transition name (possibly with a" 918 f" success/failure indicator string) but first token is" 919 f" {first!r}." 920 ) 921 922 try: 923 second = tokens[useIndex + 1] 924 third = tokens[useIndex + 2] 925 except IndexError: 926 return ((first, []), useIndex) 927 928 if second != Lexeme.withDetails or isinstance(third, Lexeme): 929 return ((first, []), useIndex) 930 931 outcomes = [] 932 for char in third: 933 if char == self.successIndicator: 934 outcomes.append(True) 935 elif char == self.failureIndicator: 936 outcomes.append(False) 937 else: 938 return ((first, []), useIndex) 939 940 return ((first, outcomes), useIndex + 2)
Parses a base.TransitionWithOutcomes from a tokens list,
accepting either a transition name or a transition name followed
by a Lexeme.withDetails followed by a string of success and
failure indicator characters. Returns a tuple containing a
base.TransitionWithOutcomes and an integer indicating the end
index of the parsed item within the tokens.
942 def parseTransitionWithOutcomes( 943 self, 944 content: str 945 ) -> base.TransitionWithOutcomes: 946 """ 947 Takes a transition that may have outcomes listed as a series of 948 s/f strings after a colon and returns the corresponding 949 `TransitionWithOutcomes` tuple. Calls `lex` and then 950 `parseAnyTransitionFromTokens`. 951 """ 952 return self.parseAnyTransitionFromTokens(self.lex(content))[0]
Takes a transition that may have outcomes listed as a series of
s/f strings after a colon and returns the corresponding
TransitionWithOutcomes tuple. Calls lex and then
parseAnyTransitionFromTokens.
954 def unparseTransitionWithOutocmes( 955 self, 956 transition: base.AnyTransition 957 ) -> str: 958 """ 959 Turns a `base.AnyTransition` back into a string that would parse 960 to an equivalent `base.TransitionWithOutcomes` via 961 `parseTransitionWithOutcomes`. If a bare `base.Transition` is 962 given, returns a string that would result in a 963 `base.TransitionWithOutcomes` that has an empty outcomes 964 sequence. 965 """ 966 if isinstance(transition, base.Transition): 967 return transition 968 elif ( 969 isinstance(transition, tuple) 970 and len(transition) == 2 971 and isinstance(transition[0], base.Transition) 972 and isinstance(transition[1], list) 973 and all(isinstance(sfi, bool) for sfi in transition[1]) 974 ): 975 if len(transition[1]) == 0: 976 return transition[0] 977 else: 978 result = transition[0] + self.formatDict[Lexeme.withDetails] 979 for outcome in transition[1]: 980 if outcome: 981 result += self.successIndicator 982 else: 983 result += self.failureIndicator 984 return result 985 else: 986 raise TypeError( 987 f"Invalid AnyTransition: neither a string, nor a" 988 f" length-2 tuple consisting of a string followed by a" 989 f" list of booleans. Got: {transition!r}" 990 )
Turns a base.AnyTransition back into a string that would parse
to an equivalent base.TransitionWithOutcomes via
parseTransitionWithOutcomes. If a bare base.Transition is
given, returns a string that would result in a
base.TransitionWithOutcomes that has an empty outcomes
sequence.
992 def parseSpecificTransition( 993 self, 994 content: str 995 ) -> Tuple[base.DecisionName, base.Transition]: 996 """ 997 Splits a decision:transition pair to the decision and transition 998 part, using a custom separator if one is defined. 999 """ 1000 sep = self.formatDict[Lexeme.withDetails] 1001 n = content.count(sep) 1002 if n == 0: 1003 raise ParseError( 1004 f"Cannot split '{content}' into a decision name and a" 1005 f" transition name (no separator '{sep}' found)." 1006 ) 1007 elif n > 1: 1008 raise ParseError( 1009 f"Cannot split '{content}' into a decision name and a" 1010 f" transition name (too many ({n}) '{sep}' separators" 1011 f" found)." 1012 ) 1013 else: 1014 return cast( 1015 Tuple[base.DecisionName, base.Transition], 1016 tuple(content.split(sep)) 1017 )
Splits a decision:transition pair to the decision and transition part, using a custom separator if one is defined.
1019 def splitDirections( 1020 self, 1021 content: str 1022 ) -> Tuple[Optional[str], Optional[str]]: 1023 """ 1024 Splits a piece of text using the 'Lexeme.reciprocalSeparator' 1025 into two pieces. If there is no separator, the second piece will 1026 be `None`; if either side of the separator is blank, that side 1027 will be `None`, and if there is more than one separator, a 1028 `ParseError` will be raised. Whitespace will be stripped from 1029 both sides of each result. 1030 1031 Examples: 1032 1033 >>> pf = ParseFormat() 1034 >>> pf.splitDirections('abc / def') 1035 ('abc', 'def') 1036 >>> pf.splitDirections('abc def ') 1037 ('abc def', None) 1038 >>> pf.splitDirections('abc def /') 1039 ('abc def', None) 1040 >>> pf.splitDirections('/abc def') 1041 (None, 'abc def') 1042 >>> pf.splitDirections('a/b/c') # doctest: +IGNORE_EXCEPTION_DETAIL 1043 Traceback (most recent call last): 1044 ... 1045 ParseError: ... 1046 """ 1047 sep = self.formatDict[Lexeme.reciprocalSeparator] 1048 count = content.count(sep) 1049 if count > 1: 1050 raise ParseError( 1051 f"Too many split points ('{sep}') in content:" 1052 f" '{content}' (only one is allowed)." 1053 ) 1054 1055 elif count == 1: 1056 before, after = content.split(sep) 1057 before = before.strip() 1058 after = after.strip() 1059 return (before or None, after or None) 1060 1061 else: # no split points 1062 stripped = content.strip() 1063 if stripped: 1064 return stripped, None 1065 else: 1066 return None, None
Splits a piece of text using the 'Lexeme.reciprocalSeparator'
into two pieces. If there is no separator, the second piece will
be None; if either side of the separator is blank, that side
will be None, and if there is more than one separator, a
ParseError will be raised. Whitespace will be stripped from
both sides of each result.
Examples:
>>> pf = ParseFormat()
>>> pf.splitDirections('abc / def')
('abc', 'def')
>>> pf.splitDirections('abc def ')
('abc def', None)
>>> pf.splitDirections('abc def /')
('abc def', None)
>>> pf.splitDirections('/abc def')
(None, 'abc def')
>>> pf.splitDirections('a/b/c') # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ParseError: ...
1068 def parseItem( 1069 self, 1070 item: str 1071 ) -> Union[ 1072 base.Capability, 1073 Tuple[base.Token, int], 1074 Tuple[base.MechanismName, base.MechanismState] 1075 ]: 1076 """ 1077 Parses an item, which is a capability (just a string), a 1078 token-type*number pair (returned as a tuple with the number 1079 converted to an integer), or a mechanism-name:state pair 1080 (returned as a tuple with the state as a string). The 1081 'Lexeme.tokenCount' and `Lexeme.mechanismSeparator` format 1082 values determine the separators that this looks for. 1083 """ 1084 tsep = self.formatDict[Lexeme.tokenCount] 1085 msep = self.formatDict[Lexeme.mechanismSeparator] 1086 if tsep in item: 1087 # It's a token w/ an associated count 1088 parts = item.split(tsep) 1089 if len(parts) != 2: 1090 raise ParseError( 1091 f"Item '{item}' has a '{tsep}' but doesn't separate" 1092 f" into a token type and a count." 1093 ) 1094 typ, count = parts 1095 try: 1096 num = int(count) 1097 except ValueError: 1098 raise ParseError( 1099 f"Item '{item}' has invalid token count '{count}'." 1100 ) 1101 1102 return (typ, num) 1103 elif msep in item: 1104 parts = item.split(msep) 1105 mechanism = msep.join(parts[:-1]) 1106 state = parts[-1] 1107 if mechanism.endswith(':'): 1108 # Just a zone-qualified name... 1109 return item 1110 else: 1111 return (mechanism, state) 1112 else: 1113 # It's just a capability 1114 return item
Parses an item, which is a capability (just a string), a
token-type*number pair (returned as a tuple with the number
converted to an integer), or a mechanism-name:state pair
(returned as a tuple with the state as a string). The
'Lexeme.tokenCount' and Lexeme.mechanismSeparator format
values determine the separators that this looks for.
1116 def unparseAnyDecision(self, decision: base.AnyDecisionSpecifier) -> str: 1117 """ 1118 Turns any kind of decision specifier (ID, 1119 `base.DecisionSpecifier`, or name string) into a string that 1120 should parse back using `parseDecisionSpecifier`. 1121 1122 Raises a `TypeError` if given something that isn't a decision 1123 identifier. 1124 1125 For example: 1126 1127 >>> pf = ParseFormat() 1128 >>> pf.unparseAnyDecision( 1129 ... base.DecisionSpecifier("domain", "zone", "D") 1130 ... ) 1131 'domain//zone::D' 1132 >>> pf.unparseAnyDecision(3) 1133 '3' 1134 >>> pf.unparseAnyDecision('D') 1135 'D' 1136 >>> pf.unparseAnyDecision('domain//zone::D') 1137 'domain//zone::D' 1138 >>> pf.unparseAnyDecision([1, 2]) 1139 Traceback (most recent call last): 1140 ... 1141 TypeError... 1142 """ 1143 if isinstance(decision, base.DecisionSpecifier): 1144 return self.unparseDecisionSpecifier(decision) 1145 elif isinstance(decision, (base.DecisionID, base.DecisionName)): 1146 # leave as-is OR convert integer ID to string 1147 return str(decision) 1148 else: 1149 raise TypeError( 1150 "Unrecognized decision identifier type " + type(decision) 1151 )
Turns any kind of decision specifier (ID,
base.DecisionSpecifier, or name string) into a string that
should parse back using parseDecisionSpecifier.
Raises a TypeError if given something that isn't a decision
identifier.
For example:
>>> pf = ParseFormat()
>>> pf.unparseAnyDecision(
... base.DecisionSpecifier("domain", "zone", "D")
... )
'domain//zone::D'
>>> pf.unparseAnyDecision(3)
'3'
>>> pf.unparseAnyDecision('D')
'D'
>>> pf.unparseAnyDecision('domain//zone::D')
'domain//zone::D'
>>> pf.unparseAnyDecision([1, 2])
Traceback (most recent call last):
...
TypeError...
1153 def unparseDecisionSpecifier(self, spec: base.DecisionSpecifier) -> str: 1154 """ 1155 Turns a decision specifier back into a string, which would be 1156 parsed as a decision specifier as part of various different 1157 things. 1158 1159 For example: 1160 1161 >>> pf = ParseFormat() 1162 >>> pf.unparseDecisionSpecifier( 1163 ... base.DecisionSpecifier(None, None, 'where') 1164 ... ) 1165 'where' 1166 >>> pf.unparseDecisionSpecifier( 1167 ... base.DecisionSpecifier(None, 'zone', 'where') 1168 ... ) 1169 'zone::where' 1170 >>> pf.unparseDecisionSpecifier( 1171 ... base.DecisionSpecifier('domain', 'zone', 'where') 1172 ... ) 1173 'domain//zone::where' 1174 >>> pf.unparseDecisionSpecifier( 1175 ... base.DecisionSpecifier('domain', None, 'where') 1176 ... ) 1177 'domain//where' 1178 """ 1179 result = spec.name 1180 if spec.zone is not None: 1181 result = ( 1182 spec.zone 1183 + self.formatDict[Lexeme.zoneSeparator] 1184 + result 1185 ) 1186 if spec.domain is not None: 1187 result = ( 1188 spec.domain 1189 + self.formatDict[Lexeme.domainSeparator] 1190 + result 1191 ) 1192 return result
Turns a decision specifier back into a string, which would be parsed as a decision specifier as part of various different things.
For example:
>>> pf = ParseFormat()
>>> pf.unparseDecisionSpecifier(
... base.DecisionSpecifier(None, None, 'where')
... )
'where'
>>> pf.unparseDecisionSpecifier(
... base.DecisionSpecifier(None, 'zone', 'where')
... )
'zone::where'
>>> pf.unparseDecisionSpecifier(
... base.DecisionSpecifier('domain', 'zone', 'where')
... )
'domain//zone::where'
>>> pf.unparseDecisionSpecifier(
... base.DecisionSpecifier('domain', None, 'where')
... )
'domain//where'
1194 def unparseMechanismSpecifier( 1195 self, 1196 spec: base.MechanismSpecifier 1197 ) -> str: 1198 """ 1199 Turns a mechanism specifier back into a string, which would be 1200 parsed as a mechanism specifier as part of various different 1201 things. Note that a mechanism specifier with a zone part but no 1202 decision part is not valid, since it would parse as a decision 1203 part instead. 1204 1205 For example: 1206 1207 >>> pf = ParseFormat() 1208 >>> pf.unparseMechanismSpecifier( 1209 ... base.MechanismSpecifier(None, None, None, 'lever') 1210 ... ) 1211 'lever' 1212 >>> pf.unparseMechanismSpecifier( 1213 ... base.MechanismSpecifier('domain', 'zone', 'decision', 'door') 1214 ... ) 1215 'domain//zone::decision::door' 1216 >>> pf.unparseMechanismSpecifier( 1217 ... base.MechanismSpecifier('domain', None, None, 'door') 1218 ... ) 1219 'domain//door' 1220 >>> pf.unparseMechanismSpecifier( 1221 ... base.MechanismSpecifier(None, 'a', 'b', 'door') 1222 ... ) 1223 'a::b::door' 1224 >>> pf.unparseMechanismSpecifier( 1225 ... base.MechanismSpecifier(None, 'a', None, 'door') 1226 ... ) 1227 Traceback (most recent call last): 1228 ... 1229 exploration.base.InvalidMechanismSpecifierError... 1230 >>> pf.unparseMechanismSpecifier( 1231 ... base.MechanismSpecifier(None, None, 'a', 'door') 1232 ... ) 1233 'a::door' 1234 >>> pf.unparseMechanismSpecifier( 1235 ... base.MechanismSpecifier(None, None, 37, 'door') 1236 ... ) 1237 '37::door' 1238 """ 1239 if spec.decision is None and spec.zone is not None: 1240 raise base.InvalidMechanismSpecifierError( 1241 f"Mechanism specifier has a zone part but no decision" 1242 f" part; it cannot be unparsed since it would parse" 1243 f" differently:\n{spec}" 1244 ) 1245 result = spec.name 1246 if spec.decision is not None: 1247 result = ( 1248 str(spec.decision) 1249 + self.formatDict[Lexeme.zoneSeparator] 1250 + result 1251 ) 1252 if spec.zone is not None: 1253 result = ( 1254 spec.zone 1255 + self.formatDict[Lexeme.zoneSeparator] 1256 + result 1257 ) 1258 if spec.domain is not None: 1259 result = ( 1260 spec.domain 1261 + self.formatDict[Lexeme.domainSeparator] 1262 + result 1263 ) 1264 return result
Turns a mechanism specifier back into a string, which would be parsed as a mechanism specifier as part of various different things. Note that a mechanism specifier with a zone part but no decision part is not valid, since it would parse as a decision part instead.
For example:
>>> pf = ParseFormat()
>>> pf.unparseMechanismSpecifier(
... base.MechanismSpecifier(None, None, None, 'lever')
... )
'lever'
>>> pf.unparseMechanismSpecifier(
... base.MechanismSpecifier('domain', 'zone', 'decision', 'door')
... )
'domain//zone::decision::door'
>>> pf.unparseMechanismSpecifier(
... base.MechanismSpecifier('domain', None, None, 'door')
... )
'domain//door'
>>> pf.unparseMechanismSpecifier(
... base.MechanismSpecifier(None, 'a', 'b', 'door')
... )
'a::b::door'
>>> pf.unparseMechanismSpecifier(
... base.MechanismSpecifier(None, 'a', None, 'door')
... )
Traceback (most recent call last):
...
exploration.base.InvalidMechanismSpecifierError...
>>> pf.unparseMechanismSpecifier(
... base.MechanismSpecifier(None, None, 'a', 'door')
... )
'a::door'
>>> pf.unparseMechanismSpecifier(
... base.MechanismSpecifier(None, None, 37, 'door')
... )
'37::door'
1266 def effectType(self, effectMarker: str) -> Optional[base.EffectType]: 1267 """ 1268 Returns the `base.EffectType` string corresponding to the 1269 given effect marker string. Returns `None` for an unrecognized 1270 marker. 1271 """ 1272 return self.effectNames.get(effectMarker)
Returns the base.EffectType string corresponding to the
given effect marker string. Returns None for an unrecognized
marker.
1274 def parseCommandFromTokens( 1275 self, 1276 tokens: LexedTokens, 1277 start: int = 0, 1278 end: int = -1 1279 ) -> commands.Command: 1280 """ 1281 Given tokens that specify a `commands.Command`, parses that 1282 command and returns it. Really just turns the tokens back into 1283 strings and calls `commands.command`. 1284 1285 For example: 1286 1287 >>> pf = ParseFormat() 1288 >>> t = ['val', '5'] 1289 >>> c = commands.command(*t) 1290 >>> pf.parseCommandFromTokens(t) == c 1291 True 1292 >>> t = ['op', Lexeme.tokenCount, '$val', '$val'] 1293 >>> c = commands.command('op', '*', '$val', '$val') 1294 >>> pf.parseCommandFromTokens(t) == c 1295 True 1296 """ 1297 start, end, nTokens = normalizeEnds(tokens, start, end) 1298 args: List[str] = [] 1299 for token in tokens[start:end + 1]: 1300 if isinstance(token, Lexeme): 1301 args.append(self.formatDict[token]) 1302 else: 1303 args.append(token) 1304 1305 if len(args) == 0: 1306 raise ParseError( 1307 f"No arguments for command:\n{tokens[start:end + 1]}" 1308 ) 1309 return commands.command(*args)
Given tokens that specify a commands.Command, parses that
command and returns it. Really just turns the tokens back into
strings and calls commands.command.
For example:
>>> pf = ParseFormat()
>>> t = ['val', '5']
>>> c = commands.command(*t)
>>> pf.parseCommandFromTokens(t) == c
True
>>> t = ['op', Lexeme.tokenCount, '$val', '$val']
>>> c = commands.command('op', '*', '$val', '$val')
>>> pf.parseCommandFromTokens(t) == c
True
1311 def unparseCommand(self, command: commands.Command) -> str: 1312 """ 1313 Turns a `Command` back into the string that would produce that 1314 command when parsed using `parseCommandList`. 1315 1316 Note that the results will be more explicit in some cases than what 1317 `parseCommandList` would accept as input. 1318 1319 For example: 1320 1321 >>> pf = ParseFormat() 1322 >>> pf.unparseCommand( 1323 ... commands.LiteralValue(command='val', value='5') 1324 ... ) 1325 'val 5' 1326 >>> pf.unparseCommand( 1327 ... commands.LiteralValue(command='val', value='"5"') 1328 ... ) 1329 'val "5"' 1330 >>> pf.unparseCommand( 1331 ... commands.EstablishCollection( 1332 ... command='empty', 1333 ... collection='list' 1334 ... ) 1335 ... ) 1336 'empty list' 1337 >>> pf.unparseCommand( 1338 ... commands.AppendValue(command='append', value='$_') 1339 ... ) 1340 'append $_' 1341 """ 1342 candidate = None 1343 for k, v in commands.COMMAND_SETUP.items(): 1344 if v[0] == type(command): 1345 if candidate is None: 1346 candidate = k 1347 else: 1348 raise ValueError( 1349 f"COMMAND_SETUP includes multiple keys with" 1350 f" {type(command)} as their value type:" 1351 f" '{candidate}' and '{k}'." 1352 ) 1353 1354 if candidate is None: 1355 raise ValueError( 1356 f"COMMAND_SETUP has no key with {type(command)} as its" 1357 f" value type." 1358 ) 1359 1360 result = candidate 1361 for x in command[1:]: 1362 # TODO: Is this hack good enough? 1363 result += ' ' + str(x) 1364 return result
Turns a Command back into the string that would produce that
command when parsed using parseCommandList.
Note that the results will be more explicit in some cases than what
parseCommandList would accept as input.
For example:
>>> pf = ParseFormat()
>>> pf.unparseCommand(
... commands.LiteralValue(command='val', value='5')
... )
'val 5'
>>> pf.unparseCommand(
... commands.LiteralValue(command='val', value='"5"')
... )
'val "5"'
>>> pf.unparseCommand(
... commands.EstablishCollection(
... command='empty',
... collection='list'
... )
... )
'empty list'
>>> pf.unparseCommand(
... commands.AppendValue(command='append', value='$_')
... )
'append $_'
1366 def unparseCommandList(self, commands: List[commands.Command]) -> str: 1367 """ 1368 Takes a list of commands and returns a string that would parse 1369 into them using `parseOneEffectArg`. The result contains 1370 newlines and indentation to make it easier to read. 1371 1372 For example: 1373 1374 >>> pf = ParseFormat() 1375 >>> pf.unparseCommandList( 1376 ... [commands.command('val', '5'), commands.command('pop')] 1377 ... ) 1378 '{\\n val 5;\\n pop;\\n}' 1379 """ 1380 result = self.formatDict[Lexeme.openCurly] 1381 for cmd in commands: 1382 result += f'\n {self.unparseCommand(cmd)};' 1383 if len(commands) > 0: 1384 result += '\n' 1385 return result + self.formatDict[Lexeme.closeCurly]
Takes a list of commands and returns a string that would parse
into them using parseOneEffectArg. The result contains
newlines and indentation to make it easier to read.
For example:
>>> pf = ParseFormat()
>>> pf.unparseCommandList(
... [commands.command('val', '5'), commands.command('pop')]
... )
'{\n val 5;\n pop;\n}'
1387 def parseCommandListFromTokens( 1388 self, 1389 tokens: LexedTokens, 1390 start: int = 0 1391 ) -> Tuple[List[commands.Command], int]: 1392 """ 1393 Parses a command list from a list of lexed tokens, which must 1394 start with `Lexeme.openCurly`. Returns the parsed command list 1395 as a list of `commands.Command` objects, along with the end 1396 index of that command list (which will be the matching curly 1397 brace. 1398 """ 1399 end = self.matchingBrace( 1400 tokens, 1401 start, 1402 Lexeme.openCurly, 1403 Lexeme.closeCurly 1404 ) 1405 parts = list( 1406 findSeparatedParts( 1407 tokens, 1408 Lexeme.consequenceSeparator, 1409 start + 1, 1410 end - 1, 1411 Lexeme.openCurly, 1412 Lexeme.closeCurly, 1413 ) 1414 ) 1415 return ( 1416 [ 1417 self.parseCommandFromTokens(tokens, fromIndex, toIndex) 1418 for fromIndex, toIndex in parts 1419 if fromIndex <= toIndex # ignore empty parts 1420 ], 1421 end 1422 )
Parses a command list from a list of lexed tokens, which must
start with Lexeme.openCurly. Returns the parsed command list
as a list of commands.Command objects, along with the end
index of that command list (which will be the matching curly
brace.
1424 def parseOneEffectArg( 1425 self, 1426 tokens: LexedTokens, 1427 start: int = 0, 1428 limit: Optional[int] = None 1429 ) -> Tuple[ 1430 Union[ 1431 base.Capability, # covers 'str' possibility 1432 Tuple[base.Token, base.TokenCount], 1433 Tuple[Literal['skill'], base.Skill, base.Level], 1434 Tuple[base.MechanismSpecifier, base.MechanismState], 1435 base.DecisionSpecifier, 1436 base.DecisionID, 1437 Literal[Lexeme.inCommon, Lexeme.isHidden], 1438 Tuple[Literal[Lexeme.sepOrDelay, Lexeme.effectCharges], int], 1439 List[commands.Command] 1440 ], 1441 int 1442 ]: 1443 """ 1444 Looks at tokens starting at the specified position and parses 1445 one or more of them as an effect argument (an argument that 1446 could be given to `base.effect`). Looks at various key `Lexeme`s 1447 to determine which type to use. 1448 1449 Items in the tokens list beyond the specified limit will not be 1450 considered, even when they in theory could be grouped with items 1451 up to the limit into a more complex argument. 1452 1453 For example: 1454 1455 >>> pf = ParseFormat() 1456 >>> pf.parseOneEffectArg(['hi']) 1457 ('hi', 0) 1458 >>> pf.parseOneEffectArg(['hi'], 1) 1459 Traceback (most recent call last): 1460 ... 1461 IndexError... 1462 >>> pf.parseOneEffectArg(['hi', 'bye']) 1463 ('hi', 0) 1464 >>> pf.parseOneEffectArg(['hi', 'bye'], 1) 1465 ('bye', 1) 1466 >>> pf.parseOneEffectArg( 1467 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1468 ... 0 1469 ... ) 1470 ((MechanismSpecifier(domain=None, zone=None, decision=None,\ 1471 name='gate'), 'open'), 2) 1472 >>> pf.parseOneEffectArg( 1473 ... ['set', 'gate', Lexeme.mechanismSeparator, 'open'], 1474 ... 1 1475 ... ) 1476 ((MechanismSpecifier(domain=None, zone=None, decision=None,\ 1477 name='gate'), 'open'), 3) 1478 >>> pf.parseOneEffectArg( 1479 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1480 ... 1 1481 ... ) 1482 Traceback (most recent call last): 1483 ... 1484 exploration.parsing.ParseError... 1485 >>> pf.parseOneEffectArg( 1486 ... ['gate', Lexeme.mechanismSeparator, 'open'], 1487 ... 2 1488 ... ) 1489 ('open', 2) 1490 >>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, '10'], 0) 1491 (('gold', 10), 2) 1492 >>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, 'ten'], 0) 1493 Traceback (most recent call last): 1494 ... 1495 exploration.parsing.ParseError... 1496 >>> pf.parseOneEffectArg([Lexeme.inCommon], 0) 1497 (<Lexeme.inCommon: ...>, 0) 1498 >>> pf.parseOneEffectArg([Lexeme.isHidden], 0) 1499 (<Lexeme.isHidden: ...>, 0) 1500 >>> pf.parseOneEffectArg([Lexeme.tokenCount, '3'], 0) 1501 Traceback (most recent call last): 1502 ... 1503 exploration.parsing.ParseError... 1504 >>> pf.parseOneEffectArg([Lexeme.effectCharges, '3'], 0) 1505 ((<Lexeme.effectCharges: ...>, 3), 1) 1506 >>> pf.parseOneEffectArg([Lexeme.tokenCount, 3], 0) # int is a lexeme 1507 Traceback (most recent call last): 1508 ... 1509 exploration.parsing.ParseError... 1510 >>> pf.parseOneEffectArg([Lexeme.sepOrDelay, '-2'], 0) 1511 ((<Lexeme.sepOrDelay: ...>, -2), 1) 1512 >>> pf.parseOneEffectArg(['agility', Lexeme.skillLevel, '3'], 0) 1513 (('skill', 'agility', 3), 2) 1514 >>> pf.parseOneEffectArg( 1515 ... [ 1516 ... 'main', 1517 ... Lexeme.domainSeparator, 1518 ... 'zone', 1519 ... Lexeme.zoneSeparator, 1520 ... 'decision', 1521 ... Lexeme.zoneSeparator, 1522 ... 'compass', 1523 ... Lexeme.mechanismSeparator, 1524 ... 'north', 1525 ... 'south', 1526 ... 'east', 1527 ... 'west' 1528 ... ], 1529 ... 0 1530 ... ) 1531 ((MechanismSpecifier(domain='main', zone='zone',\ 1532 decision='decision', name='compass'), 'north'), 8) 1533 >>> pf.parseOneEffectArg( 1534 ... [ 1535 ... 'before', 1536 ... 'main', 1537 ... Lexeme.domainSeparator, 1538 ... 'zone', 1539 ... Lexeme.zoneSeparator, 1540 ... 'decision', 1541 ... Lexeme.zoneSeparator, 1542 ... 'compass', 1543 ... 'north', 1544 ... 'south', 1545 ... 'east', 1546 ... 'west' 1547 ... ], 1548 ... 1 1549 ... ) # a mechanism specifier without a state will become a 1550 ... # decision specifier 1551 (DecisionSpecifier(domain='main', zone='zone',\ 1552 name='decision'), 5) 1553 >>> tokens = [ 1554 ... 'set', 1555 ... 'main', 1556 ... Lexeme.domainSeparator, 1557 ... 'zone', 1558 ... Lexeme.zoneSeparator, 1559 ... 'compass', 1560 ... 'north', 1561 ... 'bounce', 1562 ... ] 1563 >>> pf.parseOneEffectArg(tokens, 0) 1564 ('set', 0) 1565 >>> pf.parseDecisionSpecifierFromTokens(tokens, 1) 1566 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 1567 >>> pf.parseOneEffectArg(tokens, 1) 1568 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 1569 >>> pf.parseOneEffectArg(tokens, 6) 1570 ('north', 6) 1571 >>> pf.parseOneEffectArg(tokens, 7) 1572 ('bounce', 7) 1573 >>> pf.parseOneEffectArg( 1574 ... [ 1575 ... "fort", Lexeme.zoneSeparator, "gate", 1576 ... Lexeme.mechanismSeparator, "open", 1577 ... ], 1578 ... 0 1579 ... ) 1580 ((MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1581 name='gate'), 'open'), 4) 1582 >>> pf.parseOneEffectArg( 1583 ... [Lexeme.openCurly, 'val', '5', Lexeme.closeCurly], 1584 ... 0 1585 ... ) == ([commands.command('val', '5')], 3) 1586 True 1587 >>> a = [ 1588 ... Lexeme.openCurly, 'val', '5', Lexeme.closeCurly, 1589 ... Lexeme.openCurly, 'append', Lexeme.consequenceSeparator, 1590 ... 'pop', Lexeme.closeCurly 1591 ... ] 1592 >>> cl = [ 1593 ... [commands.command('val', '5')], 1594 ... [commands.command('append'), commands.command('pop')] 1595 ... ] 1596 >>> pf.parseOneEffectArg(a, 0) == (cl[0], 3) 1597 True 1598 >>> pf.parseOneEffectArg(a, 4) == (cl[1], 8) 1599 True 1600 >>> pf.parseOneEffectArg(a, 1) 1601 ('val', 1) 1602 >>> pf.parseOneEffectArg(a, 2) 1603 ('5', 2) 1604 >>> pf.parseOneEffectArg(a, 3) 1605 Traceback (most recent call last): 1606 ... 1607 exploration.parsing.ParseError... 1608 """ 1609 start, limit, nTokens = normalizeEnds( 1610 tokens, 1611 start, 1612 limit if limit is not None else -1 1613 ) 1614 if nTokens == 0: 1615 raise ParseError("No effect arguments available.") 1616 1617 first = tokens[start] 1618 1619 if nTokens == 1: 1620 if first in (Lexeme.inCommon, Lexeme.isHidden): 1621 return (first, start) 1622 elif not isinstance(first, str): 1623 raise ParseError( 1624 f"Only one token and it's a special character" 1625 f" ({first} = {repr(self.formatDict[first])})" 1626 ) 1627 else: 1628 return (cast(base.Capability, first), start) 1629 1630 assert (nTokens > 1) 1631 1632 second = tokens[start + 1] 1633 1634 # Command lists start with an open curly brace and effect 1635 # modifiers start with a Lexme, but nothing else may 1636 if first == Lexeme.openCurly: 1637 return self.parseCommandListFromTokens(tokens, start) 1638 elif first in (Lexeme.inCommon, Lexeme.isHidden): 1639 return (first, start) 1640 elif first in (Lexeme.sepOrDelay, Lexeme.effectCharges): 1641 if not isinstance(second, str): 1642 raise ParseError( 1643 f"Token following a modifier that needs a count" 1644 f" must be a string in tokens:" 1645 f"\n{tokens[start:limit or len(tokens)]}" 1646 ) 1647 try: 1648 val = int(second) 1649 except ValueError: 1650 raise ParseError( 1651 f"Token following a modifier that needs a count" 1652 f" must be convertible to an int:" 1653 f"\n{tokens[start:limit or len(tokens)]}" 1654 ) 1655 1656 first = cast( 1657 Literal[Lexeme.sepOrDelay, Lexeme.effectCharges], 1658 first 1659 ) 1660 return ((first, val), start + 1) 1661 elif not isinstance(first, str): 1662 raise ParseError( 1663 f"First token must be a string unless it's a modifier" 1664 f" lexeme or command/reversion-set opener. Got:" 1665 f"\n{tokens[start:limit or len(tokens)]}" 1666 ) 1667 1668 # If we have two strings in a row, then the first is our parsed 1669 # value alone and we'll parse the second separately. 1670 if isinstance(second, str): 1671 return (first, start) 1672 elif second in (Lexeme.inCommon, Lexeme.isHidden): 1673 return (first, start) 1674 1675 # Must have at least 3 tokens at this point, or else we need to 1676 # have the inCommon or isHidden lexeme second. 1677 if nTokens < 3: 1678 return (first, start) 1679 1680 third = tokens[start + 2] 1681 if not isinstance(third, str): 1682 return (first, start) 1683 1684 second = cast(Lexeme, second) 1685 third = cast(str, third) 1686 1687 if second in (Lexeme.tokenCount, Lexeme.skillLevel): 1688 try: 1689 num = int(third) 1690 except ValueError: 1691 raise ParseError( 1692 f"Invalid effect tokens: count for Tokens or level" 1693 f" for Skill must be convertible to an integer." 1694 f"\n{tokens[start:limit + 1]}" 1695 ) 1696 if second == Lexeme.tokenCount: 1697 return ((first, num), start + 2) # token/count pair 1698 else: 1699 return (('skill', first, num), start + 2) # token/count pair 1700 1701 elif second == Lexeme.mechanismSeparator: # bare mechanism 1702 return ( 1703 ( 1704 base.MechanismSpecifier( 1705 domain=None, 1706 zone=None, 1707 decision=None, 1708 name=first 1709 ), 1710 third 1711 ), 1712 start + 2 1713 ) 1714 1715 elif second in (Lexeme.domainSeparator, Lexeme.zoneSeparator): 1716 try: 1717 mSpec, mEnd = self.parseMechanismSpecifierFromTokens( 1718 tokens, 1719 start 1720 ) # works whether it's a mechanism or decision specifier... 1721 except ParseError: 1722 return self.parseDecisionSpecifierFromTokens(tokens, start) 1723 if mEnd + 2 > limit: 1724 # No room for following mechanism separator + state 1725 return self.parseDecisionSpecifierFromTokens(tokens, start) 1726 sep = tokens[mEnd + 1] 1727 after = tokens[mEnd + 2] 1728 if sep == Lexeme.mechanismSeparator: 1729 if not isinstance(after, str): 1730 raise ParseError( 1731 f"Mechanism separator not followed by state:" 1732 f"\n{tokens[start]}" 1733 ) 1734 return ((mSpec, after), mEnd + 2) 1735 else: 1736 # No mechanism separator afterwards 1737 return self.parseDecisionSpecifierFromTokens(tokens, start) 1738 1739 else: # unrecognized as a longer combo 1740 return (first, start)
Looks at tokens starting at the specified position and parses
one or more of them as an effect argument (an argument that
could be given to base.effect). Looks at various key Lexemes
to determine which type to use.
Items in the tokens list beyond the specified limit will not be considered, even when they in theory could be grouped with items up to the limit into a more complex argument.
For example:
>>> pf = ParseFormat()
>>> pf.parseOneEffectArg(['hi'])
('hi', 0)
>>> pf.parseOneEffectArg(['hi'], 1)
Traceback (most recent call last):
...
IndexError...
>>> pf.parseOneEffectArg(['hi', 'bye'])
('hi', 0)
>>> pf.parseOneEffectArg(['hi', 'bye'], 1)
('bye', 1)
>>> pf.parseOneEffectArg(
... ['gate', Lexeme.mechanismSeparator, 'open'],
... 0
... )
((MechanismSpecifier(domain=None, zone=None, decision=None, name='gate'), 'open'), 2)
>>> pf.parseOneEffectArg(
... ['set', 'gate', Lexeme.mechanismSeparator, 'open'],
... 1
... )
((MechanismSpecifier(domain=None, zone=None, decision=None, name='gate'), 'open'), 3)
>>> pf.parseOneEffectArg(
... ['gate', Lexeme.mechanismSeparator, 'open'],
... 1
... )
Traceback (most recent call last):
...
ParseError...
>>> pf.parseOneEffectArg(
... ['gate', Lexeme.mechanismSeparator, 'open'],
... 2
... )
('open', 2)
>>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, '10'], 0)
(('gold', 10), 2)
>>> pf.parseOneEffectArg(['gold', Lexeme.tokenCount, 'ten'], 0)
Traceback (most recent call last):
...
ParseError...
>>> pf.parseOneEffectArg([Lexeme.inCommon], 0)
(<Lexeme.inCommon: ...>, 0)
>>> pf.parseOneEffectArg([Lexeme.isHidden], 0)
(<Lexeme.isHidden: ...>, 0)
>>> pf.parseOneEffectArg([Lexeme.tokenCount, '3'], 0)
Traceback (most recent call last):
...
ParseError...
>>> pf.parseOneEffectArg([Lexeme.effectCharges, '3'], 0)
((<Lexeme.effectCharges: ...>, 3), 1)
>>> pf.parseOneEffectArg([Lexeme.tokenCount, 3], 0) # int is a lexeme
Traceback (most recent call last):
...
ParseError...
>>> pf.parseOneEffectArg([Lexeme.sepOrDelay, '-2'], 0)
((<Lexeme.sepOrDelay: ...>, -2), 1)
>>> pf.parseOneEffectArg(['agility', Lexeme.skillLevel, '3'], 0)
(('skill', 'agility', 3), 2)
>>> pf.parseOneEffectArg(
... [
... 'main',
... Lexeme.domainSeparator,
... 'zone',
... Lexeme.zoneSeparator,
... 'decision',
... Lexeme.zoneSeparator,
... 'compass',
... Lexeme.mechanismSeparator,
... 'north',
... 'south',
... 'east',
... 'west'
... ],
... 0
... )
((MechanismSpecifier(domain='main', zone='zone', decision='decision', name='compass'), 'north'), 8)
>>> pf.parseOneEffectArg(
... [
... 'before',
... 'main',
... Lexeme.domainSeparator,
... 'zone',
... Lexeme.zoneSeparator,
... 'decision',
... Lexeme.zoneSeparator,
... 'compass',
... 'north',
... 'south',
... 'east',
... 'west'
... ],
... 1
... ) # a mechanism specifier without a state will become a
... # decision specifier
(DecisionSpecifier(domain='main', zone='zone', name='decision'), 5)
>>> tokens = [
... 'set',
... 'main',
... Lexeme.domainSeparator,
... 'zone',
... Lexeme.zoneSeparator,
... 'compass',
... 'north',
... 'bounce',
... ]
>>> pf.parseOneEffectArg(tokens, 0)
('set', 0)
>>> pf.parseDecisionSpecifierFromTokens(tokens, 1)
(DecisionSpecifier(domain='main', zone='zone', name='compass'), 5)
>>> pf.parseOneEffectArg(tokens, 1)
(DecisionSpecifier(domain='main', zone='zone', name='compass'), 5)
>>> pf.parseOneEffectArg(tokens, 6)
('north', 6)
>>> pf.parseOneEffectArg(tokens, 7)
('bounce', 7)
>>> pf.parseOneEffectArg(
... [
... "fort", Lexeme.zoneSeparator, "gate",
... Lexeme.mechanismSeparator, "open",
... ],
... 0
... )
((MechanismSpecifier(domain=None, zone=None, decision='fort', name='gate'), 'open'), 4)
>>> pf.parseOneEffectArg(
... [Lexeme.openCurly, 'val', '5', Lexeme.closeCurly],
... 0
... ) == ([commands.command('val', '5')], 3)
True
>>> a = [
... Lexeme.openCurly, 'val', '5', Lexeme.closeCurly,
... Lexeme.openCurly, 'append', Lexeme.consequenceSeparator,
... 'pop', Lexeme.closeCurly
... ]
>>> cl = [
... [commands.command('val', '5')],
... [commands.command('append'), commands.command('pop')]
... ]
>>> pf.parseOneEffectArg(a, 0) == (cl[0], 3)
True
>>> pf.parseOneEffectArg(a, 4) == (cl[1], 8)
True
>>> pf.parseOneEffectArg(a, 1)
('val', 1)
>>> pf.parseOneEffectArg(a, 2)
('5', 2)
>>> pf.parseOneEffectArg(a, 3)
Traceback (most recent call last):
...
ParseError...
1742 def coalesceEffectArgs( 1743 self, 1744 tokens: LexedTokens, 1745 start: int = 0, 1746 end: int = -1 1747 ) -> Tuple[ 1748 List[ # List of effect args 1749 Union[ 1750 base.Capability, # covers 'str' possibility 1751 Tuple[base.Token, base.TokenCount], 1752 Tuple[Literal['skill'], base.Skill, base.Level], 1753 Tuple[base.MechanismSpecifier, base.MechanismState], 1754 base.DecisionSpecifier, 1755 List[commands.Command], 1756 Set[str] 1757 ] 1758 ], 1759 Tuple[ # Slots for modifiers: common/hidden/charges/delay 1760 Optional[bool], 1761 Optional[bool], 1762 Optional[int], 1763 Optional[int], 1764 ] 1765 ]: 1766 """ 1767 Given a region of a lexed tokens list which contains one or more 1768 effect arguments, combines token sequences representing things 1769 like capabilities, mechanism states, token counts, and skill 1770 levels, representing these using the tuples that would be passed 1771 to `base.effect`. Returns a tuple with two elements: 1772 1773 - First, a list that contains several different kinds of 1774 objects, each of which is distinguishable by its type or 1775 part of its value. 1776 - Next, a tuple with four entires for common, hidden, charges, 1777 and/or delay values based on the presence of modifier 1778 sequences. Any or all of these may be `None` if the relevant 1779 modifier was not present (the usual case). 1780 1781 For example: 1782 1783 >>> pf = ParseFormat() 1784 >>> pf.coalesceEffectArgs(["jump"]) 1785 (['jump'], (None, None, None, None)) 1786 >>> pf.coalesceEffectArgs(["coin", Lexeme.tokenCount, "3", "fly"]) 1787 ([('coin', 3), 'fly'], (None, None, None, None)) 1788 >>> pf.coalesceEffectArgs( 1789 ... [ 1790 ... "fort", Lexeme.zoneSeparator, "gate", 1791 ... Lexeme.mechanismSeparator, "open" 1792 ... ] 1793 ... ) 1794 ([(MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1795 name='gate'), 'open')], (None, None, None, None)) 1796 >>> pf.coalesceEffectArgs( 1797 ... [ 1798 ... "main", Lexeme.domainSeparator, "cliff" 1799 ... ] 1800 ... ) 1801 ([DecisionSpecifier(domain='main', zone=None, name='cliff')],\ 1802 (None, None, None, None)) 1803 >>> pf.coalesceEffectArgs( 1804 ... [ 1805 ... "door", Lexeme.mechanismSeparator, "open" 1806 ... ] 1807 ... ) 1808 ([(MechanismSpecifier(domain=None, zone=None, decision=None,\ 1809 name='door'), 'open')], (None, None, None, None)) 1810 >>> pf.coalesceEffectArgs( 1811 ... [ 1812 ... "fort", Lexeme.zoneSeparator, "gate", 1813 ... Lexeme.mechanismSeparator, "open", 1814 ... "canJump", 1815 ... "coins", Lexeme.tokenCount, "3", 1816 ... Lexeme.inCommon, 1817 ... "agility", Lexeme.skillLevel, "-1", 1818 ... Lexeme.sepOrDelay, "0", 1819 ... "main", Lexeme.domainSeparator, "cliff" 1820 ... ] 1821 ... ) 1822 ([(MechanismSpecifier(domain=None, zone=None, decision='fort',\ 1823 name='gate'), 'open'), 'canJump', ('coins', 3), ('skill', 'agility', -1),\ 1824 DecisionSpecifier(domain='main', zone=None, name='cliff')],\ 1825 (True, None, None, 0)) 1826 >>> pf.coalesceEffectArgs(["bounce", Lexeme.isHidden]) 1827 (['bounce'], (None, True, None, None)) 1828 >>> pf.coalesceEffectArgs( 1829 ... ["goto", "3", Lexeme.inCommon, Lexeme.isHidden] 1830 ... ) 1831 (['goto', '3'], (True, True, None, None)) 1832 """ 1833 start, end, nTokens = normalizeEnds(tokens, start, end) 1834 where = start 1835 result: List[ # List of effect args 1836 Union[ 1837 base.Capability, # covers 'str' possibility 1838 Tuple[base.Token, base.TokenCount], 1839 Tuple[Literal['skill'], base.Skill, base.Level], 1840 Tuple[base.MechanismSpecifier, base.MechanismState], 1841 base.DecisionSpecifier, 1842 List[commands.Command], 1843 Set[str] 1844 ] 1845 ] = [] 1846 inCommon: Optional[bool] = None 1847 isHidden: Optional[bool] = None 1848 charges: Optional[int] = None 1849 delay: Optional[int] = None 1850 while where <= end: 1851 following, thisEnd = self.parseOneEffectArg(tokens, where, end) 1852 if following == Lexeme.inCommon: 1853 if inCommon is not None: 1854 raise ParseError( 1855 f"In-common effect modifier specified more than" 1856 f" once in effect args:" 1857 f"\n{tokens[start:end + 1]}" 1858 ) 1859 inCommon = True 1860 elif following == Lexeme.isHidden: 1861 if isHidden is not None: 1862 raise ParseError( 1863 f"Is-hidden effect modifier specified more than" 1864 f" once in effect args:" 1865 f"\n{tokens[start:end + 1]}" 1866 ) 1867 isHidden = True 1868 elif ( 1869 isinstance(following, tuple) 1870 and len(following) == 2 1871 and following[0] in (Lexeme.effectCharges, Lexeme.sepOrDelay) 1872 and isinstance(following[1], int) 1873 ): 1874 if following[0] == Lexeme.effectCharges: 1875 if charges is not None: 1876 raise ParseError( 1877 f"Charges effect modifier specified more than" 1878 f" once in effect args:" 1879 f"\n{tokens[start:end + 1]}" 1880 ) 1881 charges = following[1] 1882 else: 1883 if delay is not None: 1884 raise ParseError( 1885 f"Delay effect modifier specified more than" 1886 f" once in effect args:" 1887 f"\n{tokens[start:end + 1]}" 1888 ) 1889 delay = following[1] 1890 elif ( 1891 isinstance(following, base.Capability) 1892 or ( 1893 isinstance(following, tuple) 1894 and len(following) == 2 1895 and isinstance(following[0], base.Token) 1896 and isinstance(following[1], base.TokenCount) 1897 ) or ( 1898 isinstance(following, tuple) 1899 and len(following) == 3 1900 and following[0] == 'skill' 1901 and isinstance(following[1], base.Skill) 1902 and isinstance(following[2], base.Level) 1903 ) or ( 1904 isinstance(following, tuple) 1905 and len(following) == 2 1906 and isinstance(following[0], base.MechanismSpecifier) 1907 and isinstance(following[1], base.MechanismState) 1908 ) or ( 1909 isinstance(following, base.DecisionSpecifier) 1910 ) or ( 1911 isinstance(following, list) 1912 and all(isinstance(item, tuple) for item in following) 1913 # TODO: Stricter command list check here? 1914 ) or ( 1915 isinstance(following, set) 1916 and all(isinstance(item, str) for item in following) 1917 ) 1918 ): 1919 result.append(following) 1920 else: 1921 raise ParseError(f"Invalid coalesced argument: {following}") 1922 where = thisEnd + 1 1923 1924 return (result, (inCommon, isHidden, charges, delay))
Given a region of a lexed tokens list which contains one or more
effect arguments, combines token sequences representing things
like capabilities, mechanism states, token counts, and skill
levels, representing these using the tuples that would be passed
to base.effect. Returns a tuple with two elements:
- First, a list that contains several different kinds of objects, each of which is distinguishable by its type or part of its value.
- Next, a tuple with four entires for common, hidden, charges,
and/or delay values based on the presence of modifier
sequences. Any or all of these may be
Noneif the relevant modifier was not present (the usual case).
For example:
>>> pf = ParseFormat()
>>> pf.coalesceEffectArgs(["jump"])
(['jump'], (None, None, None, None))
>>> pf.coalesceEffectArgs(["coin", Lexeme.tokenCount, "3", "fly"])
([('coin', 3), 'fly'], (None, None, None, None))
>>> pf.coalesceEffectArgs(
... [
... "fort", Lexeme.zoneSeparator, "gate",
... Lexeme.mechanismSeparator, "open"
... ]
... )
([(MechanismSpecifier(domain=None, zone=None, decision='fort', name='gate'), 'open')], (None, None, None, None))
>>> pf.coalesceEffectArgs(
... [
... "main", Lexeme.domainSeparator, "cliff"
... ]
... )
([DecisionSpecifier(domain='main', zone=None, name='cliff')], (None, None, None, None))
>>> pf.coalesceEffectArgs(
... [
... "door", Lexeme.mechanismSeparator, "open"
... ]
... )
([(MechanismSpecifier(domain=None, zone=None, decision=None, name='door'), 'open')], (None, None, None, None))
>>> pf.coalesceEffectArgs(
... [
... "fort", Lexeme.zoneSeparator, "gate",
... Lexeme.mechanismSeparator, "open",
... "canJump",
... "coins", Lexeme.tokenCount, "3",
... Lexeme.inCommon,
... "agility", Lexeme.skillLevel, "-1",
... Lexeme.sepOrDelay, "0",
... "main", Lexeme.domainSeparator, "cliff"
... ]
... )
([(MechanismSpecifier(domain=None, zone=None, decision='fort', name='gate'), 'open'), 'canJump', ('coins', 3), ('skill', 'agility', -1), DecisionSpecifier(domain='main', zone=None, name='cliff')], (True, None, None, 0))
>>> pf.coalesceEffectArgs(["bounce", Lexeme.isHidden])
(['bounce'], (None, True, None, None))
>>> pf.coalesceEffectArgs(
... ["goto", "3", Lexeme.inCommon, Lexeme.isHidden]
... )
(['goto', '3'], (True, True, None, None))
1926 def parseEffectFromTokens( 1927 self, 1928 tokens: LexedTokens, 1929 start: int = 0, 1930 end: int = -1 1931 ) -> base.Effect: 1932 """ 1933 Given a region of a list of lexed tokens specifying an effect, 1934 returns the `Effect` object that those tokens specify. 1935 """ 1936 start, end, nTokens = normalizeEnds(tokens, start, end) 1937 1938 # Check for empty list 1939 if nTokens == 0: 1940 raise ParseError( 1941 "Effect must include at least a type." 1942 ) 1943 1944 firstPart = tokens[start] 1945 1946 if isinstance(firstPart, Lexeme): 1947 raise ParseError( 1948 f"First part of effect must be an effect type. Got" 1949 f" {firstPart} ({repr(self.formatDict[firstPart])})." 1950 ) 1951 1952 firstPart = cast(str, firstPart) 1953 1954 # Get the effect type 1955 fType = self.effectType(firstPart) 1956 1957 if fType is None: 1958 raise ParseError( 1959 f"Unrecognized effect type {firstPart!r}. Check the" 1960 f" EffectType entries in the effect names dictionary." 1961 ) 1962 1963 if start + 1 > end: # No tokens left: set empty args 1964 groupedArgs: List[ 1965 Union[ 1966 base.Capability, # covers 'str' possibility 1967 Tuple[base.Token, base.TokenCount], 1968 Tuple[Literal['skill'], base.Skill, base.Level], 1969 Tuple[base.MechanismSpecifier, base.MechanismState], 1970 base.DecisionSpecifier, 1971 List[commands.Command], 1972 Set[str] 1973 ] 1974 ] = [] 1975 modifiers: Tuple[ 1976 Optional[bool], 1977 Optional[bool], 1978 Optional[int], 1979 Optional[int] 1980 ] = (None, None, None, None) 1981 else: # Coalesce remaining tokens if there are any 1982 groupedArgs, modifiers = self.coalesceEffectArgs( 1983 tokens, 1984 start + 1, 1985 end 1986 ) 1987 1988 # Set up arguments for base.effect and handle modifiers first 1989 args: Dict[ 1990 str, 1991 Union[ 1992 None, 1993 base.ContextSpecifier, 1994 base.Capability, 1995 Tuple[base.Token, base.TokenCount], 1996 Tuple[Literal['skill'], base.Skill, base.Level], 1997 Tuple[base.MechanismSpecifier, base.MechanismState], 1998 Tuple[base.MechanismSpecifier, List[base.MechanismState]], 1999 List[base.Capability], 2000 base.AnyDecisionSpecifier, 2001 Tuple[base.AnyDecisionSpecifier, base.FocalPointName], 2002 bool, 2003 int, 2004 base.SaveSlot, 2005 Tuple[base.SaveSlot, Set[str]] 2006 ] 2007 ] = {} 2008 if modifiers[0]: 2009 args['applyTo'] = 'common' 2010 if modifiers[1]: 2011 args['hidden'] = True 2012 else: 2013 args['hidden'] = False 2014 if modifiers[2] is not None: 2015 args['charges'] = modifiers[2] 2016 if modifiers[3] is not None: 2017 args['delay'] = modifiers[3] 2018 2019 # Now handle the main effect-type-based argument 2020 if fType in ("gain", "lose"): 2021 if len(groupedArgs) != 1: 2022 raise ParseError( 2023 f"'{fType}' effect must have exactly one grouped" 2024 f" argument (got {len(groupedArgs)}:\n{groupedArgs}" 2025 ) 2026 thing = groupedArgs[0] 2027 if isinstance(thing, tuple): 2028 if len(thing) == 2: 2029 if ( 2030 not isinstance(thing[0], base.Token) 2031 or not isinstance(thing[1], base.TokenCount) 2032 ): 2033 raise ParseError( 2034 f"'{fType}' effect grouped arg pair must be a" 2035 f" (token, amount) pair. Got:\n{thing}" 2036 ) 2037 elif len(thing) == 3: 2038 if ( 2039 thing[0] != 'skill' 2040 or not isinstance(thing[1], base.Skill) 2041 or not isinstance(thing[2], base.Level) 2042 ): 2043 raise ParseError( 2044 f"'{fType}' effect grouped arg pair must be a" 2045 f" (token, amount) pair. Got:\n{thing}" 2046 ) 2047 else: 2048 raise ParseError( 2049 f"'{fType}' effect grouped arg tuple must have" 2050 f" length 2 or 3. Got (length {len(thing)}):\n{thing}" 2051 ) 2052 elif not isinstance(thing, base.Capability): 2053 raise ParseError( 2054 f"'{fType}' effect grouped arg must be a capability" 2055 f" or a (token, amount) tuple. Got:\n{thing}" 2056 ) 2057 args[fType] = thing 2058 return base.effect(**args) # type:ignore 2059 2060 elif fType == "set": 2061 if len(groupedArgs) != 1: 2062 raise ParseError( 2063 f"'{fType}' effect must have exactly one grouped" 2064 f" argument (got {len(groupedArgs)}:\n{groupedArgs}" 2065 ) 2066 setVal = groupedArgs[0] 2067 if not isinstance( 2068 setVal, 2069 tuple 2070 ): 2071 raise ParseError( 2072 f"'{fType}' effect grouped arg must be a tuple. Got:" 2073 f"\n{setVal}" 2074 ) 2075 if len(setVal) == 2: 2076 setWhat, setTo = setVal 2077 if ( 2078 isinstance(setWhat, base.Token) 2079 and isinstance(setTo, base.TokenCount) 2080 ) or ( 2081 isinstance(setWhat, base.MechanismSpecifier) 2082 and isinstance(setTo, base.MechanismState) 2083 ): 2084 args[fType] = setVal 2085 return base.effect(**args) # type:ignore 2086 else: 2087 raise ParseError( 2088 f"Invalid '{fType}' effect grouped args:" 2089 f"\n{groupedArgs}" 2090 ) 2091 elif len(setVal) == 3: 2092 indicator, whichSkill, setTo = setVal 2093 if ( 2094 indicator == 'skill' 2095 and isinstance(whichSkill, base.Skill) 2096 and isinstance(setTo, base.Level) 2097 ): 2098 args[fType] = setVal 2099 return base.effect(**args) # type:ignore 2100 else: 2101 raise ParseError( 2102 f"Invalid '{fType}' effect grouped args (not a" 2103 f" skill):\n{groupedArgs}" 2104 ) 2105 else: 2106 raise ParseError( 2107 f"Invalid '{fType}' effect grouped args (wrong" 2108 f" length tuple):\n{groupedArgs}" 2109 ) 2110 2111 elif fType == "toggle": 2112 if len(groupedArgs) == 0: 2113 raise ParseError( 2114 f"'{fType}' effect must have at least one grouped" 2115 f" argument. Got:\n{groupedArgs}" 2116 ) 2117 if ( 2118 isinstance(groupedArgs[0], tuple) 2119 and len(groupedArgs[0]) == 2 2120 and isinstance(groupedArgs[0][0], base.MechanismSpecifier) 2121 and isinstance(groupedArgs[0][1], base.MechanismState) 2122 and all( 2123 isinstance(a, base.MechanismState) 2124 for a in groupedArgs[1:] 2125 ) 2126 ): # a mechanism toggle 2127 args[fType] = ( 2128 groupedArgs[0][0], 2129 cast( 2130 List[base.MechanismState], 2131 [groupedArgs[0][1]] + groupedArgs[1:] 2132 ) 2133 ) 2134 return base.effect(**args) # type:ignore 2135 elif all(isinstance(a, base.Capability) for a in groupedArgs): 2136 # a capability toggle 2137 args[fType] = cast(List[base.Capability], groupedArgs) 2138 return base.effect(**args) # type:ignore 2139 else: 2140 raise ParseError( 2141 f"Invalid arguments for '{fType}' effect. Got:" 2142 f"\n{groupedArgs}" 2143 ) 2144 2145 elif fType in ("bounce", "deactivate"): 2146 if len(groupedArgs) != 0: 2147 raise ParseError( 2148 f"'{fType}' effect may not include any" 2149 f" arguments. Got {len(groupedArgs)}):" 2150 f"\n{groupedArgs}" 2151 ) 2152 args[fType] = True 2153 return base.effect(**args) # type:ignore 2154 2155 elif fType == "follow": 2156 if len(groupedArgs) != 1: 2157 raise ParseError( 2158 f"'{fType}' effect must include exactly one" 2159 f" argument. Got {len(groupedArgs)}):" 2160 f"\n{groupedArgs}" 2161 ) 2162 2163 transition = groupedArgs[0] 2164 if not isinstance(transition, base.Transition): 2165 raise ParseError( 2166 f"Invalid argument for '{fType}' effect. Needed a" 2167 f" transition but got:\n{groupedArgs}" 2168 ) 2169 args[fType] = transition 2170 return base.effect(**args) # type:ignore 2171 2172 elif fType == "edit": 2173 if len(groupedArgs) == 0: 2174 raise ParseError( 2175 "An 'edit' effect requires at least one argument." 2176 ) 2177 for i, arg in enumerate(groupedArgs): 2178 if not isinstance(arg, list): 2179 raise ParseError( 2180 f"'edit' effect argument {i} is not a sub-list:" 2181 f"\n {arg!r}" 2182 f"\nAmong arguments:" 2183 f"\n {groupedArgs}" 2184 ) 2185 for j, cmd in enumerate(arg): 2186 if not isinstance(cmd, tuple): 2187 raise ParseError( 2188 f"'edit' effect argument {i} contains" 2189 f" non-tuple part {j}:" 2190 f"\n {cmd!r}" 2191 f"\nAmong arguments:" 2192 f"\n {groupedArgs}" 2193 ) 2194 2195 args[fType] = groupedArgs # type:ignore 2196 return base.effect(**args) # type:ignore 2197 2198 elif fType == "goto": 2199 if len(groupedArgs) not in (1, 2): 2200 raise ParseError( 2201 f"A 'goto' effect must include either one or two" 2202 f" grouped arguments. Got {len(groupedArgs)}:" 2203 f"\n{groupedArgs}" 2204 ) 2205 2206 first = groupedArgs[0] 2207 if not isinstance( 2208 first, 2209 (base.DecisionName, base.DecisionSpecifier) 2210 ): 2211 raise ParseError( 2212 f"'{fType}' effect must first specify a destination" 2213 f" decision. Got:\n{groupedArgs}" 2214 ) 2215 2216 # Check if it's really a decision ID 2217 dSpec: base.AnyDecisionSpecifier 2218 if isinstance(first, base.DecisionName): 2219 try: 2220 dSpec = int(first) 2221 except ValueError: 2222 dSpec = first 2223 else: 2224 dSpec = first 2225 2226 if len(groupedArgs) == 2: 2227 second = groupedArgs[1] 2228 if not isinstance(second, base.FocalPointName): 2229 raise ParseError( 2230 f"'{fType}' effect must have a focal point name" 2231 f" if it has a second part. Got:\n{groupedArgs}" 2232 ) 2233 args[fType] = (dSpec, second) 2234 else: 2235 args[fType] = dSpec 2236 2237 return base.effect(**args) # type:ignore 2238 2239 elif fType == "save": 2240 if len(groupedArgs) not in (0, 1): 2241 raise ParseError( 2242 f"'{fType}' effect must include exactly zero or one" 2243 f" argument(s). Got {len(groupedArgs)}):" 2244 f"\n{groupedArgs}" 2245 ) 2246 2247 if len(groupedArgs) == 1: 2248 slot = groupedArgs[0] 2249 else: 2250 slot = base.DEFAULT_SAVE_SLOT 2251 if not isinstance(slot, base.SaveSlot): 2252 raise ParseError( 2253 f"Invalid argument for '{fType}' effect. Needed a" 2254 f" save slot but got:\n{groupedArgs}" 2255 ) 2256 args[fType] = slot 2257 return base.effect(**args) # type:ignore 2258 2259 else: 2260 raise ParseError(f"Invalid effect type: '{fType}'.")
Given a region of a list of lexed tokens specifying an effect,
returns the Effect object that those tokens specify.
2262 def parseEffect(self, effectStr: str) -> base.Effect: 2263 """ 2264 Works like `parseEffectFromTokens` but starts with a raw string. 2265 For example: 2266 2267 >>> pf = ParseFormat() 2268 >>> pf.parseEffect("gain jump") == base.effect(gain='jump') 2269 True 2270 >>> pf.parseEffect("set door:open") == base.effect( 2271 ... set=( 2272 ... base.MechanismSpecifier(None, None, None, 'door'), 2273 ... 'open' 2274 ... ) 2275 ... ) 2276 True 2277 >>> pf.parseEffect("set coins*10") == base.effect(set=('coins', 10)) 2278 True 2279 >>> pf.parseEffect("set agility^3") == base.effect( 2280 ... set=('skill', 'agility', 3) 2281 ... ) 2282 True 2283 """ 2284 return self.parseEffectFromTokens(self.lex(effectStr))
Works like parseEffectFromTokens but starts with a raw string.
For example:
>>> pf = ParseFormat()
>>> pf.parseEffect("gain jump") == base.effect(gain='jump')
True
>>> pf.parseEffect("set door:open") == base.effect(
... set=(
... base.MechanismSpecifier(None, None, None, 'door'),
... 'open'
... )
... )
True
>>> pf.parseEffect("set coins*10") == base.effect(set=('coins', 10))
True
>>> pf.parseEffect("set agility^3") == base.effect(
... set=('skill', 'agility', 3)
... )
True
2286 def unparseEffect(self, effect: base.Effect) -> str: 2287 """ 2288 The opposite of `parseEffect`; turns an effect back into a 2289 string reprensentation. 2290 2291 For example: 2292 2293 >>> pf = ParseFormat() 2294 >>> e = { 2295 ... "type": "gain", 2296 ... "applyTo": "active", 2297 ... "value": "flight", 2298 ... "delay": None, 2299 ... "charges": None, 2300 ... "hidden": False 2301 ... } 2302 >>> pf.unparseEffect(e) 2303 'gain flight' 2304 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2305 True 2306 >>> s = 'gain flight' 2307 >>> pf.unparseEffect(pf.parseEffect(s)) == s 2308 True 2309 >>> s2 = ' gain\\nflight' 2310 >>> pf.unparseEffect(pf.parseEffect(s2)) == s 2311 True 2312 >>> e = { 2313 ... "type": "gain", 2314 ... "applyTo": "active", 2315 ... "value": ("gold", 5), 2316 ... "delay": 1, 2317 ... "charges": 2, 2318 ... "hidden": False 2319 ... } 2320 >>> pf.unparseEffect(e) 2321 'gain gold*5 ,1 =2' 2322 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2323 True 2324 >>> e = { 2325 ... "type": "set", 2326 ... "applyTo": "active", 2327 ... "value": ( 2328 ... base.MechanismSpecifier(None, None, None, "gears"), 2329 ... "on" 2330 ... ), 2331 ... "delay": None, 2332 ... "charges": 1, 2333 ... "hidden": False 2334 ... } 2335 >>> pf.unparseEffect(e) 2336 'set gears:on =1' 2337 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2338 True 2339 >>> e = { 2340 ... "type": "toggle", 2341 ... "applyTo": "active", 2342 ... "value": ["red", "blue"], 2343 ... "delay": None, 2344 ... "charges": None, 2345 ... "hidden": False 2346 ... } 2347 >>> pf.unparseEffect(e) 2348 'toggle red blue' 2349 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2350 True 2351 >>> e = { 2352 ... "type": "toggle", 2353 ... "applyTo": "active", 2354 ... "value": ( 2355 ... base.MechanismSpecifier(None, None, None, "switch"), 2356 ... ["on", "off"] 2357 ... ), 2358 ... "delay": None, 2359 ... "charges": None, 2360 ... "hidden": False 2361 ... } 2362 >>> pf.unparseEffect(e) 2363 'toggle switch:on off' 2364 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2365 True 2366 >>> e = { 2367 ... "type": "deactivate", 2368 ... "applyTo": "active", 2369 ... "value": None, 2370 ... "delay": 2, 2371 ... "charges": None, 2372 ... "hidden": False 2373 ... } 2374 >>> pf.unparseEffect(e) 2375 'deactivate ,2' 2376 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2377 True 2378 >>> e = { 2379 ... "type": "goto", 2380 ... "applyTo": "common", 2381 ... "value": 3, 2382 ... "delay": None, 2383 ... "charges": None, 2384 ... "hidden": False 2385 ... } 2386 >>> pf.unparseEffect(e) 2387 'goto 3 +c' 2388 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2389 True 2390 >>> e = { 2391 ... "type": "goto", 2392 ... "applyTo": "common", 2393 ... "value": 3, 2394 ... "delay": None, 2395 ... "charges": None, 2396 ... "hidden": True 2397 ... } 2398 >>> pf.unparseEffect(e) 2399 'goto 3 +c +h' 2400 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2401 True 2402 >>> e = { 2403 ... "type": "goto", 2404 ... "applyTo": "active", 2405 ... "value": 'home', 2406 ... "delay": None, 2407 ... "charges": None, 2408 ... "hidden": False 2409 ... } 2410 >>> pf.unparseEffect(e) 2411 'goto home' 2412 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2413 True 2414 >>> e = base.effect(edit=[ 2415 ... [ 2416 ... commands.command('val', '5'), 2417 ... commands.command('empty', 'list'), 2418 ... commands.command('append', '$_') 2419 ... ], 2420 ... [ 2421 ... commands.command('val', '11'), 2422 ... commands.command('assign', 'var', '$_'), 2423 ... commands.command('op', '+', '$var', '$var') 2424 ... ], 2425 ... ]) 2426 >>> pf.unparseEffect(e) 2427 'edit {\\n val 5;\\n empty list;\\n append $_;\\n}\ 2428 {\\n val 11;\\n assign var $_;\\n op + $var $var;\\n}' 2429 >>> pf.parseEffect(pf.unparseEffect(e)) == e 2430 True 2431 >>> e = base.effect(set=('coins', 3)) 2432 >>> pf.unparseEffect(e) 2433 'set coins*3' 2434 >>> e = base.effect(set=('skill', 'mashing', 3)) 2435 >>> pf.unparseEffect(e) 2436 'set mashing^3' 2437 """ 2438 result: List[str] = [] 2439 2440 # Reverse the effect type into a marker 2441 eType = effect['type'] 2442 for key, val in self.effectNames.items(): 2443 if val == eType: 2444 if len(result) != 0: 2445 raise ParseError( 2446 f"Effect map contains multiple matching entries" 2447 f"for effect type '{effect['type']}':" 2448 f" '{result[0]}' and '{key}'" 2449 ) 2450 result.append(key) 2451 # Don't break 'cause we'd like to check uniqueness 2452 2453 eVal = effect['value'] 2454 if eType in ('gain', 'lose'): 2455 eVal = cast(Union[base.Capability, Tuple[base.Token, int]], eVal) 2456 if isinstance(eVal, str): # a capability 2457 result.append(eVal) 2458 else: # a token 2459 result.append( 2460 eVal[0] 2461 + self.formatDict[Lexeme.tokenCount] 2462 + str(eVal[1]) 2463 ) 2464 elif eType == 'set': 2465 eVal = cast( 2466 # TODO: Add skill level setting here & elsewhere 2467 Union[ 2468 Tuple[base.Token, base.TokenCount], 2469 Tuple[base.MechanismSpecifier, base.MechanismState] 2470 ], 2471 eVal 2472 ) 2473 if len(eVal) not in (2, 3): 2474 raise ValueError( 2475 f"'set' effect has value with length other than 2" 2476 f" or 3:\n {repr(effect)}" 2477 ) 2478 if len(eVal) == 3: 2479 if eVal[0] != "skill": 2480 raise ValueError( 2481 f"'set' effect with length-3 value doesn't" 2482 f" start with string 'skill':\n {repr(effect)}" 2483 ) 2484 result.append( 2485 eVal[1] 2486 + self.formatDict[Lexeme.skillLevel] 2487 + str(eVal[2]) 2488 ) 2489 elif isinstance(eVal[1], int): # a token count 2490 result.append( 2491 eVal[0] 2492 + self.formatDict[Lexeme.tokenCount] 2493 + str(eVal[1]) 2494 ) 2495 else: # a mechanism 2496 if isinstance(eVal[0], base.MechanismSpecifier): 2497 mSpec = self.unparseMechanismSpecifier(eVal[0]) 2498 elif isinstance(eVal[0], base.MechanismID): 2499 # TODO: Specify mechanism by decision name + 2500 # mechanism name? Would require threading through a 2501 # DecisionGraph and using mechanismDetails 2502 mSpec = "" + eVal[0] 2503 else: 2504 assert isinstance(eVal[0], base.MechanismName) 2505 mSpec = eVal[0] 2506 result.append( 2507 mSpec 2508 + self.formatDict[Lexeme.mechanismSeparator] 2509 + eVal[1] 2510 ) 2511 elif eType == 'toggle': 2512 if isinstance(eVal, tuple): # mechanism states 2513 tSpec, states = cast( 2514 Tuple[ 2515 base.AnyMechanismSpecifier, 2516 List[base.MechanismState] 2517 ], 2518 eVal 2519 ) 2520 firstState = states[0] 2521 restStates = states[1:] 2522 if isinstance(tSpec, base.MechanismSpecifier): 2523 mStr = self.unparseMechanismSpecifier(tSpec) 2524 else: 2525 # Could be ID or name 2526 mStr = str(tSpec) 2527 result.append( 2528 mStr 2529 + self.formatDict[Lexeme.mechanismSeparator] 2530 + firstState 2531 ) 2532 result.extend(restStates) 2533 else: # capabilities 2534 assert isinstance(eVal, list) 2535 eVal = cast(List[base.Capability], eVal) 2536 result.extend(eVal) 2537 elif eType in ('deactivate', 'bounce'): 2538 if eVal is not None: 2539 raise ValueError( 2540 f"'{eType}' effect has non-None value:" 2541 f"\n {repr(effect)}" 2542 ) 2543 elif eType == 'follow': 2544 eVal = cast(base.Token, eVal) 2545 result.append(eVal) 2546 elif eType == 'edit': 2547 eVal = cast(List[List[commands.Command]], eVal) 2548 if len(eVal) == 0: 2549 result[-1] = '{}' 2550 else: 2551 for cmdList in eVal: 2552 result.append( 2553 self.unparseCommandList(cmdList) 2554 ) 2555 elif eType == 'goto': 2556 if ( 2557 isinstance(eVal, tuple) 2558 and len(eVal) == 2 2559 and isinstance(eVal[1], base.FocalPointName) 2560 ): 2561 result.append( 2562 self.unparseAnyDecision( 2563 cast(base.AnyDecisionSpecifier, eVal[0]) 2564 ) 2565 ) 2566 result.append(eVal[1]) 2567 else: 2568 assert isinstance( 2569 eVal, 2570 (base.DecisionID, base.DecisionSpecifier, str) 2571 ) 2572 result.append(self.unparseAnyDecision(eVal)) 2573 elif eType == 'save': 2574 # It's just a string naming the save slot 2575 eVal = cast(str, eVal) 2576 result.append(eVal) 2577 else: 2578 raise ValueError( 2579 f"Unrecognized effect type '{eType}' in effect:" 2580 f"\n {repr(effect)}" 2581 ) 2582 2583 # Add modifier strings 2584 if effect['applyTo'] == 'common': 2585 result.append(self.formatDict[Lexeme.inCommon]) 2586 2587 if effect['hidden']: 2588 result.append(self.formatDict[Lexeme.isHidden]) 2589 2590 dVal = effect['delay'] 2591 if dVal is not None: 2592 result.append( 2593 self.formatDict[Lexeme.sepOrDelay] + str(dVal) 2594 ) 2595 2596 cVal = effect['charges'] 2597 if cVal is not None: 2598 result.append( 2599 self.formatDict[Lexeme.effectCharges] + str(cVal) 2600 ) 2601 2602 joined = '' 2603 before = False 2604 for r in result: 2605 if ( 2606 r.startswith(' ') 2607 or r.startswith('\n') 2608 or r.endswith(' ') 2609 or r.endswith('\n') 2610 ): 2611 joined += r 2612 before = False 2613 else: 2614 joined += (' ' if before else '') + r 2615 before = True 2616 return joined
The opposite of parseEffect; turns an effect back into a
string reprensentation.
For example:
>>> pf = ParseFormat()
>>> e = {
... "type": "gain",
... "applyTo": "active",
... "value": "flight",
... "delay": None,
... "charges": None,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'gain flight'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> s = 'gain flight'
>>> pf.unparseEffect(pf.parseEffect(s)) == s
True
>>> s2 = ' gain\nflight'
>>> pf.unparseEffect(pf.parseEffect(s2)) == s
True
>>> e = {
... "type": "gain",
... "applyTo": "active",
... "value": ("gold", 5),
... "delay": 1,
... "charges": 2,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'gain gold*5 ,1 =2'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = {
... "type": "set",
... "applyTo": "active",
... "value": (
... base.MechanismSpecifier(None, None, None, "gears"),
... "on"
... ),
... "delay": None,
... "charges": 1,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'set gears:on =1'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = {
... "type": "toggle",
... "applyTo": "active",
... "value": ["red", "blue"],
... "delay": None,
... "charges": None,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'toggle red blue'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = {
... "type": "toggle",
... "applyTo": "active",
... "value": (
... base.MechanismSpecifier(None, None, None, "switch"),
... ["on", "off"]
... ),
... "delay": None,
... "charges": None,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'toggle switch:on off'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = {
... "type": "deactivate",
... "applyTo": "active",
... "value": None,
... "delay": 2,
... "charges": None,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'deactivate ,2'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = {
... "type": "goto",
... "applyTo": "common",
... "value": 3,
... "delay": None,
... "charges": None,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'goto 3 +c'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = {
... "type": "goto",
... "applyTo": "common",
... "value": 3,
... "delay": None,
... "charges": None,
... "hidden": True
... }
>>> pf.unparseEffect(e)
'goto 3 +c +h'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = {
... "type": "goto",
... "applyTo": "active",
... "value": 'home',
... "delay": None,
... "charges": None,
... "hidden": False
... }
>>> pf.unparseEffect(e)
'goto home'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = base.effect(edit=[
... [
... commands.command('val', '5'),
... commands.command('empty', 'list'),
... commands.command('append', '$_')
... ],
... [
... commands.command('val', '11'),
... commands.command('assign', 'var', '$_'),
... commands.command('op', '+', '$var', '$var')
... ],
... ])
>>> pf.unparseEffect(e)
'edit {\n val 5;\n empty list;\n append $_;\n} {\n val 11;\n assign var $_;\n op + $var $var;\n}'
>>> pf.parseEffect(pf.unparseEffect(e)) == e
True
>>> e = base.effect(set=('coins', 3))
>>> pf.unparseEffect(e)
'set coins*3'
>>> e = base.effect(set=('skill', 'mashing', 3))
>>> pf.unparseEffect(e)
'set mashing^3'
2618 def parseDecisionSpecifierFromTokens( 2619 self, 2620 tokens: LexedTokens, 2621 start: int = 0 2622 ) -> Tuple[Union[base.DecisionSpecifier, int], int]: 2623 """ 2624 Parses a decision specifier starting at the specified position 2625 in the given tokens list. No ending position is specified, but 2626 instead this function returns a tuple containing the parsed 2627 `base.DecisionSpecifier` along with an index in the tokens list 2628 where the end of the specifier was found. 2629 2630 For example: 2631 2632 >>> pf = ParseFormat() 2633 >>> pf.parseDecisionSpecifierFromTokens(['m']) 2634 (DecisionSpecifier(domain=None, zone=None, name='m'), 0) 2635 >>> pf.parseDecisionSpecifierFromTokens(['12']) # ID specifier 2636 (12, 0) 2637 >>> pf.parseDecisionSpecifierFromTokens(['a', 'm']) 2638 (DecisionSpecifier(domain=None, zone=None, name='a'), 0) 2639 >>> pf.parseDecisionSpecifierFromTokens(['a', 'm'], 1) 2640 (DecisionSpecifier(domain=None, zone=None, name='m'), 1) 2641 >>> pf.parseDecisionSpecifierFromTokens( 2642 ... ['a', Lexeme.domainSeparator, 'm'] 2643 ... ) 2644 (DecisionSpecifier(domain='a', zone=None, name='m'), 2) 2645 >>> pf.parseDecisionSpecifierFromTokens( 2646 ... ['a', Lexeme.zoneSeparator, 'm'] 2647 ... ) 2648 (DecisionSpecifier(domain=None, zone='a', name='m'), 2) 2649 >>> pf.parseDecisionSpecifierFromTokens( 2650 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm'] 2651 ... ) 2652 (DecisionSpecifier(domain=None, zone='a', name='b'), 2) 2653 >>> pf.parseDecisionSpecifierFromTokens( 2654 ... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm'] 2655 ... ) 2656 (DecisionSpecifier(domain='a', zone='b', name='m'), 4) 2657 >>> pf.parseDecisionSpecifierFromTokens( 2658 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'] 2659 ... ) 2660 (DecisionSpecifier(domain=None, zone='a', name='b'), 2) 2661 >>> pf.parseDecisionSpecifierFromTokens( # ID-style name w/ zone 2662 ... ['a', Lexeme.zoneSeparator, '5'], 2663 ... ) 2664 Traceback (most recent call last): 2665 ... 2666 exploration.base.InvalidDecisionSpecifierError... 2667 >>> pf.parseDecisionSpecifierFromTokens( 2668 ... ['d', Lexeme.domainSeparator, '123'] 2669 ... ) 2670 Traceback (most recent call last): 2671 ... 2672 exploration.base.InvalidDecisionSpecifierError... 2673 >>> pf.parseDecisionSpecifierFromTokens( 2674 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 2675 ... 1 2676 ... ) 2677 Traceback (most recent call last): 2678 ... 2679 exploration.parsing.ParseError... 2680 >>> pf.parseDecisionSpecifierFromTokens( 2681 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 2682 ... 2 2683 ... ) 2684 (DecisionSpecifier(domain='b', zone=None, name='m'), 4) 2685 >>> pf.parseDecisionSpecifierFromTokens( 2686 ... [ 2687 ... 'a', 2688 ... Lexeme.domainSeparator, 2689 ... 'b', 2690 ... Lexeme.zoneSeparator, 2691 ... 'c', 2692 ... Lexeme.zoneSeparator, 2693 ... 'm' 2694 ... ] 2695 ... ) 2696 (DecisionSpecifier(domain='a', zone='b', name='c'), 4) 2697 >>> pf.parseDecisionSpecifierFromTokens( 2698 ... [ 2699 ... 'a', 2700 ... Lexeme.domainSeparator, 2701 ... 'b', 2702 ... Lexeme.zoneSeparator, 2703 ... 'c', 2704 ... Lexeme.zoneSeparator, 2705 ... 'm' 2706 ... ], 2707 ... 2 2708 ... ) 2709 (DecisionSpecifier(domain=None, zone='b', name='c'), 4) 2710 >>> pf.parseDecisionSpecifierFromTokens( 2711 ... [ 2712 ... 'a', 2713 ... Lexeme.domainSeparator, 2714 ... 'b', 2715 ... Lexeme.zoneSeparator, 2716 ... 'c', 2717 ... Lexeme.zoneSeparator, 2718 ... 'm' 2719 ... ], 2720 ... 4 2721 ... ) 2722 (DecisionSpecifier(domain=None, zone='c', name='m'), 6) 2723 >>> pf.parseDecisionSpecifierFromTokens( 2724 ... [ 2725 ... 'set', 2726 ... 'main', 2727 ... Lexeme.domainSeparator, 2728 ... 'zone', 2729 ... Lexeme.zoneSeparator, 2730 ... 'compass', 2731 ... 'north', 2732 ... 'bounce', 2733 ... ], 2734 ... 1 2735 ... ) 2736 (DecisionSpecifier(domain='main', zone='zone', name='compass'), 5) 2737 """ 2738 # Check bounds & normalize start index 2739 nTokens = len(tokens) 2740 if start < -nTokens: 2741 raise IndexError( 2742 f"Invalid start index {start} for {nTokens} tokens (too" 2743 f" negative)." 2744 ) 2745 elif start >= nTokens: 2746 raise IndexError( 2747 f"Invalid start index {start} for {nTokens} tokens (too" 2748 f" big)." 2749 ) 2750 elif start < 0: 2751 start = nTokens + start 2752 2753 assert (start < nTokens) 2754 2755 first = tokens[start] 2756 if not isinstance(first, str): 2757 raise ParseError( 2758 f"Invalid domain specifier (must start with a name or" 2759 f" id; got: {first} = {self.formatDict[first]})." 2760 ) 2761 2762 ds = base.DecisionSpecifier(None, None, first) 2763 result = (base.idOrDecisionSpecifier(ds), start) 2764 2765 domain = None 2766 zoneOrDecision = None 2767 2768 if start + 1 >= nTokens: # at end of tokens 2769 return result 2770 2771 firstSep = tokens[start + 1] 2772 if firstSep == Lexeme.domainSeparator: 2773 domain = first 2774 elif firstSep == Lexeme.zoneSeparator: 2775 zoneOrDecision = first 2776 else: 2777 return result 2778 2779 if start + 2 >= nTokens: 2780 return result 2781 2782 second = tokens[start + 2] 2783 if isinstance(second, Lexeme): 2784 return result 2785 2786 ds = base.DecisionSpecifier(domain, zoneOrDecision, second) 2787 result = (base.idOrDecisionSpecifier(ds), start + 2) 2788 2789 if start + 3 >= nTokens: 2790 return result 2791 2792 secondSep = tokens[start + 3] 2793 if start + 4 >= nTokens: 2794 return result 2795 2796 third = tokens[start + 4] 2797 if secondSep == Lexeme.zoneSeparator: 2798 if zoneOrDecision is not None: # two in a row 2799 return result 2800 else: 2801 if not isinstance(third, base.DecisionName): 2802 return result 2803 else: 2804 zoneOrDecision = second 2805 else: 2806 return result 2807 2808 if isinstance(third, Lexeme): 2809 return result 2810 2811 ds = base.DecisionSpecifier(domain, zoneOrDecision, third) 2812 return (base.idOrDecisionSpecifier(ds), start + 4)
Parses a decision specifier starting at the specified position
in the given tokens list. No ending position is specified, but
instead this function returns a tuple containing the parsed
base.DecisionSpecifier along with an index in the tokens list
where the end of the specifier was found.
For example:
>>> pf = ParseFormat()
>>> pf.parseDecisionSpecifierFromTokens(['m'])
(DecisionSpecifier(domain=None, zone=None, name='m'), 0)
>>> pf.parseDecisionSpecifierFromTokens(['12']) # ID specifier
(12, 0)
>>> pf.parseDecisionSpecifierFromTokens(['a', 'm'])
(DecisionSpecifier(domain=None, zone=None, name='a'), 0)
>>> pf.parseDecisionSpecifierFromTokens(['a', 'm'], 1)
(DecisionSpecifier(domain=None, zone=None, name='m'), 1)
>>> pf.parseDecisionSpecifierFromTokens(
... ['a', Lexeme.domainSeparator, 'm']
... )
(DecisionSpecifier(domain='a', zone=None, name='m'), 2)
>>> pf.parseDecisionSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'm']
... )
(DecisionSpecifier(domain=None, zone='a', name='m'), 2)
>>> pf.parseDecisionSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm']
... )
(DecisionSpecifier(domain=None, zone='a', name='b'), 2)
>>> pf.parseDecisionSpecifierFromTokens(
... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm']
... )
(DecisionSpecifier(domain='a', zone='b', name='m'), 4)
>>> pf.parseDecisionSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm']
... )
(DecisionSpecifier(domain=None, zone='a', name='b'), 2)
>>> pf.parseDecisionSpecifierFromTokens( # ID-style name w/ zone
... ['a', Lexeme.zoneSeparator, '5'],
... )
Traceback (most recent call last):
...
exploration.base.InvalidDecisionSpecifierError...
>>> pf.parseDecisionSpecifierFromTokens(
... ['d', Lexeme.domainSeparator, '123']
... )
Traceback (most recent call last):
...
exploration.base.InvalidDecisionSpecifierError...
>>> pf.parseDecisionSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'],
... 1
... )
Traceback (most recent call last):
...
ParseError...
>>> pf.parseDecisionSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'],
... 2
... )
(DecisionSpecifier(domain='b', zone=None, name='m'), 4)
>>> pf.parseDecisionSpecifierFromTokens(
... [
... 'a',
... Lexeme.domainSeparator,
... 'b',
... Lexeme.zoneSeparator,
... 'c',
... Lexeme.zoneSeparator,
... 'm'
... ]
... )
(DecisionSpecifier(domain='a', zone='b', name='c'), 4)
>>> pf.parseDecisionSpecifierFromTokens(
... [
... 'a',
... Lexeme.domainSeparator,
... 'b',
... Lexeme.zoneSeparator,
... 'c',
... Lexeme.zoneSeparator,
... 'm'
... ],
... 2
... )
(DecisionSpecifier(domain=None, zone='b', name='c'), 4)
>>> pf.parseDecisionSpecifierFromTokens(
... [
... 'a',
... Lexeme.domainSeparator,
... 'b',
... Lexeme.zoneSeparator,
... 'c',
... Lexeme.zoneSeparator,
... 'm'
... ],
... 4
... )
(DecisionSpecifier(domain=None, zone='c', name='m'), 6)
>>> pf.parseDecisionSpecifierFromTokens(
... [
... 'set',
... 'main',
... Lexeme.domainSeparator,
... 'zone',
... Lexeme.zoneSeparator,
... 'compass',
... 'north',
... 'bounce',
... ],
... 1
... )
(DecisionSpecifier(domain='main', zone='zone', name='compass'), 5)
2814 def parseDecisionSpecifier( 2815 self, 2816 specString: str 2817 ) -> Union[base.DecisionID, base.DecisionSpecifier]: 2818 """ 2819 Parses a full `DecisionSpecifier` from a single string. Can 2820 parse integer decision IDs in string form, and returns a 2821 `DecisionID` in that case, otherwise returns a 2822 `DecisionSpecifier`. Assumes that all int-convertible strings 2823 are decision IDs, so it cannot deal with feature names which are 2824 just numbers. 2825 2826 For example: 2827 2828 >>> pf = ParseFormat() 2829 >>> pf.parseDecisionSpecifier('example') 2830 DecisionSpecifier(domain=None, zone=None, name='example') 2831 >>> pf.parseDecisionSpecifier('outer::example') 2832 DecisionSpecifier(domain=None, zone='outer', name='example') 2833 >>> pf.parseDecisionSpecifier('domain//region::feature') 2834 DecisionSpecifier(domain='domain', zone='region', name='feature') 2835 >>> pf.parseDecisionSpecifier('123') 2836 123 2837 >>> pf.parseDecisionSpecifier('region::domain//feature') 2838 Traceback (most recent call last): 2839 ... 2840 exploration.base.InvalidDecisionSpecifierError... 2841 >>> pf.parseDecisionSpecifier('domain1//domain2//feature') 2842 Traceback (most recent call last): 2843 ... 2844 exploration.base.InvalidDecisionSpecifierError... 2845 >>> pf.parseDecisionSpecifier('domain//123') 2846 Traceback (most recent call last): 2847 ... 2848 exploration.base.InvalidDecisionSpecifierError... 2849 >>> pf.parseDecisionSpecifier('region::123') 2850 Traceback (most recent call last): 2851 ... 2852 exploration.base.InvalidDecisionSpecifierError... 2853 """ 2854 try: 2855 return int(specString) 2856 except ValueError: 2857 tokens = self.lex(specString) 2858 result, end = self.parseDecisionSpecifierFromTokens(tokens) 2859 if end != len(tokens) - 1: 2860 raise base.InvalidDecisionSpecifierError( 2861 f"Junk after end of decision specifier:" 2862 f"\n{tokens[end + 1:]}" 2863 ) 2864 return result
Parses a full DecisionSpecifier from a single string. Can
parse integer decision IDs in string form, and returns a
DecisionID in that case, otherwise returns a
DecisionSpecifier. Assumes that all int-convertible strings
are decision IDs, so it cannot deal with feature names which are
just numbers.
For example:
>>> pf = ParseFormat()
>>> pf.parseDecisionSpecifier('example')
DecisionSpecifier(domain=None, zone=None, name='example')
>>> pf.parseDecisionSpecifier('outer::example')
DecisionSpecifier(domain=None, zone='outer', name='example')
>>> pf.parseDecisionSpecifier('domain//region::feature')
DecisionSpecifier(domain='domain', zone='region', name='feature')
>>> pf.parseDecisionSpecifier('123')
123
>>> pf.parseDecisionSpecifier('region::domain//feature')
Traceback (most recent call last):
...
exploration.base.InvalidDecisionSpecifierError...
>>> pf.parseDecisionSpecifier('domain1//domain2//feature')
Traceback (most recent call last):
...
exploration.base.InvalidDecisionSpecifierError...
>>> pf.parseDecisionSpecifier('domain//123')
Traceback (most recent call last):
...
exploration.base.InvalidDecisionSpecifierError...
>>> pf.parseDecisionSpecifier('region::123')
Traceback (most recent call last):
...
exploration.base.InvalidDecisionSpecifierError...
2866 def parseFeatureSpecifierFromTokens( 2867 self, 2868 tokens: LexedTokens, 2869 start: int = 0, 2870 limit: int = -1 2871 ) -> Tuple[base.FeatureSpecifier, int]: 2872 """ 2873 Parses a `FeatureSpecifier` starting from the specified part of 2874 a tokens list. Returns a tuple containing the feature specifier 2875 and the end position of the end of the feature specifier. 2876 2877 Can parse integer feature IDs in string form, as well as nested 2878 feature specifiers and plain feature specifiers. Assumes that 2879 all int-convertible strings are feature IDs, so it cannot deal 2880 with feature names which are just numbers. 2881 2882 For example: 2883 2884 >>> pf = ParseFormat() 2885 >>> pf.parseFeatureSpecifierFromTokens(['example']) 2886 (FeatureSpecifier(domain=None, within=[], feature='example',\ 2887 part=None), 0) 2888 >>> pf.parseFeatureSpecifierFromTokens(['example1', 'example2'], 1) 2889 (FeatureSpecifier(domain=None, within=[], feature='example2',\ 2890 part=None), 1) 2891 >>> pf.parseFeatureSpecifierFromTokens( 2892 ... [ 2893 ... 'domain', 2894 ... Lexeme.domainSeparator, 2895 ... 'region', 2896 ... Lexeme.zoneSeparator, 2897 ... 'feature', 2898 ... Lexeme.partSeparator, 2899 ... 'part' 2900 ... ] 2901 ... ) 2902 (FeatureSpecifier(domain='domain', within=['region'],\ 2903 feature='feature', part='part'), 6) 2904 >>> pf.parseFeatureSpecifierFromTokens( 2905 ... [ 2906 ... 'outerRegion', 2907 ... Lexeme.zoneSeparator, 2908 ... 'midRegion', 2909 ... Lexeme.zoneSeparator, 2910 ... 'innerRegion', 2911 ... Lexeme.zoneSeparator, 2912 ... 'feature' 2913 ... ] 2914 ... ) 2915 (FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion',\ 2916 'innerRegion'], feature='feature', part=None), 6) 2917 >>> pf.parseFeatureSpecifierFromTokens( 2918 ... [ 2919 ... 'outerRegion', 2920 ... Lexeme.zoneSeparator, 2921 ... 'midRegion', 2922 ... Lexeme.zoneSeparator, 2923 ... 'innerRegion', 2924 ... Lexeme.zoneSeparator, 2925 ... 'feature' 2926 ... ], 2927 ... 1 2928 ... ) 2929 Traceback (most recent call last): 2930 ... 2931 exploration.parsing.InvalidFeatureSpecifierError... 2932 >>> pf.parseFeatureSpecifierFromTokens( 2933 ... [ 2934 ... 'outerRegion', 2935 ... Lexeme.zoneSeparator, 2936 ... 'midRegion', 2937 ... Lexeme.zoneSeparator, 2938 ... 'innerRegion', 2939 ... Lexeme.zoneSeparator, 2940 ... 'feature' 2941 ... ], 2942 ... 2 2943 ... ) 2944 (FeatureSpecifier(domain=None, within=['midRegion', 'innerRegion'],\ 2945 feature='feature', part=None), 6) 2946 >>> pf.parseFeatureSpecifierFromTokens( 2947 ... [ 2948 ... 'outerRegion', 2949 ... Lexeme.zoneSeparator, 2950 ... 'feature', 2951 ... Lexeme.domainSeparator, 2952 ... 'after', 2953 ... ] 2954 ... ) 2955 (FeatureSpecifier(domain=None, within=['outerRegion'],\ 2956 feature='feature', part=None), 2) 2957 >>> pf.parseFeatureSpecifierFromTokens( 2958 ... [ 2959 ... 'outerRegion', 2960 ... Lexeme.zoneSeparator, 2961 ... 'feature', 2962 ... Lexeme.domainSeparator, 2963 ... 'after', 2964 ... ], 2965 ... 2 2966 ... ) 2967 (FeatureSpecifier(domain='feature', within=[], feature='after',\ 2968 part=None), 4) 2969 >>> # Including a limit: 2970 >>> pf.parseFeatureSpecifierFromTokens( 2971 ... [ 2972 ... 'outerRegion', 2973 ... Lexeme.zoneSeparator, 2974 ... 'midRegion', 2975 ... Lexeme.zoneSeparator, 2976 ... 'feature', 2977 ... ], 2978 ... 0, 2979 ... 2 2980 ... ) 2981 (FeatureSpecifier(domain=None, within=['outerRegion'],\ 2982 feature='midRegion', part=None), 2) 2983 >>> pf.parseFeatureSpecifierFromTokens( 2984 ... [ 2985 ... 'outerRegion', 2986 ... Lexeme.zoneSeparator, 2987 ... 'midRegion', 2988 ... Lexeme.zoneSeparator, 2989 ... 'feature', 2990 ... ], 2991 ... 0, 2992 ... 0 2993 ... ) 2994 (FeatureSpecifier(domain=None, within=[], feature='outerRegion',\ 2995 part=None), 0) 2996 >>> pf.parseFeatureSpecifierFromTokens( 2997 ... [ 2998 ... 'region', 2999 ... Lexeme.zoneSeparator, 3000 ... Lexeme.zoneSeparator, 3001 ... 'feature', 3002 ... ] 3003 ... ) 3004 (FeatureSpecifier(domain=None, within=[], feature='region',\ 3005 part=None), 0) 3006 """ 3007 start, limit, nTokens = normalizeEnds(tokens, start, limit) 3008 3009 if nTokens == 0: 3010 raise InvalidFeatureSpecifierError( 3011 "Can't parse a feature specifier from 0 tokens." 3012 ) 3013 first = tokens[start] 3014 if isinstance(first, Lexeme): 3015 raise InvalidFeatureSpecifierError( 3016 f"Feature specifier can't begin with a special token." 3017 f"Got:\n{tokens[start:limit + 1]}" 3018 ) 3019 3020 if nTokens in (1, 2): 3021 # 2 tokens isn't enough for a second part 3022 fs = base.FeatureSpecifier( 3023 domain=None, 3024 within=[], 3025 feature=first, 3026 part=None 3027 ) 3028 return (base.normalizeFeatureSpecifier(fs), start) 3029 3030 firstSep = tokens[start + 1] 3031 secondPart = tokens[start + 2] 3032 3033 if ( 3034 firstSep not in ( 3035 Lexeme.domainSeparator, 3036 Lexeme.zoneSeparator, 3037 Lexeme.partSeparator 3038 ) 3039 or not isinstance(secondPart, str) 3040 ): 3041 # Following tokens won't work out 3042 fs = base.FeatureSpecifier( 3043 domain=None, 3044 within=[], 3045 feature=first, 3046 part=None 3047 ) 3048 return (base.normalizeFeatureSpecifier(fs), start) 3049 3050 if firstSep == Lexeme.domainSeparator: 3051 if start + 2 > limit: 3052 return ( 3053 base.FeatureSpecifier( 3054 domain=first, 3055 within=[], 3056 feature=secondPart, 3057 part=None 3058 ), 3059 start + 2 3060 ) 3061 else: 3062 rest, restEnd = self.parseFeatureSpecifierFromTokens( 3063 tokens, 3064 start + 2, 3065 limit 3066 ) 3067 if rest.domain is not None: # two domainSeparators in a row 3068 fs = base.FeatureSpecifier( 3069 domain=first, 3070 within=[], 3071 feature=rest.domain, 3072 part=None 3073 ) 3074 return (base.normalizeFeatureSpecifier(fs), start + 2) 3075 else: 3076 fs = base.FeatureSpecifier( 3077 domain=first, 3078 within=rest.within, 3079 feature=rest.feature, 3080 part=rest.part 3081 ) 3082 return (base.normalizeFeatureSpecifier(fs), restEnd) 3083 3084 elif firstSep == Lexeme.zoneSeparator: 3085 if start + 2 > limit: 3086 fs = base.FeatureSpecifier( 3087 domain=None, 3088 within=[first], 3089 feature=secondPart, 3090 part=None 3091 ) 3092 return (base.normalizeFeatureSpecifier(fs), start + 2) 3093 else: 3094 rest, restEnd = self.parseFeatureSpecifierFromTokens( 3095 tokens, 3096 start + 2, 3097 limit 3098 ) 3099 if rest.domain is not None: # domain sep after zone sep 3100 fs = base.FeatureSpecifier( 3101 domain=None, 3102 within=[first], 3103 feature=rest.domain, 3104 part=None 3105 ) 3106 return (base.normalizeFeatureSpecifier(fs), start + 2) 3107 else: 3108 within = [first] 3109 within.extend(rest.within) 3110 fs = base.FeatureSpecifier( 3111 domain=None, 3112 within=within, 3113 feature=rest.feature, 3114 part=rest.part 3115 ) 3116 return (base.normalizeFeatureSpecifier(fs), restEnd) 3117 3118 else: # must be partSeparator 3119 fs = base.FeatureSpecifier( 3120 domain=None, 3121 within=[], 3122 feature=first, 3123 part=secondPart 3124 ) 3125 return (base.normalizeFeatureSpecifier(fs), start + 2)
Parses a FeatureSpecifier starting from the specified part of
a tokens list. Returns a tuple containing the feature specifier
and the end position of the end of the feature specifier.
Can parse integer feature IDs in string form, as well as nested feature specifiers and plain feature specifiers. Assumes that all int-convertible strings are feature IDs, so it cannot deal with feature names which are just numbers.
For example:
>>> pf = ParseFormat()
>>> pf.parseFeatureSpecifierFromTokens(['example'])
(FeatureSpecifier(domain=None, within=[], feature='example', part=None), 0)
>>> pf.parseFeatureSpecifierFromTokens(['example1', 'example2'], 1)
(FeatureSpecifier(domain=None, within=[], feature='example2', part=None), 1)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'domain',
... Lexeme.domainSeparator,
... 'region',
... Lexeme.zoneSeparator,
... 'feature',
... Lexeme.partSeparator,
... 'part'
... ]
... )
(FeatureSpecifier(domain='domain', within=['region'], feature='feature', part='part'), 6)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'outerRegion',
... Lexeme.zoneSeparator,
... 'midRegion',
... Lexeme.zoneSeparator,
... 'innerRegion',
... Lexeme.zoneSeparator,
... 'feature'
... ]
... )
(FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion', 'innerRegion'], feature='feature', part=None), 6)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'outerRegion',
... Lexeme.zoneSeparator,
... 'midRegion',
... Lexeme.zoneSeparator,
... 'innerRegion',
... Lexeme.zoneSeparator,
... 'feature'
... ],
... 1
... )
Traceback (most recent call last):
...
InvalidFeatureSpecifierError...
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'outerRegion',
... Lexeme.zoneSeparator,
... 'midRegion',
... Lexeme.zoneSeparator,
... 'innerRegion',
... Lexeme.zoneSeparator,
... 'feature'
... ],
... 2
... )
(FeatureSpecifier(domain=None, within=['midRegion', 'innerRegion'], feature='feature', part=None), 6)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'outerRegion',
... Lexeme.zoneSeparator,
... 'feature',
... Lexeme.domainSeparator,
... 'after',
... ]
... )
(FeatureSpecifier(domain=None, within=['outerRegion'], feature='feature', part=None), 2)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'outerRegion',
... Lexeme.zoneSeparator,
... 'feature',
... Lexeme.domainSeparator,
... 'after',
... ],
... 2
... )
(FeatureSpecifier(domain='feature', within=[], feature='after', part=None), 4)
>>> # Including a limit:
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'outerRegion',
... Lexeme.zoneSeparator,
... 'midRegion',
... Lexeme.zoneSeparator,
... 'feature',
... ],
... 0,
... 2
... )
(FeatureSpecifier(domain=None, within=['outerRegion'], feature='midRegion', part=None), 2)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'outerRegion',
... Lexeme.zoneSeparator,
... 'midRegion',
... Lexeme.zoneSeparator,
... 'feature',
... ],
... 0,
... 0
... )
(FeatureSpecifier(domain=None, within=[], feature='outerRegion', part=None), 0)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'region',
... Lexeme.zoneSeparator,
... Lexeme.zoneSeparator,
... 'feature',
... ]
... )
(FeatureSpecifier(domain=None, within=[], feature='region', part=None), 0)
3127 def parseFeatureSpecifier(self, specString: str) -> base.FeatureSpecifier: 3128 """ 3129 Parses a full `FeatureSpecifier` from a single string. See 3130 `parseFeatureSpecifierFromTokens`. 3131 3132 >>> pf = ParseFormat() 3133 >>> pf.parseFeatureSpecifier('example') 3134 FeatureSpecifier(domain=None, within=[], feature='example', part=None) 3135 >>> pf.parseFeatureSpecifier('outer::example') 3136 FeatureSpecifier(domain=None, within=['outer'], feature='example',\ 3137 part=None) 3138 >>> pf.parseFeatureSpecifier('example%%middle') 3139 FeatureSpecifier(domain=None, within=[], feature='example',\ 3140 part='middle') 3141 >>> pf.parseFeatureSpecifier('domain//region::feature%%part') 3142 FeatureSpecifier(domain='domain', within=['region'],\ 3143 feature='feature', part='part') 3144 >>> pf.parseFeatureSpecifier( 3145 ... 'outerRegion::midRegion::innerRegion::feature' 3146 ... ) 3147 FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion',\ 3148 'innerRegion'], feature='feature', part=None) 3149 >>> pf.parseFeatureSpecifier('region::domain//feature') 3150 Traceback (most recent call last): 3151 ... 3152 exploration.parsing.InvalidFeatureSpecifierError... 3153 >>> pf.parseFeatureSpecifier('feature%%part1%%part2') 3154 Traceback (most recent call last): 3155 ... 3156 exploration.parsing.InvalidFeatureSpecifierError... 3157 >>> pf.parseFeatureSpecifier('domain1//domain2//feature') 3158 Traceback (most recent call last): 3159 ... 3160 exploration.parsing.InvalidFeatureSpecifierError... 3161 >>> # TODO: Issue warnings for these... 3162 >>> pf.parseFeatureSpecifier('domain//123') # domain discarded 3163 FeatureSpecifier(domain=None, within=[], feature=123, part=None) 3164 >>> pf.parseFeatureSpecifier('region::123') # zone discarded 3165 FeatureSpecifier(domain=None, within=[], feature=123, part=None) 3166 >>> pf.parseFeatureSpecifier('123%%part') 3167 FeatureSpecifier(domain=None, within=[], feature=123, part='part') 3168 """ 3169 tokens = self.lex(specString) 3170 result, rEnd = self.parseFeatureSpecifierFromTokens(tokens) 3171 if rEnd != len(tokens) - 1: 3172 raise InvalidFeatureSpecifierError( 3173 f"Feature specifier has extra stuff at end:" 3174 f" {tokens[rEnd + 1:]}" 3175 ) 3176 else: 3177 return result
Parses a full FeatureSpecifier from a single string. See
parseFeatureSpecifierFromTokens.
>>> pf = ParseFormat()
>>> pf.parseFeatureSpecifier('example')
FeatureSpecifier(domain=None, within=[], feature='example', part=None)
>>> pf.parseFeatureSpecifier('outer::example')
FeatureSpecifier(domain=None, within=['outer'], feature='example', part=None)
>>> pf.parseFeatureSpecifier('example%%middle')
FeatureSpecifier(domain=None, within=[], feature='example', part='middle')
>>> pf.parseFeatureSpecifier('domain//region::feature%%part')
FeatureSpecifier(domain='domain', within=['region'], feature='feature', part='part')
>>> pf.parseFeatureSpecifier(
... 'outerRegion::midRegion::innerRegion::feature'
... )
FeatureSpecifier(domain=None, within=['outerRegion', 'midRegion', 'innerRegion'], feature='feature', part=None)
>>> pf.parseFeatureSpecifier('region::domain//feature')
Traceback (most recent call last):
...
InvalidFeatureSpecifierError...
>>> pf.parseFeatureSpecifier('feature%%part1%%part2')
Traceback (most recent call last):
...
InvalidFeatureSpecifierError...
>>> pf.parseFeatureSpecifier('domain1//domain2//feature')
Traceback (most recent call last):
...
InvalidFeatureSpecifierError...
>>> # TODO: Issue warnings for these...
>>> pf.parseFeatureSpecifier('domain//123') # domain discarded
FeatureSpecifier(domain=None, within=[], feature=123, part=None)
>>> pf.parseFeatureSpecifier('region::123') # zone discarded
FeatureSpecifier(domain=None, within=[], feature=123, part=None)
>>> pf.parseFeatureSpecifier('123%%part')
FeatureSpecifier(domain=None, within=[], feature=123, part='part')
3179 def normalizeFeatureSpecifier( 3180 self, 3181 spec: base.AnyFeatureSpecifier 3182 ) -> base.FeatureSpecifier: 3183 """ 3184 Normalizes any kind of feature specifier into an official 3185 `FeatureSpecifier` tuple. 3186 3187 For example: 3188 3189 >>> pf = ParseFormat() 3190 >>> pf.normalizeFeatureSpecifier('town') 3191 FeatureSpecifier(domain=None, within=[], feature='town', part=None) 3192 >>> pf.normalizeFeatureSpecifier(5) 3193 FeatureSpecifier(domain=None, within=[], feature=5, part=None) 3194 >>> pf.parseFeatureSpecifierFromTokens( 3195 ... [ 3196 ... 'domain', 3197 ... Lexeme.domainSeparator, 3198 ... 'region', 3199 ... Lexeme.zoneSeparator, 3200 ... 'feature', 3201 ... Lexeme.partSeparator, 3202 ... 'part' 3203 ... ] 3204 ... ) 3205 (FeatureSpecifier(domain='domain', within=['region'],\ 3206 feature='feature', part='part'), 6) 3207 >>> pf.normalizeFeatureSpecifier('dom//one::two::three%%middle') 3208 FeatureSpecifier(domain='dom', within=['one', 'two'],\ 3209 feature='three', part='middle') 3210 >>> pf.normalizeFeatureSpecifier( 3211 ... base.FeatureSpecifier(None, ['region'], 'place', None) 3212 ... ) 3213 FeatureSpecifier(domain=None, within=['region'], feature='place',\ 3214 part=None) 3215 >>> fs = base.FeatureSpecifier(None, [], 'place', None) 3216 >>> ns = pf.normalizeFeatureSpecifier(fs) 3217 >>> ns is fs # Doesn't create unnecessary clones 3218 True 3219 """ 3220 if isinstance(spec, base.FeatureSpecifier): 3221 return spec 3222 elif isinstance(spec, base.FeatureID): 3223 return base.FeatureSpecifier(None, [], spec, None) 3224 elif isinstance(spec, str): 3225 return self.parseFeatureSpecifier(spec) 3226 else: 3227 raise TypeError(f"Invalid feature specifier type: '{type(spec)}'")
Normalizes any kind of feature specifier into an official
FeatureSpecifier tuple.
For example:
>>> pf = ParseFormat()
>>> pf.normalizeFeatureSpecifier('town')
FeatureSpecifier(domain=None, within=[], feature='town', part=None)
>>> pf.normalizeFeatureSpecifier(5)
FeatureSpecifier(domain=None, within=[], feature=5, part=None)
>>> pf.parseFeatureSpecifierFromTokens(
... [
... 'domain',
... Lexeme.domainSeparator,
... 'region',
... Lexeme.zoneSeparator,
... 'feature',
... Lexeme.partSeparator,
... 'part'
... ]
... )
(FeatureSpecifier(domain='domain', within=['region'], feature='feature', part='part'), 6)
>>> pf.normalizeFeatureSpecifier('dom//one::two::three%%middle')
FeatureSpecifier(domain='dom', within=['one', 'two'], feature='three', part='middle')
>>> pf.normalizeFeatureSpecifier(
... base.FeatureSpecifier(None, ['region'], 'place', None)
... )
FeatureSpecifier(domain=None, within=['region'], feature='place', part=None)
>>> fs = base.FeatureSpecifier(None, [], 'place', None)
>>> ns = pf.normalizeFeatureSpecifier(fs)
>>> ns is fs # Doesn't create unnecessary clones
True
3229 def unparseChallenge(self, challenge: base.Challenge) -> str: 3230 """ 3231 Turns a `base.Challenge` into a string that can be turned back 3232 into an equivalent challenge by `parseChallenge`. For example: 3233 3234 >>> pf = ParseFormat() 3235 >>> c = base.challenge( 3236 ... skills=base.BestSkill('brains', 'brawn'), 3237 ... level=2, 3238 ... success=[base.effect(set=('switch', 'on'))], 3239 ... failure=[ 3240 ... base.effect(deactivate=True, delay=1), 3241 ... base.effect(bounce=True) 3242 ... ], 3243 ... outcome=True 3244 ... ) 3245 >>> r = pf.unparseChallenge(c) 3246 >>> r 3247 '<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}' 3248 >>> pf.parseChallenge(r) == c 3249 True 3250 >>> c2 = base.challenge( 3251 ... skills=base.CombinedSkill( 3252 ... -2, 3253 ... base.ConditionalSkill( 3254 ... base.ReqCapability('tough'), 3255 ... base.BestSkill(1), 3256 ... base.BestSkill(-1) 3257 ... ) 3258 ... ), 3259 ... level=-2, 3260 ... success=[base.effect(gain='orb')], 3261 ... failure=[], 3262 ... outcome=None 3263 ... ) 3264 >>> r2 = pf.unparseChallenge(c2) 3265 >>> r2 3266 '<-2>sum(-2, if(tough, best(1), best(-1))){gain orb}{}' 3267 >>> # TODO: let this parse through without BestSkills... 3268 >>> pf.parseChallenge(r2) == c2 3269 True 3270 """ 3271 lt = self.formatDict[Lexeme.angleLeft] 3272 gt = self.formatDict[Lexeme.angleRight] 3273 result = ( 3274 lt + str(challenge['level']) + gt 3275 + challenge['skills'].unparse() 3276 ) 3277 if challenge['outcome'] is True: 3278 result += gt 3279 result += self.unparseConsequence(challenge['success']) 3280 if challenge['outcome'] is False: 3281 result += gt 3282 result += self.unparseConsequence(challenge['failure']) 3283 return result
Turns a base.Challenge into a string that can be turned back
into an equivalent challenge by parseChallenge. For example:
>>> pf = ParseFormat()
>>> c = base.challenge(
... skills=base.BestSkill('brains', 'brawn'),
... level=2,
... success=[base.effect(set=('switch', 'on'))],
... failure=[
... base.effect(deactivate=True, delay=1),
... base.effect(bounce=True)
... ],
... outcome=True
... )
>>> r = pf.unparseChallenge(c)
>>> r
'<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}'
>>> pf.parseChallenge(r) == c
True
>>> c2 = base.challenge(
... skills=base.CombinedSkill(
... -2,
... base.ConditionalSkill(
... base.ReqCapability('tough'),
... base.BestSkill(1),
... base.BestSkill(-1)
... )
... ),
... level=-2,
... success=[base.effect(gain='orb')],
... failure=[],
... outcome=None
... )
>>> r2 = pf.unparseChallenge(c2)
>>> r2
'<-2>sum(-2, if(tough, best(1), best(-1))){gain orb}{}'
>>> # TODO: let this parse through without BestSkills...
>>> pf.parseChallenge(r2) == c2
True
3285 def unparseCondition(self, condition: base.Condition) -> str: 3286 """ 3287 Given a `base.Condition` returns a string that would result in 3288 that condition if given to `parseCondition`. For example: 3289 3290 >>> pf = ParseFormat() 3291 >>> c = base.condition( 3292 ... condition=base.ReqAny([ 3293 ... base.ReqCapability('brawny'), 3294 ... base.ReqNot(base.ReqTokens('weights', 3)) 3295 ... ]), 3296 ... consequence=[base.effect(gain='power')] 3297 ... ) 3298 >>> r = pf.unparseCondition(c) 3299 >>> r 3300 '??((brawny|!(weights*3))){gain power}{}' 3301 >>> pf.parseCondition(r) == c 3302 True 3303 """ 3304 return ( 3305 self.formatDict[Lexeme.doubleQuestionmark] 3306 + self.formatDict[Lexeme.openParen] 3307 + condition['condition'].unparse() 3308 + self.formatDict[Lexeme.closeParen] 3309 + self.unparseConsequence(condition['consequence']) 3310 + self.unparseConsequence(condition['alternative']) 3311 )
Given a base.Condition returns a string that would result in
that condition if given to parseCondition. For example:
>>> pf = ParseFormat()
>>> c = base.condition(
... condition=base.ReqAny([
... base.ReqCapability('brawny'),
... base.ReqNot(base.ReqTokens('weights', 3))
... ]),
... consequence=[base.effect(gain='power')]
... )
>>> r = pf.unparseCondition(c)
>>> r
'??((brawny|!(weights*3))){gain power}{}'
>>> pf.parseCondition(r) == c
True
3313 def unparseConsequence(self, consequence: base.Consequence) -> str: 3314 """ 3315 Given a `base.Consequence`, returns a string encoding of it, 3316 using the same format that `parseConsequence` will parse. Uses 3317 function-call-like syntax and curly braces to denote different 3318 sub-consequences. See also `SkillCombination.unparse` and 3319 `Requirement.unparse` For example: 3320 3321 >>> pf = ParseFormat() 3322 >>> c = [base.effect(gain='one'), base.effect(lose='one')] 3323 >>> pf.unparseConsequence(c) 3324 '{gain one; lose one}' 3325 >>> c = [ 3326 ... base.challenge( 3327 ... skills=base.BestSkill('brains', 'brawn'), 3328 ... level=2, 3329 ... success=[base.effect(set=('switch', 'on'))], 3330 ... failure=[ 3331 ... base.effect(deactivate=True, delay=1), 3332 ... base.effect(bounce=True) 3333 ... ], 3334 ... outcome=True 3335 ... ) 3336 ... ] 3337 >>> pf.unparseConsequence(c) 3338 '{<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}}' 3339 >>> c[0]['outcome'] = False 3340 >>> pf.unparseConsequence(c) 3341 '{<2>best(brains, brawn){set switch:on}>{deactivate ,1; bounce}}' 3342 >>> c[0]['outcome'] = None 3343 >>> pf.unparseConsequence(c) 3344 '{<2>best(brains, brawn){set switch:on}{deactivate ,1; bounce}}' 3345 >>> c = [ 3346 ... base.condition( 3347 ... condition=base.ReqAny([ 3348 ... base.ReqCapability('brawny'), 3349 ... base.ReqNot(base.ReqTokens('weights', 3)) 3350 ... ]), 3351 ... consequence=[ 3352 ... base.challenge( 3353 ... skills=base.CombinedSkill('brains', 'brawn'), 3354 ... level=3, 3355 ... success=[base.effect(goto='home')], 3356 ... failure=[base.effect(bounce=True)], 3357 ... outcome=None 3358 ... ) 3359 ... ] # no alternative -> empty list 3360 ... ) 3361 ... ] 3362 >>> pf.unparseConsequence(c) 3363 '{??((brawny|!(weights*3))){\ 3364<3>sum(brains, brawn){goto home}{bounce}}{}}' 3365 >>> c = [base.effect(gain='if(power){gain "mimic"}')] 3366 >>> # TODO: Make this work! 3367 >>> # pf.unparseConsequence(c) 3368 3369 '{gain "if(power){gain \\\\"mimic\\\\"}"}' 3370 """ 3371 result = self.formatDict[Lexeme.openCurly] 3372 for item in consequence: 3373 if 'skills' in item: # a Challenge 3374 item = cast(base.Challenge, item) 3375 result += self.unparseChallenge(item) 3376 3377 elif 'value' in item: # an Effect 3378 item = cast(base.Effect, item) 3379 result += self.unparseEffect(item) 3380 3381 elif 'condition' in item: # a Condition 3382 item = cast(base.Condition, item) 3383 result += self.unparseCondition(item) 3384 3385 else: # bad dict 3386 raise TypeError( 3387 f"Invalid consequence: items in the list must be" 3388 f" Effects, Challenges, or Conditions (got a dictionary" 3389 f" without 'skills', 'value', or 'condition' keys)." 3390 f"\nGot item: {repr(item)}" 3391 ) 3392 result += '; ' 3393 3394 if result.endswith('; '): 3395 result = result[:-2] 3396 3397 return result + self.formatDict[Lexeme.closeCurly]
Given a base.Consequence, returns a string encoding of it,
using the same format that parseConsequence will parse. Uses
function-call-like syntax and curly braces to denote different
sub-consequences. See also SkillCombination.unparse and
Requirement.unparse For example:
>>> pf = ParseFormat()
>>> c = [base.effect(gain='one'), base.effect(lose='one')]
>>> pf.unparseConsequence(c)
'{gain one; lose one}'
>>> c = [
... base.challenge(
... skills=base.BestSkill('brains', 'brawn'),
... level=2,
... success=[base.effect(set=('switch', 'on'))],
... failure=[
... base.effect(deactivate=True, delay=1),
... base.effect(bounce=True)
... ],
... outcome=True
... )
... ]
>>> pf.unparseConsequence(c)
'{<2>best(brains, brawn)>{set switch:on}{deactivate ,1; bounce}}'
>>> c[0]['outcome'] = False
>>> pf.unparseConsequence(c)
'{<2>best(brains, brawn){set switch:on}>{deactivate ,1; bounce}}'
>>> c[0]['outcome'] = None
>>> pf.unparseConsequence(c)
'{<2>best(brains, brawn){set switch:on}{deactivate ,1; bounce}}'
>>> c = [
... base.condition(
... condition=base.ReqAny([
... base.ReqCapability('brawny'),
... base.ReqNot(base.ReqTokens('weights', 3))
... ]),
... consequence=[
... base.challenge(
... skills=base.CombinedSkill('brains', 'brawn'),
... level=3,
... success=[base.effect(goto='home')],
... failure=[base.effect(bounce=True)],
... outcome=None
... )
... ] # no alternative -> empty list
... )
... ]
>>> pf.unparseConsequence(c)
'{??((brawny|!(weights*3))){<3>sum(brains, brawn){goto home}{bounce}}{}}'
>>> c = [base.effect(gain='if(power){gain "mimic"}')]
>>> # TODO: Make this work!
>>> # pf.unparseConsequence(c)
'{gain "if(power){gain \"mimic\"}"}'
3399 def parseMechanismSpecifierFromTokens( 3400 self, 3401 tokens: LexedTokens, 3402 start: int = 0 3403 ) -> Tuple[base.MechanismSpecifier, int]: 3404 """ 3405 Parses a mechanism specifier starting at the specified position 3406 in the given tokens list. No ending position is specified, but 3407 instead this function returns a tuple containing the parsed 3408 `base.MechanismSpecifier` along with an index in the tokens list 3409 where the end of the specifier was found. 3410 3411 For example: 3412 3413 >>> pf = ParseFormat() 3414 >>> pf.parseMechanismSpecifierFromTokens(['m']) 3415 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3416 name='m'), 0) 3417 >>> pf.parseMechanismSpecifierFromTokens(['a', 'm']) 3418 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3419 name='a'), 0) 3420 >>> pf.parseMechanismSpecifierFromTokens(['a', 'm'], 1) 3421 (MechanismSpecifier(domain=None, zone=None, decision=None,\ 3422 name='m'), 1) 3423 >>> pf.parseMechanismSpecifierFromTokens( 3424 ... ['a', Lexeme.domainSeparator, 'm'] 3425 ... ) 3426 (MechanismSpecifier(domain='a', zone=None, decision=None,\ 3427 name='m'), 2) 3428 >>> pf.parseMechanismSpecifierFromTokens( 3429 ... ['a', Lexeme.zoneSeparator, 'm'] 3430 ... ) 3431 (MechanismSpecifier(domain=None, zone=None, decision='a',\ 3432 name='m'), 2) 3433 >>> pf.parseMechanismSpecifierFromTokens( 3434 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm'] 3435 ... ) 3436 (MechanismSpecifier(domain=None, zone='a', decision='b',\ 3437 name='m'), 4) 3438 >>> pf.parseMechanismSpecifierFromTokens( 3439 ... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm'] 3440 ... ) 3441 (MechanismSpecifier(domain='a', zone=None, decision='b',\ 3442 name='m'), 4) 3443 >>> pf.parseMechanismSpecifierFromTokens( 3444 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'] 3445 ... ) 3446 (MechanismSpecifier(domain=None, zone=None, decision='a',\ 3447 name='b'), 2) 3448 >>> pf.parseMechanismSpecifierFromTokens( 3449 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 3450 ... 1 3451 ... ) 3452 Traceback (most recent call last): 3453 ... 3454 exploration.parsing.ParseError... 3455 >>> pf.parseMechanismSpecifierFromTokens( 3456 ... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'], 3457 ... 2 3458 ... ) 3459 (MechanismSpecifier(domain='b', zone=None, decision=None,\ 3460 name='m'), 4) 3461 >>> pf.parseMechanismSpecifierFromTokens( 3462 ... [ 3463 ... 'a', 3464 ... Lexeme.domainSeparator, 3465 ... 'b', 3466 ... Lexeme.zoneSeparator, 3467 ... 'c', 3468 ... Lexeme.zoneSeparator, 3469 ... 'm' 3470 ... ] 3471 ... ) 3472 (MechanismSpecifier(domain='a', zone='b', decision='c', name='m'), 6) 3473 >>> pf.parseMechanismSpecifierFromTokens( 3474 ... [ 3475 ... 'a', 3476 ... Lexeme.domainSeparator, 3477 ... 'b', 3478 ... Lexeme.zoneSeparator, 3479 ... 'c', 3480 ... Lexeme.zoneSeparator, 3481 ... 'm' 3482 ... ], 3483 ... 2 3484 ... ) 3485 (MechanismSpecifier(domain=None, zone='b', decision='c',\ 3486 name='m'), 6) 3487 >>> pf.parseMechanismSpecifierFromTokens( 3488 ... [ 3489 ... 'a', 3490 ... Lexeme.domainSeparator, 3491 ... 'b', 3492 ... Lexeme.zoneSeparator, 3493 ... 'c', 3494 ... Lexeme.zoneSeparator, 3495 ... 'm' 3496 ... ], 3497 ... 4 3498 ... ) 3499 (MechanismSpecifier(domain=None, zone=None, decision='c',\ 3500 name='m'), 6) 3501 >>> pf.parseMechanismSpecifierFromTokens( 3502 ... [ 3503 ... 'roomB', 3504 ... Lexeme.zoneSeparator, 3505 ... 'switch', 3506 ... Lexeme.mechanismSeparator, 3507 ... 'on' 3508 ... ] 3509 ... ) 3510 (MechanismSpecifier(domain=None, zone=None, decision='roomB',\ 3511 name='switch'), 2) 3512 >>> pf.parseMechanismSpecifierFromTokens( 3513 ... [ 3514 ... '500', 3515 ... Lexeme.zoneSeparator, 3516 ... 'm' 3517 ... ], 3518 ... 0 3519 ... ) 3520 (MechanismSpecifier(domain=None, zone=None, decision=500,\ 3521 name='m'), 2) 3522 >>> pf.parseMechanismSpecifierFromTokens( 3523 ... [ 3524 ... '500', 3525 ... Lexeme.zoneSeparator, 3526 ... Lexeme.zoneSeparator, 3527 ... 'm' 3528 ... ], 3529 ... 0 3530 ... ) 3531 Traceback (most recent call last): 3532 ... 3533 exploration.parsing.ParseError... 3534 >>> pf.parseMechanismSpecifierFromTokens( 3535 ... [ 3536 ... '500', 3537 ... Lexeme.zoneSeparator, 3538 ... 'm', 3539 ... Lexeme.mechanismSeparator, 3540 ... 'on' 3541 ... ], 3542 ... 0 3543 ... ) 3544 (MechanismSpecifier(domain=None, zone=None, decision=500,\ 3545 name='m'), 2) 3546 >>> pf.parseMechanismSpecifierFromTokens( 3547 ... [ 3548 ... '500', 3549 ... Lexeme.zoneSeparator, 3550 ... 'd', 3551 ... Lexeme.zoneSeparator, 3552 ... 'm' 3553 ... ], 3554 ... 0 3555 ... ) 3556 (MechanismSpecifier(domain=None, zone='500', decision='d',\ 3557 name='m'), 4) 3558 """ 3559 start, tEnd, nLeft = normalizeEnds(tokens, start, -1) 3560 3561 try: 3562 dSpec, dEnd = self.parseDecisionSpecifierFromTokens( 3563 tokens, 3564 start 3565 ) 3566 except ParseError: 3567 raise ParseError( 3568 "Failed to parse mechanism specifier couldn't parse" 3569 " initial mechanism name." 3570 ) 3571 3572 # Note: This doesn't normally happen because the mechanism name 3573 # makes it seem like the integer decision ID is really a zone 3574 # name. 3575 if isinstance(dSpec, int): 3576 sep = tokens[dEnd + 1] 3577 after = tokens[dEnd + 2] 3578 3579 if sep == Lexeme.zoneSeparator and not isinstance(after, Lexeme): 3580 return ( 3581 base.MechanismSpecifier( 3582 domain=None, 3583 zone=None, 3584 decision=dSpec, 3585 name=after 3586 ), 3587 dEnd + 2 3588 ) 3589 else: 3590 raise ParseError( 3591 f"Invalid mechanism specifier: got a decision ID" 3592 f" NOT followed by a zone separator and mechanism" 3593 f" name. Got: {tokens[start:]}" 3594 ) 3595 3596 mDomain = dSpec.domain 3597 if dEnd == tEnd or dEnd == tEnd - 1: 3598 if dSpec.zone is not None: 3599 try: 3600 # Case for integer "zone" -> integer decision ID 3601 zID = int(dSpec.zone) 3602 return ( 3603 base.MechanismSpecifier( 3604 domain=None, 3605 zone=None, 3606 decision=zID, 3607 name=dSpec.name 3608 ), 3609 dEnd 3610 ) 3611 except ValueError: 3612 pass 3613 return ( 3614 base.MechanismSpecifier( 3615 domain=mDomain, 3616 zone=None, 3617 decision=dSpec.zone, 3618 name=dSpec.name 3619 ), 3620 dEnd 3621 ) 3622 3623 sep = tokens[dEnd + 1] 3624 after = tokens[dEnd + 2] 3625 3626 mDec: Optional[Union[base.DecisionName, base.DecisionID]] 3627 if sep == Lexeme.zoneSeparator: 3628 if isinstance(after, Lexeme): 3629 mZone = None 3630 mDec = dSpec.zone 3631 mName = dSpec.name 3632 mEnd = dEnd 3633 else: 3634 mZone = dSpec.zone 3635 mDec = dSpec.name 3636 mName = after 3637 mEnd = dEnd + 2 3638 else: 3639 mZone = None 3640 mDec = dSpec.zone 3641 mName = dSpec.name 3642 mEnd = dEnd 3643 3644 # Treat numerical decision "names" as decision IDs 3645 if mDec is not None: 3646 try: 3647 mDec = int(mDec) 3648 if mDomain is not None or mZone is not None: 3649 raise ParseError( 3650 f"Invalid mechanism specifier: got a numerical" 3651 f" decision ID but also a domain and/or zone." 3652 f" Got: {tokens[start:]}" 3653 ) 3654 except ValueError: 3655 pass 3656 3657 return ( 3658 base.MechanismSpecifier( 3659 domain=mDomain, 3660 zone=mZone, 3661 decision=mDec, 3662 name=mName 3663 ), 3664 mEnd 3665 )
Parses a mechanism specifier starting at the specified position
in the given tokens list. No ending position is specified, but
instead this function returns a tuple containing the parsed
base.MechanismSpecifier along with an index in the tokens list
where the end of the specifier was found.
For example:
>>> pf = ParseFormat()
>>> pf.parseMechanismSpecifierFromTokens(['m'])
(MechanismSpecifier(domain=None, zone=None, decision=None, name='m'), 0)
>>> pf.parseMechanismSpecifierFromTokens(['a', 'm'])
(MechanismSpecifier(domain=None, zone=None, decision=None, name='a'), 0)
>>> pf.parseMechanismSpecifierFromTokens(['a', 'm'], 1)
(MechanismSpecifier(domain=None, zone=None, decision=None, name='m'), 1)
>>> pf.parseMechanismSpecifierFromTokens(
... ['a', Lexeme.domainSeparator, 'm']
... )
(MechanismSpecifier(domain='a', zone=None, decision=None, name='m'), 2)
>>> pf.parseMechanismSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'm']
... )
(MechanismSpecifier(domain=None, zone=None, decision='a', name='m'), 2)
>>> pf.parseMechanismSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.zoneSeparator, 'm']
... )
(MechanismSpecifier(domain=None, zone='a', decision='b', name='m'), 4)
>>> pf.parseMechanismSpecifierFromTokens(
... ['a', Lexeme.domainSeparator, 'b', Lexeme.zoneSeparator, 'm']
... )
(MechanismSpecifier(domain='a', zone=None, decision='b', name='m'), 4)
>>> pf.parseMechanismSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm']
... )
(MechanismSpecifier(domain=None, zone=None, decision='a', name='b'), 2)
>>> pf.parseMechanismSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'],
... 1
... )
Traceback (most recent call last):
...
ParseError...
>>> pf.parseMechanismSpecifierFromTokens(
... ['a', Lexeme.zoneSeparator, 'b', Lexeme.domainSeparator, 'm'],
... 2
... )
(MechanismSpecifier(domain='b', zone=None, decision=None, name='m'), 4)
>>> pf.parseMechanismSpecifierFromTokens(
... [
... 'a',
... Lexeme.domainSeparator,
... 'b',
... Lexeme.zoneSeparator,
... 'c',
... Lexeme.zoneSeparator,
... 'm'
... ]
... )
(MechanismSpecifier(domain='a', zone='b', decision='c', name='m'), 6)
>>> pf.parseMechanismSpecifierFromTokens(
... [
... 'a',
... Lexeme.domainSeparator,
... 'b',
... Lexeme.zoneSeparator,
... 'c',
... Lexeme.zoneSeparator,
... 'm'
... ],
... 2
... )
(MechanismSpecifier(domain=None, zone='b', decision='c', name='m'), 6)
>>> pf.parseMechanismSpecifierFromTokens(
... [
... 'a',
... Lexeme.domainSeparator,
... 'b',
... Lexeme.zoneSeparator,
... 'c',
... Lexeme.zoneSeparator,
... 'm'
... ],
... 4
... )
(MechanismSpecifier(domain=None, zone=None, decision='c', name='m'), 6)
>>> pf.parseMechanismSpecifierFromTokens(
... [
... 'roomB',
... Lexeme.zoneSeparator,
... 'switch',
... Lexeme.mechanismSeparator,
... 'on'
... ]
... )
(MechanismSpecifier(domain=None, zone=None, decision='roomB', name='switch'), 2)
>>> pf.parseMechanismSpecifierFromTokens(
... [
... '500',
... Lexeme.zoneSeparator,
... 'm'
... ],
... 0
... )
(MechanismSpecifier(domain=None, zone=None, decision=500, name='m'), 2)
>>> pf.parseMechanismSpecifierFromTokens(
... [
... '500',
... Lexeme.zoneSeparator,
... Lexeme.zoneSeparator,
... 'm'
... ],
... 0
... )
Traceback (most recent call last):
...
ParseError...
>>> pf.parseMechanismSpecifierFromTokens(
... [
... '500',
... Lexeme.zoneSeparator,
... 'm',
... Lexeme.mechanismSeparator,
... 'on'
... ],
... 0
... )
(MechanismSpecifier(domain=None, zone=None, decision=500, name='m'), 2)
>>> pf.parseMechanismSpecifierFromTokens(
... [
... '500',
... Lexeme.zoneSeparator,
... 'd',
... Lexeme.zoneSeparator,
... 'm'
... ],
... 0
... )
(MechanismSpecifier(domain=None, zone='500', decision='d', name='m'), 4)
3667 def groupReqTokens( 3668 self, 3669 tokens: LexedTokens, 3670 start: int = 0, 3671 end: int = -1 3672 ) -> GroupedTokens: 3673 """ 3674 Groups tokens for a requirement, stripping out all parentheses 3675 but replacing parenthesized expressions with sub-lists of tokens. 3676 3677 For example: 3678 3679 >>> pf = ParseFormat() 3680 >>> pf.groupReqTokens(['jump']) 3681 ['jump'] 3682 >>> pf.groupReqTokens([Lexeme.openParen, 'jump']) 3683 Traceback (most recent call last): 3684 ... 3685 exploration.parsing.ParseError... 3686 >>> pf.groupReqTokens([Lexeme.closeParen, 'jump']) 3687 Traceback (most recent call last): 3688 ... 3689 exploration.parsing.ParseError... 3690 >>> pf.groupReqTokens(['jump', Lexeme.closeParen]) 3691 Traceback (most recent call last): 3692 ... 3693 exploration.parsing.ParseError... 3694 >>> pf.groupReqTokens([Lexeme.openParen, 'jump', Lexeme.closeParen]) 3695 [['jump']] 3696 >>> pf.groupReqTokens( 3697 ... [ 3698 ... Lexeme.openParen, 3699 ... 'jump', 3700 ... Lexeme.orBar, 3701 ... 'climb', 3702 ... Lexeme.closeParen, 3703 ... Lexeme.ampersand, 3704 ... 'crawl', 3705 ... ] 3706 ... ) 3707 [['jump', <Lexeme.orBar: ...>, 'climb'], <Lexeme.ampersand: ...>,\ 3708 'crawl'] 3709 """ 3710 start, end, nTokens = normalizeEnds(tokens, start, end) 3711 if nTokens == 0: 3712 raise ParseError("Ran out of tokens.") 3713 3714 resultsStack: List[GroupedTokens] = [[]] 3715 here = start 3716 while here <= end: 3717 token = tokens[here] 3718 here += 1 3719 if token == Lexeme.closeParen: 3720 if len(resultsStack) == 1: 3721 raise ParseError( 3722 f"Too many closing parens at index {here - 1}" 3723 f" in:\n{tokens[start:end + 1]}" 3724 ) 3725 else: 3726 closed = resultsStack.pop() 3727 resultsStack[-1].append(closed) 3728 elif token == Lexeme.openParen: 3729 resultsStack.append([]) 3730 else: 3731 resultsStack[-1].append(token) 3732 if len(resultsStack) != 1: 3733 raise ParseError( 3734 f"Mismatched parentheses in tokens:" 3735 f"\n{tokens[start:end + 1]}" 3736 ) 3737 return resultsStack[0]
Groups tokens for a requirement, stripping out all parentheses but replacing parenthesized expressions with sub-lists of tokens.
For example:
>>> pf = ParseFormat()
>>> pf.groupReqTokens(['jump'])
['jump']
>>> pf.groupReqTokens([Lexeme.openParen, 'jump'])
Traceback (most recent call last):
...
ParseError...
>>> pf.groupReqTokens([Lexeme.closeParen, 'jump'])
Traceback (most recent call last):
...
ParseError...
>>> pf.groupReqTokens(['jump', Lexeme.closeParen])
Traceback (most recent call last):
...
ParseError...
>>> pf.groupReqTokens([Lexeme.openParen, 'jump', Lexeme.closeParen])
[['jump']]
>>> pf.groupReqTokens(
... [
... Lexeme.openParen,
... 'jump',
... Lexeme.orBar,
... 'climb',
... Lexeme.closeParen,
... Lexeme.ampersand,
... 'crawl',
... ]
... )
[['jump', <Lexeme.orBar: ...>, 'climb'], <Lexeme.ampersand: ...>, 'crawl']
3739 def groupReqTokensByPrecedence( 3740 self, 3741 tokenGroups: GroupedTokens 3742 ) -> GroupedRequirementParts: 3743 """ 3744 Re-groups requirement tokens that have been grouped using 3745 `groupReqTokens` according to operator precedence, effectively 3746 creating an equivalent result which would have been obtained by 3747 `groupReqTokens` if all possible non-redundant explicit 3748 parentheses had been included. 3749 3750 Also turns each leaf part into a `Requirement`. 3751 3752 TODO: Make this actually reasonably efficient T_T 3753 3754 Examples: 3755 3756 >>> pf = ParseFormat() 3757 >>> r = pf.parseRequirement('capability&roomB::switch:on') 3758 >>> pf.groupReqTokensByPrecedence( 3759 ... [ 3760 ... ['jump', Lexeme.orBar, 'climb'], 3761 ... Lexeme.ampersand, 3762 ... Lexeme.notMarker, 3763 ... 'coin', 3764 ... Lexeme.tokenCount, 3765 ... '3' 3766 ... ] 3767 ... ) 3768 [\ 3769[\ 3770[[ReqCapability('jump'), <Lexeme.orBar: ...>, ReqCapability('climb')]],\ 3771 <Lexeme.ampersand: ...>,\ 3772 [<Lexeme.notMarker: ...>, ReqTokens('coin', 3)]\ 3773]\ 3774] 3775 """ 3776 subgrouped: List[Union[Lexeme, str, GroupedRequirementParts]] = [] 3777 # First recursively group all parenthesized expressions 3778 for i, item in enumerate(tokenGroups): 3779 if isinstance(item, list): 3780 subgrouped.append(self.groupReqTokensByPrecedence(item)) 3781 else: 3782 subgrouped.append(item) 3783 3784 # Now process all leaf requirements 3785 leavesConverted: GroupedRequirementParts = [] 3786 i = 0 3787 while i < len(subgrouped): 3788 gItem = subgrouped[i] 3789 3790 if isinstance(gItem, list): 3791 leavesConverted.append(gItem) 3792 elif isinstance(gItem, Lexeme): 3793 leavesConverted.append(gItem) 3794 elif i == len(subgrouped) - 1: 3795 if isinstance(gItem, Lexeme): 3796 raise ParseError( 3797 f"Lexeme at end of requirement. Grouped tokens:" 3798 f"\n{tokenGroups}" 3799 ) 3800 else: 3801 assert isinstance(gItem, str) 3802 if gItem == 'X': 3803 leavesConverted.append(base.ReqImpossible()) 3804 elif gItem == 'O': 3805 leavesConverted.append(base.ReqNothing()) 3806 else: 3807 leavesConverted.append(base.ReqCapability(gItem)) 3808 else: 3809 assert isinstance(gItem, str) 3810 try: 3811 # TODO: Avoid list copy here... 3812 couldBeMechanismSpecifier: LexedTokens = [] 3813 for ii in range(i, len(subgrouped)): 3814 lexemeOrStr = subgrouped[ii] 3815 if isinstance(lexemeOrStr, (Lexeme, str)): 3816 couldBeMechanismSpecifier.append(lexemeOrStr) 3817 else: 3818 break 3819 mSpec, mEnd = self.parseMechanismSpecifierFromTokens( 3820 couldBeMechanismSpecifier 3821 ) 3822 mEnd += i 3823 if ( 3824 mEnd >= len(subgrouped) - 2 3825 or subgrouped[mEnd + 1] != Lexeme.mechanismSeparator 3826 ): 3827 raise ParseError("Not a mechanism requirement.") 3828 3829 mState = subgrouped[mEnd + 2] 3830 if not isinstance(mState, base.MechanismState): 3831 raise ParseError("Not a mechanism requirement.") 3832 leavesConverted.append(base.ReqMechanism(mSpec, mState)) 3833 i = mEnd + 2 # + 1 will happen automatically below 3834 except ParseError: 3835 following = subgrouped[i + 1] 3836 if following in ( 3837 Lexeme.tokenCount, 3838 Lexeme.mechanismSeparator, 3839 Lexeme.wigglyLine, 3840 Lexeme.skillLevel 3841 ): 3842 if ( 3843 i == len(subgrouped) - 2 3844 or isinstance(subgrouped[i + 2], Lexeme) 3845 ): 3846 if following == Lexeme.wigglyLine: 3847 # Default tag value is 1 3848 leavesConverted.append(base.ReqTag(gItem, 1)) 3849 i += 1 # another +1 automatic below 3850 else: 3851 raise ParseError( 3852 f"Lexeme at end of requirement. Grouped" 3853 f" tokens:\n{tokenGroups}" 3854 ) 3855 else: 3856 afterwards = subgrouped[i + 2] 3857 if not isinstance(afterwards, str): 3858 raise ParseError( 3859 f"Lexeme after token/mechanism/tag/skill" 3860 f" separator at index {i}." 3861 f" Grouped tokens:\n{tokenGroups}" 3862 ) 3863 i += 2 # another +1 automatic below 3864 if following == Lexeme.tokenCount: 3865 try: 3866 tCount = int(afterwards) 3867 except ValueError: 3868 raise ParseError( 3869 f"Token count could not be" 3870 f" parsed as an integer:" 3871 f" {afterwards!r}. Grouped" 3872 f" tokens:\n{tokenGroups}" 3873 ) 3874 leavesConverted.append( 3875 base.ReqTokens(gItem, tCount) 3876 ) 3877 elif following == Lexeme.mechanismSeparator: 3878 leavesConverted.append( 3879 base.ReqMechanism(gItem, afterwards) 3880 ) 3881 elif following == Lexeme.wigglyLine: 3882 tVal = self.parseTagValue(afterwards) 3883 leavesConverted.append( 3884 base.ReqTag(gItem, tVal) 3885 ) 3886 else: 3887 assert following == Lexeme.skillLevel 3888 try: 3889 sLevel = int(afterwards) 3890 except ValueError: 3891 raise ParseError( 3892 f"Skill level could not be" 3893 f" parsed as an integer:" 3894 f" {afterwards!r}. Grouped" 3895 f" tokens:\n{tokenGroups}" 3896 ) 3897 leavesConverted.append( 3898 base.ReqLevel(gItem, sLevel) 3899 ) 3900 else: 3901 if gItem == 'X': 3902 leavesConverted.append(base.ReqImpossible()) 3903 elif gItem == 'O': 3904 leavesConverted.append(base.ReqNothing()) 3905 else: 3906 leavesConverted.append( 3907 base.ReqCapability(gItem) 3908 ) 3909 3910 # Finally, increment our index: 3911 i += 1 3912 3913 # Now group all NOT operators 3914 i = 0 3915 notsGrouped: GroupedRequirementParts = [] 3916 while i < len(leavesConverted): 3917 leafItem = leavesConverted[i] 3918 group = [] 3919 while leafItem == Lexeme.notMarker: 3920 group.append(leafItem) 3921 i += 1 3922 if i >= len(leavesConverted): 3923 raise ParseError( 3924 f"NOT at end of tokens:\n{leavesConverted}" 3925 ) 3926 leafItem = leavesConverted[i] 3927 if group == []: 3928 notsGrouped.append(leafItem) 3929 i += 1 3930 else: 3931 group.append(leafItem) 3932 i += 1 3933 notsGrouped.append(group) 3934 3935 # Next group all AND operators 3936 i = 0 3937 andsGrouped: GroupedRequirementParts = [] 3938 while i < len(notsGrouped): 3939 notGroupItem = notsGrouped[i] 3940 if notGroupItem == Lexeme.ampersand: 3941 if i == len(notsGrouped) - 1: 3942 raise ParseError( 3943 f"AND at end of group in tokens:" 3944 f"\n{tokenGroups}" 3945 f"Which had been grouped into:" 3946 f"\n{notsGrouped}" 3947 ) 3948 itemAfter = notsGrouped[i + 1] 3949 if isinstance(itemAfter, Lexeme): 3950 raise ParseError( 3951 f"Lexeme after AND in of group in tokens:" 3952 f"\n{tokenGroups}" 3953 f"Which had been grouped into:" 3954 f"\n{notsGrouped}" 3955 ) 3956 assert isinstance(itemAfter, (base.Requirement, list)) 3957 prev = andsGrouped[-1] 3958 if ( 3959 isinstance(prev, list) 3960 and len(prev) > 2 3961 and prev[1] == Lexeme.ampersand 3962 ): 3963 prev.extend(notsGrouped[i:i + 2]) 3964 i += 1 # with an extra +1 below 3965 else: 3966 andsGrouped.append( 3967 [andsGrouped.pop()] + notsGrouped[i:i + 2] 3968 ) 3969 i += 1 # extra +1 below 3970 else: 3971 andsGrouped.append(notGroupItem) 3972 i += 1 3973 3974 # Finally check that we only have OR operators left over 3975 i = 0 3976 finalResult: GroupedRequirementParts = [] 3977 while i < len(andsGrouped): 3978 andGroupItem = andsGrouped[i] 3979 if andGroupItem == Lexeme.orBar: 3980 if i == len(andsGrouped) - 1: 3981 raise ParseError( 3982 f"OR at end of group in tokens:" 3983 f"\n{tokenGroups}" 3984 f"Which had been grouped into:" 3985 f"\n{andsGrouped}" 3986 ) 3987 itemAfter = andsGrouped[i + 1] 3988 if isinstance(itemAfter, Lexeme): 3989 raise ParseError( 3990 f"Lexeme after OR in of group in tokens:" 3991 f"\n{tokenGroups}" 3992 f"Which had been grouped into:" 3993 f"\n{andsGrouped}" 3994 ) 3995 assert isinstance(itemAfter, (base.Requirement, list)) 3996 prev = finalResult[-1] 3997 if ( 3998 isinstance(prev, list) 3999 and len(prev) > 2 4000 and prev[1] == Lexeme.orBar 4001 ): 4002 prev.extend(andsGrouped[i:i + 2]) 4003 i += 1 # with an extra +1 below 4004 else: 4005 finalResult.append( 4006 [finalResult.pop()] + andsGrouped[i:i + 2] 4007 ) 4008 i += 1 # extra +1 below 4009 elif isinstance(andGroupItem, Lexeme): 4010 raise ParseError( 4011 f"Leftover lexeme when grouping ORs at index {i}" 4012 f" in grouped tokens:\n{andsGrouped}" 4013 f"\nOriginal tokens were:\n{tokenGroups}" 4014 ) 4015 else: 4016 finalResult.append(andGroupItem) 4017 i += 1 4018 4019 return finalResult
Re-groups requirement tokens that have been grouped using
groupReqTokens according to operator precedence, effectively
creating an equivalent result which would have been obtained by
groupReqTokens if all possible non-redundant explicit
parentheses had been included.
Also turns each leaf part into a Requirement.
TODO: Make this actually reasonably efficient T_T
Examples:
>>> pf = ParseFormat()
>>> r = pf.parseRequirement('capability&roomB::switch:on')
>>> pf.groupReqTokensByPrecedence(
... [
... ['jump', Lexeme.orBar, 'climb'],
... Lexeme.ampersand,
... Lexeme.notMarker,
... 'coin',
... Lexeme.tokenCount,
... '3'
... ]
... )
[[[[ReqCapability('jump'), <Lexeme.orBar: ...>, ReqCapability('climb')]], <Lexeme.ampersand: ...>, [<Lexeme.notMarker: ...>, ReqTokens('coin', 3)]]]
4021 def parseRequirementFromRegroupedTokens( 4022 self, 4023 reqGroups: GroupedRequirementParts 4024 ) -> base.Requirement: 4025 """ 4026 Recursive parser that works once tokens have been turned into 4027 requirements at the leaves and grouped by operator precedence 4028 otherwise (see `groupReqTokensByPrecedence`). 4029 4030 TODO: Simplify by just doing this while grouping... ? 4031 """ 4032 if len(reqGroups) == 0: 4033 raise ParseError("Ran out of tokens.") 4034 4035 elif len(reqGroups) == 1: 4036 only = reqGroups[0] 4037 if isinstance(only, list): 4038 return self.parseRequirementFromRegroupedTokens(only) 4039 elif isinstance(only, base.Requirement): 4040 return only 4041 else: 4042 raise ParseError(f"Invalid singleton group:\n{only}") 4043 elif reqGroups[0] == Lexeme.notMarker: 4044 if ( 4045 not all(x == Lexeme.notMarker for x in reqGroups[:-1]) 4046 or not isinstance(reqGroups[-1], (list, base.Requirement)) 4047 ): 4048 raise ParseError(f"Invalid negation group:\n{reqGroups}") 4049 result = reqGroups[-1] 4050 if isinstance(result, list): 4051 result = self.parseRequirementFromRegroupedTokens(result) 4052 assert isinstance(result, base.Requirement) 4053 for i in range(len(reqGroups) - 1): 4054 result = base.ReqNot(result) 4055 return result 4056 elif len(reqGroups) % 2 == 0: 4057 raise ParseError(f"Even-length non-negation group:\n{reqGroups}") 4058 else: 4059 if ( 4060 reqGroups[1] not in (Lexeme.ampersand, Lexeme.orBar) 4061 or not all( 4062 reqGroups[i] == reqGroups[1] 4063 for i in range(1, len(reqGroups), 2) 4064 ) 4065 ): 4066 raise ParseError( 4067 f"Inconsistent operator(s) in group:\n{reqGroups}" 4068 ) 4069 op = reqGroups[1] 4070 operands = [ 4071 ( 4072 self.parseRequirementFromRegroupedTokens(x) 4073 if isinstance(x, list) 4074 else x 4075 ) 4076 for x in reqGroups[::2] 4077 ] 4078 if not all(isinstance(x, base.Requirement) for x in operands): 4079 raise ParseError( 4080 f"Item not reducible to Requirement in AND group:" 4081 f"\n{reqGroups}" 4082 ) 4083 reqSequence = cast(Sequence[base.Requirement], operands) 4084 if op == Lexeme.ampersand: 4085 return base.ReqAll(reqSequence).flatten() 4086 else: 4087 assert op == Lexeme.orBar 4088 return base.ReqAny(reqSequence).flatten()
Recursive parser that works once tokens have been turned into
requirements at the leaves and grouped by operator precedence
otherwise (see groupReqTokensByPrecedence).
TODO: Simplify by just doing this while grouping... ?
4090 def parseRequirementFromGroupedTokens( 4091 self, 4092 tokenGroups: GroupedTokens 4093 ) -> base.Requirement: 4094 """ 4095 Parses a `base.Requirement` from a pre-grouped tokens list (see 4096 `groupReqTokens`). Uses the 'orBar', 'ampersand', 'notMarker', 4097 'tokenCount', and 'mechanismSeparator' `Lexeme`s to provide 4098 'or', 'and', and 'not' operators along with distinguishing 4099 between capabilities, tokens, and mechanisms. 4100 4101 Precedence ordering is not, then and, then or, but you are 4102 encouraged to use parentheses for explicit grouping (the 4103 'openParen' and 'closeParen' `Lexeme`s, although these must be 4104 handled by `groupReqTokens` so this function won't see them 4105 directly). 4106 4107 You can also use 'X' (without quotes) for a never-satisfied 4108 requirement, and 'O' (without quotes) for an always-satisfied 4109 requirement. 4110 4111 Note that when '!' is applied to a token requirement it flips 4112 the sense of the integer from 'must have at least this many' to 4113 'must have strictly less than this many'. 4114 4115 Raises a `ParseError` if the grouped tokens it is given cannot 4116 be parsed as a `Requirement`. 4117 4118 Examples: 4119 4120 >>> pf = ParseFormat() 4121 >>> pf.parseRequirementFromGroupedTokens(['capability']) 4122 ReqCapability('capability') 4123 >>> pf.parseRequirementFromGroupedTokens( 4124 ... ['token', Lexeme.tokenCount, '3'] 4125 ... ) 4126 ReqTokens('token', 3) 4127 >>> pf.parseRequirementFromGroupedTokens( 4128 ... ['mechanism', Lexeme.mechanismSeparator, 'state'] 4129 ... ) 4130 ReqMechanism('mechanism', 'state') 4131 >>> pf.parseRequirementFromGroupedTokens( 4132 ... ['capability', Lexeme.orBar, 'token', 4133 ... Lexeme.tokenCount, '3'] 4134 ... ) 4135 ReqAny([ReqCapability('capability'), ReqTokens('token', 3)]) 4136 >>> pf.parseRequirementFromGroupedTokens( 4137 ... ['one', Lexeme.ampersand, 'two', Lexeme.orBar, 'three'] 4138 ... ) 4139 ReqAny([ReqAll([ReqCapability('one'), ReqCapability('two')]),\ 4140 ReqCapability('three')]) 4141 >>> pf.parseRequirementFromGroupedTokens( 4142 ... [ 4143 ... 'one', 4144 ... Lexeme.ampersand, 4145 ... [ 4146 ... 'two', 4147 ... Lexeme.orBar, 4148 ... 'three' 4149 ... ] 4150 ... ] 4151 ... ) 4152 ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'),\ 4153 ReqCapability('three')])]) 4154 >>> pf.parseRequirementFromTokens(['X']) 4155 ReqImpossible() 4156 >>> pf.parseRequirementFromTokens(['O']) 4157 ReqNothing() 4158 >>> pf.parseRequirementFromTokens( 4159 ... [Lexeme.openParen, 'O', Lexeme.closeParen] 4160 ... ) 4161 ReqNothing() 4162 """ 4163 if len(tokenGroups) == 0: 4164 raise ParseError("Ran out of tokens.") 4165 4166 reGrouped = self.groupReqTokensByPrecedence(tokenGroups) 4167 4168 return self.parseRequirementFromRegroupedTokens(reGrouped)
Parses a base.Requirement from a pre-grouped tokens list (see
groupReqTokens). Uses the 'orBar', 'ampersand', 'notMarker',
'tokenCount', and 'mechanismSeparator' Lexemes to provide
'or', 'and', and 'not' operators along with distinguishing
between capabilities, tokens, and mechanisms.
Precedence ordering is not, then and, then or, but you are
encouraged to use parentheses for explicit grouping (the
'openParen' and 'closeParen' Lexemes, although these must be
handled by groupReqTokens so this function won't see them
directly).
You can also use 'X' (without quotes) for a never-satisfied requirement, and 'O' (without quotes) for an always-satisfied requirement.
Note that when '!' is applied to a token requirement it flips the sense of the integer from 'must have at least this many' to 'must have strictly less than this many'.
Raises a ParseError if the grouped tokens it is given cannot
be parsed as a Requirement.
Examples:
>>> pf = ParseFormat()
>>> pf.parseRequirementFromGroupedTokens(['capability'])
ReqCapability('capability')
>>> pf.parseRequirementFromGroupedTokens(
... ['token', Lexeme.tokenCount, '3']
... )
ReqTokens('token', 3)
>>> pf.parseRequirementFromGroupedTokens(
... ['mechanism', Lexeme.mechanismSeparator, 'state']
... )
ReqMechanism('mechanism', 'state')
>>> pf.parseRequirementFromGroupedTokens(
... ['capability', Lexeme.orBar, 'token',
... Lexeme.tokenCount, '3']
... )
ReqAny([ReqCapability('capability'), ReqTokens('token', 3)])
>>> pf.parseRequirementFromGroupedTokens(
... ['one', Lexeme.ampersand, 'two', Lexeme.orBar, 'three']
... )
ReqAny([ReqAll([ReqCapability('one'), ReqCapability('two')]), ReqCapability('three')])
>>> pf.parseRequirementFromGroupedTokens(
... [
... 'one',
... Lexeme.ampersand,
... [
... 'two',
... Lexeme.orBar,
... 'three'
... ]
... ]
... )
ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'), ReqCapability('three')])])
>>> pf.parseRequirementFromTokens(['X'])
ReqImpossible()
>>> pf.parseRequirementFromTokens(['O'])
ReqNothing()
>>> pf.parseRequirementFromTokens(
... [Lexeme.openParen, 'O', Lexeme.closeParen]
... )
ReqNothing()
4170 def parseRequirementFromTokens( 4171 self, 4172 tokens: LexedTokens, 4173 start: int = 0, 4174 end: int = -1 4175 ) -> base.Requirement: 4176 """ 4177 Parses a requirement from `LexedTokens` by grouping them first 4178 and then using `parseRequirementFromGroupedTokens`. 4179 4180 For example: 4181 4182 >>> pf = ParseFormat() 4183 >>> pf.parseRequirementFromTokens( 4184 ... [ 4185 ... 'one', 4186 ... Lexeme.ampersand, 4187 ... Lexeme.openParen, 4188 ... 'two', 4189 ... Lexeme.orBar, 4190 ... 'three', 4191 ... Lexeme.closeParen 4192 ... ] 4193 ... ) 4194 ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'),\ 4195 ReqCapability('three')])]) 4196 """ 4197 grouped = self.groupReqTokens(tokens, start, end) 4198 return self.parseRequirementFromGroupedTokens(grouped)
Parses a requirement from LexedTokens by grouping them first
and then using parseRequirementFromGroupedTokens.
For example:
>>> pf = ParseFormat()
>>> pf.parseRequirementFromTokens(
... [
... 'one',
... Lexeme.ampersand,
... Lexeme.openParen,
... 'two',
... Lexeme.orBar,
... 'three',
... Lexeme.closeParen
... ]
... )
ReqAll([ReqCapability('one'), ReqAny([ReqCapability('two'), ReqCapability('three')])])
4200 def parseRequirement(self, encoded: str) -> base.Requirement: 4201 """ 4202 Parses a `base.Requirement` from a string by calling `lex` and 4203 then feeding it into `ParseFormat.parseRequirementFromTokens`. 4204 As stated in `parseRequirementFromTokens`, the precedence 4205 binding order is NOT, then AND, then OR. 4206 4207 For example: 4208 4209 >>> pf = ParseFormat() 4210 >>> pf.parseRequirement('! coin * 3') 4211 ReqNot(ReqTokens('coin', 3)) 4212 >>> pf.parseRequirement( 4213 ... ' oneWord | "two words"|"three words words" ' 4214 ... ) 4215 ReqAny([ReqCapability('oneWord'), ReqCapability('"two words"'),\ 4216 ReqCapability('"three words words"')]) 4217 >>> pf.parseRequirement('words-with-dashes') 4218 ReqCapability('words-with-dashes') 4219 >>> r = pf.parseRequirement('capability&roomB::switch:on') 4220 >>> r 4221 ReqAll([ReqCapability('capability'),\ 4222 ReqMechanism(MechanismSpecifier(domain=None, zone=None, decision='roomB',\ 4223 name='switch'), 'on')]) 4224 >>> r.unparse() 4225 '(capability&roomB::switch:on)' 4226 >>> pf.parseRequirement('!!!one') 4227 ReqNot(ReqNot(ReqNot(ReqCapability('one')))) 4228 >>> pf.parseRequirement('domain//zone::where::mechanism:state') 4229 ReqMechanism(MechanismSpecifier(domain='domain', zone='zone',\ 4230 decision='where', name='mechanism'), 'state') 4231 >>> pf.parseRequirement('domain//mechanism:state') 4232 ReqMechanism(MechanismSpecifier(domain='domain', zone=None,\ 4233 decision=None, name='mechanism'), 'state') 4234 >>> pf.parseRequirement('where::mechanism:state') 4235 ReqMechanism(MechanismSpecifier(domain=None, zone=None,\ 4236 decision='where', name='mechanism'), 'state') 4237 >>> pf.parseRequirement('zone::where::mechanism:state') 4238 ReqMechanism(MechanismSpecifier(domain=None, zone='zone',\ 4239 decision='where', name='mechanism'), 'state') 4240 >>> pf.parseRequirement('tag~') 4241 ReqTag('tag', 1) 4242 >>> pf.parseRequirement('tag~&tag2~') 4243 ReqAll([ReqTag('tag', 1), ReqTag('tag2', 1)]) 4244 >>> pf.parseRequirement('tag~value|tag~3|tag~3.5|skill^3') 4245 ReqAny([ReqTag('tag', 'value'), ReqTag('tag', 3),\ 4246 ReqTag('tag', 3.5), ReqLevel('skill', 3)]) 4247 >>> pf.parseRequirement('tag~True|tag~False|tag~None') 4248 ReqAny([ReqTag('tag', True), ReqTag('tag', False), ReqTag('tag', None)]) 4249 4250 Precedence examples: 4251 4252 >>> pf.parseRequirement('A|B&C') 4253 ReqAny([ReqCapability('A'), ReqAll([ReqCapability('B'),\ 4254 ReqCapability('C')])]) 4255 >>> pf.parseRequirement('A&B|C') 4256 ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4257 ReqCapability('C')]) 4258 >>> pf.parseRequirement('(A&B)|C') 4259 ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4260 ReqCapability('C')]) 4261 >>> pf.parseRequirement('(A&B|C)&D') 4262 ReqAll([ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]),\ 4263 ReqCapability('C')]), ReqCapability('D')]) 4264 4265 Error examples: 4266 4267 >>> pf.parseRequirement('one ! Word') 4268 Traceback (most recent call last): 4269 ... 4270 exploration.parsing.ParseError... 4271 >>> pf.parseRequirement('a|') 4272 Traceback (most recent call last): 4273 ... 4274 exploration.parsing.ParseError... 4275 >>> pf.parseRequirement('b!') 4276 Traceback (most recent call last): 4277 ... 4278 exploration.parsing.ParseError... 4279 >>> pf.parseRequirement('*emph*') 4280 Traceback (most recent call last): 4281 ... 4282 exploration.parsing.ParseError... 4283 >>> pf.parseRequirement('one&&two') 4284 Traceback (most recent call last): 4285 ... 4286 exploration.parsing.ParseError... 4287 >>> pf.parseRequirement('one!|two') 4288 Traceback (most recent call last): 4289 ... 4290 exploration.parsing.ParseError... 4291 >>> pf.parseRequirement('one*two') 4292 Traceback (most recent call last): 4293 ... 4294 exploration.parsing.ParseError... 4295 >>> pf.parseRequirement('one*') 4296 Traceback (most recent call last): 4297 ... 4298 exploration.parsing.ParseError... 4299 >>> pf.parseRequirement('()') 4300 Traceback (most recent call last): 4301 ... 4302 exploration.parsing.ParseError... 4303 >>> pf.parseRequirement('(one)*3') 4304 Traceback (most recent call last): 4305 ... 4306 exploration.parsing.ParseError... 4307 >>> pf.parseRequirement('a:') 4308 Traceback (most recent call last): 4309 ... 4310 exploration.parsing.ParseError... 4311 >>> pf.parseRequirement('a:b:c') 4312 Traceback (most recent call last): 4313 ... 4314 exploration.parsing.ParseError... 4315 >>> pf.parseRequirement('where::capability') 4316 Traceback (most recent call last): 4317 ... 4318 exploration.parsing.ParseError... 4319 """ 4320 return self.parseRequirementFromTokens( 4321 lex(encoded, self.reverseFormat) 4322 )
Parses a base.Requirement from a string by calling lex and
then feeding it into ParseFormat.parseRequirementFromTokens.
As stated in parseRequirementFromTokens, the precedence
binding order is NOT, then AND, then OR.
For example:
>>> pf = ParseFormat()
>>> pf.parseRequirement('! coin * 3')
ReqNot(ReqTokens('coin', 3))
>>> pf.parseRequirement(
... ' oneWord | "two words"|"three words words" '
... )
ReqAny([ReqCapability('oneWord'), ReqCapability('"two words"'), ReqCapability('"three words words"')])
>>> pf.parseRequirement('words-with-dashes')
ReqCapability('words-with-dashes')
>>> r = pf.parseRequirement('capability&roomB::switch:on')
>>> r
ReqAll([ReqCapability('capability'), ReqMechanism(MechanismSpecifier(domain=None, zone=None, decision='roomB', name='switch'), 'on')])
>>> r.unparse()
'(capability&roomB::switch:on)'
>>> pf.parseRequirement('!!!one')
ReqNot(ReqNot(ReqNot(ReqCapability('one'))))
>>> pf.parseRequirement('domain//zone::where::mechanism:state')
ReqMechanism(MechanismSpecifier(domain='domain', zone='zone', decision='where', name='mechanism'), 'state')
>>> pf.parseRequirement('domain//mechanism:state')
ReqMechanism(MechanismSpecifier(domain='domain', zone=None, decision=None, name='mechanism'), 'state')
>>> pf.parseRequirement('where::mechanism:state')
ReqMechanism(MechanismSpecifier(domain=None, zone=None, decision='where', name='mechanism'), 'state')
>>> pf.parseRequirement('zone::where::mechanism:state')
ReqMechanism(MechanismSpecifier(domain=None, zone='zone', decision='where', name='mechanism'), 'state')
>>> pf.parseRequirement('tag~')
ReqTag('tag', 1)
>>> pf.parseRequirement('tag~&tag2~')
ReqAll([ReqTag('tag', 1), ReqTag('tag2', 1)])
>>> pf.parseRequirement('tag~value|tag~3|tag~3.5|skill^3')
ReqAny([ReqTag('tag', 'value'), ReqTag('tag', 3), ReqTag('tag', 3.5), ReqLevel('skill', 3)])
>>> pf.parseRequirement('tag~True|tag~False|tag~None')
ReqAny([ReqTag('tag', True), ReqTag('tag', False), ReqTag('tag', None)])
Precedence examples:
>>> pf.parseRequirement('A|B&C')
ReqAny([ReqCapability('A'), ReqAll([ReqCapability('B'), ReqCapability('C')])])
>>> pf.parseRequirement('A&B|C')
ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]), ReqCapability('C')])
>>> pf.parseRequirement('(A&B)|C')
ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]), ReqCapability('C')])
>>> pf.parseRequirement('(A&B|C)&D')
ReqAll([ReqAny([ReqAll([ReqCapability('A'), ReqCapability('B')]), ReqCapability('C')]), ReqCapability('D')])
Error examples:
>>> pf.parseRequirement('one ! Word')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('a|')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('b!')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('*emph*')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('one&&two')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('one!|two')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('one*two')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('one*')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('()')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('(one)*3')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('a:')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('a:b:c')
Traceback (most recent call last):
...
ParseError...
>>> pf.parseRequirement('where::capability')
Traceback (most recent call last):
...
ParseError...
4324 def parseSkillCombinationFromTokens( 4325 self, 4326 tokens: LexedTokens, 4327 start: int = 0, 4328 end: int = -1 4329 ) -> Union[base.Skill, base.SkillCombination]: 4330 """ 4331 Parses a skill combination from the specified range within the 4332 given tokens list. If just a single string token is selected, it 4333 will be returned as a `base.BestSkill` with just that skill 4334 inside. 4335 4336 For example: 4337 4338 >>> pf = ParseFormat() 4339 >>> pf.parseSkillCombinationFromTokens(['climbing']) 4340 BestSkill('climbing') 4341 >>> tokens = [ 4342 ... 'best', 4343 ... Lexeme.openParen, 4344 ... 'brains', 4345 ... Lexeme.sepOrDelay, 4346 ... 'brawn', 4347 ... Lexeme.closeParen, 4348 ... ] 4349 >>> pf.parseSkillCombinationFromTokens(tokens) 4350 BestSkill('brains', 'brawn') 4351 >>> tokens[2] = '3' # not a lexeme so it's a string 4352 >>> pf.parseSkillCombinationFromTokens(tokens) 4353 BestSkill(3, 'brawn') 4354 >>> tokens = [ 4355 ... Lexeme.wigglyLine, 4356 ... Lexeme.wigglyLine, 4357 ... 'yes', 4358 ... ] 4359 >>> pf.parseSkillCombinationFromTokens(tokens) 4360 InverseSkill(InverseSkill('yes')) 4361 """ 4362 start, end, nTokens = normalizeEnds(tokens, start, end) 4363 4364 first = tokens[start] 4365 if nTokens == 1: 4366 if isinstance(first, base.Skill): 4367 try: 4368 level = int(first) 4369 return base.BestSkill(level) 4370 except ValueError: 4371 return base.BestSkill(first) 4372 else: 4373 raise ParseError( 4374 "Invalid SkillCombination:\n{tokens[start:end + 1]" 4375 ) 4376 4377 if first == Lexeme.wigglyLine: 4378 inv = self.parseSkillCombinationFromTokens( 4379 tokens, 4380 start + 1, 4381 end 4382 ) 4383 if isinstance(inv, base.BestSkill) and len(inv.skills) == 1: 4384 return base.InverseSkill(inv.skills[0]) 4385 else: 4386 return base.InverseSkill(inv) 4387 4388 second = tokens[start + 1] 4389 if second != Lexeme.openParen: 4390 raise ParseError( 4391 f"Invalid SkillCombination (missing paren):" 4392 f"\n{tokens[start:end + 1]}" 4393 ) 4394 4395 parenEnd = self.matchingBrace( 4396 tokens, 4397 start + 1, 4398 Lexeme.openParen, 4399 Lexeme.closeParen 4400 ) 4401 if parenEnd != end: 4402 raise ParseError( 4403 f"Extra junk after SkillCombination:" 4404 f"\n{tokens[parenEnd + 1:end + 1]}" 4405 ) 4406 4407 if first == 'if': 4408 parts = list( 4409 findSeparatedParts( 4410 tokens, 4411 Lexeme.sepOrDelay, 4412 start + 2, 4413 end - 1, 4414 Lexeme.openParen, 4415 Lexeme.closeParen 4416 ) 4417 ) 4418 if len(parts) != 3: 4419 raise ParseError( 4420 f"Wrong number of parts for ConditionalSkill (needs" 4421 f" 3, got {len(parts)}:" 4422 f"\n{tokens[start + 2:end]}" 4423 ) 4424 reqStart, reqEnd = parts[0] 4425 ifStart, ifEnd = parts[1] 4426 elseStart, elseEnd = parts[2] 4427 return base.ConditionalSkill( 4428 self.parseRequirementFromTokens(tokens, reqStart, reqEnd), 4429 self.parseSkillCombinationFromTokens(tokens, ifStart, ifEnd), 4430 self.parseSkillCombinationFromTokens( 4431 tokens, 4432 elseStart, 4433 elseEnd 4434 ), 4435 ) 4436 elif first in ('sum', 'best', 'worst'): 4437 make: type[base.SkillCombination] 4438 if first == 'sum': 4439 make = base.CombinedSkill 4440 elif first == 'best': 4441 make = base.BestSkill 4442 else: 4443 make = base.WorstSkill 4444 4445 subs = [] 4446 for partStart, partEnd in findSeparatedParts( 4447 tokens, 4448 Lexeme.sepOrDelay, 4449 start + 2, 4450 end - 1, 4451 Lexeme.openParen, 4452 Lexeme.closeParen 4453 ): 4454 sub = self.parseSkillCombinationFromTokens( 4455 tokens, 4456 partStart, 4457 partEnd 4458 ) 4459 if ( 4460 isinstance(sub, base.BestSkill) 4461 and len(sub.skills) == 1 4462 ): 4463 subs.append(sub.skills[0]) 4464 else: 4465 subs.append(sub) 4466 4467 return make(*subs) 4468 else: 4469 raise ParseError( 4470 "Invalid SkillCombination:\n{tokens[start:end + 1]" 4471 )
Parses a skill combination from the specified range within the
given tokens list. If just a single string token is selected, it
will be returned as a base.BestSkill with just that skill
inside.
For example:
>>> pf = ParseFormat()
>>> pf.parseSkillCombinationFromTokens(['climbing'])
BestSkill('climbing')
>>> tokens = [
... 'best',
... Lexeme.openParen,
... 'brains',
... Lexeme.sepOrDelay,
... 'brawn',
... Lexeme.closeParen,
... ]
>>> pf.parseSkillCombinationFromTokens(tokens)
BestSkill('brains', 'brawn')
>>> tokens[2] = '3' # not a lexeme so it's a string
>>> pf.parseSkillCombinationFromTokens(tokens)
BestSkill(3, 'brawn')
>>> tokens = [
... Lexeme.wigglyLine,
... Lexeme.wigglyLine,
... 'yes',
... ]
>>> pf.parseSkillCombinationFromTokens(tokens)
InverseSkill(InverseSkill('yes'))
4473 def parseSkillCombination( 4474 self, 4475 encoded: str 4476 ) -> base.SkillCombination: 4477 """ 4478 Parses a `SkillCombination` from a string. Calls `lex` and then 4479 `parseSkillCombinationFromTokens`. 4480 """ 4481 result = self.parseSkillCombinationFromTokens( 4482 lex(encoded, self.reverseFormat) 4483 ) 4484 if not isinstance(result, base.SkillCombination): 4485 return base.BestSkill(result) 4486 else: 4487 return result
Parses a SkillCombination from a string. Calls lex and then
parseSkillCombinationFromTokens.
4489 def parseConditionFromTokens( 4490 self, 4491 tokens: LexedTokens, 4492 start: int = 0, 4493 end: int = -1 4494 ) -> base.Condition: 4495 """ 4496 Parses a `base.Condition` from a lexed tokens list. For example: 4497 4498 >>> pf = ParseFormat() 4499 >>> tokens = [ 4500 ... Lexeme.doubleQuestionmark, 4501 ... Lexeme.openParen, 4502 ... "fire", 4503 ... Lexeme.ampersand, 4504 ... "water", 4505 ... Lexeme.closeParen, 4506 ... Lexeme.openCurly, 4507 ... "gain", 4508 ... "wind", 4509 ... Lexeme.closeCurly, 4510 ... Lexeme.openCurly, 4511 ... Lexeme.closeCurly, 4512 ... ] 4513 >>> pf.parseConditionFromTokens(tokens) == base.condition( 4514 ... condition=base.ReqAll([ 4515 ... base.ReqCapability('fire'), 4516 ... base.ReqCapability('water') 4517 ... ]), 4518 ... consequence=[base.effect(gain='wind')] 4519 ... ) 4520 True 4521 """ 4522 start, end, nTokens = normalizeEnds(tokens, start, end) 4523 if nTokens < 8: 4524 raise ParseError( 4525 f"A Condition requires at least 8 tokens (got {nTokens})." 4526 ) 4527 if tokens[start] != Lexeme.doubleQuestionmark: 4528 raise ParseError( 4529 f"A Condition must start with" 4530 f" {repr(self.formatDict[Lexeme.doubleQuestionmark])}" 4531 ) 4532 try: 4533 consequenceStart = tokens.index(Lexeme.openCurly, start) 4534 except ValueError: 4535 raise ParseError("A condition must include a consequence block.") 4536 consequenceEnd = self.matchingBrace(tokens, consequenceStart) 4537 altStart = consequenceEnd + 1 4538 altEnd = self.matchingBrace(tokens, altStart) 4539 4540 if altEnd != end: 4541 raise ParseError( 4542 f"Junk after condition:\n{tokens[altEnd + 1: end + 1]}" 4543 ) 4544 4545 return base.condition( 4546 condition=self.parseRequirementFromTokens( 4547 tokens, 4548 start + 1, 4549 consequenceStart - 1 4550 ), 4551 consequence=self.parseConsequenceFromTokens( 4552 tokens, 4553 consequenceStart, 4554 consequenceEnd 4555 ), 4556 alternative=self.parseConsequenceFromTokens( 4557 tokens, 4558 altStart, 4559 altEnd 4560 ) 4561 )
Parses a base.Condition from a lexed tokens list. For example:
>>> pf = ParseFormat()
>>> tokens = [
... Lexeme.doubleQuestionmark,
... Lexeme.openParen,
... "fire",
... Lexeme.ampersand,
... "water",
... Lexeme.closeParen,
... Lexeme.openCurly,
... "gain",
... "wind",
... Lexeme.closeCurly,
... Lexeme.openCurly,
... Lexeme.closeCurly,
... ]
>>> pf.parseConditionFromTokens(tokens) == base.condition(
... condition=base.ReqAll([
... base.ReqCapability('fire'),
... base.ReqCapability('water')
... ]),
... consequence=[base.effect(gain='wind')]
... )
True
4563 def parseCondition( 4564 self, 4565 encoded: str 4566 ) -> base.Condition: 4567 """ 4568 Lexes the given string and then calls `parseConditionFromTokens` 4569 to return a `base.Condition`. 4570 """ 4571 return self.parseConditionFromTokens( 4572 lex(encoded, self.reverseFormat) 4573 )
Lexes the given string and then calls parseConditionFromTokens
to return a base.Condition.
4575 def parseChallengeFromTokens( 4576 self, 4577 tokens: LexedTokens, 4578 start: int = 0, 4579 end: int = -1 4580 ) -> base.Challenge: 4581 """ 4582 Parses a `base.Challenge` from a lexed tokens list. 4583 4584 For example: 4585 4586 >>> pf = ParseFormat() 4587 >>> tokens = [ 4588 ... Lexeme.angleLeft, 4589 ... '2', 4590 ... Lexeme.angleRight, 4591 ... 'best', 4592 ... Lexeme.openParen, 4593 ... "chess", 4594 ... Lexeme.sepOrDelay, 4595 ... "checkers", 4596 ... Lexeme.closeParen, 4597 ... Lexeme.openCurly, 4598 ... "gain", 4599 ... "coin", 4600 ... Lexeme.tokenCount, 4601 ... "5", 4602 ... Lexeme.closeCurly, 4603 ... Lexeme.angleRight, 4604 ... Lexeme.openCurly, 4605 ... "lose", 4606 ... "coin", 4607 ... Lexeme.tokenCount, 4608 ... "5", 4609 ... Lexeme.closeCurly, 4610 ... ] 4611 >>> c = pf.parseChallengeFromTokens(tokens) 4612 >>> c['skills'] == base.BestSkill('chess', 'checkers') 4613 True 4614 >>> c['level'] 4615 2 4616 >>> c['success'] == [base.effect(gain=('coin', 5))] 4617 True 4618 >>> c['failure'] == [base.effect(lose=('coin', 5))] 4619 True 4620 >>> c['outcome'] 4621 False 4622 >>> c == base.challenge( 4623 ... skills=base.BestSkill('chess', 'checkers'), 4624 ... level=2, 4625 ... success=[base.effect(gain=('coin', 5))], 4626 ... failure=[base.effect(lose=('coin', 5))], 4627 ... outcome=False 4628 ... ) 4629 True 4630 >>> t2 = ['hi'] + tokens + ['bye'] # parsing only part of the list 4631 >>> c == pf.parseChallengeFromTokens(t2, 1, -2) 4632 True 4633 """ 4634 start, end, nTokens = normalizeEnds(tokens, start, end) 4635 if nTokens < 8: 4636 raise ParseError( 4637 f"Not enough tokens for a challenge: {nTokens}" 4638 ) 4639 if tokens[start] != Lexeme.angleLeft: 4640 raise ParseError( 4641 f"Challenge must start with" 4642 f" {repr(self.formatDict[Lexeme.angleLeft])}" 4643 ) 4644 levelStr = tokens[start + 1] 4645 if isinstance(levelStr, Lexeme): 4646 raise ParseError( 4647 f"Challenge must start with a level in angle brackets" 4648 f" (got {repr(self.formatDict[levelStr])})." 4649 ) 4650 if tokens[start + 2] != Lexeme.angleRight: 4651 raise ParseError( 4652 f"Challenge must include" 4653 f" {repr(self.formatDict[Lexeme.angleRight])} after" 4654 f" the level." 4655 ) 4656 try: 4657 level = int(levelStr) 4658 except ValueError: 4659 raise ParseError( 4660 f"Challenge level must be an integer (got" 4661 f" {repr(tokens[start + 1])}." 4662 ) 4663 try: 4664 successStart = tokens.index(Lexeme.openCurly, start) 4665 skillsEnd = successStart - 1 4666 except ValueError: 4667 raise ParseError("A challenge must include a consequence block.") 4668 4669 outcome: Optional[bool] = None 4670 if tokens[skillsEnd] == Lexeme.angleRight: 4671 skillsEnd -= 1 4672 outcome = True 4673 successEnd = self.matchingBrace(tokens, successStart) 4674 failStart = successEnd + 1 4675 if tokens[failStart] == Lexeme.angleRight: 4676 failStart += 1 4677 if outcome is not None: 4678 raise ParseError( 4679 "Cannot indicate both success and failure as" 4680 " outcomes in a challenge." 4681 ) 4682 outcome = False 4683 failEnd = self.matchingBrace(tokens, failStart) 4684 4685 if failEnd != end: 4686 raise ParseError( 4687 f"Junk after condition:\n{tokens[failEnd + 1:end + 1]}" 4688 ) 4689 4690 skills = self.parseSkillCombinationFromTokens( 4691 tokens, 4692 start + 3, 4693 skillsEnd 4694 ) 4695 if isinstance(skills, base.Skill): 4696 skills = base.BestSkill(skills) 4697 4698 return base.challenge( 4699 level=level, 4700 outcome=outcome, 4701 skills=skills, 4702 success=self.parseConsequenceFromTokens( 4703 tokens[successStart:successEnd + 1] 4704 ), 4705 failure=self.parseConsequenceFromTokens( 4706 tokens[failStart:failEnd + 1] 4707 ) 4708 )
Parses a base.Challenge from a lexed tokens list.
For example:
>>> pf = ParseFormat()
>>> tokens = [
... Lexeme.angleLeft,
... '2',
... Lexeme.angleRight,
... 'best',
... Lexeme.openParen,
... "chess",
... Lexeme.sepOrDelay,
... "checkers",
... Lexeme.closeParen,
... Lexeme.openCurly,
... "gain",
... "coin",
... Lexeme.tokenCount,
... "5",
... Lexeme.closeCurly,
... Lexeme.angleRight,
... Lexeme.openCurly,
... "lose",
... "coin",
... Lexeme.tokenCount,
... "5",
... Lexeme.closeCurly,
... ]
>>> c = pf.parseChallengeFromTokens(tokens)
>>> c['skills'] == base.BestSkill('chess', 'checkers')
True
>>> c['level']
2
>>> c['success'] == [base.effect(gain=('coin', 5))]
True
>>> c['failure'] == [base.effect(lose=('coin', 5))]
True
>>> c['outcome']
False
>>> c == base.challenge(
... skills=base.BestSkill('chess', 'checkers'),
... level=2,
... success=[base.effect(gain=('coin', 5))],
... failure=[base.effect(lose=('coin', 5))],
... outcome=False
... )
True
>>> t2 = ['hi'] + tokens + ['bye'] # parsing only part of the list
>>> c == pf.parseChallengeFromTokens(t2, 1, -2)
True
4710 def parseChallenge( 4711 self, 4712 encoded: str 4713 ) -> base.Challenge: 4714 """ 4715 Lexes the given string and then calls `parseChallengeFromTokens` 4716 to return a `base.Challenge`. 4717 """ 4718 return self.parseChallengeFromTokens( 4719 lex(encoded, self.reverseFormat) 4720 )
Lexes the given string and then calls parseChallengeFromTokens
to return a base.Challenge.
4722 def parseConsequenceFromTokens( 4723 self, 4724 tokens: LexedTokens, 4725 start: int = 0, 4726 end: int = -1 4727 ) -> base.Consequence: 4728 """ 4729 Parses a consequence from a lexed token list. If start and/or end 4730 are specified, only processes the part of the list between those 4731 two indices (inclusive). Use `lex` to turn a string into a 4732 `LexedTokens` list (or use `ParseFormat.parseConsequence` which 4733 does that for you). 4734 4735 An example: 4736 4737 >>> pf = ParseFormat() 4738 >>> tokens = [ 4739 ... Lexeme.openCurly, 4740 ... 'gain', 4741 ... 'power', 4742 ... Lexeme.closeCurly 4743 ... ] 4744 >>> c = pf.parseConsequenceFromTokens(tokens) 4745 >>> c == [base.effect(gain='power')] 4746 True 4747 >>> tokens.append('hi') 4748 >>> c == pf.parseConsequenceFromTokens(tokens, end=-2) 4749 True 4750 >>> c == pf.parseConsequenceFromTokens(tokens, end=3) 4751 True 4752 """ 4753 start, end, nTokens = normalizeEnds(tokens, start, end) 4754 4755 if nTokens < 2: 4756 raise ParseError("Consequence must have at least two tokens.") 4757 4758 if tokens[start] != Lexeme.openCurly: 4759 raise ParseError( 4760 f"Consequence must start with an open curly brace:" 4761 f" {repr(self.formatDict[Lexeme.openCurly])}." 4762 ) 4763 4764 if tokens[end] != Lexeme.closeCurly: 4765 raise ParseError( 4766 f"Consequence must end with a closing curly brace:" 4767 f" {repr(self.formatDict[Lexeme.closeCurly])}." 4768 ) 4769 4770 if nTokens == 2: 4771 return [] 4772 4773 result: base.Consequence = [] 4774 for partStart, partEnd in findSeparatedParts( 4775 tokens, 4776 Lexeme.consequenceSeparator, 4777 start + 1, 4778 end - 1, 4779 Lexeme.openCurly, 4780 Lexeme.closeCurly 4781 ): 4782 if partEnd - partStart < 0: 4783 raise ParseError("Empty consequence part.") 4784 if tokens[partStart] == Lexeme.angleLeft: # a challenge 4785 result.append( 4786 self.parseChallengeFromTokens( 4787 tokens, 4788 partStart, 4789 partEnd 4790 ) 4791 ) 4792 elif tokens[partStart] == Lexeme.doubleQuestionmark: # condition 4793 result.append( 4794 self.parseConditionFromTokens( 4795 tokens, 4796 partStart, 4797 partEnd 4798 ) 4799 ) 4800 else: # Must be an effect 4801 result.append( 4802 self.parseEffectFromTokens( 4803 tokens, 4804 partStart, 4805 partEnd 4806 ) 4807 ) 4808 4809 return result
Parses a consequence from a lexed token list. If start and/or end
are specified, only processes the part of the list between those
two indices (inclusive). Use lex to turn a string into a
LexedTokens list (or use ParseFormat.parseConsequence which
does that for you).
An example:
>>> pf = ParseFormat()
>>> tokens = [
... Lexeme.openCurly,
... 'gain',
... 'power',
... Lexeme.closeCurly
... ]
>>> c = pf.parseConsequenceFromTokens(tokens)
>>> c == [base.effect(gain='power')]
True
>>> tokens.append('hi')
>>> c == pf.parseConsequenceFromTokens(tokens, end=-2)
True
>>> c == pf.parseConsequenceFromTokens(tokens, end=3)
True
4811 def parseConsequence(self, encoded: str) -> base.Consequence: 4812 """ 4813 Parses a consequence from a string. Uses `lex` and 4814 `ParseFormat.parseConsequenceFromTokens`. For example: 4815 4816 >>> pf = ParseFormat() 4817 >>> c = pf.parseConsequence( 4818 ... '{gain power}' 4819 ... ) 4820 >>> c == [base.effect(gain='power')] 4821 True 4822 >>> pf.unparseConsequence(c) 4823 '{gain power}' 4824 >>> c = pf.parseConsequence( 4825 ... '{\\n' 4826 ... ' ??(brawny|!weights*3){\\n' 4827 ... ' <3>sum(brains, brawn){goto home}>{bounce}\\n' 4828 ... ' }{};\\n' 4829 ... ' lose coin*1\\n' 4830 ... '}' 4831 ... ) 4832 >>> len(c) 4833 2 4834 >>> c[0]['condition'] == base.ReqAny([ 4835 ... base.ReqCapability('brawny'), 4836 ... base.ReqNot(base.ReqTokens('weights', 3)) 4837 ... ]) 4838 True 4839 >>> len(c[0]['consequence']) 4840 1 4841 >>> len(c[0]['alternative']) 4842 0 4843 >>> cons = c[0]['consequence'][0] 4844 >>> cons['skills'] == base.CombinedSkill('brains', 'brawn') 4845 True 4846 >>> cons['level'] 4847 3 4848 >>> len(cons['success']) 4849 1 4850 >>> len(cons['failure']) 4851 1 4852 >>> cons['success'][0] == base.effect(goto='home') 4853 True 4854 >>> cons['failure'][0] == base.effect(bounce=True) 4855 True 4856 >>> cons['outcome'] = False 4857 >>> c[0] == base.condition( 4858 ... condition=base.ReqAny([ 4859 ... base.ReqCapability('brawny'), 4860 ... base.ReqNot(base.ReqTokens('weights', 3)) 4861 ... ]), 4862 ... consequence=[ 4863 ... base.challenge( 4864 ... skills=base.CombinedSkill('brains', 'brawn'), 4865 ... level=3, 4866 ... success=[base.effect(goto='home')], 4867 ... failure=[base.effect(bounce=True)], 4868 ... outcome=False 4869 ... ) 4870 ... ] 4871 ... ) 4872 True 4873 >>> c[1] == base.effect(lose=('coin', 1)) 4874 True 4875 """ 4876 return self.parseConsequenceFromTokens( 4877 lex(encoded, self.reverseFormat) 4878 )
Parses a consequence from a string. Uses lex and
ParseFormat.parseConsequenceFromTokens. For example:
>>> pf = ParseFormat()
>>> c = pf.parseConsequence(
... '{gain power}'
... )
>>> c == [base.effect(gain='power')]
True
>>> pf.unparseConsequence(c)
'{gain power}'
>>> c = pf.parseConsequence(
... '{\n'
... ' ??(brawny|!weights*3){\n'
... ' <3>sum(brains, brawn){goto home}>{bounce}\n'
... ' }{};\n'
... ' lose coin*1\n'
... '}'
... )
>>> len(c)
2
>>> c[0]['condition'] == base.ReqAny([
... base.ReqCapability('brawny'),
... base.ReqNot(base.ReqTokens('weights', 3))
... ])
True
>>> len(c[0]['consequence'])
1
>>> len(c[0]['alternative'])
0
>>> cons = c[0]['consequence'][0]
>>> cons['skills'] == base.CombinedSkill('brains', 'brawn')
True
>>> cons['level']
3
>>> len(cons['success'])
1
>>> len(cons['failure'])
1
>>> cons['success'][0] == base.effect(goto='home')
True
>>> cons['failure'][0] == base.effect(bounce=True)
True
>>> cons['outcome'] = False
>>> c[0] == base.condition(
... condition=base.ReqAny([
... base.ReqCapability('brawny'),
... base.ReqNot(base.ReqTokens('weights', 3))
... ]),
... consequence=[
... base.challenge(
... skills=base.CombinedSkill('brains', 'brawn'),
... level=3,
... success=[base.effect(goto='home')],
... failure=[base.effect(bounce=True)],
... outcome=False
... )
... ]
... )
True
>>> c[1] == base.effect(lose=('coin', 1))
True
4885class ParsedDotGraph(TypedDict): 4886 """ 4887 Represents a parsed `graphviz` dot-format graph consisting of nodes, 4888 edges, and subgraphs, with attributes attached to nodes and/or 4889 edges. An intermediate format during conversion to a full 4890 `DecisionGraph`. Includes the following slots: 4891 4892 - `'nodes'`: A list of tuples each holding a node ID followed by a 4893 list of name/value attribute pairs. 4894 - `'edges'`: A list of tuples each holding a from-ID, a to-ID, 4895 and then a list of name/value attribute pairs. 4896 - `'attrs'`: A list of tuples each holding a name/value attribute 4897 pair for graph-level attributes. 4898 - `'subgraphs'`: A list of subgraphs (each a tuple with a subgraph 4899 name and then another dictionary in the same format as this 4900 one). 4901 """ 4902 nodes: List[Tuple[int, List[Tuple[str, str]]]] 4903 edges: List[Tuple[int, int, List[Tuple[str, str]]]] 4904 attrs: List[Tuple[str, str]] 4905 subgraphs: List[Tuple[str, 'ParsedDotGraph']]
Represents a parsed graphviz dot-format graph consisting of nodes,
edges, and subgraphs, with attributes attached to nodes and/or
edges. An intermediate format during conversion to a full
DecisionGraph. Includes the following slots:
'nodes': A list of tuples each holding a node ID followed by a list of name/value attribute pairs.'edges': A list of tuples each holding a from-ID, a to-ID, and then a list of name/value attribute pairs.'attrs': A list of tuples each holding a name/value attribute pair for graph-level attributes.'subgraphs': A list of subgraphs (each a tuple with a subgraph name and then another dictionary in the same format as this one).
4908def parseSimpleDotAttrs(fragment: str) -> List[Tuple[str, str]]: 4909 """ 4910 Given a string fragment that starts with '[' and ends with ']', 4911 parses a simple attribute list in `graphviz` dot format from that 4912 fragment, returning a list of name/value attribute tuples. Raises a 4913 `DotParseError` if the fragment doesn't have the right format. 4914 4915 Examples: 4916 4917 >>> parseSimpleDotAttrs('[ name=value ]') 4918 [('name', 'value')] 4919 >>> parseSimpleDotAttrs('[ a=b c=d e=f ]') 4920 [('a', 'b'), ('c', 'd'), ('e', 'f')] 4921 >>> parseSimpleDotAttrs('[ a=b "c d"="e f" ]') 4922 [('a', 'b'), ('c d', 'e f')] 4923 >>> parseSimpleDotAttrs('[a=b "c d"="e f"]') 4924 [('a', 'b'), ('c d', 'e f')] 4925 >>> parseSimpleDotAttrs('[ a=b "c d"="e f"') 4926 Traceback (most recent call last): 4927 ... 4928 exploration.parsing.DotParseError... 4929 >>> parseSimpleDotAttrs('a=b "c d"="e f" ]') 4930 Traceback (most recent call last): 4931 ... 4932 exploration.parsing.DotParseError... 4933 >>> parseSimpleDotAttrs('[ a b=c ]') 4934 Traceback (most recent call last): 4935 ... 4936 exploration.parsing.DotParseError... 4937 >>> parseSimpleDotAttrs('[ a=b c ]') 4938 Traceback (most recent call last): 4939 ... 4940 exploration.parsing.DotParseError... 4941 >>> parseSimpleDotAttrs('[ name="value" ]') 4942 [('name', 'value')] 4943 >>> parseSimpleDotAttrs('[ name="\\\\"value\\\\"" ]') 4944 [('name', '"value"')] 4945 """ 4946 if not fragment.startswith('[') or not fragment.endswith(']'): 4947 raise DotParseError( 4948 f"Simple attrs fragment missing delimiters:" 4949 f"\n {repr(fragment)}" 4950 ) 4951 result = [] 4952 rest = fragment[1:-1].strip() 4953 while rest: 4954 # Get possibly-quoted attribute name: 4955 if rest.startswith('"'): 4956 try: 4957 aName, rest = utils.unquoted(rest) 4958 except ValueError: 4959 raise DotParseError( 4960 f"Malformed quoted attribute name in" 4961 f" fragment:\n {repr(fragment)}" 4962 ) 4963 rest = rest.lstrip() 4964 if not rest.startswith('='): 4965 raise DotParseError( 4966 f"Missing '=' in attribute block in" 4967 f" fragment:\n {repr(fragment)}" 4968 ) 4969 rest = rest[1:].lstrip() 4970 else: 4971 try: 4972 eqInd = rest.index('=') 4973 except ValueError: 4974 raise DotParseError( 4975 f"Missing '=' in attribute block in" 4976 f" fragment:\n {repr(fragment)}" 4977 ) 4978 aName = rest[:eqInd] 4979 if ' ' in aName: 4980 raise DotParseError( 4981 f"Malformed unquoted attribute name" 4982 f" {repr(aName)} in fragment:" 4983 f"\n {repr(fragment)}" 4984 ) 4985 rest = rest[eqInd + 1:].lstrip() 4986 4987 # Get possibly-quoted attribute value: 4988 if rest.startswith('"'): 4989 try: 4990 aVal, rest = utils.unquoted(rest) 4991 except ValueError: 4992 raise DotParseError( 4993 f"Malformed quoted attribute value in" 4994 f" fragment:\n {repr(fragment)}" 4995 ) 4996 rest = rest.lstrip() 4997 else: 4998 try: 4999 spInd = rest.index(' ') 5000 except ValueError: 5001 spInd = len(rest) 5002 aVal = rest[:spInd] 5003 rest = rest[spInd:].lstrip() 5004 5005 # Append this attribute pair and continue parsing 5006 result.append((aName, aVal)) 5007 5008 return result
Given a string fragment that starts with '[' and ends with ']',
parses a simple attribute list in graphviz dot format from that
fragment, returning a list of name/value attribute tuples. Raises a
DotParseError if the fragment doesn't have the right format.
Examples:
>>> parseSimpleDotAttrs('[ name=value ]')
[('name', 'value')]
>>> parseSimpleDotAttrs('[ a=b c=d e=f ]')
[('a', 'b'), ('c', 'd'), ('e', 'f')]
>>> parseSimpleDotAttrs('[ a=b "c d"="e f" ]')
[('a', 'b'), ('c d', 'e f')]
>>> parseSimpleDotAttrs('[a=b "c d"="e f"]')
[('a', 'b'), ('c d', 'e f')]
>>> parseSimpleDotAttrs('[ a=b "c d"="e f"')
Traceback (most recent call last):
...
DotParseError...
>>> parseSimpleDotAttrs('a=b "c d"="e f" ]')
Traceback (most recent call last):
...
DotParseError...
>>> parseSimpleDotAttrs('[ a b=c ]')
Traceback (most recent call last):
...
DotParseError...
>>> parseSimpleDotAttrs('[ a=b c ]')
Traceback (most recent call last):
...
DotParseError...
>>> parseSimpleDotAttrs('[ name="value" ]')
[('name', 'value')]
>>> parseSimpleDotAttrs('[ name="\\"value\\"" ]')
[('name', '"value"')]
5011def parseDotNode( 5012 nodeLine: str 5013) -> Tuple[int, Union[bool, List[Tuple[str, str]]]]: 5014 """ 5015 Given a line of text from a `graphviz` dot-format graph 5016 (possibly ending in an '[' to indicate attributes to follow, or 5017 possible including a '[ ... ]' block with attributes in-line), 5018 parses it as a node declaration, returning the ID of the node, 5019 along with a boolean indicating whether attributes follow or 5020 not. If an inline attribute block is present, the second member 5021 of the tuple will be a list of attribute name/value pairs. In 5022 that case, all attribute names and values must either be quoted 5023 or not include spaces. 5024 Examples: 5025 5026 >>> parseDotNode('1') 5027 (1, False) 5028 >>> parseDotNode(' 1 [ ') 5029 (1, True) 5030 >>> parseDotNode(' 1 [ a=b "c d"="e f" ] ') 5031 (1, [('a', 'b'), ('c d', 'e f')]) 5032 >>> parseDotNode(' 3 [ name="A = \\\\"grate:open\\\\"" ]') 5033 (3, [('name', 'A = "grate:open"')]) 5034 >>> parseDotNode(' "1"[') 5035 (1, True) 5036 >>> parseDotNode(' 100[') 5037 (100, True) 5038 >>> parseDotNode(' 1 2') 5039 Traceback (most recent call last): 5040 ... 5041 exploration.parsing.DotParseError... 5042 >>> parseDotNode(' 1 [ 2') 5043 Traceback (most recent call last): 5044 ... 5045 exploration.parsing.DotParseError... 5046 >>> parseDotNode(' 1 2') 5047 Traceback (most recent call last): 5048 ... 5049 exploration.parsing.DotParseError... 5050 >>> parseDotNode(' 1 [ junk not=attrs ]') 5051 Traceback (most recent call last): 5052 ... 5053 exploration.parsing.DotParseError... 5054 >>> parseDotNode(' \\n') 5055 Traceback (most recent call last): 5056 ... 5057 exploration.parsing.DotParseError... 5058 """ 5059 stripped = nodeLine.strip() 5060 if len(stripped) == 0: 5061 raise DotParseError( 5062 "Empty node in dot graph on line:\n {repr(nodeLine)}" 5063 ) 5064 hasAttrs: Union[bool, List[Tuple[str, str]]] = False 5065 if stripped.startswith('"'): 5066 nodeName, rest = utils.unquoted(stripped) 5067 rest = rest.strip() 5068 if rest == '[': 5069 hasAttrs = True 5070 elif rest.startswith('[') and rest.endswith(']'): 5071 hasAttrs = parseSimpleDotAttrs(rest) 5072 elif rest: 5073 raise DotParseError( 5074 f"Extra junk {repr(rest)} after node on line:" 5075 f"\n {repr(nodeLine)}" 5076 ) 5077 5078 else: 5079 if stripped.endswith('['): 5080 hasAttrs = True 5081 stripped = stripped[:-1].rstrip() 5082 elif stripped.endswith(']'): 5083 try: 5084 # TODO: Why did this used to be rindex? Was that 5085 # important in some case? (That doesn't work since the 5086 # value may contain a quoted open bracket). 5087 attrStart = stripped.index('[') 5088 except ValueError: 5089 raise DotParseError( 5090 f"Unmatched ']' on line:\n {repr(nodeLine)}" 5091 ) 5092 hasAttrs = parseSimpleDotAttrs( 5093 stripped[attrStart:] 5094 ) 5095 stripped = stripped[:attrStart].rstrip() 5096 5097 if ' ' in stripped: 5098 raise DotParseError( 5099 f"Unquoted multi-word node on line:\n {repr(nodeLine)}" 5100 ) 5101 else: 5102 nodeName = stripped 5103 5104 try: 5105 nodeID = int(nodeName) 5106 except ValueError: 5107 raise DotParseError( 5108 f"Node name f{repr(nodeName)} is not an integer on" 5109 f" line:\n {repr(nodeLine)}" 5110 ) 5111 5112 return (nodeID, hasAttrs)
Given a line of text from a graphviz dot-format graph
(possibly ending in an '[' to indicate attributes to follow, or
possible including a '[ ... ]' block with attributes in-line),
parses it as a node declaration, returning the ID of the node,
along with a boolean indicating whether attributes follow or
not. If an inline attribute block is present, the second member
of the tuple will be a list of attribute name/value pairs. In
that case, all attribute names and values must either be quoted
or not include spaces.
Examples:
>>> parseDotNode('1')
(1, False)
>>> parseDotNode(' 1 [ ')
(1, True)
>>> parseDotNode(' 1 [ a=b "c d"="e f" ] ')
(1, [('a', 'b'), ('c d', 'e f')])
>>> parseDotNode(' 3 [ name="A = \\"grate:open\\"" ]')
(3, [('name', 'A = "grate:open"')])
>>> parseDotNode(' "1"[')
(1, True)
>>> parseDotNode(' 100[')
(100, True)
>>> parseDotNode(' 1 2')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotNode(' 1 [ 2')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotNode(' 1 2')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotNode(' 1 [ junk not=attrs ]')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotNode(' \n')
Traceback (most recent call last):
...
DotParseError...
5115def parseDotAttr(attrLine: str) -> Tuple[str, str]: 5116 """ 5117 Given a line of text from a `graphviz` dot-format graph, parses 5118 it as an attribute (maybe-quoted-attr-name = 5119 maybe-quoted-attr-value). Returns the (maybe-unquoted) attr-name 5120 and the (maybe-unquoted) attr-value as a pair of strings. Raises 5121 a `DotParseError` if the line cannot be parsed as an attribute. 5122 Examples: 5123 5124 >>> parseDotAttr("a=b") 5125 ('a', 'b') 5126 >>> parseDotAttr(" a = b ") 5127 ('a', 'b') 5128 >>> parseDotAttr('"a" = "b"') 5129 ('a', 'b') 5130 >>> parseDotAttr('"a" -> "b"') 5131 Traceback (most recent call last): 5132 ... 5133 exploration.parsing.DotParseError... 5134 >>> parseDotAttr('"a" = "b" c') 5135 Traceback (most recent call last): 5136 ... 5137 exploration.parsing.DotParseError... 5138 >>> parseDotAttr('a') 5139 Traceback (most recent call last): 5140 ... 5141 exploration.parsing.DotParseError... 5142 >>> parseDotAttr('') 5143 Traceback (most recent call last): 5144 ... 5145 exploration.parsing.DotParseError... 5146 >>> parseDotAttr('0 [ name="A" ]') 5147 Traceback (most recent call last): 5148 ... 5149 exploration.parsing.DotParseError... 5150 """ 5151 stripped = attrLine.lstrip() 5152 if len(stripped) == 0: 5153 raise DotParseError( 5154 "Empty attribute in dot graph on line:\n {repr(attrLine)}" 5155 ) 5156 if stripped.endswith(']') or stripped.endswith('['): 5157 raise DotParseError( 5158 f"Node attribute ends in '[' or ']' on line:" 5159 f"\n {repr(attrLine)}" 5160 ) 5161 if stripped.startswith('"'): 5162 try: 5163 attrName, rest = utils.unquoted(stripped) 5164 except ValueError: 5165 raise DotParseError( 5166 f"Unmatched quotes in line:\n {repr(attrLine)}" 5167 ) 5168 rest = rest.lstrip() 5169 if len(rest) == 0 or rest[0] != '=': 5170 raise DotParseError( 5171 f"No equals sign following attribute name on" 5172 f" line:\n {repr(attrLine)}" 5173 ) 5174 rest = rest[1:].lstrip() 5175 else: 5176 try: 5177 eqInd = stripped.index('=') 5178 except ValueError: 5179 raise DotParseError( 5180 f"No equals sign in attribute line:" 5181 f"\n {repr(attrLine)}" 5182 ) 5183 attrName = stripped[:eqInd].rstrip() 5184 rest = stripped[eqInd + 1:].lstrip() 5185 5186 if rest[0] == '"': 5187 try: 5188 attrVal, rest = utils.unquoted(rest) 5189 except ValueError: 5190 raise DotParseError( 5191 f"Unmatched quotes in line:\n {repr(attrLine)}" 5192 ) 5193 if rest.strip(): 5194 raise DotParseError( 5195 f"Junk after attribute on line:" 5196 f"\n {repr(attrLine)}" 5197 ) 5198 else: 5199 attrVal = rest.rstrip() 5200 5201 return attrName, attrVal
Given a line of text from a graphviz dot-format graph, parses
it as an attribute (maybe-quoted-attr-name =
maybe-quoted-attr-value). Returns the (maybe-unquoted) attr-name
and the (maybe-unquoted) attr-value as a pair of strings. Raises
a DotParseError if the line cannot be parsed as an attribute.
Examples:
>>> parseDotAttr("a=b")
('a', 'b')
>>> parseDotAttr(" a = b ")
('a', 'b')
>>> parseDotAttr('"a" = "b"')
('a', 'b')
>>> parseDotAttr('"a" -> "b"')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotAttr('"a" = "b" c')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotAttr('a')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotAttr('')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotAttr('0 [ name="A" ]')
Traceback (most recent call last):
...
DotParseError...
5204def parseDotEdge(edgeLine: str) -> Tuple[int, int, bool]: 5205 """ 5206 Given a line of text from a `graphviz` dot-format graph, parses 5207 it as an edge (fromID -> toID). Returns a tuple containing the 5208 from ID, the to ID, and a boolean indicating whether attributes 5209 follow the edge on subsequent lines (true if the line ends with 5210 '['). Raises a `DotParseError` if the line cannot be parsed as 5211 an edge pair. Examples: 5212 5213 >>> parseDotEdge("1 -> 2") 5214 (1, 2, False) 5215 >>> parseDotEdge(" 1 -> 2 ") 5216 (1, 2, False) 5217 >>> parseDotEdge('"1" -> "2"') 5218 (1, 2, False) 5219 >>> parseDotEdge('"1" -> "2" [') 5220 (1, 2, True) 5221 >>> parseDotEdge("a -> b") 5222 Traceback (most recent call last): 5223 ... 5224 exploration.parsing.DotParseError... 5225 >>> parseDotEdge('"1" = "1"') 5226 Traceback (most recent call last): 5227 ... 5228 exploration.parsing.DotParseError... 5229 >>> parseDotEdge('"1" -> "2" c') 5230 Traceback (most recent call last): 5231 ... 5232 exploration.parsing.DotParseError... 5233 >>> parseDotEdge('1') 5234 Traceback (most recent call last): 5235 ... 5236 exploration.parsing.DotParseError... 5237 >>> parseDotEdge('') 5238 Traceback (most recent call last): 5239 ... 5240 exploration.parsing.DotParseError... 5241 """ 5242 stripped = edgeLine.lstrip() 5243 if len(stripped) == 0: 5244 raise DotParseError( 5245 "Empty edge in dot graph on line:\n {repr(edgeLine)}" 5246 ) 5247 if stripped.startswith('"'): 5248 try: 5249 fromStr, rest = utils.unquoted(stripped) 5250 except ValueError: 5251 raise DotParseError( 5252 f"Unmatched quotes in line:\n {repr(edgeLine)}" 5253 ) 5254 rest = rest.lstrip() 5255 if rest[:2] != '->': 5256 raise DotParseError( 5257 f"No arrow sign following source name on" 5258 f" line:\n {repr(edgeLine)}" 5259 ) 5260 rest = rest[2:].lstrip() 5261 else: 5262 try: 5263 arrowInd = stripped.index('->') 5264 except ValueError: 5265 raise DotParseError( 5266 f"No arrow in edge line:" 5267 f"\n {repr(edgeLine)}" 5268 ) 5269 fromStr = stripped[:arrowInd].rstrip() 5270 rest = stripped[arrowInd + 2:].lstrip() 5271 if ' ' in fromStr: 5272 raise DotParseError( 5273 f"Unquoted multi-word edge source on line:" 5274 f"\n {repr(edgeLine)}" 5275 ) 5276 5277 hasAttrs = False 5278 if rest[0] == '"': 5279 try: 5280 toStr, rest = utils.unquoted(rest) 5281 except ValueError: 5282 raise DotParseError( 5283 f"Unmatched quotes in line:\n {repr(edgeLine)}" 5284 ) 5285 stripped = rest.strip() 5286 if stripped == '[': 5287 hasAttrs = True 5288 elif stripped: 5289 raise DotParseError( 5290 f"Junk after edge on line:" 5291 f"\n {repr(edgeLine)}" 5292 ) 5293 else: 5294 toStr = rest.rstrip() 5295 if toStr.endswith('['): 5296 toStr = toStr[:-1].rstrip() 5297 hasAttrs = True 5298 if ' ' in toStr: 5299 raise DotParseError( 5300 f"Unquoted multi-word edge destination on line:" 5301 f"\n {repr(edgeLine)}" 5302 ) 5303 5304 try: 5305 fromID = int(fromStr) 5306 except ValueError: 5307 raise DotParseError( 5308 f"Invalid 'from' ID: {repr(fromStr)} on line:" 5309 f"\n {repr(edgeLine)}" 5310 ) 5311 5312 try: 5313 toID = int(toStr) 5314 except ValueError: 5315 raise DotParseError( 5316 f"Invalid 'to' ID: {repr(toStr)} on line:" 5317 f"\n {repr(edgeLine)}" 5318 ) 5319 5320 return (fromID, toID, hasAttrs)
Given a line of text from a graphviz dot-format graph, parses
it as an edge (fromID -> toID). Returns a tuple containing the
from ID, the to ID, and a boolean indicating whether attributes
follow the edge on subsequent lines (true if the line ends with
'['). Raises a DotParseError if the line cannot be parsed as
an edge pair. Examples:
>>> parseDotEdge("1 -> 2")
(1, 2, False)
>>> parseDotEdge(" 1 -> 2 ")
(1, 2, False)
>>> parseDotEdge('"1" -> "2"')
(1, 2, False)
>>> parseDotEdge('"1" -> "2" [')
(1, 2, True)
>>> parseDotEdge("a -> b")
Traceback (most recent call last):
...
DotParseError...
>>> parseDotEdge('"1" = "1"')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotEdge('"1" -> "2" c')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotEdge('1')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotEdge('')
Traceback (most recent call last):
...
DotParseError...
5323def parseDotAttrList( 5324 lines: List[str] 5325) -> Tuple[List[Tuple[str, str]], List[str]]: 5326 """ 5327 Given a list of lines of text from a `graphviz` dot-format 5328 graph which starts with an attribute line, parses multiple 5329 attribute lines until a line containing just ']' is found. 5330 Returns a list of the parsed name/value attribute pair tuples, 5331 along with a list of remaining unparsed strings (not counting 5332 the closing ']' line). Raises a `DotParseError` if it finds a 5333 non-attribute line or if it fails to find a closing ']' line. 5334 Examples: 5335 5336 >>> parseDotAttrList([ 5337 ... 'a=b\\n', 5338 ... 'c=d\\n', 5339 ... ']\\n', 5340 ... ]) 5341 ([('a', 'b'), ('c', 'd')], []) 5342 >>> parseDotAttrList([ 5343 ... 'a=b', 5344 ... 'c=d', 5345 ... ' ]', 5346 ... 'more', 5347 ... 'lines', 5348 ... ]) 5349 ([('a', 'b'), ('c', 'd')], ['more', 'lines']) 5350 >>> parseDotAttrList([ 5351 ... 'a=b', 5352 ... 'c=d', 5353 ... ]) 5354 Traceback (most recent call last): 5355 ... 5356 exploration.parsing.DotParseError... 5357 """ 5358 index = 0 5359 found = [] 5360 while index < len(lines): 5361 thisLine = lines[index] 5362 try: 5363 found.append(parseDotAttr(thisLine)) 5364 except DotParseError: 5365 if thisLine.strip() == ']': 5366 return (found, lines[index + 1:]) 5367 else: 5368 raise DotParseError( 5369 f"Could not parse attribute from line:" 5370 f"\n {repr(thisLine)}" 5371 f"\nAttributes block starts on line:" 5372 f"\n {repr(lines[0])}" 5373 ) 5374 index += 1 5375 5376 raise DotParseError( 5377 f"No list terminator (']') for attributes starting on line:" 5378 f"\n {repr(lines[0])}" 5379 )
Given a list of lines of text from a graphviz dot-format
graph which starts with an attribute line, parses multiple
attribute lines until a line containing just ']' is found.
Returns a list of the parsed name/value attribute pair tuples,
along with a list of remaining unparsed strings (not counting
the closing ']' line). Raises a DotParseError if it finds a
non-attribute line or if it fails to find a closing ']' line.
Examples:
>>> parseDotAttrList([
... 'a=b\n',
... 'c=d\n',
... ']\n',
... ])
([('a', 'b'), ('c', 'd')], [])
>>> parseDotAttrList([
... 'a=b',
... 'c=d',
... ' ]',
... 'more',
... 'lines',
... ])
([('a', 'b'), ('c', 'd')], ['more', 'lines'])
>>> parseDotAttrList([
... 'a=b',
... 'c=d',
... ])
Traceback (most recent call last):
...
DotParseError...
5382def parseDotSubgraphStart(line: str) -> str: 5383 """ 5384 Parses the start of a subgraph from a line of a graph file. The 5385 line must start with the word 'subgraph' and then have a name, 5386 followed by a '{' at the end of the line. Raises a 5387 `DotParseError` if this format doesn't match. Examples: 5388 5389 >>> parseDotSubgraphStart('subgraph A {') 5390 'A' 5391 >>> parseDotSubgraphStart('subgraph A B {') 5392 Traceback (most recent call last): 5393 ... 5394 exploration.parsing.DotParseError... 5395 >>> parseDotSubgraphStart('subgraph "A B" {') 5396 'A B' 5397 >>> parseDotSubgraphStart('subgraph A') 5398 Traceback (most recent call last): 5399 ... 5400 exploration.parsing.DotParseError... 5401 """ 5402 stripped = line.strip() 5403 if len(stripped) == 0: 5404 raise DotParseError( 5405 f"Empty line where subgraph was expected:" 5406 f"\n {repr(line)}" 5407 ) 5408 5409 if not stripped.startswith('subgraph '): 5410 raise DotParseError( 5411 f"Subgraph doesn't start with 'subgraph' on line:" 5412 f"\n {repr(line)}" 5413 ) 5414 5415 stripped = stripped[9:] 5416 if stripped.startswith('"'): 5417 try: 5418 name, rest = utils.unquoted(stripped) 5419 except ValueError: 5420 raise DotParseError( 5421 f"Malformed quotes on subgraph line:\n {repr(line)}" 5422 ) 5423 if rest.strip() != '{': 5424 raise DotParseError( 5425 f"Junk or missing '{{' on subgraph line:\n {repr(line)}" 5426 ) 5427 else: 5428 parts = stripped.split() 5429 if len(parts) != 2 or parts[1] != '{': 5430 raise DotParseError( 5431 f"Junk or missing '{{' on subgraph line:\n {repr(line)}" 5432 ) 5433 name, _ = parts 5434 5435 return name
Parses the start of a subgraph from a line of a graph file. The
line must start with the word 'subgraph' and then have a name,
followed by a '{' at the end of the line. Raises a
DotParseError if this format doesn't match. Examples:
>>> parseDotSubgraphStart('subgraph A {')
'A'
>>> parseDotSubgraphStart('subgraph A B {')
Traceback (most recent call last):
...
DotParseError...
>>> parseDotSubgraphStart('subgraph "A B" {')
'A B'
>>> parseDotSubgraphStart('subgraph A')
Traceback (most recent call last):
...
DotParseError...
5438def parseDotGraphContents( 5439 lines: List[str] 5440) -> Tuple[ParsedDotGraph, List[str]]: 5441 """ 5442 Given a list of lines from a `graphviz` dot-format string, 5443 parses the list as the contents of a graph (or subgraph), 5444 stopping when it reaches a line that just contains '}'. Raises a 5445 `DotParseError` if it cannot do so or if the terminator is 5446 missing. Returns a tuple containing the parsed graph data (see 5447 `ParsedDotGraph` and the list of remaining lines after the 5448 terminator. Recursively parses subgraphs. Example: 5449 5450 >>> bits = parseDotGraphContents([ 5451 ... '"graph attr"=1', 5452 ... '1 [', 5453 ... ' attr=value', 5454 ... ']', 5455 ... '1 -> 2 [', 5456 ... ' fullLabel="to_B"', 5457 ... ' quality=number', 5458 ... ']', 5459 ... 'subgraph name {', 5460 ... ' 300', 5461 ... ' 400', 5462 ... ' 300 -> 400 [', 5463 ... ' fullLabel=forward', 5464 ... ' ]', 5465 ... '}', 5466 ... '}', 5467 ... ]) 5468 >>> len(bits) 5469 2 5470 >>> g = bits[0] 5471 >>> bits[1] 5472 [] 5473 >>> sorted(g.keys()) 5474 ['attrs', 'edges', 'nodes', 'subgraphs'] 5475 >>> g['nodes'] 5476 [(1, [('attr', 'value')])] 5477 >>> g['edges'] 5478 [(1, 2, [('fullLabel', 'to_B'), ('quality', 'number')])] 5479 >>> g['attrs'] 5480 [('graph attr', '1')] 5481 >>> sgs = g['subgraphs'] 5482 >>> len(sgs) 5483 1 5484 >>> len(sgs[0]) 5485 2 5486 >>> sgs[0][0] 5487 'name' 5488 >>> sg = sgs[0][1] 5489 >>> sorted(sg.keys()) 5490 ['attrs', 'edges', 'nodes', 'subgraphs'] 5491 >>> sg["nodes"] 5492 [(300, []), (400, [])] 5493 >>> sg["edges"] 5494 [(300, 400, [('fullLabel', 'forward')])] 5495 >>> sg["attrs"] 5496 [] 5497 >>> sg["subgraphs"] 5498 [] 5499 """ 5500 result: ParsedDotGraph = { 5501 'nodes': [], 5502 'edges': [], 5503 'attrs': [], 5504 'subgraphs': [], 5505 } 5506 index = 0 5507 remainder = None 5508 # Consider each line: 5509 while index < len(lines): 5510 # Grab line and pre-increment index 5511 thisLine = lines[index] 5512 index += 1 5513 5514 # Check for } first because it could be parsed as a node 5515 stripped = thisLine.strip() 5516 if stripped == '}': 5517 remainder = lines[index:] 5518 break 5519 elif stripped == '': # ignore blank lines 5520 continue 5521 5522 # Cascading parsing attempts, since the possibilities are 5523 # mostly mutually exclusive. 5524 # TODO: Node/attr confusion with = in a node name? 5525 try: 5526 attrName, attrVal = parseDotAttr(thisLine) 5527 result['attrs'].append((attrName, attrVal)) 5528 except DotParseError: 5529 try: 5530 fromNode, toNode, hasEAttrs = parseDotEdge( 5531 thisLine 5532 ) 5533 if hasEAttrs: 5534 attrs, rest = parseDotAttrList( 5535 lines[index:] 5536 ) 5537 # Restart to process rest 5538 lines = rest 5539 index = 0 5540 else: 5541 attrs = [] 5542 result['edges'].append((fromNode, toNode, attrs)) 5543 except DotParseError: 5544 try: 5545 nodeName, hasNAttrs = parseDotNode( 5546 thisLine 5547 ) 5548 if hasNAttrs is True: 5549 attrs, rest = parseDotAttrList( 5550 lines[index:] 5551 ) 5552 # Restart to process rest 5553 lines = rest 5554 index = 0 5555 elif hasNAttrs: 5556 attrs = hasNAttrs 5557 else: 5558 attrs = [] 5559 result['nodes'].append((nodeName, attrs)) 5560 except DotParseError: 5561 try: 5562 subName = parseDotSubgraphStart( 5563 thisLine 5564 ) 5565 subStuff, rest = \ 5566 parseDotGraphContents( 5567 lines[index:] 5568 ) 5569 result['subgraphs'].append((subName, subStuff)) 5570 # Restart to process rest 5571 lines = rest 5572 index = 0 5573 except DotParseError: 5574 raise DotParseError( 5575 f"Unrecognizable graph line (possibly" 5576 f" beginning of unfinished structure):" 5577 f"\n {repr(thisLine)}" 5578 ) 5579 if remainder is None: 5580 raise DotParseError( 5581 f"Graph (or subgraph) is missing closing '}}'. Starts" 5582 f" on line:\n {repr(lines[0])}" 5583 ) 5584 else: 5585 return (result, remainder)
Given a list of lines from a graphviz dot-format string,
parses the list as the contents of a graph (or subgraph),
stopping when it reaches a line that just contains '}'. Raises a
DotParseError if it cannot do so or if the terminator is
missing. Returns a tuple containing the parsed graph data (see
ParsedDotGraph and the list of remaining lines after the
terminator. Recursively parses subgraphs. Example:
>>> bits = parseDotGraphContents([
... '"graph attr"=1',
... '1 [',
... ' attr=value',
... ']',
... '1 -> 2 [',
... ' fullLabel="to_B"',
... ' quality=number',
... ']',
... 'subgraph name {',
... ' 300',
... ' 400',
... ' 300 -> 400 [',
... ' fullLabel=forward',
... ' ]',
... '}',
... '}',
... ])
>>> len(bits)
2
>>> g = bits[0]
>>> bits[1]
[]
>>> sorted(g.keys())
['attrs', 'edges', 'nodes', 'subgraphs']
>>> g['nodes']
[(1, [('attr', 'value')])]
>>> g['edges']
[(1, 2, [('fullLabel', 'to_B'), ('quality', 'number')])]
>>> g['attrs']
[('graph attr', '1')]
>>> sgs = g['subgraphs']
>>> len(sgs)
1
>>> len(sgs[0])
2
>>> sgs[0][0]
'name'
>>> sg = sgs[0][1]
>>> sorted(sg.keys())
['attrs', 'edges', 'nodes', 'subgraphs']
>>> sg["nodes"]
[(300, []), (400, [])]
>>> sg["edges"]
[(300, 400, [('fullLabel', 'forward')])]
>>> sg["attrs"]
[]
>>> sg["subgraphs"]
[]
5588def parseDot( 5589 dotStr: str, 5590 parseFormat: ParseFormat = ParseFormat() 5591) -> core.DecisionGraph: 5592 """ 5593 Converts a `graphviz` dot-format string into a `core.DecisionGraph`. 5594 A custom `ParseFormat` may be specified if desired; the default 5595 `ParseFormat` is used if not. Note that this relies on specific 5596 indentation schemes used by `toDot` so a hand-edited dot-format 5597 graph will probably not work. A `DotParseError` is raised if the 5598 provided string can't be parsed. Example 5599 5600 >>> parseDotNode(' 3 [ label="A = \\\\"grate:open\\\\"" ]') 5601 (3, [('label', 'A = "grate:open"')]) 5602 >>> sg = '''\ 5603 ... subgraph __requirements__ { 5604 ... 3 [ label="A = \\\\"grate:open\\\\"" ] 5605 ... 4 [ label="B = \\\\"!(helmet)\\\\"" ] 5606 ... 5 [ label="C = \\\\"helmet\\\\"" ] 5607 ... }''' 5608 >>> parseDotGraphContents(sg.splitlines()[1:]) 5609 ({'nodes': [(3, [('label', 'A = "grate:open"')]),\ 5610 (4, [('label', 'B = "!(helmet)"')]), (5, [('label', 'C = "helmet"')])],\ 5611 'edges': [], 'attrs': [], 'subgraphs': []}, []) 5612 >>> from . import core 5613 >>> dg = core.DecisionGraph.example('simple') 5614 >>> encoded = toDot(dg) 5615 >>> reconstructed = parseDot(encoded) 5616 >>> for diff in dg.listDifferences(reconstructed): 5617 ... print(diff) 5618 >>> reconstructed == dg 5619 True 5620 >>> dg = core.DecisionGraph.example('abc') 5621 >>> encoded = toDot(dg) 5622 >>> reconstructed = parseDot(encoded) 5623 >>> for diff in dg.listDifferences(reconstructed): 5624 ... print(diff) 5625 >>> reconstructed == dg 5626 True 5627 >>> tg = core.DecisionGraph() 5628 >>> tg.addDecision('A') 5629 0 5630 >>> tg.addDecision('B') 5631 1 5632 >>> tg.addTransition('A', 'up', 'B', 'down') 5633 >>> same = parseDot(''' 5634 ... digraph { 5635 ... 0 [ name=A label=A ] 5636 ... 0 -> 1 [ 5637 ... label=up 5638 ... fullLabel=up 5639 ... reciprocal=down 5640 ... ] 5641 ... 1 [ name=B label=B ] 5642 ... 1 -> 0 [ 5643 ... label=down 5644 ... fullLabel=down 5645 ... reciprocal=up 5646 ... ] 5647 ... }''') 5648 >>> for diff in tg.listDifferences(same): 5649 ... print(diff) 5650 >>> same == tg 5651 True 5652 >>> pf = ParseFormat() 5653 >>> tg.setTransitionRequirement('A', 'up', pf.parseRequirement('one|two')) 5654 >>> tg.setConsequence( 5655 ... 'B', 5656 ... 'down', 5657 ... [base.effect(gain="one")] 5658 ... ) 5659 >>> test = parseDot(''' 5660 ... digraph { 5661 ... 0 [ name="A = \\\\"one|two\\\\"" label="A = \\\\"one|two\\\\"" ] 5662 ... } 5663 ... ''') 5664 >>> list(test.nodes) 5665 [0] 5666 >>> test.nodes[0]['name'] 5667 'A = "one|two"' 5668 >>> eff = ( 5669 ... r'"A = \\"[{\\\\\\"type\\\\\\": \\\\\\"gain\\\\\\",' 5670 ... r' \\\\\\"applyTo\\\\\\": \\\\\\"active\\\\\\",' 5671 ... r' \\\\\\"value\\\\\\": \\\\\\"one\\\\\\",' 5672 ... r' \\\\\\"charges\\\\\\": null, \\\\\\"hidden\\\\\\": false,' 5673 ... r' \\\\\\"delay\\\\\\": null}]\\""' 5674 ... ) 5675 >>> utils.unquoted(eff)[1] 5676 '' 5677 >>> test2 = parseDot( 5678 ... 'digraph {\\n 0 [ name=' + eff + ' label=' + eff + ' ]\\n}' 5679 ... ) 5680 >>> s = test2.nodes[0]['name'] 5681 >>> s[:25] 5682 'A = "[{\\\\"type\\\\": \\\\"gain\\\\"' 5683 >>> s[25:50] 5684 ', \\\\"applyTo\\\\": \\\\"active\\\\"' 5685 >>> s[50:70] 5686 ', \\\\"value\\\\": \\\\"one\\\\"' 5687 >>> s[70:89] 5688 ', \\\\"charges\\\\": null' 5689 >>> s[89:108] 5690 ', \\\\"hidden\\\\": false' 5691 >>> s[108:] 5692 ', \\\\"delay\\\\": null}]"' 5693 >>> ae = s[s.index('=') + 1:].strip() 5694 >>> uq, after = utils.unquoted(ae) 5695 >>> after 5696 '' 5697 >>> fromJSON(uq) == [base.effect(gain="one")] 5698 True 5699 >>> same = parseDot(''' 5700 ... digraph { 5701 ... 0 [ name=A label=A ] 5702 ... 0 -> 1 [ 5703 ... label=up 5704 ... fullLabel=up 5705 ... reciprocal=down 5706 ... req=A 5707 ... ] 5708 ... 1 [ name=B label=B ] 5709 ... 1 -> 0 [ 5710 ... label=down 5711 ... fullLabel=down 5712 ... reciprocal=up 5713 ... consequence=A 5714 ... ] 5715 ... subgraph __requirements__ { 5716 ... 2 [ label="A = \\\\"one|two\\\\"" ] 5717 ... } 5718 ... subgraph __consequences__ { 5719 ... 3 [ label=''' + eff + ''' ] 5720 ... } 5721 ... }''') 5722 >>> c = {'tags': {}, 'annotations': [], 'reciprocal': 'up', 'consequence': [{'type': 'gain', 'applyTo': 'active', 'value': 'one', 'delay': None, 'charges': None}]}['consequence'] # noqa 5723 5724 >>> for diff in tg.listDifferences(same): 5725 ... print(diff) 5726 >>> same == tg 5727 True 5728 """ 5729 lines = dotStr.splitlines() 5730 while lines[0].strip() == '': 5731 lines.pop(0) 5732 if lines.pop(0).strip() != "digraph {": 5733 raise DotParseError("Input doesn't begin with 'digraph {'.") 5734 5735 # Create our result 5736 result = core.DecisionGraph() 5737 5738 # Parse to intermediate graph data structure 5739 graphStuff, remaining = parseDotGraphContents(lines) 5740 if remaining: 5741 if len(remaining) <= 4: 5742 junk = '\n '.join(repr(line) for line in remaining) 5743 else: 5744 junk = '\n '.join(repr(line) for line in remaining[:4]) 5745 junk += '\n ...' 5746 raise DotParseError("Extra junk after graph:\n {junk}") 5747 5748 # Sort out subgraphs to find legends 5749 zoneSubs = [] 5750 reqLegend = None 5751 consequenceLegend = None 5752 mechanismLegend = None 5753 for sub in graphStuff['subgraphs']: 5754 if sub[0] == '__requirements__': 5755 reqLegend = sub[1] 5756 elif sub[0] == '__consequences__': 5757 consequenceLegend = sub[1] 5758 elif sub[0] == '__mechanisms__': 5759 mechanismLegend = sub[1] 5760 else: 5761 zoneSubs.append(sub) 5762 5763 # Build out our mapping from requirement abbreviations to actual 5764 # requirement objects 5765 reqMap: Dict[str, base.Requirement] = {} 5766 if reqLegend is not None: 5767 if reqLegend['edges']: 5768 raise DotParseError( 5769 f"Requirements legend subgraph has edges:" 5770 f"\n {repr(reqLegend['edges'])}" 5771 f"\n(It should only have nodes.)" 5772 ) 5773 if reqLegend['attrs']: 5774 raise DotParseError( 5775 f"Requirements legend subgraph has attributes:" 5776 f"\n {repr(reqLegend['attrs'])}" 5777 f"\n(It should only have nodes.)" 5778 ) 5779 if reqLegend['subgraphs']: 5780 raise DotParseError( 5781 f"Requirements legend subgraph has subgraphs:" 5782 f"\n {repr(reqLegend['subgraphs'])}" 5783 f"\n(It should only have nodes.)" 5784 ) 5785 for node, attrs in reqLegend['nodes']: 5786 if not attrs: 5787 raise DotParseError( 5788 f"Node in requirements legend missing attributes:" 5789 f"\n {repr(attrs)}" 5790 ) 5791 if len(attrs) != 1: 5792 raise DotParseError( 5793 f"Node in requirements legend has multiple" 5794 f" attributes:\n {repr(attrs)}" 5795 ) 5796 reqStr = attrs[0][1] 5797 try: 5798 eqInd = reqStr.index('=') 5799 except ValueError: 5800 raise DotParseError( 5801 f"Missing '=' in requirement specifier:" 5802 f"\n {repr(reqStr)}" 5803 ) 5804 ab = reqStr[:eqInd].rstrip() 5805 encoded = reqStr[eqInd + 1:].lstrip() 5806 try: 5807 encVal, empty = utils.unquoted(encoded) 5808 except ValueError: 5809 raise DotParseError( 5810 f"Invalid quoted requirement value:" 5811 f"\n {repr(encoded)}" 5812 ) 5813 if empty.strip(): 5814 raise DotParseError( 5815 f"Extra junk after requirement value:" 5816 f"\n {repr(empty)}" 5817 ) 5818 try: 5819 req = parseFormat.parseRequirement(encVal) 5820 except ValueError: 5821 raise DotParseError( 5822 f"Invalid encoded requirement in requirements" 5823 f" legend:\n {repr(encVal)}" 5824 ) 5825 if ab in reqMap: 5826 raise DotParseError( 5827 f"Abbreviation '{ab}' was defined multiple" 5828 f" times in requirements legend." 5829 ) 5830 reqMap[ab] = req 5831 5832 # Build out our mapping from consequence abbreviations to actual 5833 # consequence lists 5834 consequenceMap: Dict[str, base.Consequence] = {} 5835 if consequenceLegend is not None: 5836 if consequenceLegend['edges']: 5837 raise DotParseError( 5838 f"Consequences legend subgraph has edges:" 5839 f"\n {repr(consequenceLegend['edges'])}" 5840 f"\n(It should only have nodes.)" 5841 ) 5842 if consequenceLegend['attrs']: 5843 raise DotParseError( 5844 f"Consequences legend subgraph has attributes:" 5845 f"\n {repr(consequenceLegend['attrs'])}" 5846 f"\n(It should only have nodes.)" 5847 ) 5848 if consequenceLegend['subgraphs']: 5849 raise DotParseError( 5850 f"Consequences legend subgraph has subgraphs:" 5851 f"\n {repr(consequenceLegend['subgraphs'])}" 5852 f"\n(It should only have nodes.)" 5853 ) 5854 for node, attrs in consequenceLegend['nodes']: 5855 if not attrs: 5856 raise DotParseError( 5857 f"Node in consequence legend missing attributes:" 5858 f"\n {repr(attrs)}" 5859 ) 5860 if len(attrs) != 1: 5861 raise DotParseError( 5862 f"Node in consequences legend has multiple" 5863 f" attributes:\n {repr(attrs)}" 5864 ) 5865 consStr = attrs[0][1] 5866 try: 5867 eqInd = consStr.index('=') 5868 except ValueError: 5869 raise DotParseError( 5870 f"Missing '=' in consequence string:" 5871 f"\n {repr(consStr)}" 5872 ) 5873 ab = consStr[:eqInd].rstrip() 5874 encoded = consStr[eqInd + 1:].lstrip() 5875 try: 5876 encVal, empty = utils.unquoted(encoded) 5877 except ValueError: 5878 raise DotParseError( 5879 f"Invalid quoted consequence value:" 5880 f"\n {repr(encoded)}" 5881 ) 5882 if empty.strip(): 5883 raise DotParseError( 5884 f"Extra junk after consequence value:" 5885 f"\n {repr(empty)}" 5886 ) 5887 try: 5888 consequences = fromJSON(encVal) 5889 except json.decoder.JSONDecodeError: 5890 raise DotParseError( 5891 f"Invalid encoded consequence in requirements" 5892 f" legend:\n {repr(encVal)}" 5893 ) 5894 if ab in consequenceMap: 5895 raise DotParseError( 5896 f"Abbreviation '{ab}' was defined multiple" 5897 f" times in effects legend." 5898 ) 5899 consequenceMap[ab] = consequences 5900 5901 # Reconstruct mechanisms 5902 if mechanismLegend is not None: 5903 if mechanismLegend['edges']: 5904 raise DotParseError( 5905 f"Mechanisms legend subgraph has edges:" 5906 f"\n {repr(mechanismLegend['edges'])}" 5907 f"\n(It should only have nodes.)" 5908 ) 5909 if mechanismLegend['attrs']: 5910 raise DotParseError( 5911 f"Mechanisms legend subgraph has attributes:" 5912 f"\n {repr(mechanismLegend['attrs'])}" 5913 f"\n(It should only have nodes.)" 5914 ) 5915 if mechanismLegend['subgraphs']: 5916 raise DotParseError( 5917 f"Mechanisms legend subgraph has subgraphs:" 5918 f"\n {repr(mechanismLegend['subgraphs'])}" 5919 f"\n(It should only have nodes.)" 5920 ) 5921 for node, attrs in mechanismLegend['nodes']: 5922 if not attrs: 5923 raise DotParseError( 5924 f"Node in mechanisms legend missing attributes:" 5925 f"\n {repr(attrs)}" 5926 ) 5927 if len(attrs) != 1: 5928 raise DotParseError( 5929 f"Node in mechanisms legend has multiple" 5930 f" attributes:\n {repr(attrs)}" 5931 ) 5932 mechStr = attrs[0][1] 5933 try: 5934 atInd = mechStr.index('@') 5935 colonInd = mechStr.index(':') 5936 except ValueError: 5937 raise DotParseError( 5938 f"Missing '@' or ':' in mechanism string:" 5939 f"\n {repr(mechStr)}" 5940 ) 5941 if atInd > colonInd: 5942 raise DotParseError( 5943 f"':' after '@' in mechanism string:" 5944 f"\n {repr(mechStr)}" 5945 ) 5946 mID: base.MechanismID 5947 where: Optional[base.DecisionID] 5948 mName: base.MechanismName 5949 try: 5950 mID = int(mechStr[:atInd].rstrip()) 5951 except ValueError: 5952 raise DotParseError( 5953 f"Invalid mechanism ID in mechanism string:" 5954 f"\n {repr(mechStr)}" 5955 ) 5956 try: 5957 whereStr = mechStr[atInd + 1:colonInd].strip() 5958 if whereStr == "None": 5959 where = None 5960 else: 5961 where = int(whereStr) 5962 except ValueError: 5963 raise DotParseError( 5964 f"Invalid mechanism location in mechanism string:" 5965 f"\n {repr(mechStr)}" 5966 ) 5967 mName, rest = utils.unquoted(mechStr[colonInd + 1:].lstrip()) 5968 if rest.strip(): 5969 raise DotParseError( 5970 f"Junk after mechanism name in mechanism string:" 5971 f"\n {repr(mechStr)}" 5972 ) 5973 result.mechanisms[mID] = (where, mName) 5974 if where is None: 5975 result.globalMechanisms[mName] = mID 5976 5977 # Add zones to the graph based on parent info 5978 # Map from zones to children we should add to them once all 5979 # zones are created: 5980 zoneChildMap: Dict[str, List[str]] = {} 5981 for prefixedName, graphData in zoneSubs: 5982 # Chop off cluster_ or _ prefix: 5983 zoneName = prefixedName[prefixedName.index('_') + 1:] 5984 if graphData['edges']: 5985 raise DotParseError( 5986 f"Zone subgraph for zone {repr(zoneName)} has edges:" 5987 f"\n {repr(graphData['edges'])}" 5988 f"\n(It should only have nodes and attributes.)" 5989 ) 5990 if graphData['subgraphs']: 5991 raise DotParseError( 5992 f"Zone subgraph for zone {repr(zoneName)} has" 5993 f" subgraphs:" 5994 f"\n {repr(graphData['subgraphs'])}" 5995 f"\n(It should only have nodes and attributes.)" 5996 ) 5997 # Note: we ignore nodes as that info is used for 5998 # visualization but is redundant with the zone parent info 5999 # stored in nodes, and it would be tricky to tease apart 6000 # direct vs. indirect relationships from merged info. 6001 parents = None 6002 level = None 6003 for attr, aVal in graphData['attrs']: 6004 if attr == 'parents': 6005 try: 6006 parents = set(fromJSON(aVal)) 6007 except json.decoder.JSONDecodeError: 6008 raise DotParseError( 6009 f"Invalid parents JSON in zone subgraph for" 6010 f" zone '{zoneName}':\n {repr(aVal)}" 6011 ) 6012 elif attr == 'level': 6013 try: 6014 level = int(aVal) 6015 except ValueError: 6016 raise DotParseError( 6017 f"Invalid level in zone subgraph for" 6018 f" zone '{zoneName}':\n {repr(aVal)}" 6019 ) 6020 elif attr == 'label': 6021 pass # name already extracted from the subgraph name 6022 6023 else: 6024 raise DotParseError( 6025 f"Unexpected attribute '{attr}' in zone" 6026 f" subgraph for zone '{zoneName}'" 6027 ) 6028 if parents is None: 6029 raise DotParseError( 6030 f"No parents attribute for zone '{zoneName}'." 6031 f" Graph is:\n {repr(graphData)}" 6032 ) 6033 if level is None: 6034 raise DotParseError( 6035 f"No level attribute for zone '{zoneName}'." 6036 f" Graph is:\n {repr(graphData)}" 6037 ) 6038 6039 # Add ourself to our parents in the child map 6040 for parent in parents: 6041 zoneChildMap.setdefault(parent, []).append(zoneName) 6042 6043 # Create this zone 6044 result.createZone(zoneName, level) 6045 6046 # Add zone parent/child relationships 6047 for parent, children in zoneChildMap.items(): 6048 for child in children: 6049 result.addZoneToZone(child, parent) 6050 6051 # Add nodes to the graph 6052 for (node, attrs) in graphStuff['nodes']: 6053 name: Optional[str] = None 6054 annotations = [] 6055 tags: Dict[base.Tag, base.TagValue] = {} 6056 zones = [] 6057 for attr, aVal in attrs: 6058 if attr == 'name': # it's the name 6059 name = aVal 6060 elif attr == 'label': # zone + name; redundant 6061 pass 6062 elif attr.startswith('t_'): # it's a tag 6063 tagName = attr[2:] 6064 try: 6065 tagAny = fromJSON(aVal) 6066 except json.decoder.JSONDecodeError: 6067 raise DotParseError( 6068 f"Error in JSON for tag attr '{attr}' of node" 6069 f" '{node}'" 6070 ) 6071 if isinstance(tagAny, base.TagValueTypes): 6072 tagVal: base.TagValue = cast(base.TagValue, tagAny) 6073 else: 6074 raise DotParseError( 6075 f"JSON for tag value encodes disallowed tag" 6076 f" value of type {type(tagAny)}. Value is:" 6077 f"\n {repr(tagAny)}" 6078 ) 6079 tags[tagName] = tagVal 6080 elif attr.startswith('z_'): # it's a zone 6081 zones.append(attr[2:]) 6082 elif attr == 'annotations': # It's the annotations 6083 try: 6084 annotations = fromJSON(aVal) 6085 except json.decoder.JSONDecodeError: 6086 raise DotParseError( 6087 f"Bad JSON in attribute '{attr}' of node" 6088 f" '{node}'" 6089 ) 6090 else: 6091 raise DotParseError( 6092 f"Unrecognized node attribute '{attr}' for node" 6093 f" '{node}'" 6094 ) 6095 6096 # TODO: Domains here? 6097 if name is None: 6098 raise DotParseError(f"Node '{node}' does not have a name.") 6099 6100 result.addIdentifiedDecision( 6101 node, 6102 name, 6103 tags=tags, 6104 annotations=annotations 6105 ) 6106 for zone in zones: 6107 try: 6108 result.addDecisionToZone(node, zone) 6109 except core.MissingZoneError: 6110 raise DotParseError( 6111 f"Zone '{zone}' for node {node} does not" 6112 f" exist." 6113 ) 6114 6115 # Add mechanisms to each node: 6116 for (mID, (where, mName)) in result.mechanisms.items(): 6117 mPool = result.nodes[where].setdefault('mechanisms', {}) 6118 if mName in mPool: 6119 raise DotParseError( 6120 f"Multiple mechanisms named {mName!r} at" 6121 f" decision {where}." 6122 ) 6123 mPool[mName] = mID 6124 6125 # Reciprocals to double-check once all edges are added 6126 recipChecks: Dict[ 6127 Tuple[base.DecisionID, base.Transition], 6128 base.Transition 6129 ] = {} 6130 6131 # Add each edge 6132 for (source, dest, attrs) in graphStuff['edges']: 6133 annotations = [] 6134 tags = {} 6135 label = None 6136 requirements = None 6137 consequence = None 6138 reciprocal = None 6139 for attr, aVal in attrs: 6140 if attr.startswith('t_'): 6141 try: 6142 tags[attr[2:]] = fromJSON(aVal) 6143 except json.decoder.JSONDecodeError: 6144 raise DotParseError( 6145 f"Invalid JSON in edge tag '{attr}' for edge" 6146 f"from '{source}' to '{dest}':" 6147 f"\n {repr(aVal)}" 6148 ) 6149 elif attr == "label": # We ignore the short-label 6150 pass 6151 elif attr == "fullLabel": # This is our transition name 6152 label = aVal 6153 elif attr == "reciprocal": 6154 reciprocal = aVal 6155 elif attr == "req": 6156 reqAbbr = aVal 6157 if reqAbbr not in reqMap: 6158 raise DotParseError( 6159 f"Edge from '{source}' to '{dest}' has" 6160 f" requirement abbreviation '{reqAbbr}'" 6161 f" but that abbreviation was not listed" 6162 f" in the '__requirements__' subgraph." 6163 ) 6164 requirements = reqMap[reqAbbr] 6165 elif attr == "consequence": 6166 consequenceAbbr = aVal 6167 if consequenceAbbr not in reqMap: 6168 raise DotParseError( 6169 f"Edge from '{source}' to '{dest}' has" 6170 f" consequence abbreviation" 6171 f" '{consequenceAbbr}' but that" 6172 f" abbreviation was not listed in the" 6173 f" '__consequences__' subgraph." 6174 ) 6175 consequence = consequenceMap[consequenceAbbr] 6176 elif attr == "annotations": 6177 try: 6178 annotations = fromJSON(aVal) 6179 except json.decoder.JSONDecodeError: 6180 raise DotParseError( 6181 f"Invalid JSON in edge annotations for" 6182 f" edge from '{source}' to '{dest}':" 6183 f"\n {repr(aVal)}" 6184 ) 6185 else: 6186 raise DotParseError( 6187 f"Unrecognized edge attribute '{attr}' for edge" 6188 f" from '{source}' to '{dest}'" 6189 ) 6190 6191 if label is None: 6192 raise DotParseError( 6193 f"Edge from '{source}' to '{dest}' is missing" 6194 f" a 'fullLabel' attribute." 6195 ) 6196 6197 # Add the requested transition 6198 result.addTransition( 6199 source, 6200 label, 6201 dest, 6202 tags=tags, 6203 annotations=annotations, 6204 requires=requirements, # None works here 6205 consequence=consequence # None works here 6206 ) 6207 # Either we're first or our reciprocal is, so this will only 6208 # trigger for one of the pair 6209 if reciprocal is not None: 6210 recipDest = result.getDestination(dest, reciprocal) 6211 if recipDest is None: 6212 recipChecks[(source, label)] = reciprocal 6213 # we'll get set as a reciprocal when that edge is 6214 # instantiated, we hope, but let's check that later 6215 elif recipDest != source: 6216 raise DotParseError( 6217 f"Transition '{label}' from '{source}' to" 6218 f" '{dest}' lists reciprocal '{reciprocal}'" 6219 f" but that transition from '{dest}' goes to" 6220 f" '{recipDest}', not '{source}'." 6221 ) 6222 else: 6223 # At this point we know the reciprocal edge exists 6224 # and has the appropriate destination (our source). 6225 # No need to check for a pre-existing reciprocal as 6226 # this edge is newly created and cannot already have 6227 # a reciprocal assigned. 6228 result.setReciprocal(source, label, reciprocal) 6229 6230 # Double-check skipped reciprocals 6231 for ((source, transition), reciprocal) in recipChecks.items(): 6232 actual = result.getReciprocal(source, transition) 6233 if actual != reciprocal: 6234 raise DotParseError( 6235 f"Transition '{transition}' from '{source}' was" 6236 f" expecting to have reciprocal '{reciprocal}' but" 6237 f" all edges have been processed and its reciprocal" 6238 f" is {repr(actual)}." 6239 ) 6240 6241 # Finally get graph-level attribute values 6242 for (name, value) in graphStuff['attrs']: 6243 if name == "unknownCount": 6244 try: 6245 result.unknownCount = int(value) 6246 except ValueError: 6247 raise DotParseError( 6248 f"Invalid 'unknownCount' value {repr(value)}." 6249 ) 6250 elif name == "nextID": 6251 try: 6252 result.nextID = int(value) 6253 except ValueError: 6254 raise DotParseError( 6255 f"Invalid 'nextID' value:" 6256 f"\n {repr(value)}" 6257 ) 6258 collisionCourse = [x for x in result if x >= result.nextID] 6259 if len(collisionCourse) > 0: 6260 raise DotParseError( 6261 f"Next ID {value} is wrong because the graph" 6262 f" already contains one or more node(s) with" 6263 f" ID(s) that is/are at least that large:" 6264 f" {collisionCourse}" 6265 ) 6266 elif name == "nextMechanismID": 6267 try: 6268 result.nextMechanismID = int(value) 6269 except ValueError: 6270 raise DotParseError( 6271 f"Invalid 'nextMechanismID' value:" 6272 f"\n {repr(value)}" 6273 ) 6274 elif name in ( 6275 "equivalences", 6276 "reversionTypes", 6277 "mechanisms", 6278 "globalMechanisms", 6279 "nameLookup" 6280 ): 6281 try: 6282 setattr(result, name, fromJSON(value)) 6283 except json.decoder.JSONDecodeError: 6284 raise DotParseError( 6285 f"Invalid JSON in '{name}' attribute:" 6286 f"\n {repr(value)}" 6287 ) 6288 else: 6289 raise DotParseError( 6290 f"Graph has unexpected attribute '{name}'." 6291 ) 6292 6293 # Final check for mechanism ID value after both mechanism ID and 6294 # mechanisms dictionary have been parsed: 6295 leftBehind = [ 6296 x 6297 for x in result.mechanisms 6298 if x >= result.nextMechanismID 6299 ] 6300 if len(leftBehind) > 0: 6301 raise DotParseError( 6302 f"Next mechanism ID {value} is wrong because" 6303 f" the graph already contains one or more" 6304 f" node(s) with ID(s) that is/are at least that" 6305 f" large: {leftBehind}" 6306 ) 6307 6308 # And we're done! 6309 return result
Converts a graphviz dot-format string into a core.DecisionGraph.
A custom ParseFormat may be specified if desired; the default
ParseFormat is used if not. Note that this relies on specific
indentation schemes used by toDot so a hand-edited dot-format
graph will probably not work. A DotParseError is raised if the
provided string can't be parsed. Example
>>> parseDotNode(' 3 [ label="A = \\"grate:open\\"" ]')
(3, [('label', 'A = "grate:open"')])
>>> sg = ''' ... subgraph __requirements__ {
... 3 [ label="A = \\"grate:open\\"" ]
... 4 [ label="B = \\"!(helmet)\\"" ]
... 5 [ label="C = \\"helmet\\"" ]
... }'''
>>> parseDotGraphContents(sg.splitlines()[1:])
({'nodes': [(3, [('label', 'A = "grate:open"')]), (4, [('label', 'B = "!(helmet)"')]), (5, [('label', 'C = "helmet"')])], 'edges': [], 'attrs': [], 'subgraphs': []}, [])
>>> from . import core
>>> dg = core.DecisionGraph.example('simple')
>>> encoded = toDot(dg)
>>> reconstructed = parseDot(encoded)
>>> for diff in dg.listDifferences(reconstructed):
... print(diff)
>>> reconstructed == dg
True
>>> dg = core.DecisionGraph.example('abc')
>>> encoded = toDot(dg)
>>> reconstructed = parseDot(encoded)
>>> for diff in dg.listDifferences(reconstructed):
... print(diff)
>>> reconstructed == dg
True
>>> tg = core.DecisionGraph()
>>> tg.addDecision('A')
0
>>> tg.addDecision('B')
1
>>> tg.addTransition('A', 'up', 'B', 'down')
>>> same = parseDot('''
... digraph {
... 0 [ name=A label=A ]
... 0 -> 1 [
... label=up
... fullLabel=up
... reciprocal=down
... ]
... 1 [ name=B label=B ]
... 1 -> 0 [
... label=down
... fullLabel=down
... reciprocal=up
... ]
... }''')
>>> for diff in tg.listDifferences(same):
... print(diff)
>>> same == tg
True
>>> pf = ParseFormat()
>>> tg.setTransitionRequirement('A', 'up', pf.parseRequirement('one|two'))
>>> tg.setConsequence(
... 'B',
... 'down',
... [base.effect(gain="one")]
... )
>>> test = parseDot('''
... digraph {
... 0 [ name="A = \\"one|two\\"" label="A = \\"one|two\\"" ]
... }
... ''')
>>> list(test.nodes)
[0]
>>> test.nodes[0]['name']
'A = "one|two"'
>>> eff = (
... r'"A = \"[{\\\"type\\\": \\\"gain\\\",'
... r' \\\"applyTo\\\": \\\"active\\\",'
... r' \\\"value\\\": \\\"one\\\",'
... r' \\\"charges\\\": null, \\\"hidden\\\": false,'
... r' \\\"delay\\\": null}]\""'
... )
>>> utils.unquoted(eff)[1]
''
>>> test2 = parseDot(
... 'digraph {\n 0 [ name=' + eff + ' label=' + eff + ' ]\n}'
... )
>>> s = test2.nodes[0]['name']
>>> s[:25]
'A = "[{\\"type\\": \\"gain\\"'
>>> s[25:50]
', \\"applyTo\\": \\"active\\"'
>>> s[50:70]
', \\"value\\": \\"one\\"'
>>> s[70:89]
', \\"charges\\": null'
>>> s[89:108]
', \\"hidden\\": false'
>>> s[108:]
', \\"delay\\": null}]"'
>>> ae = s[s.index('=') + 1:].strip()
>>> uq, after = utils.unquoted(ae)
>>> after
''
>>> fromJSON(uq) == [base.effect(gain="one")]
True
>>> same = parseDot('''
... digraph {
... 0 [ name=A label=A ]
... 0 -> 1 [
... label=up
... fullLabel=up
... reciprocal=down
... req=A
... ]
... 1 [ name=B label=B ]
... 1 -> 0 [
... label=down
... fullLabel=down
... reciprocal=up
... consequence=A
... ]
... subgraph __requirements__ {
... 2 [ label="A = \\"one|two\\"" ]
... }
... subgraph __consequences__ {
... 3 [ label=''' + eff + ''' ]
... }
... }''')
>>> c = {'tags': {}, 'annotations': [], 'reciprocal': 'up', 'consequence': [{'type': 'gain', 'applyTo': 'active', 'value': 'one', 'delay': None, 'charges': None}]}['consequence'] # noqa
>>> for diff in tg.listDifferences(same):
... print(diff)
>>> same == tg
True
6312def toDot( 6313 graph: core.DecisionGraph, 6314 clusterLevels: Union[str, List[int]] = [0] 6315) -> str: 6316 """ 6317 Converts the decision graph into a "dot"-format string suitable 6318 for processing by `graphviz`. 6319 6320 See [the dot language 6321 specification](https://graphviz.org/doc/info/lang.html) for more 6322 detail on the syntax we convert to. 6323 6324 If `clusterLevels` is given, it should be either the string '*', 6325 or a list of integers. '*' means that all zone levels should be 6326 cluster-style subgraphs, while a list of integers specifies that 6327 zones at those levels should be cluster-style subgraphs. This 6328 will prefix the subgraph names with 'cluster_' instead of just 6329 '_'. 6330 6331 TODO: Check edge cases for quotes in capability names, tag names, 6332 transition names, annotations, etc. 6333 6334 TODO: At least colons not allowed in tag names! 6335 6336 TODO: Spaces in decision/transition names? Other special 6337 characters in those names? 6338 """ 6339 # Set up result including unknownCount and nextID 6340 result = ( 6341 f"digraph {{" 6342 f"\n unknownCount={graph.unknownCount}" 6343 f"\n nextID={graph.nextID}" 6344 f"\n nextMechanismID={graph.nextMechanismID}" 6345 f"\n" 6346 ) 6347 6348 # Dictionaries for using letters to substitute for unique 6349 # requirements/consequences found throughout the graph. Keys are 6350 # quoted requirement or consequence reprs, and values are 6351 # abbreviation strings for them. 6352 currentReqKey = utils.nextAbbrKey(None) 6353 currentEffectKey = utils.nextAbbrKey(None) 6354 reqKeys: Dict[str, str] = {} 6355 consequenceKeys: Dict[str, str] = {} 6356 6357 # Add all decision and transition info 6358 decision: base.DecisionID # TODO: Fix Multidigraph type stubs 6359 for decision in graph.nodes: 6360 nodeInfo = graph.nodes[decision] 6361 tags = nodeInfo.get('tags', {}) 6362 annotations = toJSON(nodeInfo.get('annotations', [])) 6363 zones = nodeInfo.get('zones', set()) 6364 nodeAttrs = f"\n name={utils.quoted(nodeInfo['name'])}" 6365 immediateZones = [z for z in zones if graph.zoneHierarchyLevel(z) == 0] 6366 if len(immediateZones) > 0: 6367 useZone = sorted(immediateZones)[0] 6368 # TODO: Don't hardcode :: here? 6369 withZone = useZone + "::" + nodeInfo['name'] 6370 nodeAttrs += f"\n label={utils.quoted(withZone)}" 6371 else: 6372 nodeAttrs += f"\n label={utils.quoted(nodeInfo['name'])}" 6373 for tag, value in tags.items(): 6374 rep = utils.quoted(toJSON(value)) 6375 nodeAttrs += f"\n t_{tag}={rep}" 6376 for z in sorted(zones): 6377 nodeAttrs += f"\n z_{z}=1" 6378 if annotations: 6379 nodeAttrs += '\n annotations=' + utils.quoted(annotations) 6380 6381 result += f'\n {decision} [{nodeAttrs}\n ]' 6382 6383 for (transition, destination) in graph._byEdge[decision].items(): 6384 edgeAttrs = ( 6385 '\n label=' 6386 + utils.quoted(utils.abbr(transition)) 6387 ) 6388 edgeAttrs += ( 6389 '\n fullLabel=' 6390 + utils.quoted(transition) 6391 ) 6392 reciprocal = graph.getReciprocal(decision, transition) 6393 if reciprocal is not None: 6394 edgeAttrs += ( 6395 '\n reciprocal=' 6396 + utils.quoted(reciprocal) 6397 ) 6398 info = graph.edges[ 6399 decision, # type:ignore 6400 destination, 6401 transition 6402 ] 6403 if 'requirement' in info: 6404 # Get string rep for requirement 6405 rep = utils.quoted(info['requirement'].unparse()) 6406 # Get assigned abbreviation or assign one 6407 if rep in reqKeys: 6408 ab = reqKeys[rep] 6409 else: 6410 ab = currentReqKey 6411 reqKeys[rep] = ab 6412 currentReqKey = utils.nextAbbrKey(currentReqKey) 6413 # Add abbreviation as edge attribute 6414 edgeAttrs += f'\n req={ab}' 6415 if 'consequence' in info: 6416 # Get string representation of consequences 6417 rep = utils.quoted( 6418 toJSON(info['consequence']) 6419 ) 6420 # Get abbreviation for that or assign one: 6421 if rep in consequenceKeys: 6422 ab = consequenceKeys[rep] 6423 else: 6424 ab = currentEffectKey 6425 consequenceKeys[rep] = ab 6426 currentEffectKey = utils.nextAbbrKey( 6427 currentEffectKey 6428 ) 6429 # Add abbreviation as an edge attribute 6430 edgeAttrs += f'\n consequence={ab}' 6431 for (tag, value) in info["tags"].items(): 6432 # Get string representation of tag value 6433 rep = utils.quoted(toJSON(value)) 6434 # Add edge attribute for tag 6435 edgeAttrs += f'\n t_{tag}={rep}' 6436 if 'annotations' in info: 6437 edgeAttrs += ( 6438 '\n annotations=' 6439 + utils.quoted(toJSON(info['annotations'])) 6440 ) 6441 result += f'\n {decision} -> {destination}' 6442 result += f' [{edgeAttrs}\n ]' 6443 6444 # Add zone info as subgraph structure 6445 for z, zinfo in graph.zones.items(): 6446 parents = utils.quoted(toJSON(sorted(zinfo.parents))) 6447 if clusterLevels == '*' or zinfo.level in clusterLevels: 6448 zName = "cluster_" + z 6449 else: 6450 zName = '_' + z 6451 zoneSubgraph = f'\n subgraph {utils.quoted(zName)} {{' 6452 zoneSubgraph += f'\n label={z}' 6453 zoneSubgraph += f'\n level={zinfo.level}' 6454 zoneSubgraph += f'\n parents={parents}' 6455 for decision in sorted(graph.allDecisionsInZone(z)): 6456 zoneSubgraph += f'\n {decision}' 6457 zoneSubgraph += '\n }' 6458 result += zoneSubgraph 6459 6460 # Add equivalences, mechanisms, etc. 6461 for attr in [ 6462 "equivalences", 6463 "reversionTypes", 6464 "mechanisms", 6465 "globalMechanisms", 6466 "nameLookup" 6467 ]: 6468 aRep = utils.quoted(toJSON(getattr(graph, attr))) 6469 result += f'\n {attr}={aRep}' 6470 6471 # Add legend subgraphs to represent abbreviations 6472 useID = graph.nextID 6473 if reqKeys: 6474 result += '\n subgraph __requirements__ {' 6475 for rrepr, ab in reqKeys.items(): 6476 nStr = utils.quoted(ab + ' = ' + rrepr) 6477 result += ( 6478 f"\n {useID} [ label={nStr} ]" 6479 ) 6480 useID += 1 6481 result += '\n }' 6482 6483 if consequenceKeys: 6484 result += '\n subgraph __consequences__ {' 6485 for erepr, ab in consequenceKeys.items(): 6486 nStr = utils.quoted(ab + ' = ' + erepr) 6487 result += ( 6488 f"\n {useID} [ label={nStr} ]" 6489 ) 6490 useID += 1 6491 result += '\n }' 6492 6493 if graph.mechanisms: 6494 result += '\n subgraph __mechanisms__ {' 6495 mID: base.MechanismID 6496 mWhere: Optional[base.DecisionID] 6497 mName: base.MechanismName 6498 for (mID, (mWhere, mName)) in graph.mechanisms.items(): 6499 qName = utils.quoted(mName) 6500 nStr = utils.quoted(f"{mID}@{mWhere}:{qName}") 6501 result += ( 6502 f"\n {useID} [ label={nStr} ]" 6503 ) 6504 useID += 1 6505 result += '\n }' 6506 6507 result += "\n}\n" 6508 return result
Converts the decision graph into a "dot"-format string suitable
for processing by graphviz.
See the dot language specification for more detail on the syntax we convert to.
If clusterLevels is given, it should be either the string '',
or a list of integers. '' means that all zone levels should be
cluster-style subgraphs, while a list of integers specifies that
zones at those levels should be cluster-style subgraphs. This
will prefix the subgraph names with 'cluster_' instead of just
'_'.
TODO: Check edge cases for quotes in capability names, tag names, transition names, annotations, etc.
TODO: At least colons not allowed in tag names!
TODO: Spaces in decision/transition names? Other special characters in those names?
Type var for loadCustom.
6519def loadCustom(stream: TextIO, loadAs: Type[T]) -> T: 6520 """ 6521 Loads a new JSON-encodable object from the JSON data in the 6522 given text stream (e.g., a file open in read mode). See 6523 `CustomJSONDecoder` for details on the format and which object types 6524 are supported. 6525 6526 This casts the result to the specified type, but errors out with a 6527 `TypeError` if it doesn't match. 6528 """ 6529 result = json.load(stream, cls=CustomJSONDecoder) 6530 if isinstance(result, loadAs): 6531 return result 6532 else: 6533 raise TypeError( 6534 f"Expected to load a {loadAs} but got a {type(result)}." 6535 )
Loads a new JSON-encodable object from the JSON data in the
given text stream (e.g., a file open in read mode). See
CustomJSONDecoder for details on the format and which object types
are supported.
This casts the result to the specified type, but errors out with a
TypeError if it doesn't match.
6538def saveCustom( 6539 toSave: Union[ # TODO: More in this union? 6540 base.MetricSpace, 6541 core.DecisionGraph, 6542 core.DiscreteExploration, 6543 ], 6544 stream: TextIO 6545) -> None: 6546 """ 6547 Saves a JSON-encodable object as JSON into the given text stream 6548 (e.g., a file open in writing mode). See `CustomJSONEncoder` for 6549 details on the format and which types are supported.. 6550 """ 6551 json.dump(toSave, stream, cls=CustomJSONEncoder)
Saves a JSON-encodable object as JSON into the given text stream
(e.g., a file open in writing mode). See CustomJSONEncoder for
details on the format and which types are supported..
6554def toJSON(obj: Any) -> str: 6555 """ 6556 Defines the standard object -> JSON operation using the 6557 `CustomJSONEncoder` as well as not using `sort_keys`. 6558 """ 6559 return CustomJSONEncoder(sort_keys=False).encode(obj)
Defines the standard object -> JSON operation using the
CustomJSONEncoder as well as not using sort_keys.
6562def fromJSON(encoded: str) -> Any: 6563 """ 6564 Defines the standard JSON -> object operation using 6565 `CustomJSONDecoder`. 6566 """ 6567 return json.loads(encoded, cls=CustomJSONDecoder)
Defines the standard JSON -> object operation using
CustomJSONDecoder.
6570class CustomJSONEncoder(json.JSONEncoder): 6571 """ 6572 A custom JSON encoder that has special protocols for handling the 6573 smae objects that `CustomJSONDecoder` decodes. It handles these 6574 objects specially so that they can be decoded back to their original 6575 form. 6576 6577 Examples: 6578 6579 >>> from . import core 6580 >>> tupList = [(1, 1), (2, 2)] 6581 >>> encTup = toJSON(tupList) 6582 >>> encTup 6583 '[{"^^d": "t", "values": [1, 1]}, {"^^d": "t", "values": [2, 2]}]' 6584 >>> fromJSON(encTup) == tupList 6585 True 6586 >>> dg = core.DecisionGraph.example('simple') 6587 >>> fromJSON(toJSON(dg)) == dg 6588 True 6589 >>> dg = core.DecisionGraph.example('abc') 6590 >>> zi = dg.getZoneInfo('upZone') 6591 >>> zi 6592 ZoneInfo(level=1, parents=set(), contents={'zoneA'}, tags={},\ 6593 annotations=[]) 6594 >>> zj = toJSON(zi) 6595 >>> zj 6596 '{"^^d": "nt", "name": "ZoneInfo", "values":\ 6597 {"level": 1, "parents": {"^^d": "s", "values": []},\ 6598 "contents": {"^^d": "s", "values": ["zoneA"]}, "tags": {},\ 6599 "annotations": []}}' 6600 >>> fromJSON(toJSON(zi)) 6601 ZoneInfo(level=1, parents=set(), contents={'zoneA'}, tags={},\ 6602 annotations=[]) 6603 >>> fromJSON(toJSON(zi)) == zi 6604 True 6605 >>> toJSON({'a': 'b', 1: 2}) 6606 '{"^^d": "d", "items": [["a", "b"], [1, 2]]}' 6607 >>> toJSON(((1, 2), (3, 4))) 6608 '{"^^d": "t", "values": [{"^^d": "t", "values": [1, 2]},\ 6609 {"^^d": "t", "values": [3, 4]}]}' 6610 >>> toJSON(base.effect(set=('grate', 'open'))) 6611 '{"type": "set", "applyTo": "active",\ 6612 "value": {"^^d": "t",\ 6613 "values": [{"^^d": "nt", "name": "MechanismSpecifier",\ 6614 "values": {"domain": null, "zone": null, "decision": null, "name": "grate"}},\ 6615 "open"]}, "delay": null, "charges": null, "hidden": false}' 6616 >>> j = toJSON(dg) 6617 >>> expected = ( 6618 ... '{"^^d": "DG",' 6619 ... ' "props": {},' 6620 ... ' "node_links": {"directed": true,' 6621 ... ' "multigraph": true,' 6622 ... ' "graph": {},' 6623 ... ' "nodes": [' 6624 ... '{"name": "A", "domain": "main", "tags": {},' 6625 ... ' "annotations": ["This is a multi-word \\\\"annotation.\\\\""],' 6626 ... ' "zones": {"^^d": "s", "values": ["zoneA"]},' 6627 ... ' "mechanisms": {"grate": 0},' 6628 ... ' "id": 0' 6629 ... '},' 6630 ... ' {' 6631 ... '"name": "B",' 6632 ... ' "domain": "main",' 6633 ... ' "tags": {"b": 1, "tag2": "\\\\"value\\\\""},' 6634 ... ' "annotations": [],' 6635 ... ' "zones": {"^^d": "s", "values": ["zoneB"]},' 6636 ... ' "id": 1' 6637 ... '},' 6638 ... ' {' 6639 ... '"name": "C",' 6640 ... ' "domain": "main",' 6641 ... ' "tags": {"aw\\\\"ful": "ha\\'ha"},' 6642 ... ' "annotations": [],' 6643 ... ' "zones": {"^^d": "s", "values": ["zoneA"]},' 6644 ... ' "id": 2' 6645 ... '}' 6646 ... '],' 6647 ... ' "links": [' 6648 ... '{' 6649 ... '"tags": {},' 6650 ... ' "annotations": [],' 6651 ... ' "reciprocal": "right",' 6652 ... ' "source": 0,' 6653 ... ' "target": 1,' 6654 ... ' "key": "left"' 6655 ... '},' 6656 ... ' {' 6657 ... '"tags": {},' 6658 ... ' "annotations": [],' 6659 ... ' "reciprocal": "up_right",' 6660 ... ' "requirement": {"^^d": "R", "value": "grate:open"},' 6661 ... ' "source": 0,' 6662 ... ' "target": 1,' 6663 ... ' "key": "up_left"' 6664 ... '},' 6665 ... ' {' 6666 ... '"tags": {},' 6667 ... ' "annotations": ["Transition \\'annotation.\\'"],' 6668 ... ' "reciprocal": "up",' 6669 ... ' "source": 0,' 6670 ... ' "target": 2,' 6671 ... ' "key": "down"' 6672 ... '},' 6673 ... ' {' 6674 ... '"tags": {},' 6675 ... ' "annotations": [],' 6676 ... ' "reciprocal": "left",' 6677 ... ' "source": 1,' 6678 ... ' "target": 0,' 6679 ... ' "key": "right"' 6680 ... '},' 6681 ... ' {' 6682 ... '"tags": {},' 6683 ... ' "annotations": [],' 6684 ... ' "reciprocal": "up_left",' 6685 ... ' "requirement": {"^^d": "R", "value": "grate:open"},' 6686 ... ' "source": 1,' 6687 ... ' "target": 0,' 6688 ... ' "key": "up_right"' 6689 ... '},' 6690 ... ' {' 6691 ... '"tags": {"fast": 1},' 6692 ... ' "annotations": [],' 6693 ... ' "reciprocal": "down",' 6694 ... ' "source": 2,' 6695 ... ' "target": 0,' 6696 ... ' "key": "up"' 6697 ... '},' 6698 ... ' {' 6699 ... '"tags": {},' 6700 ... ' "annotations": [],' 6701 ... ' "requirement": {"^^d": "R", "value": "!(helmet)"},' 6702 ... ' "consequence": [' 6703 ... '{' 6704 ... '"type": "gain", "applyTo": "active", "value": "helmet",' 6705 ... ' "delay": null, "charges": null, "hidden": false' 6706 ... '},' 6707 ... ' {' 6708 ... '"type": "deactivate",' 6709 ... ' "applyTo": "active", "value": null,' 6710 ... ' "delay": 3, "charges": null, "hidden": false' 6711 ... '}' 6712 ... '],' 6713 ... ' "source": 2,' 6714 ... ' "target": 2,' 6715 ... ' "key": "grab_helmet"' 6716 ... '},' 6717 ... ' {' 6718 ... '"tags": {},' 6719 ... ' "annotations": [],' 6720 ... ' "requirement": {"^^d": "R", "value": "helmet"},' 6721 ... ' "consequence": [' 6722 ... '{"type": "lose", "applyTo": "active", "value": "helmet",' 6723 ... ' "delay": null, "charges": null, "hidden": false},' 6724 ... ' {"type": "gain", "applyTo": "active",' 6725 ... ' "value": {"^^d": "t", "values": ["token", 1]},' 6726 ... ' "delay": null, "charges": null, "hidden": false' 6727 ... '},' 6728 ... ' {"condition":' 6729 ... ' {"^^d": "R", "value": "token*2"},' 6730 ... ' "consequence": [' 6731 ... '{"type": "set", "applyTo": "active",' 6732 ... ' "value": {"^^d": "t", "values": [' 6733 ... '{"^^d": "nt", "name": "MechanismSpecifier",' 6734 ... ' "values": {"domain": null, "zone": null, "decision": null,' 6735 ... ' "name": "grate"}}, "open"]},' 6736 ... ' "delay": null, "charges": null, "hidden": false' 6737 ... '},' 6738 ... ' {"type": "deactivate", "applyTo": "active", "value": null,' 6739 ... ' "delay": null, "charges": null, "hidden": false' 6740 ... '}' 6741 ... '],' 6742 ... ' "alternative": []' 6743 ... '}' 6744 ... '],' 6745 ... ' "source": 2,' 6746 ... ' "target": 2,' 6747 ... ' "key": "pull_lever"' 6748 ... '}' 6749 ... ']' 6750 ... '},' 6751 ... ' "_byEdge": {"^^d": "d", "items":' 6752 ... ' [[0, {"left": 1, "up_left": 1, "down": 2}],' 6753 ... ' [1, {"right": 0, "up_right": 0}],' 6754 ... ' [2, {"up": 0, "grab_helmet": 2, "pull_lever": 2}]]},' 6755 ... ' "zones": {"zoneA":' 6756 ... ' {"^^d": "nt", "name": "ZoneInfo",' 6757 ... ' "values": {' 6758 ... '"level": 0,' 6759 ... ' "parents": {"^^d": "s", "values": ["upZone"]},' 6760 ... ' "contents": {"^^d": "s", "values": [0, 2]},' 6761 ... ' "tags": {},' 6762 ... ' "annotations": []' 6763 ... '}' 6764 ... '},' 6765 ... ' "zoneB":' 6766 ... ' {"^^d": "nt", "name": "ZoneInfo",' 6767 ... ' "values": {' 6768 ... '"level": 0,' 6769 ... ' "parents": {"^^d": "s", "values": []},' 6770 ... ' "contents": {"^^d": "s", "values": [1]},' 6771 ... ' "tags": {},' 6772 ... ' "annotations": []' 6773 ... '}' 6774 ... '},' 6775 ... ' "upZone":' 6776 ... ' {"^^d": "nt", "name": "ZoneInfo",' 6777 ... ' "values": {' 6778 ... '"level": 1,' 6779 ... ' "parents": {"^^d": "s", "values": []},' 6780 ... ' "contents": {"^^d": "s", "values": ["zoneA"]},' 6781 ... ' "tags": {},' 6782 ... ' "annotations": []' 6783 ... '}' 6784 ... '}' 6785 ... '},' 6786 ... ' "unknownCount": 0,' 6787 ... ' "equivalences": {"^^d": "d", "items": [' 6788 ... '[{"^^d": "t", "values": [0, "open"]},' 6789 ... ' {"^^d": "s", "values": [' 6790 ... '{"^^d": "R", "value": "helmet"}]}]' 6791 ... ']},' 6792 ... ' "reversionTypes": {},' 6793 ... ' "nextID": 3,' 6794 ... ' "nextMechanismID": 1,' 6795 ... ' "mechanisms": {"^^d": "d", "items": [' 6796 ... '[0, {"^^d": "t", "values": [0, "grate"]}]]},' 6797 ... ' "globalMechanisms": {},' 6798 ... ' "nameLookup": {"A": [0], "B": [1], "C": [2]}' 6799 ... '}' 6800 ... ) 6801 >>> for i in range(len(j)): 6802 ... if j[i] != expected[i:i+1]: 6803 ... print( 6804 ... 'exp: ' + expected[i-10:i+50] + '\\ngot: ' + j[i-10:i+50] 6805 ... ) 6806 ... break 6807 >>> j == expected 6808 True 6809 >>> rec = fromJSON(j) 6810 >>> rec.nodes == dg.nodes 6811 True 6812 >>> rec.edges == dg.edges 6813 True 6814 >>> rec.unknownCount == dg.unknownCount 6815 True 6816 >>> rec.equivalences == dg.equivalences 6817 True 6818 >>> rec.reversionTypes == dg.reversionTypes 6819 True 6820 >>> rec._byEdge == dg._byEdge 6821 True 6822 >>> rec.zones == dg.zones 6823 True 6824 >>> for diff in dg.listDifferences(rec): 6825 ... print(diff) 6826 >>> rec == dg 6827 True 6828 6829 `base.MetricSpace` example: 6830 6831 >>> ms = base.MetricSpace("test") 6832 >>> ms.addPoint([2, 3]) 6833 0 6834 >>> ms.addPoint([2, 7, 0]) 6835 1 6836 >>> ms.addPoint([2, 7]) 6837 2 6838 >>> toJSON(ms) 6839 '{"^^d": "MS", "name": "test",\ 6840 "points": {"^^d": "d", "items": [[0, [2, 3]], [1, [2, 7,\ 6841 0]], [2, [2, 7]]]}, "lastID": 2}' 6842 >>> ms.removePoint(0) 6843 >>> ms.removePoint(1) 6844 >>> ms.removePoint(2) 6845 >>> toJSON(ms) 6846 '{"^^d": "MS", "name": "test", "points": {}, "lastID": 2}' 6847 >>> ms.addPoint([5, 6]) 6848 3 6849 >>> ms.addPoint([7, 8]) 6850 4 6851 >>> toJSON(ms) 6852 '{"^^d": "MS", "name": "test",\ 6853 "points": {"^^d": "d", "items": [[3, [5, 6]], [4, [7, 8]]]}, "lastID": 4}' 6854 6855 # TODO: more examples, including one for a DiscreteExploration 6856 """ 6857 6858 def default(self, o: Any) -> Any: 6859 """ 6860 Re-writes objects for encoding. We re-write the following 6861 objects: 6862 6863 - `set` 6864 - `dict` (if the keys aren't all strings) 6865 - `tuple`/`namedtuple` 6866 - `ZoneInfo` 6867 - `Requirement` 6868 - `SkillCombination` 6869 - `DecisionGraph` 6870 - `DiscreteExploration` 6871 - `MetricSpace` 6872 6873 TODO: FeatureGraph... 6874 """ 6875 if isinstance(o, list): 6876 return [self.default(x) for x in o] 6877 6878 elif isinstance(o, set): 6879 return { 6880 '^^d': 's', 6881 'values': sorted( 6882 [self.default(e) for e in o], 6883 key=lambda x: str(x) 6884 ) 6885 } 6886 6887 elif isinstance(o, dict): 6888 if all(isinstance(k, str) for k in o): 6889 return { 6890 k: self.default(v) 6891 for k, v in o.items() 6892 } 6893 else: 6894 return { 6895 '^^d': 'd', 6896 'items': [ 6897 [self.default(k), self.default(v)] 6898 for (k, v) in o.items() 6899 ] 6900 } 6901 6902 elif isinstance(o, tuple): 6903 if hasattr(o, '_fields') and hasattr(o, '_asdict'): 6904 # Named tuple 6905 return { 6906 '^^d': 'nt', 6907 'name': o.__class__.__name__, 6908 'values': { 6909 k: self.default(v) 6910 for k, v in o._asdict().items() 6911 } 6912 } 6913 else: 6914 # Normal tuple 6915 return { 6916 '^^d': 't', 6917 "values": [self.default(e) for e in o] 6918 } 6919 6920 elif isinstance(o, base.Requirement): 6921 return { 6922 '^^d': 'R', 6923 'value': o.unparse() 6924 } 6925 6926 elif isinstance(o, base.SkillCombination): 6927 return { 6928 '^^d': 'SC', 6929 'value': o.unparse() 6930 } 6931 # TODO: Consequence, Condition, Challenge, and Effect here? 6932 6933 elif isinstance(o, core.DecisionGraph): 6934 return { 6935 '^^d': 'DG', 6936 'props': self.default(o.graph), # type:ignore [attr-defined] 6937 'node_links': self.default( 6938 networkx.node_link_data(o, edges="links") # type: ignore 6939 # TODO: Fix networkx stubs 6940 ), 6941 '_byEdge': self.default(o._byEdge), 6942 'zones': self.default(o.zones), 6943 'unknownCount': o.unknownCount, 6944 'equivalences': self.default(o.equivalences), 6945 'reversionTypes': self.default(o.reversionTypes), 6946 'nextID': o.nextID, 6947 'nextMechanismID': o.nextMechanismID, 6948 'mechanisms': self.default(o.mechanisms), 6949 'globalMechanisms': self.default(o.globalMechanisms), 6950 'nameLookup': self.default(o.nameLookup) 6951 } 6952 6953 elif isinstance(o, core.DiscreteExploration): 6954 return { 6955 '^^d': 'DE', 6956 'situations': self.default(o.situations) 6957 } 6958 6959 elif isinstance(o, base.MetricSpace): 6960 return { 6961 '^^d': 'MS', 6962 'name': o.name, 6963 'points': self.default(o.points), 6964 'lastID': o.lastID() 6965 } 6966 6967 else: 6968 return o 6969 6970 def encode(self, o: Any) -> str: 6971 """ 6972 Custom encode function since we need to override behavior for 6973 tuples and dicts. 6974 """ 6975 if isinstance(o, (tuple, dict, set)): 6976 o = self.default(o) 6977 elif isinstance(o, list): 6978 o = [self.default(x) for x in o] 6979 6980 try: 6981 return super().encode(o) 6982 except TypeError: 6983 return super().encode(self.default(o)) 6984 6985 def iterencode( 6986 self, 6987 o: Any, 6988 _one_shot: bool = False 6989 ) -> Generator[str, None, None]: 6990 """ 6991 Custom iterencode function since we need to override behavior for 6992 tuples and dicts. 6993 """ 6994 if isinstance(o, (tuple, dict)): 6995 o = self.default(o) 6996 6997 yield from super().iterencode(o, _one_shot=_one_shot)
A custom JSON encoder that has special protocols for handling the
smae objects that CustomJSONDecoder decodes. It handles these
objects specially so that they can be decoded back to their original
form.
Examples:
>>> from . import core
>>> tupList = [(1, 1), (2, 2)]
>>> encTup = toJSON(tupList)
>>> encTup
'[{"^^d": "t", "values": [1, 1]}, {"^^d": "t", "values": [2, 2]}]'
>>> fromJSON(encTup) == tupList
True
>>> dg = core.DecisionGraph.example('simple')
>>> fromJSON(toJSON(dg)) == dg
True
>>> dg = core.DecisionGraph.example('abc')
>>> zi = dg.getZoneInfo('upZone')
>>> zi
ZoneInfo(level=1, parents=set(), contents={'zoneA'}, tags={}, annotations=[])
>>> zj = toJSON(zi)
>>> zj
'{"^^d": "nt", "name": "ZoneInfo", "values": {"level": 1, "parents": {"^^d": "s", "values": []}, "contents": {"^^d": "s", "values": ["zoneA"]}, "tags": {}, "annotations": []}}'
>>> fromJSON(toJSON(zi))
ZoneInfo(level=1, parents=set(), contents={'zoneA'}, tags={}, annotations=[])
>>> fromJSON(toJSON(zi)) == zi
True
>>> toJSON({'a': 'b', 1: 2})
'{"^^d": "d", "items": [["a", "b"], [1, 2]]}'
>>> toJSON(((1, 2), (3, 4)))
'{"^^d": "t", "values": [{"^^d": "t", "values": [1, 2]}, {"^^d": "t", "values": [3, 4]}]}'
>>> toJSON(base.effect(set=('grate', 'open')))
'{"type": "set", "applyTo": "active", "value": {"^^d": "t", "values": [{"^^d": "nt", "name": "MechanismSpecifier", "values": {"domain": null, "zone": null, "decision": null, "name": "grate"}}, "open"]}, "delay": null, "charges": null, "hidden": false}'
>>> j = toJSON(dg)
>>> expected = (
... '{"^^d": "DG",'
... ' "props": {},'
... ' "node_links": {"directed": true,'
... ' "multigraph": true,'
... ' "graph": {},'
... ' "nodes": ['
... '{"name": "A", "domain": "main", "tags": {},'
... ' "annotations": ["This is a multi-word \\"annotation.\\""],'
... ' "zones": {"^^d": "s", "values": ["zoneA"]},'
... ' "mechanisms": {"grate": 0},'
... ' "id": 0'
... '},'
... ' {'
... '"name": "B",'
... ' "domain": "main",'
... ' "tags": {"b": 1, "tag2": "\\"value\\""},'
... ' "annotations": [],'
... ' "zones": {"^^d": "s", "values": ["zoneB"]},'
... ' "id": 1'
... '},'
... ' {'
... '"name": "C",'
... ' "domain": "main",'
... ' "tags": {"aw\\"ful": "ha\'ha"},'
... ' "annotations": [],'
... ' "zones": {"^^d": "s", "values": ["zoneA"]},'
... ' "id": 2'
... '}'
... '],'
... ' "links": ['
... '{'
... '"tags": {},'
... ' "annotations": [],'
... ' "reciprocal": "right",'
... ' "source": 0,'
... ' "target": 1,'
... ' "key": "left"'
... '},'
... ' {'
... '"tags": {},'
... ' "annotations": [],'
... ' "reciprocal": "up_right",'
... ' "requirement": {"^^d": "R", "value": "grate:open"},'
... ' "source": 0,'
... ' "target": 1,'
... ' "key": "up_left"'
... '},'
... ' {'
... '"tags": {},'
... ' "annotations": ["Transition \'annotation.\'"],'
... ' "reciprocal": "up",'
... ' "source": 0,'
... ' "target": 2,'
... ' "key": "down"'
... '},'
... ' {'
... '"tags": {},'
... ' "annotations": [],'
... ' "reciprocal": "left",'
... ' "source": 1,'
... ' "target": 0,'
... ' "key": "right"'
... '},'
... ' {'
... '"tags": {},'
... ' "annotations": [],'
... ' "reciprocal": "up_left",'
... ' "requirement": {"^^d": "R", "value": "grate:open"},'
... ' "source": 1,'
... ' "target": 0,'
... ' "key": "up_right"'
... '},'
... ' {'
... '"tags": {"fast": 1},'
... ' "annotations": [],'
... ' "reciprocal": "down",'
... ' "source": 2,'
... ' "target": 0,'
... ' "key": "up"'
... '},'
... ' {'
... '"tags": {},'
... ' "annotations": [],'
... ' "requirement": {"^^d": "R", "value": "!(helmet)"},'
... ' "consequence": ['
... '{'
... '"type": "gain", "applyTo": "active", "value": "helmet",'
... ' "delay": null, "charges": null, "hidden": false'
... '},'
... ' {'
... '"type": "deactivate",'
... ' "applyTo": "active", "value": null,'
... ' "delay": 3, "charges": null, "hidden": false'
... '}'
... '],'
... ' "source": 2,'
... ' "target": 2,'
... ' "key": "grab_helmet"'
... '},'
... ' {'
... '"tags": {},'
... ' "annotations": [],'
... ' "requirement": {"^^d": "R", "value": "helmet"},'
... ' "consequence": ['
... '{"type": "lose", "applyTo": "active", "value": "helmet",'
... ' "delay": null, "charges": null, "hidden": false},'
... ' {"type": "gain", "applyTo": "active",'
... ' "value": {"^^d": "t", "values": ["token", 1]},'
... ' "delay": null, "charges": null, "hidden": false'
... '},'
... ' {"condition":'
... ' {"^^d": "R", "value": "token*2"},'
... ' "consequence": ['
... '{"type": "set", "applyTo": "active",'
... ' "value": {"^^d": "t", "values": ['
... '{"^^d": "nt", "name": "MechanismSpecifier",'
... ' "values": {"domain": null, "zone": null, "decision": null,'
... ' "name": "grate"}}, "open"]},'
... ' "delay": null, "charges": null, "hidden": false'
... '},'
... ' {"type": "deactivate", "applyTo": "active", "value": null,'
... ' "delay": null, "charges": null, "hidden": false'
... '}'
... '],'
... ' "alternative": []'
... '}'
... '],'
... ' "source": 2,'
... ' "target": 2,'
... ' "key": "pull_lever"'
... '}'
... ']'
... '},'
... ' "_byEdge": {"^^d": "d", "items":'
... ' [[0, {"left": 1, "up_left": 1, "down": 2}],'
... ' [1, {"right": 0, "up_right": 0}],'
... ' [2, {"up": 0, "grab_helmet": 2, "pull_lever": 2}]]},'
... ' "zones": {"zoneA":'
... ' {"^^d": "nt", "name": "ZoneInfo",'
... ' "values": {'
... '"level": 0,'
... ' "parents": {"^^d": "s", "values": ["upZone"]},'
... ' "contents": {"^^d": "s", "values": [0, 2]},'
... ' "tags": {},'
... ' "annotations": []'
... '}'
... '},'
... ' "zoneB":'
... ' {"^^d": "nt", "name": "ZoneInfo",'
... ' "values": {'
... '"level": 0,'
... ' "parents": {"^^d": "s", "values": []},'
... ' "contents": {"^^d": "s", "values": [1]},'
... ' "tags": {},'
... ' "annotations": []'
... '}'
... '},'
... ' "upZone":'
... ' {"^^d": "nt", "name": "ZoneInfo",'
... ' "values": {'
... '"level": 1,'
... ' "parents": {"^^d": "s", "values": []},'
... ' "contents": {"^^d": "s", "values": ["zoneA"]},'
... ' "tags": {},'
... ' "annotations": []'
... '}'
... '}'
... '},'
... ' "unknownCount": 0,'
... ' "equivalences": {"^^d": "d", "items": ['
... '[{"^^d": "t", "values": [0, "open"]},'
... ' {"^^d": "s", "values": ['
... '{"^^d": "R", "value": "helmet"}]}]'
... ']},'
... ' "reversionTypes": {},'
... ' "nextID": 3,'
... ' "nextMechanismID": 1,'
... ' "mechanisms": {"^^d": "d", "items": ['
... '[0, {"^^d": "t", "values": [0, "grate"]}]]},'
... ' "globalMechanisms": {},'
... ' "nameLookup": {"A": [0], "B": [1], "C": [2]}'
... '}'
... )
>>> for i in range(len(j)):
... if j[i] != expected[i:i+1]:
... print(
... 'exp: ' + expected[i-10:i+50] + '\ngot: ' + j[i-10:i+50]
... )
... break
>>> j == expected
True
>>> rec = fromJSON(j)
>>> rec.nodes == dg.nodes
True
>>> rec.edges == dg.edges
True
>>> rec.unknownCount == dg.unknownCount
True
>>> rec.equivalences == dg.equivalences
True
>>> rec.reversionTypes == dg.reversionTypes
True
>>> rec._byEdge == dg._byEdge
True
>>> rec.zones == dg.zones
True
>>> for diff in dg.listDifferences(rec):
... print(diff)
>>> rec == dg
True
base.MetricSpace example:
>>> ms = base.MetricSpace("test")
>>> ms.addPoint([2, 3])
0
>>> ms.addPoint([2, 7, 0])
1
>>> ms.addPoint([2, 7])
2
>>> toJSON(ms)
'{"^^d": "MS", "name": "test", "points": {"^^d": "d", "items": [[0, [2, 3]], [1, [2, 7, 0]], [2, [2, 7]]]}, "lastID": 2}'
>>> ms.removePoint(0)
>>> ms.removePoint(1)
>>> ms.removePoint(2)
>>> toJSON(ms)
'{"^^d": "MS", "name": "test", "points": {}, "lastID": 2}'
>>> ms.addPoint([5, 6])
3
>>> ms.addPoint([7, 8])
4
>>> toJSON(ms)
'{"^^d": "MS", "name": "test", "points": {"^^d": "d", "items": [[3, [5, 6]], [4, [7, 8]]]}, "lastID": 4}'
TODO: more examples, including one for a DiscreteExploration
6858 def default(self, o: Any) -> Any: 6859 """ 6860 Re-writes objects for encoding. We re-write the following 6861 objects: 6862 6863 - `set` 6864 - `dict` (if the keys aren't all strings) 6865 - `tuple`/`namedtuple` 6866 - `ZoneInfo` 6867 - `Requirement` 6868 - `SkillCombination` 6869 - `DecisionGraph` 6870 - `DiscreteExploration` 6871 - `MetricSpace` 6872 6873 TODO: FeatureGraph... 6874 """ 6875 if isinstance(o, list): 6876 return [self.default(x) for x in o] 6877 6878 elif isinstance(o, set): 6879 return { 6880 '^^d': 's', 6881 'values': sorted( 6882 [self.default(e) for e in o], 6883 key=lambda x: str(x) 6884 ) 6885 } 6886 6887 elif isinstance(o, dict): 6888 if all(isinstance(k, str) for k in o): 6889 return { 6890 k: self.default(v) 6891 for k, v in o.items() 6892 } 6893 else: 6894 return { 6895 '^^d': 'd', 6896 'items': [ 6897 [self.default(k), self.default(v)] 6898 for (k, v) in o.items() 6899 ] 6900 } 6901 6902 elif isinstance(o, tuple): 6903 if hasattr(o, '_fields') and hasattr(o, '_asdict'): 6904 # Named tuple 6905 return { 6906 '^^d': 'nt', 6907 'name': o.__class__.__name__, 6908 'values': { 6909 k: self.default(v) 6910 for k, v in o._asdict().items() 6911 } 6912 } 6913 else: 6914 # Normal tuple 6915 return { 6916 '^^d': 't', 6917 "values": [self.default(e) for e in o] 6918 } 6919 6920 elif isinstance(o, base.Requirement): 6921 return { 6922 '^^d': 'R', 6923 'value': o.unparse() 6924 } 6925 6926 elif isinstance(o, base.SkillCombination): 6927 return { 6928 '^^d': 'SC', 6929 'value': o.unparse() 6930 } 6931 # TODO: Consequence, Condition, Challenge, and Effect here? 6932 6933 elif isinstance(o, core.DecisionGraph): 6934 return { 6935 '^^d': 'DG', 6936 'props': self.default(o.graph), # type:ignore [attr-defined] 6937 'node_links': self.default( 6938 networkx.node_link_data(o, edges="links") # type: ignore 6939 # TODO: Fix networkx stubs 6940 ), 6941 '_byEdge': self.default(o._byEdge), 6942 'zones': self.default(o.zones), 6943 'unknownCount': o.unknownCount, 6944 'equivalences': self.default(o.equivalences), 6945 'reversionTypes': self.default(o.reversionTypes), 6946 'nextID': o.nextID, 6947 'nextMechanismID': o.nextMechanismID, 6948 'mechanisms': self.default(o.mechanisms), 6949 'globalMechanisms': self.default(o.globalMechanisms), 6950 'nameLookup': self.default(o.nameLookup) 6951 } 6952 6953 elif isinstance(o, core.DiscreteExploration): 6954 return { 6955 '^^d': 'DE', 6956 'situations': self.default(o.situations) 6957 } 6958 6959 elif isinstance(o, base.MetricSpace): 6960 return { 6961 '^^d': 'MS', 6962 'name': o.name, 6963 'points': self.default(o.points), 6964 'lastID': o.lastID() 6965 } 6966 6967 else: 6968 return o
Re-writes objects for encoding. We re-write the following objects:
setdict(if the keys aren't all strings)tuple/namedtupleZoneInfoRequirementSkillCombinationDecisionGraphDiscreteExplorationMetricSpace
TODO: FeatureGraph...
6970 def encode(self, o: Any) -> str: 6971 """ 6972 Custom encode function since we need to override behavior for 6973 tuples and dicts. 6974 """ 6975 if isinstance(o, (tuple, dict, set)): 6976 o = self.default(o) 6977 elif isinstance(o, list): 6978 o = [self.default(x) for x in o] 6979 6980 try: 6981 return super().encode(o) 6982 except TypeError: 6983 return super().encode(self.default(o))
Custom encode function since we need to override behavior for tuples and dicts.
6985 def iterencode( 6986 self, 6987 o: Any, 6988 _one_shot: bool = False 6989 ) -> Generator[str, None, None]: 6990 """ 6991 Custom iterencode function since we need to override behavior for 6992 tuples and dicts. 6993 """ 6994 if isinstance(o, (tuple, dict)): 6995 o = self.default(o) 6996 6997 yield from super().iterencode(o, _one_shot=_one_shot)
Custom iterencode function since we need to override behavior for tuples and dicts.
Inherited Members
- json.encoder.JSONEncoder
- JSONEncoder
- item_separator
- key_separator
- skipkeys
- ensure_ascii
- check_circular
- allow_nan
- sort_keys
- indent
7000class CustomJSONDecoder(json.JSONDecoder): 7001 """ 7002 A custom JSON decoder that has special protocols for handling 7003 several types, including: 7004 7005 - `set` 7006 - `tuple` & `namedtuple` 7007 - `dict` (where keys aren't all strings) 7008 - `Requirement` 7009 - `SkillCombination` 7010 - `DecisionGraph` 7011 - `DiscreteExploration` 7012 - `MetricSpace` 7013 7014 Used by `toJSON` 7015 7016 When initializing it, you can st a custom parse format by supplying 7017 a 'parseFormat' keyword argument; by default a standard 7018 `ParseFormat` will be used. 7019 7020 Examples: 7021 7022 >>> r = base.ReqAny([ 7023 ... base.ReqCapability('power'), 7024 ... base.ReqTokens('money', 5) 7025 ... ]) 7026 >>> s = toJSON(r) 7027 >>> s 7028 '{"^^d": "R", "value": "(power|money*5)"}' 7029 >>> l = fromJSON(s) 7030 >>> r == l 7031 True 7032 >>> o = {1, 2, 'hi'} 7033 >>> s = toJSON(o) 7034 >>> s 7035 '{"^^d": "s", "values": [1, 2, "hi"]}' 7036 >>> l = fromJSON(s) 7037 >>> o == l 7038 True 7039 >>> zi = base.ZoneInfo(1, set(), set(), {}, []) 7040 >>> s = toJSON(zi) 7041 >>> c = ( 7042 ... '{"^^d": "nt", "name": "ZoneInfo", "values": {' 7043 ... '"level": 1,' 7044 ... ' "parents": {"^^d": "s", "values": []},' 7045 ... ' "contents": {"^^d": "s", "values": []},' 7046 ... ' "tags": {},' 7047 ... ' "annotations": []' 7048 ... '}}' 7049 ... ) 7050 >>> s == c 7051 True 7052 >>> setm = base.effect(set=("door", "open")) 7053 >>> s = toJSON(setm) 7054 >>> f = fromJSON(s) 7055 >>> f == setm 7056 True 7057 >>> pf = ParseFormat() 7058 >>> pf.unparseEffect(f) 7059 'set door:open' 7060 >>> pf.unparseEffect(f) == pf.unparseEffect(setm) 7061 True 7062 >>> g = core.DecisionGraph() 7063 >>> g.addDecision('A') 7064 0 7065 >>> g.addDecision('B') 7066 1 7067 >>> g.addTransition('A', 'up', 'B', 'down') 7068 >>> g2 = fromJSON(toJSON(g)) 7069 >>> g2.destinationsFrom('A') 7070 {'up': 1} 7071 >>> g2.destinationsFrom('B') 7072 {'down': 0} 7073 >>> g2.addDecision('C') 7074 2 7075 >>> g2.addTransition('A', 'right', 'C', 'left') 7076 >>> g2.destinationsFrom('A') 7077 {'up': 1, 'right': 2} 7078 >>> g2.destinationsFrom('C') 7079 {'left': 0} 7080 7081 TODO: SkillCombination example 7082 """ 7083 def __init__(self, *args, **kwargs): 7084 if 'object_hook' in kwargs: 7085 outerHook = kwargs['object_hook'] 7086 kwargs['object_hook'] = ( 7087 lambda o: outerHook(self.unpack(o)) 7088 ) 7089 # TODO: What if it's a positional argument? :( 7090 else: 7091 kwargs['object_hook'] = lambda o: self.unpack(o) 7092 7093 if 'parseFormat' in kwargs: 7094 self.parseFormat = kwargs['parseFormat'] 7095 del kwargs['parseFormat'] 7096 else: 7097 self.parseFormat = ParseFormat() 7098 7099 super().__init__(*args, **kwargs) 7100 7101 def unpack(self, obj: Any) -> Any: 7102 """ 7103 Unpacks an object; used as the `object_hook` for decoding. 7104 """ 7105 if '^^d' in obj: 7106 asType = obj['^^d'] 7107 if asType == 't': 7108 return tuple(obj['values']) 7109 7110 elif asType == 'nt': 7111 g = globals() 7112 name = obj['name'] 7113 values = obj['values'] 7114 # Use an existing global namedtuple class if there is 7115 # one that goes by the specified name, so that we don't 7116 # create too many spurious equivalent namedtuple 7117 # classes. But fall back on creating a new namedtuple 7118 # class if we need to: 7119 ntClass = g.get(name) 7120 if ( 7121 ntClass is None 7122 or not issubclass(ntClass, tuple) 7123 or not hasattr(ntClass, '_asdict') 7124 ): 7125 # Now try again specifically in the base module where 7126 # most of our nametuples are defined (TODO: NOT this 7127 # hack..., but it does make isinstance work...) 7128 ntClass = getattr(base, name, None) 7129 if ( 7130 ntClass is None 7131 or not issubclass(ntClass, tuple) 7132 or not hasattr(ntClass, '_asdict') 7133 ): 7134 # TODO: cache these... 7135 ntClass = collections.namedtuple( # type: ignore 7136 name, 7137 values.keys() 7138 ) 7139 ntClass = cast(Callable, ntClass) 7140 return ntClass(**values) 7141 7142 elif asType == 's': 7143 return set(obj['values']) 7144 7145 elif asType == 'd': 7146 return dict(obj['items']) 7147 7148 elif asType == 'R': 7149 return self.parseFormat.parseRequirement(obj['value']) 7150 7151 elif asType == 'SC': 7152 return self.parseFormat.parseSkillCombination(obj['value']) 7153 7154 elif asType == 'E': 7155 return self.parseFormat.parseEffect(obj['value']) 7156 7157 elif asType == 'Ch': 7158 return self.parseFormat.parseChallenge(obj['value']) 7159 7160 elif asType == 'Cd': 7161 return self.parseFormat.parseCondition(obj['value']) 7162 7163 elif asType == 'Cq': 7164 return self.parseFormat.parseConsequence(obj['value']) 7165 7166 elif asType == 'DG': 7167 baseGraph: networkx.MultiDiGraph = networkx.node_link_graph( 7168 obj['node_links'], 7169 edges="links" 7170 ) # type: ignore 7171 # TODO: Fix networkx stubs 7172 graphResult = core.DecisionGraph() 7173 # Copy over instance attributes minus internals 7174 # TODO: Do we need internals? 7175 for (attr, val) in baseGraph.__dict__.items(): 7176 # Note: __dict__ over dir() here to avoid attributes 7177 # of type and get just attributes of instance 7178 if attr == "name": # name will get copied below 7179 continue 7180 if not attr.startswith('__') or not attr.endswith('__'): 7181 setattr( 7182 graphResult, 7183 attr, 7184 copy.deepcopy(val) 7185 # TODO: Does this copying disentangle too 7186 # much? Which values even get copied this 7187 # way? 7188 ) 7189 7190 if baseGraph.name != '': 7191 graphResult.name = baseGraph.name 7192 graphResult.graph.update(obj['props']) # type:ignore [attr-defined] # noqa 7193 storedByEdge = obj['_byEdge'] 7194 graphResult._byEdge = { 7195 int(k): storedByEdge[k] 7196 for k in storedByEdge 7197 } 7198 graphResult.zones = obj['zones'] 7199 graphResult.unknownCount = obj['unknownCount'] 7200 graphResult.equivalences = obj['equivalences'] 7201 graphResult.reversionTypes = obj['reversionTypes'] 7202 graphResult.nextID = obj.get('nextID') 7203 # Old code didn't store nextID; we extrapolate if necessary 7204 if graphResult.nextID is None: 7205 graphResult.nextID = max(graphResult.nodes) + 1 7206 graphResult.nextMechanismID = obj['nextMechanismID'] 7207 graphResult.mechanisms = { 7208 int(k): v 7209 for k, v in 7210 obj['mechanisms'].items() 7211 } 7212 graphResult.globalMechanisms = obj['globalMechanisms'] 7213 graphResult.nameLookup = obj['nameLookup'] 7214 return graphResult 7215 7216 elif asType == 'DE': 7217 exResult = core.DiscreteExploration() 7218 exResult.situations = obj['situations'] 7219 return exResult 7220 7221 elif asType == 'MS': 7222 msResult = base.MetricSpace(obj['name']) 7223 msResult.points = obj['points'] 7224 msResult.nextID = obj['lastID'] + 1 7225 return msResult 7226 7227 else: 7228 raise NotImplementedError( 7229 f"No special handling has been defined for" 7230 f" decoding type '{asType}'." 7231 ) 7232 7233 else: 7234 return obj
A custom JSON decoder that has special protocols for handling several types, including:
settuple&namedtupledict(where keys aren't all strings)RequirementSkillCombinationDecisionGraphDiscreteExplorationMetricSpace
Used by toJSON
When initializing it, you can st a custom parse format by supplying
a 'parseFormat' keyword argument; by default a standard
ParseFormat will be used.
Examples:
>>> r = base.ReqAny([
... base.ReqCapability('power'),
... base.ReqTokens('money', 5)
... ])
>>> s = toJSON(r)
>>> s
'{"^^d": "R", "value": "(power|money*5)"}'
>>> l = fromJSON(s)
>>> r == l
True
>>> o = {1, 2, 'hi'}
>>> s = toJSON(o)
>>> s
'{"^^d": "s", "values": [1, 2, "hi"]}'
>>> l = fromJSON(s)
>>> o == l
True
>>> zi = base.ZoneInfo(1, set(), set(), {}, [])
>>> s = toJSON(zi)
>>> c = (
... '{"^^d": "nt", "name": "ZoneInfo", "values": {'
... '"level": 1,'
... ' "parents": {"^^d": "s", "values": []},'
... ' "contents": {"^^d": "s", "values": []},'
... ' "tags": {},'
... ' "annotations": []'
... '}}'
... )
>>> s == c
True
>>> setm = base.effect(set=("door", "open"))
>>> s = toJSON(setm)
>>> f = fromJSON(s)
>>> f == setm
True
>>> pf = ParseFormat()
>>> pf.unparseEffect(f)
'set door:open'
>>> pf.unparseEffect(f) == pf.unparseEffect(setm)
True
>>> g = core.DecisionGraph()
>>> g.addDecision('A')
0
>>> g.addDecision('B')
1
>>> g.addTransition('A', 'up', 'B', 'down')
>>> g2 = fromJSON(toJSON(g))
>>> g2.destinationsFrom('A')
{'up': 1}
>>> g2.destinationsFrom('B')
{'down': 0}
>>> g2.addDecision('C')
2
>>> g2.addTransition('A', 'right', 'C', 'left')
>>> g2.destinationsFrom('A')
{'up': 1, 'right': 2}
>>> g2.destinationsFrom('C')
{'left': 0}
TODO: SkillCombination example
7083 def __init__(self, *args, **kwargs): 7084 if 'object_hook' in kwargs: 7085 outerHook = kwargs['object_hook'] 7086 kwargs['object_hook'] = ( 7087 lambda o: outerHook(self.unpack(o)) 7088 ) 7089 # TODO: What if it's a positional argument? :( 7090 else: 7091 kwargs['object_hook'] = lambda o: self.unpack(o) 7092 7093 if 'parseFormat' in kwargs: 7094 self.parseFormat = kwargs['parseFormat'] 7095 del kwargs['parseFormat'] 7096 else: 7097 self.parseFormat = ParseFormat() 7098 7099 super().__init__(*args, **kwargs)
object_hook, if specified, will be called with the result
of every JSON object decoded and its return value will be used in
place of the given dict. This can be used to provide custom
deserializations (e.g. to support JSON-RPC class hinting).
object_pairs_hook, if specified will be called with the result of
every JSON object decoded with an ordered list of pairs. The return
value of object_pairs_hook will be used instead of the dict.
This feature can be used to implement custom decoders.
If object_hook is also defined, the object_pairs_hook takes
priority.
parse_float, if specified, will be called with the string
of every JSON float to be decoded. By default this is equivalent to
float(num_str). This can be used to use another datatype or parser
for JSON floats (e.g. decimal.Decimal).
parse_int, if specified, will be called with the string
of every JSON int to be decoded. By default this is equivalent to
int(num_str). This can be used to use another datatype or parser
for JSON integers (e.g. float).
parse_constant, if specified, will be called with one of the
following strings: -Infinity, Infinity, NaN.
This can be used to raise an exception if invalid JSON numbers
are encountered.
If strict is false (true is the default), then control
characters will be allowed inside strings. Control characters in
this context are those with character codes in the 0-31 range,
including '\t' (tab), '\n', '\r' and '\0'.
7101 def unpack(self, obj: Any) -> Any: 7102 """ 7103 Unpacks an object; used as the `object_hook` for decoding. 7104 """ 7105 if '^^d' in obj: 7106 asType = obj['^^d'] 7107 if asType == 't': 7108 return tuple(obj['values']) 7109 7110 elif asType == 'nt': 7111 g = globals() 7112 name = obj['name'] 7113 values = obj['values'] 7114 # Use an existing global namedtuple class if there is 7115 # one that goes by the specified name, so that we don't 7116 # create too many spurious equivalent namedtuple 7117 # classes. But fall back on creating a new namedtuple 7118 # class if we need to: 7119 ntClass = g.get(name) 7120 if ( 7121 ntClass is None 7122 or not issubclass(ntClass, tuple) 7123 or not hasattr(ntClass, '_asdict') 7124 ): 7125 # Now try again specifically in the base module where 7126 # most of our nametuples are defined (TODO: NOT this 7127 # hack..., but it does make isinstance work...) 7128 ntClass = getattr(base, name, None) 7129 if ( 7130 ntClass is None 7131 or not issubclass(ntClass, tuple) 7132 or not hasattr(ntClass, '_asdict') 7133 ): 7134 # TODO: cache these... 7135 ntClass = collections.namedtuple( # type: ignore 7136 name, 7137 values.keys() 7138 ) 7139 ntClass = cast(Callable, ntClass) 7140 return ntClass(**values) 7141 7142 elif asType == 's': 7143 return set(obj['values']) 7144 7145 elif asType == 'd': 7146 return dict(obj['items']) 7147 7148 elif asType == 'R': 7149 return self.parseFormat.parseRequirement(obj['value']) 7150 7151 elif asType == 'SC': 7152 return self.parseFormat.parseSkillCombination(obj['value']) 7153 7154 elif asType == 'E': 7155 return self.parseFormat.parseEffect(obj['value']) 7156 7157 elif asType == 'Ch': 7158 return self.parseFormat.parseChallenge(obj['value']) 7159 7160 elif asType == 'Cd': 7161 return self.parseFormat.parseCondition(obj['value']) 7162 7163 elif asType == 'Cq': 7164 return self.parseFormat.parseConsequence(obj['value']) 7165 7166 elif asType == 'DG': 7167 baseGraph: networkx.MultiDiGraph = networkx.node_link_graph( 7168 obj['node_links'], 7169 edges="links" 7170 ) # type: ignore 7171 # TODO: Fix networkx stubs 7172 graphResult = core.DecisionGraph() 7173 # Copy over instance attributes minus internals 7174 # TODO: Do we need internals? 7175 for (attr, val) in baseGraph.__dict__.items(): 7176 # Note: __dict__ over dir() here to avoid attributes 7177 # of type and get just attributes of instance 7178 if attr == "name": # name will get copied below 7179 continue 7180 if not attr.startswith('__') or not attr.endswith('__'): 7181 setattr( 7182 graphResult, 7183 attr, 7184 copy.deepcopy(val) 7185 # TODO: Does this copying disentangle too 7186 # much? Which values even get copied this 7187 # way? 7188 ) 7189 7190 if baseGraph.name != '': 7191 graphResult.name = baseGraph.name 7192 graphResult.graph.update(obj['props']) # type:ignore [attr-defined] # noqa 7193 storedByEdge = obj['_byEdge'] 7194 graphResult._byEdge = { 7195 int(k): storedByEdge[k] 7196 for k in storedByEdge 7197 } 7198 graphResult.zones = obj['zones'] 7199 graphResult.unknownCount = obj['unknownCount'] 7200 graphResult.equivalences = obj['equivalences'] 7201 graphResult.reversionTypes = obj['reversionTypes'] 7202 graphResult.nextID = obj.get('nextID') 7203 # Old code didn't store nextID; we extrapolate if necessary 7204 if graphResult.nextID is None: 7205 graphResult.nextID = max(graphResult.nodes) + 1 7206 graphResult.nextMechanismID = obj['nextMechanismID'] 7207 graphResult.mechanisms = { 7208 int(k): v 7209 for k, v in 7210 obj['mechanisms'].items() 7211 } 7212 graphResult.globalMechanisms = obj['globalMechanisms'] 7213 graphResult.nameLookup = obj['nameLookup'] 7214 return graphResult 7215 7216 elif asType == 'DE': 7217 exResult = core.DiscreteExploration() 7218 exResult.situations = obj['situations'] 7219 return exResult 7220 7221 elif asType == 'MS': 7222 msResult = base.MetricSpace(obj['name']) 7223 msResult.points = obj['points'] 7224 msResult.nextID = obj['lastID'] + 1 7225 return msResult 7226 7227 else: 7228 raise NotImplementedError( 7229 f"No special handling has been defined for" 7230 f" decoding type '{asType}'." 7231 ) 7232 7233 else: 7234 return obj
Unpacks an object; used as the object_hook for decoding.
Inherited Members
- json.decoder.JSONDecoder
- object_hook
- parse_float
- parse_int
- parse_constant
- strict
- object_pairs_hook
- parse_object
- parse_array
- parse_string
- memo
- scan_once
- decode
- raw_decode