Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pyparsing/helpers.py: 25%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

356 statements  

1# helpers.py 

2import html.entities 

3import operator 

4import re 

5import sys 

6import typing 

7 

8from . import __diag__ 

9from .core import * 

10from .util import ( 

11 _bslash, 

12 _flatten, 

13 _escape_regex_range_chars, 

14 make_compressed_re, 

15 replaced_by_pep8, 

16) 

17 

18 

19def _suppression(expr: Union[ParserElement, str]) -> ParserElement: 

20 # internal helper to avoid wrapping Suppress inside another Suppress 

21 if isinstance(expr, Suppress): 

22 return expr 

23 return Suppress(expr) 

24 

25 

26# 

27# global helpers 

28# 

29def counted_array( 

30 expr: ParserElement, int_expr: typing.Optional[ParserElement] = None, **kwargs 

31) -> ParserElement: 

32 """Helper to define a counted list of expressions. 

33 

34 This helper defines a pattern of the form:: 

35 

36 integer expr expr expr... 

37 

38 where the leading integer tells how many expr expressions follow. 

39 The matched tokens returns the array of expr tokens as a list - the 

40 leading count token is suppressed. 

41 

42 If ``int_expr`` is specified, it should be a pyparsing expression 

43 that produces an integer value. 

44 

45 Examples: 

46 

47 .. doctest:: 

48 

49 >>> counted_array(Word(alphas)).parse_string('2 ab cd ef') 

50 ParseResults(['ab', 'cd'], {}) 

51 

52 - In this parser, the leading integer value is given in binary, 

53 '10' indicating that 2 values are in the array: 

54 

55 .. doctest:: 

56 

57 >>> binary_constant = Word('01').set_parse_action(lambda t: int(t[0], 2)) 

58 >>> counted_array(Word(alphas), int_expr=binary_constant 

59 ... ).parse_string('10 ab cd ef') 

60 ParseResults(['ab', 'cd'], {}) 

61 

62 - If other fields must be parsed after the count but before the 

63 list items, give the fields results names and they will 

64 be preserved in the returned ParseResults: 

65 

66 .. doctest:: 

67 

68 >>> ppc = pyparsing.common 

69 >>> count_with_metadata = ppc.integer + Word(alphas)("type") 

70 >>> typed_array = counted_array(Word(alphanums), 

71 ... int_expr=count_with_metadata)("items") 

72 >>> result = typed_array.parse_string("3 bool True True False") 

73 >>> print(result.dump()) 

74 ['True', 'True', 'False'] 

75 - items: ['True', 'True', 'False'] 

76 - type: 'bool' 

77 """ 

78 intExpr: typing.Optional[ParserElement] = deprecate_argument( 

79 kwargs, "intExpr", None 

80 ) 

81 

82 intExpr = intExpr or int_expr 

83 array_expr = Forward() 

84 

85 def count_field_parse_action(s, l, t): 

86 nonlocal array_expr 

87 n = t[0] 

88 array_expr <<= (expr * n) if n else Empty() 

89 # clear list contents, but keep any named results 

90 del t[:] 

91 

92 if intExpr is None: 

93 intExpr = Word(nums).set_parse_action(lambda t: int(t[0])) 

94 else: 

95 intExpr = intExpr.copy() 

96 intExpr.set_name("arrayLen") 

97 intExpr.add_parse_action(count_field_parse_action, call_during_try=True) 

98 return (intExpr + array_expr).set_name(f"(len) {expr}...") 

99 

100 

101def match_previous_literal(expr: ParserElement) -> ParserElement: 

102 """Helper to define an expression that is indirectly defined from 

103 the tokens matched in a previous expression, that is, it looks for 

104 a 'repeat' of a previous expression. For example:: 

105 

106 .. testcode:: 

107 

108 first = Word(nums) 

109 second = match_previous_literal(first) 

110 match_expr = first + ":" + second 

111 

112 will match ``"1:1"``, but not ``"1:2"``. Because this 

113 matches a previous literal, will also match the leading 

114 ``"1:1"`` in ``"1:10"``. If this is not desired, use 

115 :class:`match_previous_expr`. Do *not* use with packrat parsing 

116 enabled. 

117 """ 

118 rep = Forward() 

119 

120 def copy_token_to_repeater(s, l, t): 

121 if not t: 

122 rep << Empty() 

123 return 

124 

125 if len(t) == 1: 

126 rep << t[0] 

127 return 

128 

129 # flatten t tokens 

130 tflat = _flatten(t.as_list()) 

131 rep << And(Literal(tt) for tt in tflat) 

132 

133 expr.add_parse_action(copy_token_to_repeater, call_during_try=True) 

134 rep.set_name(f"(prev) {expr}") 

135 return rep 

136 

137 

138def match_previous_expr(expr: ParserElement) -> ParserElement: 

139 """Helper to define an expression that is indirectly defined from 

140 the tokens matched in a previous expression, that is, it looks for 

141 a 'repeat' of a previous expression. For example: 

142 

143 .. testcode:: 

144 

145 first = Word(nums) 

146 second = match_previous_expr(first) 

147 match_expr = first + ":" + second 

148 

149 will match ``"1:1"``, but not ``"1:2"``. Because this 

150 matches by expressions, will *not* match the leading ``"1:1"`` 

151 in ``"1:10"``; the expressions are evaluated first, and then 

152 compared, so ``"1"`` is compared with ``"10"``. Do *not* use 

153 with packrat parsing enabled. 

154 

155 Matches may be nested, so that an inner match is paired with the 

156 nearest enclosing occurrence of ``expr``: 

157 

158 .. testcode:: 

159 

160 tag_name = Word(alphas) 

161 open_tag = "<" + tag_name + ">" 

162 close_tag = "</" + match_previous_expr(tag_name) + ">" 

163 

164 will match ``"<a><b></b></a>"``, but not ``"<a><b></a></b>"``. 

165 """ 

166 rep = Forward() 

167 e2 = expr.copy() 

168 rep <<= e2 

169 

170 # stack of (location, tokens) for matches of expr not yet paired with a 

