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

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

110 statements  

1# common.py 

2from .core import * 

3from .helpers import DelimitedList, any_open_tag, any_close_tag 

4from datetime import datetime, timedelta 

5import sys 

6 

7PY_310_OR_LATER = sys.version_info >= (3, 10) 

8 

9 

10# some other useful expressions - using lower-case class name since we are really using this as a namespace 

11class pyparsing_common: 

12 """Here are some common low-level expressions that may be useful in 

13 jump-starting parser development: 

14 

15 - numeric forms (:class:`integers<integer>`, :class:`reals<real>`, 

16 :class:`scientific notation<sci_real>`) 

17 - common :class:`programming identifiers<identifier>` 

18 - network addresses (:class:`MAC<mac_address>`, 

19 :class:`IPv4<ipv4_address>`, :class:`IPv6<ipv6_address>`) 

20 - ISO8601 :class:`dates<iso8601_date>` and 

21 :class:`datetime<iso8601_datetime>` 

22 - :class:`UUID<uuid>` 

23 - :class:`comma-separated list<comma_separated_list>` 

24 - :class:`url` 

25 

26 Parse actions: 

27 

28 - :class:`convert_to_integer` 

29 - :class:`convert_to_float` 

30 - :class:`convert_to_date` 

31 - :class:`convert_to_datetime` 

32 - :class:`strip_html_tags` 

33 - :class:`upcase_tokens` 

34 - :class:`downcase_tokens` 

35 

36 Examples: 

37 

38 .. testcode:: 

39 

40 pyparsing_common.number.run_tests(''' 

41 # any int or real number, returned as the appropriate type 

42 100 

43 -100 

44 +100 

45 3.14159 

46 6.02e23 

47 1e-12 

48 ''') 

49 

50 .. testoutput:: 

51 :options: +NORMALIZE_WHITESPACE 

52 

53 

54 # any int or real number, returned as the appropriate type 

55 100 

56 [100] 

57 

58 -100 

59 [-100] 

60 

61 +100 

62 [100] 

63 

64 3.14159 

65 [3.14159] 

66 

67 6.02e23 

68 [6.02e+23] 

69 

70 1e-12 

71 [1e-12] 

72 

73 .. testcode:: 

74 

75 pyparsing_common.fnumber.run_tests(''' 

76 # any int or real number, returned as float 

77 100 

78 -100 

79 +100 

80 3.14159 

81 6.02e23 

82 1e-12 

83 ''') 

84 

85 .. testoutput:: 

86 :options: +NORMALIZE_WHITESPACE 

87 

88 

89 # any int or real number, returned as float 

90 100 

91 [100.0] 

92 

93 -100 

94 [-100.0] 

95 

96 +100 

97 [100.0] 

98 

99 3.14159 

100 [3.14159] 

101 

102 6.02e23 

103 [6.02e+23] 

104 

105 1e-12 

106 [1e-12] 

107 

108 .. testcode:: 

109 

110 pyparsing_common.hex_integer.run_tests(''' 

111 # hex numbers 

112 100 

113 FF 

114 ''') 

115 

116 .. testoutput:: 

117 :options: +NORMALIZE_WHITESPACE 

118 

119 

120 # hex numbers 

121 100 

122 [256] 

123 

124 FF 

125 [255] 

126 

127 .. testcode:: 

128 

129 pyparsing_common.fraction.run_tests(''' 

130 # fractions 

131 1/2 

132 -3/4 

133 ''') 

134 

135 .. testoutput:: 

136 :options: +NORMALIZE_WHITESPACE 

137 

138 

139 # fractions 

140 1/2 

141 [0.5] 

142 

143 -3/4 

144 [-0.75] 

145 

146 .. testcode:: 

147 

148 pyparsing_common.mixed_integer.run_tests(''' 

149 # mixed fractions 

150 1 

151 1/2 

152 -3/4 

153 1-3/4 

154 ''') 

155 

156 .. testoutput:: 

157 :options: +NORMALIZE_WHITESPACE 

158 

159 

160 # mixed fractions 

161 1 

162 [1] 

163 

164 1/2 

165 [0.5] 

166 

167 -3/4 

168 [-0.75] 

169 

170 1-3/4 

171 [1.75] 

172 .. testcode:: 

173 

174 import uuid 

175 pyparsing_common.uuid.set_parse_action(token_map(uuid.UUID)) 

176 pyparsing_common.uuid.run_tests(''' 

177 # uuid 

178 12345678-1234-5678-1234-567812345678 

179 ''') 

180 

181 .. testoutput:: 

182 :options: +NORMALIZE_WHITESPACE 

183 

184 

185 # uuid 

186 12345678-1234-5678-1234-567812345678 

187 [UUID('12345678-1234-5678-1234-567812345678')] 

188 """ 