171 # match of rep, innermost last 

172 match_stack: list[tuple[int, list]] = [] 

173 last_match: typing.Optional[tuple[int, list]] = None 

174 

175 def copy_token_to_repeater(s, l, t): 

176 nonlocal last_match 

177 

178 # entries at or after this location belong to a branch that was 

179 # abandoned on backtracking, and can never be paired 

180 while match_stack and match_stack[-1][0] >= l: 

181 match_stack.pop() 

182 match_stack.append((l, _flatten(t.as_list()))) 

183 last_match = None 

184 

185 def must_match_these_tokens(s, l, t): 

186 nonlocal last_match 

187 

188 these_tokens = _flatten(t.as_list()) 

189 

190 # a memoizing parser may run this action more than once for the 

191 # same match, which must not consume a second stack entry 

192 if last_match == (l, these_tokens): 

193 return 

194 

195 if not match_stack: 

196 raise ParseException(s, l, "no previous expression to match") 

197 

198 match_tokens = match_stack[-1][1] 

199 if these_tokens != match_tokens: 

200 raise ParseException(s, l, f"Expected {match_tokens}, found{these_tokens}") 

201 match_stack.pop() 

202 last_match = (l, these_tokens) 

203 

204 e2.add_parse_action(must_match_these_tokens, call_during_try=True) 

205 expr.add_parse_action(copy_token_to_repeater, call_during_try=True) 

206 rep.set_name(f"(prev) {expr}") 

207 return rep 

208 

209 

210def one_of( 

211 strs: Union[typing.Iterable[str], str], 

212 caseless: bool = False, 

213 use_regex: bool = True, 

214 as_keyword: bool = False, 

215 **kwargs, 

216) -> ParserElement: 

217 """Helper to quickly define a set of alternative :class:`Literal` s, 

218 and makes sure to do longest-first testing when there is a conflict, 

219 regardless of the input order, but returns 

220 a :class:`MatchFirst` for best performance. 

221 

222 :param strs: a string of space-delimited literals, or a collection of 

223 string literals 

224 :param caseless: treat all literals as caseless 

225 :param use_regex: bool - as an optimization, will 

226 generate a :class:`Regex` object; otherwise, will generate 

227 a :class:`MatchFirst` object (if ``caseless=True`` or 

228 ``as_keyword=True``, or if creating a :class:`Regex` raises an exception) 

229 :param as_keyword: bool - enforce :class:`Keyword`-style matching on the 

230 generated expressions 

231 

232 Parameters ``asKeyword`` and ``useRegex`` are retained for pre-PEP8 

233 compatibility, but will be removed in a future release. 

234 

235 Example: 

236 

237 .. testcode:: 

238 

239 comp_oper = one_of("< = > <= >= !=") 

240 var = Word(alphas) 

241 number = Word(nums) 

242 term = var | number 

243 comparison_expr = term + comp_oper + term 

244 print(comparison_expr.search_string("B = 12 AA=23 B<=AA AA>12")) 

245 

246 prints: 

247 

248 .. testoutput:: 

249 

250 [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']] 

251 """ 

252 useRegex: bool = deprecate_argument(kwargs, "useRegex", True) 

253 asKeyword: bool = deprecate_argument(kwargs, "asKeyword", False) 

254 

255 asKeyword = asKeyword or as_keyword 

256 useRegex = useRegex and use_regex 

257 

258 if ( 

259 isinstance(caseless, str_type) 

260 and __diag__.warn_on_multiple_string_args_to_oneof 

261 ): 

262 warnings.warn( 

263 "warn_on_multiple_string_args_to_oneof:" 

264 " More than one string argument passed to one_of, pass" 

265 " choices as a list or space-delimited string", 

266 PyparsingDiagnosticWarning, 

267 stacklevel=2, 

268 ) 

269 

270 if caseless: 

271 is_equal = lambda a, b: a.upper() == b.upper() 

272 masks = lambda a, b: b.upper().startswith(a.upper()) 

273 else: 

274 is_equal = operator.eq 

275 masks = lambda a, b: b.startswith(a) 

276 

277 symbols: list[str] 

278 if isinstance(strs, str_type): 

279 strs = typing.cast(str, strs) 

280 symbols = strs.split() 

281 elif isinstance(strs, Iterable): 

282 symbols = list(strs) 

283 else: 

284 raise TypeError("Invalid argument to one_of, expected string or iterable") 

285 if not symbols: 

286 return NoMatch() 

287 

288 # reorder given symbols to take care to avoid masking longer choices with shorter ones 

289 # (but only if the given symbols are not just single characters) 

290 i = 0 

291 while i < len(symbols) - 1: 

292 cur = symbols[i] 

293 for j, other in enumerate(symbols[i + 1 :]): 

294 if is_equal(other, cur): 

295 del symbols[i + j + 1] 

296 break 

297 if len(other) > len(cur) and masks(cur, other): 

298 del symbols[i + j + 1] 

299 symbols.insert(i, other) 

300 break 

301 else: 

302 i += 1 

303 

304 if useRegex: 

305 re_flags: int = re.IGNORECASE if caseless else 0 

306 

307 try: 

308 if all(len(sym) == 1 for sym in symbols): 

309 # symbols are just single characters, create range regex pattern 

310 patt = f"[{''.join(_escape_regex_range_chars(sym) for sym in symbols)}]" 

311 else: 

312 patt = "|".join(re.escape(sym) for sym in symbols) 

313 

314 # wrap with \b word break markers if defining as keywords 

315 if asKeyword: 

316 patt = rf"\b(?:{patt})\b" 

317 

318 ret = Regex(patt, flags=re_flags) 

319 ret.set_name(" | ".join(repr(s) for s in symbols)) 

320 

321 if caseless: 

322 # add parse action to return symbols as specified, not in random 

323 # casing as found in input string 

324 symbol_map = {sym.lower(): sym for sym in symbols} 

325 ret.add_parse_action(lambda s, l, t: symbol_map[t[0].lower()]) 

326 

327 return ret 

328 

329 except re.error: 