189 

190 @staticmethod 

191 def convert_to_integer(_, __, t) -> list[int]: 

192 """ 

193 Parse action for converting parsed integers to Python int 

194 """ 

195 return [int(tt) for tt in t] 

196 

197 @staticmethod 

198 def convert_to_float(_, __, t) -> list[float]: 

199 """ 

200 Parse action for converting parsed numbers to Python float 

201 """ 

202 return [float(tt) for tt in t] 

203 

204 integer = ( 

205 Word(nums) 

206 .set_name("integer") 

207 .set_parse_action(convert_to_integer if PY_310_OR_LATER else token_map(int)) 

208 ) 

209 """expression that parses an unsigned integer, converts to an int""" 

210 

211 hex_integer = ( 

212 Word(hexnums).set_name("hex integer").set_parse_action(token_map(int, 16)) 

213 ) 

214 """expression that parses a hexadecimal integer, converts to an int""" 

215 

216 signed_integer = ( 

217 Regex(r"[+-]?\d+") 

218 .set_name("signed integer") 

219 .set_parse_action(convert_to_integer if PY_310_OR_LATER else token_map(int)) 

220 ) 

221 """expression that parses an integer with optional leading sign, converts to an int""" 

222 

223 fraction = ( 

224 signed_integer().set_parse_action( 

225 convert_to_float if PY_310_OR_LATER else token_map(float) 

226 ) 

227 + "/" 

228 + signed_integer().set_parse_action( 

229 convert_to_float if PY_310_OR_LATER else token_map(float) 

230 ) 

231 ).set_name("fraction") 

232 """fractional expression of an integer divided by an integer, converts to a float""" 

233 fraction.add_parse_action(lambda tt: tt[0] / tt[-1]) 

234 

235 mixed_integer = ( 

236 fraction | signed_integer + Opt(Opt("-").suppress() + fraction) 

237 ).set_name("fraction or mixed integer-fraction") 

238 """mixed integer of the form 'integer - fraction', with optional leading integer, converts to a float""" 

239 mixed_integer.add_parse_action(sum) 

240 

241 real = ( 

242 Regex(r"[+-]?(?:\d+\.\d*|\.\d+)") 

243 .set_name("real number") 

244 .set_parse_action(convert_to_float if PY_310_OR_LATER else token_map(float)) 

245 ) 

246 """expression that parses a floating point number, converts to a float""" 

247 

248 sci_real = ( 

249 Regex(r"[+-]?(?:\d+(?:[eE][+-]?\d+)|(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?)") 

250 .set_name("real number with scientific notation") 

251 .set_parse_action(convert_to_float if PY_310_OR_LATER else token_map(float)) 

252 ) 

253 """expression that parses a floating point number with optional 

254 scientific notation, converts to a float""" 

255 

256 # streamlining this expression makes the docs nicer-looking 

257 number = (sci_real | real | signed_integer).set_name("number").streamline() 

258 """any numeric expression, converts to the corresponding Python type""" 

259 

260 fnumber = ( 

261 Regex(r"[+-]?\d+\.?\d*(?:[eE][+-]?\d+)?") 

262 .set_name("fnumber") 

263 .set_parse_action(convert_to_float if PY_310_OR_LATER else token_map(float)) 

264 ) 

265 """any int or real number, always converts to a float""" 

266 

267 ieee_float = ( 

268 Regex(r"(?i:[+-]?(?:(?:\d+\.?\d*(?:e[+-]?\d+)?)|nan|inf(?:inity)?))") 

269 .set_name("ieee_float") 

270 .set_parse_action(convert_to_float if PY_310_OR_LATER else token_map(float)) 

271 ) 

272 """any floating-point literal (int, real number, infinity, or NaN), converts to a float""" 

273 

274 identifier = Word(identchars, identbodychars).set_name("identifier") 

275 """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')""" 

276 

277 ipv4_address = Regex( 

278 r"(?:25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}" 

279 ).set_name("IPv4 address") 

280 "IPv4 address (``0.0.0.0 - 255.255.255.255``)" 

281 

282 _ipv6_part = Regex(r"[0-9a-fA-F]{1,4}").set_name("hex_integer") 

283 _full_ipv6_address = (_ipv6_part + (":" + _ipv6_part) * 7).set_name( 

284 "full IPv6 address" 

285 ) 

286 _short_ipv6_address = ( 

287 Opt(_ipv6_part + (":" + _ipv6_part) * (0, 6)) 

288 + "::" 

289 + Opt(_ipv6_part + (":" + _ipv6_part) * (0, 6)) 

290 ).set_name("short IPv6 address") 

291 _short_ipv6_address.add_condition( 

292 lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8 

293 ) 

294 _mixed_ipv6_address = ("::ffff:" + ipv4_address).set_name("mixed IPv6 address") 

295 ipv6_address = Combine( 

296 (_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).set_name( 

297 "IPv6 address" 

298 ) 

299 ).set_name("IPv6 address") 

300 "IPv6 address (long, short, or mixed form)" 

301 

302 mac_address = Regex( 

303 r"[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\1[0-9a-fA-F]{2}){4}" 

304 ).set_name("MAC address") 

305 "MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)" 

306 

307 @staticmethod 

308 def convert_to_date(fmt: str = "%Y-%m-%d"): 

309 """ 

310 Helper to create a parse action for converting parsed date string to Python datetime.date 

311 

312 Params - 

313 - fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%d"``) 

314 

315 Example: 

316 

317 .. testcode:: 

318 

319 date_expr = pyparsing_common.iso8601_date.copy() 

320 date_expr.set_parse_action(pyparsing_common.convert_to_date()) 

321 print(date_expr.parse_string("1999-12-31")) 

322 

323 prints: 

324 

325 .. testoutput:: 

326 

327 [datetime.date(1999, 12, 31)] 

328 """ 

329 

330 def cvt_fn(ss, ll, tt): 

331 try: 

332 return datetime.strptime(tt[0], fmt).date() 

333 except ValueError as ve: 

334 raise ParseException(ss, ll, str(ve)) 

335 

336 return cvt_fn 

337 

338 @staticmethod 

339 def convert_to_datetime(fmt: str = "%Y-%m-%dT%H:%M:%S.%f"): 

340 """Helper to create a parse action for converting parsed 

341 datetime string to Python :class:`datetime.datetime` 

342 

343 Params - 

344 - fmt - format to be passed to :class:`datetime.strptime` (default= ``"%Y-%m-%dT%H:%M:%S.%f"``) 

345 

346 Example: 

347 

348 .. testcode:: 

349 

350 dt_expr = pyparsing_common.iso8601_datetime.copy() 

351 dt_expr.set_parse_action(pyparsing_common.convert_to_datetime()) 

352 print(dt_expr.parse_string("1999-12-31T23:59:59.999")) 

353 

354 prints: 

355 

356 .. testoutput:: 

357 

358 [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)] 

359 """ 