330 warnings.warn( 

331 "Exception creating Regex for one_of, building MatchFirst", 

332 PyparsingDiagnosticWarning, 

333 stacklevel=2, 

334 ) 

335 

336 # last resort, just use MatchFirst of Token class corresponding to caseless 

337 # and asKeyword settings 

338 CASELESS = KEYWORD = True 

339 parse_element_class = { 

340 (CASELESS, KEYWORD): CaselessKeyword, 

341 (CASELESS, not KEYWORD): CaselessLiteral, 

342 (not CASELESS, KEYWORD): Keyword, 

343 (not CASELESS, not KEYWORD): Literal, 

344 }[(caseless, asKeyword)] 

345 return MatchFirst(parse_element_class(sym) for sym in symbols).set_name( 

346 " | ".join(symbols) 

347 ) 

348 

349 

350def dict_of(key: ParserElement, value: ParserElement) -> Dict: 

351 """Helper to easily and clearly define a dictionary by specifying 

352 the respective patterns for the key and value. Takes care of 

353 defining the :class:`Dict`, :class:`ZeroOrMore`, and 

354 :class:`Group` tokens in the proper order. The key pattern 

355 can include delimiting markers or punctuation, as long as they are 

356 suppressed, thereby leaving the significant key text. The value 

357 pattern can include named results, so that the :class:`Dict` results 

358 can include named token fields. 

359 

360 Example: 

361 

362 .. doctest:: 

363 

364 >>> text = "shape: SQUARE posn: upper left color: light blue texture: burlap" 

365 

366 >>> data_word = Word(alphas) 

367 >>> label = data_word + FollowedBy(':') 

368 >>> attr_expr = ( 

369 ... label 

370 ... + Suppress(':') 

371 ... + OneOrMore(data_word, stop_on=label) 

372 ... .set_parse_action(' '.join)) 

373 >>> print(attr_expr[1, ...].parse_string(text).dump()) 

374 ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap'] 

375 

376 >>> attr_label = label 

377 >>> attr_value = Suppress(':') + OneOrMore(data_word, stop_on=label 

378 ... ).set_parse_action(' '.join) 

379 

380 # similar to Dict, but simpler call format 

381 >>> result = dict_of(attr_label, attr_value).parse_string(text) 

382 >>> print(result.dump()) 

383 [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']] 

384 - color: 'light blue' 

385 - posn: 'upper left' 

386 - shape: 'SQUARE' 

387 - texture: 'burlap' 

388 [0]: 

389 ['shape', 'SQUARE'] 

390 [1]: 

391 ['posn', 'upper left'] 

392 [2]: 

393 ['color', 'light blue'] 

394 [3]: 

395 ['texture', 'burlap'] 

396 

397 >>> print(result['shape']) 

398 SQUARE 

399 >>> print(result.shape) # object attribute access works too 

400 SQUARE 

401 >>> print(result.as_dict()) 

402 {'shape': 'SQUARE', 'posn': 'upper left', 'color': 'light blue', 'texture': 'burlap'} 

403 """ 

404 return Dict(OneOrMore(Group(key + value))) 

405 

406 

407def original_text_for( 

408 expr: ParserElement, as_string: bool = True, **kwargs 

409) -> ParserElement: 

410 """Helper to return the original, untokenized text for a given 

411 expression. Useful to restore the parsed fields of an HTML start 

412 tag into the raw tag text itself, or to revert separate tokens with 

413 intervening whitespace back to the original matching input text. By 

414 default, returns a string containing the original parsed text. 

415 

416 If the optional ``as_string`` argument is passed as 

417 ``False``, then the return value is 

418 a :class:`ParseResults` containing any results names that 

419 were originally matched, and a single token containing the original 

420 matched text from the input string. So if the expression passed to 

421 :class:`original_text_for` contains expressions with defined 

422 results names, you must set ``as_string`` to ``False`` if you 

423 want to preserve those results name values. 

424 

425 The ``asString`` pre-PEP8 argument is retained for compatibility, 

426 but will be removed in a future release. 

427 

428 Example: 

429 

430 .. testcode:: 

431 

432 src = "this is test <b> bold <i>text</i> </b> normal text " 

433 for tag in ("b", "i"): 

434 opener, closer = make_html_tags(tag) 

435 patt = original_text_for(opener + ... + closer) 

436 print(patt.search_string(src)[0]) 

437 

438 prints: 

439 

440 .. testoutput:: 

441 

442 ['<b> bold <i>text</i> </b>'] 

443 ['<i>text</i>'] 

444 """ 

445 asString: bool = deprecate_argument(kwargs, "asString", True) 

446 

447 asString = asString and as_string 

448 

449 locMarker = Empty().set_parse_action(lambda s, loc, t: loc) 

450 endlocMarker = locMarker.copy() 

451 endlocMarker.callPreparse = False 

452 matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end") 

453 if asString: 

454 extractText = lambda s, l, t: s[t._original_start : t._original_end] 

455 else: 

456 

457 def extractText(s, l, t): 

458 t[:] = [s[t.pop("_original_start") : t.pop("_original_end")]] 

459 

460 matchExpr.set_parse_action(extractText) 

461 matchExpr.ignoreExprs = expr.ignoreExprs 

462 matchExpr.suppress_warning(Diagnostics.warn_ungrouped_named_tokens_in_collection) 

463 return matchExpr 

464 

465 

466def ungroup(expr: ParserElement) -> ParserElement: 

467 """Helper to undo pyparsing's default grouping of And expressions, 

468 even if all but one are non-empty. 

469 """ 

470 return TokenConverter(expr).add_parse_action(lambda t: t[0]) 

471 

472 

473def locatedExpr(expr: ParserElement) -> ParserElement: 

474 """ 

475 .. deprecated:: 3.0.0 

476 Use the :class:`Located` class instead. Note that `Located` 

477 returns results with one less grouping level. 

478 

479 Helper to decorate a returned token with its starting and ending 

480 locations in the input string. 

481 

482 This helper adds the following results names: 

483 

484 - ``locn_start`` - location where matched expression begins 

485 - ``locn_end`` - location where matched expression ends 

486 - ``value`` - the actual parsed results 

487 

488 Be careful if the input text contains ``<TAB>`` characters, you 

489 may want to call :meth:`ParserElement.parse_with_tabs` 

490 """ 

491 warnings.warn( 

492 f"{'locatedExpr'!r} deprecated - use {'Located'!r}", 

493 PyparsingDeprecationWarning, 

494 stacklevel=2, 

495 ) 

496 

497 locator = Empty().set_parse_action(lambda ss, ll, tt: ll) 

498 return Group( 

499 locator("locn_start") 

500 + expr("value") 

501 + locator.copy().leave_whitespace()("locn_end") 

502 ) 

503 

504 

505# define special default value to permit None as a significant value for 

506# ignore_expr 

507_NO_IGNORE_EXPR_GIVEN = NoMatch() 

508 

509 

510def nested_expr( 

511 opener: Union[str, ParserElement] = "(", 

512 closer: Union[str, ParserElement] = ")", 

513 content: typing.Optional[ParserElement] = None, 

514 ignore_expr: typing.Optional[ParserElement] = _NO_IGNORE_EXPR_GIVEN, 

515 **kwargs, 

516) -> ParserElement: 

517 """Helper method for defining nested lists enclosed in opening and 

518 closing delimiters (``"("`` and ``")"`` are the default). 

519 

520 :param opener: str - opening character for a nested list 

521 (default= ``"("``); can also be a pyparsing expression 

522 

523 :param closer: str - closing character for a nested list 

524 (default= ``")"``); can also be a pyparsing expression 

525 

526 :param content: expression for items within the nested lists 

527 

528 :param ignore_expr: expression for ignoring opening and closing delimiters 

529 (default = :class:`quoted_string`) 

530 

531 Parameter ``ignoreExpr`` is retained for compatibility 

532 but will be removed in a future release. 

533 

534 If an expression is not provided for the content argument, the 

535 nested expression will capture all whitespace-delimited content 

536 between delimiters as a list of separate values. 

537 

538 Use the ``ignore_expr`` argument to define expressions that may 

539 contain opening or closing characters that should not be treated as 

540 opening or closing characters for nesting, such as quoted_string or 

541 a comment expression. Specify multiple expressions using an 

542 :class:`Or` or :class:`MatchFirst`. The default is 

543 :class:`quoted_string`, but if no expressions are to be ignored, then 

544 pass ``None`` for this argument. 

545 

546 Example: 

547 

548 .. testcode:: 

549 

550 data_type = one_of("void int short long char float double") 

551 decl_data_type = Combine(data_type + Opt(Word('*'))) 

552 ident = Word(alphas+'_', alphanums+'_') 

553 number = pyparsing_common.number 

554 arg = Group(decl_data_type + ident) 

555 LPAR, RPAR = map(Suppress, "()") 

556 

557 code_body = nested_expr('{', '}', ignore_expr=(quoted_string | c_style_comment)) 

558 

559 c_function = (decl_data_type("type") 

560 + ident("name") 

561 + LPAR + Opt(DelimitedList(arg), [])("args") + RPAR 

562 + code_body("body")) 

563 c_function.ignore(c_style_comment) 

564 

565 source_code = ''' 

566 int is_odd(int x) { 

567 return (x%2); 

568 } 

569 

570 int dec_to_hex(char hchar) { 

571 if (hchar >= '0' && hchar <= '9') { 

572 return (ord(hchar)-ord('0')); 

573 } else { 

574 return (10+ord(hchar)-ord('A')); 

575 } 

576 } 

577 ''' 

578 for func in c_function.search_string(source_code): 

579 print(f"{func.name} ({func.type}) args: {func.args}") 

580 

581 

582 prints: 

583 

584 .. testoutput:: 

585 

586 is_odd (int) args: [['int', 'x']] 

587 dec_to_hex (int) args: [['char', 'hchar']] 

588 """ 

589 ignoreExpr: ParserElement = deprecate_argument( 

590 kwargs, "ignoreExpr", _NO_IGNORE_EXPR_GIVEN 

591 ) 

592 

593 if ignoreExpr != ignore_expr: 

594 ignoreExpr = ignore_expr if ignoreExpr is _NO_IGNORE_EXPR_GIVEN else ignoreExpr # type: ignore [assignment] 

595 

596 if ignoreExpr is _NO_IGNORE_EXPR_GIVEN: 

597 ignoreExpr = quoted_string() 

598 

599 if opener == closer: 

600 raise ValueError("opening and closing strings cannot be the same") 

601 

602 if content is None: 

603 if isinstance(opener, str_type) and isinstance(closer, str_type): 

604 opener = typing.cast(str, opener) 

605 closer = typing.cast(str, closer) 

606 if len(opener) == 1 and len(closer) == 1: 

607 if ignoreExpr is not None: 

608 content = Combine( 

609 OneOrMore( 

610 ~ignoreExpr 

611 + CharsNotIn( 

612 opener + closer + ParserElement.DEFAULT_WHITE_CHARS, 

613 exact=1, 

614 ) 

615 ) 

616 ) 

617 else: 

618 content = Combine( 

619 Empty() 

620 + CharsNotIn( 

621 opener + closer + ParserElement.DEFAULT_WHITE_CHARS 

622 ) 

623 ) 

624 else: 

625 if ignoreExpr is not None: 

626 content = Combine( 

627 OneOrMore( 

628 ~ignoreExpr 

629 + ~Literal(opener) 

630 + ~Literal(closer) 

631 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1) 

632 ) 

633 ) 

634 else: 

635 content = Combine( 

636 OneOrMore( 

637 ~Literal(opener) 

638 + ~Literal(closer) 

639 + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1) 

640 ) 

641 ) 

642 else: 

643 raise ValueError( 

644 "opening and closing arguments must be strings if no content expression is given" 

645 ) 

646 

647 # for these internally-created context expressions, simulate whitespace-skipping 

648 if ParserElement.DEFAULT_WHITE_CHARS: 

649 content.set_parse_action( 

650 lambda t: t[0].strip(ParserElement.DEFAULT_WHITE_CHARS) 

651 ) 