360 

361 def cvt_fn(s, l, t): 

362 try: 

363 return datetime.strptime(t[0], fmt) 

364 except ValueError as ve: 

365 raise ParseException(s, l, str(ve)) 

366 

367 return cvt_fn 

368 

369 iso8601_date = Regex( 

370 r"(?P<year>\d{4})(?:-(?P<month>\d\d)(?:-(?P<day>\d\d))?)?" 

371 ).set_name("ISO8601 date") 

372 "ISO8601 date (``yyyy-mm-dd``)" 

373 

374 iso8601_datetime = Regex( 

375 r"(?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d)[T ](?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d(\.\d*)?)?)?(?P<tz>Z|[+-]\d\d:?\d\d)?" 

376 ).set_name("ISO8601 datetime") 

377 "ISO8601 datetime (``yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)``) - trailing seconds, milliseconds, and timezone optional; accepts separating ``'T'`` or ``' '``" 

378 

379 @staticmethod 

380 def as_datetime(s, l, t): 

381 """Parse action to convert parsed dates or datetimes to a Python 

382 :class:`datetime.datetime`. 

383 

384 This parse action will use the year, month, day, etc. results 

385 names defined in the ISO8601 date expressions, but it can be 

386 used with any expression that provides one or more of these fields. 

387 

388 Omitted fields will default to fields from Jan 1, 00:00:00. 

389 

390 Invalid dates will raise a :class:`ParseException` with the 

391 error message indicating the invalid date fields. 

392 """ 

393 year = int(t.year.lstrip("0") or 0) 

394 month = int(t.month or 1) 

395 day = int(t.day or 1) 

396 hour = int(t.hour or 0) 

397 minute = int(t.minute or 0) 

398 second = float(t.second or 0) 

399 try: 

400 # Add the fractional seconds via timedelta so a value that rounds up 

401 # to a full second (e.g. "...59.9999995") carries into the next second 

402 # instead of overflowing datetime's 0..999999 microsecond argument. 

403 return datetime(year, month, day, hour, minute, int(second)) + timedelta( 

404 microseconds=round((second % 1) * 1_000_000) 

405 ) 

406 except ValueError as ve: 

407 raise ParseException(s, l, f"Invalid date/time: {ve}").with_traceback( 

408 ve.__traceback__ 

409 ) from None 

410 

411 if PY_310_OR_LATER: 

412 iso8601_date_validated = iso8601_date().add_parse_action(as_datetime) 

413 "Validated ISO8601 date strings, raising :class:`ParseException` for invalid date values." 

414 

415 iso8601_datetime_validated = iso8601_datetime().add_parse_action(as_datetime) 

416 "Validated ISO8601 date and time strings, raising :class:`ParseException` for invalid date/time values." 

417 

418 uuid = Regex(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}").set_name( 

419 "UUID" 

420 ) 

421 "UUID (``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx``)" 

422 

423 _html_stripper = any_open_tag.suppress() | any_close_tag.suppress() 

424 

425 @staticmethod 

426 def strip_html_tags(s: str, l: int, tokens: ParseResults): 

427 """Parse action to remove HTML tags from web page HTML source 

428 

429 Example: 

430 

431 .. testcode:: 

432 

433 # strip HTML links from normal text 

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

435 td, td_end = make_html_tags("TD") 

436 table_text = td + SkipTo(td_end).set_parse_action( 

437 pyparsing_common.strip_html_tags)("body") + td_end 

438 print(table_text.parse_string(text).body) 

439 

440 Prints: 

441 

442 .. testoutput:: 

443 

444 More info at the pyparsing wiki page 

445 """ 

446 return pyparsing_common._html_stripper.transform_string(tokens[0]) 

447 

448 _commasepitem = ( 

449 Combine( 

450 OneOrMore( 

451 ~Literal(",") 

452 + ~LineEnd() 

453 + Word(printables, exclude_chars=",") 

454 + Opt(White(" \t") + ~FollowedBy(LineEnd() | ",")) 

455 ) 

456 ) 

457 .streamline() 

458 .set_name("commaItem") 

459 ) 

460 comma_separated_list = DelimitedList( 

461 Opt(quoted_string.copy() | _commasepitem, default="") 

462 ).set_name("comma separated list") 

463 """Predefined expression of 1 or more printable words or quoted strings, separated by commas.""" 

464 

465 @staticmethod 

466 def upcase_tokens(s, l, t): 

467 """Parse action to convert tokens to upper case.""" 

468 return [tt.upper() for tt in t] 

469 

470 @staticmethod 

471 def downcase_tokens(s, l, t): 

472 """Parse action to convert tokens to lower case.""" 

473 return [tt.lower() for tt in t] 

474 

475 # fmt: off 

476 url = Regex( 

477 # https://mathiasbynens.be/demo/url-regex 

478 # https://gist.github.com/dperini/729294 

479 r"(?P<url>" 

480 # protocol identifier (optional) 

481 # short syntax // still required 

482 r"(?:(?:(?P<scheme>https?|ftp):)?\/\/)" 

483 # user:pass BasicAuth (optional) 

484 r"(?:(?P<auth>\S+(?::\S*)?)@)?" 

485 r"(?P<host>" 

486 # IP address exclusion 

487 # private & local networks 

488 r"(?!(?:10|127)(?:\.\d{1,3}){3})" 

489 r"(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})" 

490 r"(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})" 

491 # IP address dotted notation octets 

492 # excludes loopback network 0.0.0.0 

493 # excludes reserved space >= 224.0.0.0 

494 # excludes network & broadcast addresses 

495 # (first & last IP address of each class) 

496 r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" 

497 r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}" 

498 r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))" 

499 r"|" 

500 # host & domain names, may end with dot 

501 # can be replaced by a shortest alternative 

502 # (?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.)+ 

503 r"(?:" 

504 r"(?:" 

505 r"[a-z0-9\u00a1-\uffff]" 

506 r"[a-z0-9\u00a1-\uffff_-]{0,62}" 

507 r")?" 

508 r"[a-z0-9\u00a1-\uffff]\." 

509 r")+" 

510 # TLD identifier name, may end with dot 

511 r"(?:[a-z\u00a1-\uffff]{2,}\.?)" 

512 r")" 

513 # port number (optional) 

514 r"(:(?P<port>\d{2,5}))?" 

515 # resource path (optional) 

516 r"(?P<path>\/[^?# ]*)?" 

517 # query string (optional) 

518 r"(\?(?P<query>[^#]*))?" 

519 # fragment (optional) 

520 r"(#(?P<fragment>\S*))?" 

521 r")" 

522 ).set_name("url") 

523 """ 

524 URL (http/https/ftp scheme) 

525  

526 .. versionchanged:: 3.1.0 

527 ``url`` named group added 

528 """ 

529 # fmt: on 

530 

531 # pre-PEP8 compatibility names 

532 # fmt: off 

533 convertToInteger = staticmethod(replaced_by_pep8("convertToInteger", convert_to_integer)) 

534 convertToFloat = staticmethod(replaced_by_pep8("convertToFloat", convert_to_float)) 

535 convertToDate = staticmethod(replaced_by_pep8("convertToDate", convert_to_date)) 

536 convertToDatetime = staticmethod(replaced_by_pep8("convertToDatetime", convert_to_datetime)) 

537 stripHTMLTags = staticmethod(replaced_by_pep8("stripHTMLTags", strip_html_tags)) 

538 upcaseTokens = staticmethod(replaced_by_pep8("upcaseTokens", upcase_tokens)) 

539 downcaseTokens = staticmethod(replaced_by_pep8("downcaseTokens", downcase_tokens)) 

540 # fmt: on 

541 

542 

543_builtin_exprs = [ 

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

545]