652 

653 ret = Forward() 

654 if ignoreExpr is not None: 

655 ret <<= Group( 

656 _suppression(opener) 

657 + ZeroOrMore(ignoreExpr | ret | content) 

658 + _suppression(closer) 

659 ) 

660 else: 

661 ret <<= Group( 

662 _suppression(opener) + ZeroOrMore(ret | content) + _suppression(closer) 

663 ) 

664 

665 ret.set_name(f"nested {opener}{closer} expression") 

666 

667 # don't override error message from content expressions 

668 ret.errmsg = None 

669 return ret 

670 

671 

672def _makeTags(tagStr, xml, suppress_LT=Suppress("<"), suppress_GT=Suppress(">")): 

673 """Internal helper to construct opening and closing tag expressions, 

674 given a tag name""" 

675 if isinstance(tagStr, str_type): 

676 resname = tagStr 

677 tagStr = Keyword(tagStr, caseless=not xml) 

678 else: 

679 resname = tagStr.name 

680 

681 tagAttrName = Word(alphas, alphanums + "_-:") 

682 if xml: 

683 tagAttrValue = dbl_quoted_string.copy().set_parse_action(remove_quotes) 

684 openTag = ( 

685 suppress_LT 

686 + tagStr("tag") 

687 + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue))) 

688 + Opt("/", default=[False])("empty").set_parse_action( 

689 lambda s, l, t: t[0] == "/" 

690 ) 

691 + suppress_GT 

692 ) 

693 else: 

694 tagAttrValue = quoted_string.copy().set_parse_action(remove_quotes) | Word( 

695 printables, exclude_chars=">" 

696 ) 

697 openTag = ( 

698 suppress_LT 

699 + tagStr("tag") 

700 + Dict( 

701 ZeroOrMore( 

702 Group( 

703 tagAttrName.set_parse_action(lambda t: t[0].lower()) 

704 + Opt(Suppress("=") + tagAttrValue) 

705 ) 

706 ) 

707 ) 

708 + Opt("/", default=[False])("empty").set_parse_action( 

709 lambda s, l, t: t[0] == "/" 

710 ) 

711 + suppress_GT 

712 ) 

713 closeTag = Combine(Literal("</") + tagStr + ">", adjacent=False) 

714 

715 openTag.set_name(f"<{resname}>") 

716 # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels 

717 openTag.add_parse_action( 

718 lambda t: t.__setitem__( 

719 "start" + "".join(resname.replace(":", " ").title().split()), t.copy() 

720 ) 

721 ) 

722 closeTag = closeTag( 

723 "end" + "".join(resname.replace(":", " ").title().split()) 

724 ).set_name(f"</{resname}>") 

725 openTag.tag = resname 

726 closeTag.tag = resname 

727 openTag.tag_body = SkipTo(closeTag()) 

728 return openTag, closeTag 

729 

730 

731def make_html_tags( 

732 tag_str: Union[str, ParserElement], 

733) -> tuple[ParserElement, ParserElement]: 

734 """Helper to construct opening and closing tag expressions for HTML, 

735 given a tag name. Matches tags in either upper or lower case, 

736 attributes with namespaces and with quoted or unquoted values. 

737 

738 Example: 

739 

740 .. testcode:: 

741 

742 text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>' 

743 # make_html_tags returns pyparsing expressions for the opening and 

744 # closing tags as a 2-tuple 

745 a, a_end = make_html_tags("A") 

746 link_expr = a + SkipTo(a_end)("link_text") + a_end 

747 

748 for link in link_expr.search_string(text): 

749 # attributes in the <A> tag (like "href" shown here) are 

750 # also accessible as named results 

751 print(link.link_text, '->', link.href) 

752 

753 prints: 

754 

755 .. testoutput:: 

756 

757 pyparsing -> https://github.com/pyparsing/pyparsing/wiki 

758 """ 

759 return _makeTags(tag_str, False) 

760 

761 

762def make_xml_tags( 

763 tag_str: Union[str, ParserElement], 

764) -> tuple[ParserElement, ParserElement]: 

765 """Helper to construct opening and closing tag expressions for XML, 

766 given a tag name. Matches tags only in the given upper/lower case. 

767 

768 Example: similar to :class:`make_html_tags` 

769 """ 

770 return _makeTags(tag_str, True) 

771 

772 

773any_open_tag: ParserElement 

774any_close_tag: ParserElement 

775any_open_tag, any_close_tag = make_html_tags( 

776 Word(alphas, alphanums + "_:").set_name("any tag") 

777) 

778 

779_htmlEntityMap = {k.rstrip(";"): v for k, v in html.entities.html5.items()} 

780_most_common_entities = "nbsp lt gt amp quot apos cent pound euro copy".replace( 

781 " ", "|" 

782) 

783common_html_entity = Regex( 

784 lambda: f"&(?P<entity>{_most_common_entities}|{make_compressed_re(_htmlEntityMap)});" 

785).set_name("common HTML entity") 

786 

787 

788def replace_html_entity(s, l, t): 

789 """Helper parser action to replace common HTML entities with their special characters""" 

790 return _htmlEntityMap.get(t.entity) 

791 

792 

793class OpAssoc(Enum): 

794 """Enumeration of operator associativity 

795 - used in constructing InfixNotationOperatorSpec for :class:`infix_notation`""" 

796 

797 LEFT = 1 

798 RIGHT = 2 

799 

800 

801InfixNotationOperatorArgType = Union[ 

802 ParserElement, str, tuple[Union[ParserElement, str], Union[ParserElement, str]] 

803] 

804InfixNotationOperatorSpec = Union[ 

805 tuple[ 

806 InfixNotationOperatorArgType, 

807 int, 

808 OpAssoc, 

809 typing.Optional[ParseAction], 

810 ], 

811 tuple[ 

812 InfixNotationOperatorArgType, 

813 int, 

814 OpAssoc, 

815 ], 

816] 

817 

818 

819def infix_notation( 

820 base_expr: ParserElement, 

821 op_list: list[InfixNotationOperatorSpec], 

822 lpar: Union[str, ParserElement] = Suppress("("), 

823 rpar: Union[str, ParserElement] = Suppress(")"), 

824) -> Forward: 

825 """Helper method for constructing grammars of expressions made up of 

826 operators working in a precedence hierarchy. Operators may be unary 

827 or binary, left- or right-associative. Parse actions can also be 

828 attached to operator expressions. The generated parser will also 

829 recognize the use of parentheses to override operator precedences 

830 (see example below). 

831 

832 Note: if you define a deep operator list, you may see performance 

833 issues when using infix_notation. See 

834 :class:`ParserElement.enable_packrat` for a mechanism to potentially 

835 improve your parser performance. 

836 

837 Parameters: 

838 

839 :param base_expr: expression representing the most basic operand to 

840 be used in the expression 

841 :param op_list: list of tuples, one for each operator precedence level 

842 in the expression grammar; each tuple is of the form ``(op_expr, 

843 num_operands, right_left_assoc, (optional)parse_action)``, where: 

844 

845 - ``op_expr`` is the pyparsing expression for the operator; may also 

846 be a string, which will be converted to a Literal; if ``num_operands`` 

847 is 3, ``op_expr`` is a tuple of two expressions, for the two 

848 operators separating the 3 terms 

849 - ``num_operands`` is the number of terms for this operator (must be 1, 

850 2, or 3) 

851 - ``right_left_assoc`` is the indicator whether the operator is right 

852 or left associative, using the pyparsing-defined constants 

853 ``OpAssoc.RIGHT`` and ``OpAssoc.LEFT``. 

854 - ``parse_action`` is the parse action to be associated with 

855 expressions matching this operator expression (the parse action 

856 tuple member may be omitted); if the parse action is passed 

857 a tuple or list of functions, this is equivalent to calling 

858 ``set_parse_action(*fn)`` 

859 (:class:`ParserElement.set_parse_action`) 

860 

861 :param lpar: expression for matching left-parentheses; if passed as a 

862 str, then will be parsed as ``Suppress(lpar)``. If lpar is passed as 

863 an expression (such as ``Literal('(')``), then it will be kept in 

864 the parsed results, and grouped with them. (default= ``Suppress('(')``) 

865 :param rpar: expression for matching right-parentheses; if passed as a 

866 str, then will be parsed as ``Suppress(rpar)``. If rpar is passed as 

867 an expression (such as ``Literal(')')``), then it will be kept in 

868 the parsed results, and grouped with them. (default= ``Suppress(')')``) 

869 

870 Example: 

871 

872 .. testcode:: 

873 

874 # simple example of four-function arithmetic with ints and 

875 # variable names 

876 integer = pyparsing_common.signed_integer 

877 varname = pyparsing_common.identifier 

878 

879 arith_expr = infix_notation(integer | varname, 

880 [ 

881 ('-', 1, OpAssoc.RIGHT), 

882 (one_of('* /'), 2, OpAssoc.LEFT), 

883 (one_of('+ -'), 2, OpAssoc.LEFT), 

884 ]) 

885 

886 arith_expr.run_tests(''' 

887 5+3*6 

888 (5+3)*6 

889 (5+x)*y 

890 -2--11 

891 ''', full_dump=False) 

892 

893 prints: 

894 

895 .. testoutput:: 

896 :options: +NORMALIZE_WHITESPACE 

897 

898 

899 5+3*6 

900 [[5, '+', [3, '*', 6]]] 

901 

902 (5+3)*6 

903 [[[5, '+', 3], '*', 6]] 

904 

905 (5+x)*y 

906 [[[5, '+', 'x'], '*', 'y']] 

907 

908 -2--11 

909 [[['-', 2], '-', ['-', 11]]] 

910 """ 

911 

912 # captive version of FollowedBy that does not do parse actions or capture results names 

913 class _FB(FollowedBy): 

914 def parseImpl(self, instring, loc, doActions=True): 

915 self.expr.try_parse(instring, loc) 

916 return loc, [] 

917 

918 _FB.__name__ = "FollowedBy>" 

919 

920 ret = Forward() 

921 ret.set_name(f"{base_expr.name}_expression") 

922 if isinstance(lpar, str): 

923 lpar = Suppress(lpar) 

924 if isinstance(rpar, str): 

925 rpar = Suppress(rpar) 

926 

927 nested_expr = (lpar + ret + rpar).set_name(f"nested_{base_expr.name}_expression") 

928 

929 # if lpar and rpar are not suppressed, wrap in group 

930 if not (isinstance(lpar, Suppress) and isinstance(rpar, Suppress)): 

931 lastExpr = base_expr | Group(nested_expr) 

932 else: 

933 lastExpr = base_expr | nested_expr 

934 

935 arity: int 

936 rightLeftAssoc: opAssoc 

937 pa: typing.Optional[ParseAction] 

938 opExpr1: ParserElement 

939 opExpr2: ParserElement 

940 matchExpr: ParserElement 

941 match_lookahead: ParserElement 

942 for operDef in op_list: 

943 opExpr, arity, rightLeftAssoc, pa = (operDef + (None,))[:4] # type: ignore[assignment] 

944 if isinstance(opExpr, str_type): 

945 opExpr = ParserElement._literalStringClass(opExpr) 

946 opExpr = typing.cast(ParserElement, opExpr) 

947 if arity == 3: 

948 if not isinstance(opExpr, (tuple, list)) or len(opExpr) != 2: 

949 raise ValueError( 

950 "if numterms=3, opExpr must be a tuple or list of two expressions" 

951 ) 

952 opExpr1, opExpr2 = opExpr 

953 term_name = f"{opExpr1}{opExpr2} operations" 

954 else: 

955 term_name = f"{opExpr} operations" 

956 

957 if not 1 <= arity <= 3: 

958 raise ValueError("operator must be unary (1), binary (2), or ternary (3)") 

959 

960 if rightLeftAssoc not in (OpAssoc.LEFT, OpAssoc.RIGHT): 

961 raise ValueError("operator must indicate right or left associativity") 

962 

963 thisExpr: ParserElement = Forward().set_name(term_name) 

964 thisExpr = typing.cast(Forward, thisExpr) 

965 match_lookahead = And([]) 

966 if rightLeftAssoc is OpAssoc.LEFT: 

967 if arity == 1: 

968 match_lookahead = _FB(lastExpr + opExpr) 

969 matchExpr = Group(lastExpr + opExpr[1, ...]) 

970 elif arity == 2: 

971 if opExpr is not None: 

972 match_lookahead = _FB(lastExpr + opExpr + lastExpr) 

973 matchExpr = Group(lastExpr + (opExpr + lastExpr)[1, ...]) 

974 else: 

975 match_lookahead = _FB(lastExpr + lastExpr) 

976 matchExpr = Group(lastExpr[2, ...]) 

977 elif arity == 3: 

978 match_lookahead = _FB( 

979 lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr 

980 ) 

981 matchExpr = Group( 

982 lastExpr + (opExpr1 + lastExpr + opExpr2 + lastExpr)[1, ...] 

983 ) 

984 elif rightLeftAssoc is OpAssoc.RIGHT: 

985 if arity == 1: 

986 # try to avoid LR with this extra test 

987 if not isinstance(opExpr, Opt): 

988 opExpr = Opt(opExpr) 

989 match_lookahead = _FB(opExpr.expr + thisExpr) 

990 matchExpr = Group(opExpr + thisExpr) 

991 elif arity == 2: 

992 if opExpr is not None: 

993 match_lookahead = _FB(lastExpr + opExpr + thisExpr) 

994 matchExpr = Group(lastExpr + (opExpr + thisExpr)[1, ...]) 

995 else: 

996 match_lookahead = _FB(lastExpr + thisExpr) 

997 matchExpr = Group(lastExpr + thisExpr[1, ...]) 

998 elif arity == 3: 

999 match_lookahead = _FB( 

1000 lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr 

1001 ) 

1002 matchExpr = Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) 

1003 

1004 # suppress lookahead expr from railroad diagrams 

1005 match_lookahead.show_in_diagram = False 

1006 

1007 # TODO - determine why this statement can't be included in the following 

1008 # if pa block 

1009 matchExpr = match_lookahead + matchExpr 

1010 

1011 if pa: 

1012 if isinstance(pa, (tuple, list)): 

1013 matchExpr.set_parse_action(*pa) 

1014 else: 

1015 matchExpr.set_parse_action(pa) 

1016 

1017 thisExpr <<= (matchExpr | lastExpr).set_name(term_name) 

1018 lastExpr = thisExpr 

1019 

1020 ret <<= lastExpr 

1021 return ret 

1022 

1023 

1024def indentedBlock(blockStatementExpr, indentStack, indent=True, backup_stacks=[]): 

1025 """ 

1026 .. deprecated:: 3.0.0 

1027 Use the :class:`IndentedBlock` class instead. Note that `IndentedBlock` 

1028 has a difference method signature. 

1029 

1030 Helper method for defining space-delimited indentation blocks, 

1031 such as those used to define block statements in Python source code. 

1032 

1033 :param blockStatementExpr: expression defining syntax of statement that 

1034 is repeated within the indented block 

1035 

1036 :param indentStack: list created by caller to manage indentation stack 

1037 (multiple ``statementWithIndentedBlock`` expressions within a single 

1038 grammar should share a common ``indentStack``) 

1039 

1040 :param indent: boolean indicating whether block must be indented beyond 

1041 the current level; set to ``False`` for block of left-most statements 

1042 

1043 A valid block must contain at least one ``blockStatement``. 

1044 

1045 (Note that indentedBlock uses internal parse actions which make it 

1046 incompatible with packrat parsing.) 

1047 

1048 Example: 

1049 

1050 .. testcode:: 

1051 

1052 data = ''' 

1053 def A(z): 

1054 A1 

1055 B = 100 

1056 G = A2 

1057 A2 

1058 A3 

1059 B 

1060 def BB(a,b,c): 

1061 BB1 

1062 def BBA(): 

1063 bba1 

1064 bba2 

1065 bba3 

1066 C 

1067 D 

1068 def spam(x,y): 

1069 def eggs(z): 

1070 pass 

1071 ''' 

1072 

1073 indentStack = [1] 

1074 stmt = Forward() 

1075 

1076 identifier = Word(alphas, alphanums) 

1077 funcDecl = ("def" + identifier + Group("(" + Opt(delimitedList(identifier)) + ")") + ":") 

1078 func_body = indentedBlock(stmt, indentStack) 

1079 funcDef = Group(funcDecl + func_body) 

1080 

1081 rvalue = Forward() 

1082 funcCall = Group(identifier + "(" + Opt(delimitedList(rvalue)) + ")") 

1083 rvalue << (funcCall | identifier | Word(nums)) 

1084 assignment = Group(identifier + "=" + rvalue) 

1085 stmt << (funcDef | assignment | identifier) 

1086 

1087 module_body = stmt[1, ...] 

1088 

1089 parseTree = module_body.parseString(data) 

1090 parseTree.pprint() 

1091 

1092 prints: 

1093 

1094 .. testoutput:: 

1095 

1096 [['def', 

1097 'A', 

1098 ['(', 'z', ')'], 

1099 ':', 

1100 [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]], 

1101 'B', 

1102 ['def', 

1103 'BB', 

1104 ['(', 'a', 'b', 'c', ')'], 

1105 ':', 

1106 [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]], 

1107 'C', 

1108 'D', 

1109 ['def', 

1110 'spam', 

1111 ['(', 'x', 'y', ')'], 

1112 ':', 

1113 [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]] 

1114 """ 

1115 warnings.warn( 

1116 f"{'indentedBlock'!r} deprecated - use {'IndentedBlock'!r}", 

1117 PyparsingDeprecationWarning, 

1118 stacklevel=2, 

1119 ) 

1120 

1121 backup_stacks.append(indentStack[:]) 

1122 

1123 def reset_stack(): 

1124 indentStack[:] = backup_stacks[-1] 

1125 

1126 def checkPeerIndent(s, l, t): 

1127 if l >= len(s): 

1128 return 

1129 curCol = col(l, s) 

1130 if curCol != indentStack[-1]: 

1131 if curCol > indentStack[-1]: 

1132 raise ParseException(s, l, "illegal nesting") 

1133 raise ParseException(s, l, "not a peer entry") 

1134 

1135 def checkSubIndent(s, l, t): 

1136 curCol = col(l, s) 

1137 if curCol > indentStack[-1]: 

1138 indentStack.append(curCol) 

1139 else: 

1140 raise ParseException(s, l, "not a subentry") 

1141 

1142 def checkUnindent(s, l, t): 

1143 if l >= len(s): 

1144 return 

1145 curCol = col(l, s) 

1146 if not (indentStack and curCol in indentStack): 

1147 raise ParseException(s, l, "not an unindent") 

1148 if curCol < indentStack[-1]: 

1149 indentStack.pop() 

1150 

1151 NL = OneOrMore(LineEnd().set_whitespace_chars("\t ").suppress()) 

1152 INDENT = (Empty() + Empty().set_parse_action(checkSubIndent)).set_name("INDENT") 

1153 PEER = Empty().set_parse_action(checkPeerIndent).set_name("") 

1154 UNDENT = Empty().set_parse_action(checkUnindent).set_name("UNINDENT") 

1155 if indent: 

1156 smExpr = Group( 

1157 Opt(NL) 

1158 + INDENT 

1159 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL)) 

1160 + UNDENT 

1161 ) 

1162 else: 

1163 smExpr = Group( 

1164 Opt(NL) 

1165 + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL)) 

1166 + Opt(UNDENT) 

1167 ) 

1168 

1169 # add a parse action to remove backup_stack from list of backups 

1170 smExpr.add_parse_action( 

1171 lambda: backup_stacks.pop(-1) and None if backup_stacks else None 

1172 ) 

1173 smExpr.set_fail_action(lambda a, b, c, d: reset_stack()) 

1174 blockStatementExpr.ignore(_bslash + LineEnd()) 

1175 return smExpr.set_name("indented block") 

1176 

1177 

1178# it's easy to get these comment structures wrong - they're very common, 

1179# so may as well make them available 

1180c_style_comment = Regex(r"/\*(?:[^*]|\*(?!/))*\*\/").set_name("C style comment") 

1181"Comment of the form ``/* ... */``" 

1182 

1183html_comment = Regex(r"<!--[\s\S]*?-->").set_name("HTML comment") 

1184"Comment of the form ``<!-- ... -->``" 

1185 

1186rest_of_line = Regex(r".*").leave_whitespace().set_name("rest of line") 

1187dbl_slash_comment = Regex(r"//(?:\\\n|[^\n])*").set_name("// comment") 

1188"Comment of the form ``// ... (to end of line)``" 

1189 

1190cpp_style_comment = Regex( 

1191 r"(?:/\*(?:[^*]|\*(?!/))*\*\/)|(?://(?:\\\n|[^\n])*)" 

1192).set_name("C++ style comment") 

1193"Comment of either form :class:`c_style_comment` or :class:`dbl_slash_comment`" 

1194 

1195java_style_comment = cpp_style_comment 

1196"Same as :class:`cpp_style_comment`" 

1197 

1198python_style_comment = Regex(r"#.*").set_name("Python style comment") 

1199"Comment of the form ``# ... (to end of line)``" 

1200 

1201 

1202# build list of built-in expressions, for future reference if a global default value 

1203# gets updated 

1204_builtin_exprs: list[ParserElement] = [ 

1205 v for v in vars().values() if isinstance(v, ParserElement) 

1206] 

1207 

1208 

1209# compatibility function, superseded by DelimitedList class 

1210def delimited_list( 

1211 expr: Union[str, ParserElement], 

1212 delim: Union[str, ParserElement] = ",", 

1213 combine: bool = False, 

1214 min: typing.Optional[int] = None, 

1215 max: typing.Optional[int] = None, 

1216 *, 

1217 allow_trailing_delim: bool = False, 

1218) -> ParserElement: 

1219 """ 

1220 .. deprecated:: 3.1.0 

1221 Use the :class:`DelimitedList` class instead. 

1222 """ 

1223 return DelimitedList( 

1224 expr, delim, combine, min, max, allow_trailing_delim=allow_trailing_delim 

1225 ) 

1226 

1227 

1228# Compatibility synonyms 

1229# fmt: off 

1230opAssoc = OpAssoc 

1231anyOpenTag = any_open_tag 

1232anyCloseTag = any_close_tag 

1233commonHTMLEntity = common_html_entity 

1234cStyleComment = c_style_comment 

1235htmlComment = html_comment 

1236restOfLine = rest_of_line 

1237dblSlashComment = dbl_slash_comment 

1238cppStyleComment = cpp_style_comment 

1239javaStyleComment = java_style_comment 

1240pythonStyleComment = python_style_comment 

1241delimitedList = replaced_by_pep8("delimitedList", DelimitedList) 

1242delimited_list = replaced_by_pep8("delimited_list", DelimitedList) 

1243countedArray = replaced_by_pep8("countedArray", counted_array) 

1244matchPreviousLiteral = replaced_by_pep8("matchPreviousLiteral", match_previous_literal) 

1245matchPreviousExpr = replaced_by_pep8("matchPreviousExpr", match_previous_expr) 

1246oneOf = replaced_by_pep8("oneOf", one_of) 

1247dictOf = replaced_by_pep8("dictOf", dict_of) 

1248originalTextFor = replaced_by_pep8("originalTextFor", original_text_for) 

1249nestedExpr = replaced_by_pep8("nestedExpr", nested_expr) 

1250makeHTMLTags = replaced_by_pep8("makeHTMLTags", make_html_tags) 

1251makeXMLTags = replaced_by_pep8("makeXMLTags", make_xml_tags) 

1252replaceHTMLEntity = replaced_by_pep8("replaceHTMLEntity", replace_html_entity) 

1253infixNotation = replaced_by_pep8("infixNotation", infix_notation) 

1254# fmt: on