Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/parse/__init__.py: 71%

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

554 statements  

1from __future__ import absolute_import 

2 

3import logging 

4import re 

5import sys 

6from datetime import datetime 

7from datetime import time 

8from datetime import timedelta 

9from datetime import tzinfo 

10from decimal import Decimal 

11from functools import partial 

12 

13 

14__version__ = "1.22.3" 

15__all__ = ["parse", "search", "findall", "with_pattern"] 

16 

17log = logging.getLogger(__name__) 

18 

19 

20def with_pattern(pattern, regex_group_count=None): 

21 r"""Attach a regular expression pattern matcher to a custom type converter 

22 function. 

23 

24 This annotates the type converter with the :attr:`pattern` attribute. 

25 

26 EXAMPLE: 

27 >>> import parse 

28 >>> @parse.with_pattern(r"\d+") 

29 ... def parse_number(text): 

30 ... return int(text) 

31 

32 is equivalent to: 

33 

34 >>> def parse_number(text): 

35 ... return int(text) 

36 >>> parse_number.pattern = r"\d+" 

37 

38 :param pattern: regular expression pattern (as text) 

39 :param regex_group_count: Indicates how many regex-groups are in pattern. 

40 :return: wrapped function 

41 """ 

42 

43 def decorator(func): 

44 func.pattern = pattern 

45 func.regex_group_count = regex_group_count 

46 return func 

47 

48 return decorator 

49 

50 

51class int_convert: 

52 """Convert a string to an integer. 

53 

54 The string may start with a sign. 

55 

56 It may be of a base other than 2, 8, 10 or 16. 

57 

58 If base isn't specified, it will be detected automatically based 

59 on a string format. When string starts with a base indicator, 0#nnnn, 

60 it overrides the default base of 10. 

61 

62 It may also have other non-numeric characters that we can ignore. 

63 """ 

64 

65 CHARS = "0123456789abcdefghijklmnopqrstuvwxyz" 

66 

67 def __init__(self, base=None): 

68 self.base = base 

69 

70 def __call__(self, string, match): 

71 if string[0] == "-": 

72 sign = -1 

73 number_start = 1 

74 elif string[0] == "+": 

75 sign = 1 

76 number_start = 1 

77 else: 

78 sign = 1 

79 number_start = 0 

80 

81 base = self.base 

82 # If base wasn't specified, detect it automatically 

83 if base is None: 

84 # Assume decimal number, unless different base is detected 

85 base = 10 

86 

87 # For number formats starting with 0b, 0o, 0x, use corresponding base ... 

88 if string[number_start] == "0" and len(string) - number_start > 2: 

89 if string[number_start + 1] in "bB": 

90 base = 2 

91 elif string[number_start + 1] in "oO": 

92 base = 8 

93 elif string[number_start + 1] in "xX": 

94 base = 16 

95 

96 chars = int_convert.CHARS[:base] 

97 string = re.sub("[^%s]" % chars, "", string.lower()) 

98 return sign * int(string, base) 

99 

100 

101class convert_first: 

102 """Convert the first element of a pair. 

103 This equivalent to lambda s,m: converter(s). But unlike a lambda function, it can be pickled 

104 """ 

105 

106 def __init__(self, converter): 

107 self.converter = converter 

108 

109 def __call__(self, string, match): 

110 return self.converter(string) 

111 

112 

113def percentage(string, match): 

114 return float(string[:-1]) / 100.0 

115 

116 

117class FixedTzOffset(tzinfo): 

118 """Fixed offset in minutes east from UTC.""" 

119 

120 ZERO = timedelta(0) 

121 

122 def __init__(self, offset, name): 

123 self._offset = timedelta(minutes=offset) 

124 self._name = name 

125 

126 def __repr__(self): 

127 return "<%s %s %s>" % (self.__class__.__name__, self._name, self._offset) 

128 

129 def utcoffset(self, dt): 

130 return self._offset 

131 

132 def tzname(self, dt): 

133 return self._name 

134 

135 def dst(self, dt): 

136 return self.ZERO 

137 

138 def __eq__(self, other): 

139 if not isinstance(other, FixedTzOffset): 

140 return NotImplemented 

141 return self._name == other._name and self._offset == other._offset 

142 

143 

144MONTHS_MAP = { 

145 "Jan": 1, 

146 "January": 1, 

147 "Feb": 2, 

148 "February": 2, 

149 "Mar": 3, 

150 "March": 3, 

151 "Apr": 4, 

152 "April": 4, 

153 "May": 5, 

154 "Jun": 6, 

155 "June": 6, 

156 "Jul": 7, 

157 "July": 7, 

158 "Aug": 8, 

159 "August": 8, 

160 "Sep": 9, 

161 "September": 9, 

162 "Oct": 10, 

163 "October": 10, 

164 "Nov": 11, 

165 "November": 11, 

166 "Dec": 12, 

167 "December": 12, 

168} 

169DAYS_PAT = r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun)" 

170MONTHS_PAT = r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)" 

171ALL_MONTHS_PAT = r"(%s)" % "|".join(MONTHS_MAP) 

172TIME_PAT = r"(\d{1,2}:\d{1,2}(:\d{1,2}(\.\d+)?)?)" 

173AM_PAT = r"(\s+[AP]M)" 

174TZ_PAT = r"(\s+[-+]\d\d?:?\d\d)" 

175 

176 

177def date_convert( 

178 string, 

179 match, 

180 ymd=None, 

181 mdy=None, 

182 dmy=None, 

183 d_m_y=None, 

184 hms=None, 

185 am=None, 

186 tz=None, 

187 mm=None, 

188 dd=None, 

189): 

190 """Convert the incoming string containing some date / time info into a 

191 datetime instance. 

192 """ 

193 groups = match.groups() 

194 time_only = False 

195 if mm and dd: 

196 y = datetime.today().year 

197 m = groups[mm] 

198 d = groups[dd] 

199 elif ymd is not None: 

200 y, m, d = re.split(r"[-/\s]", groups[ymd]) 

201 elif mdy is not None: 

202 m, d, y = re.split(r"[-/\s]", groups[mdy]) 

203 elif dmy is not None: 

204 d, m, y = re.split(r"[-/\s]", groups[dmy]) 

205 elif d_m_y is not None: 

206 d, m, y = d_m_y 

207 d = groups[d] 

208 m = groups[m] 

209 y = groups[y] 

210 else: 

211 time_only = True 

212 

213 H = M = S = u = 0 

214 if hms is not None and groups[hms]: 

215 t = groups[hms].split(":") 

216 if len(t) == 2: 

217 H, M = t 

218 else: 

219 H, M, S = t 

220 if "." in S: 

221 S, u = S.split(".") 

222 u = int(u.ljust(6, "0")[:6]) 

223 S = int(S) 

224 H = int(H) 

225 M = int(M) 

226 

227 if am is not None: 

228 am = groups[am] 

229 if am: 

230 # matching is case-insensitive (re.IGNORECASE), so normalise the 

231 # captured meridiem before the case-sensitive conversion below 

232 am = am.strip().upper() 

233 if am == "AM" and H == 12: 

234 # correction for "12" hour functioning as "0" hour: 12:15 AM = 00:15 by 24 hr clock 

235 H -= 12 

236 elif am == "PM" and H == 12: 

237 # no correction needed: 12PM is midday, 12:00 by 24 hour clock 

238 pass 

239 elif am == "PM": 

240 H += 12 

241 

242 if tz is not None: 

243 tz = groups[tz] 

244 if tz and tz.strip().upper() == "Z": 

245 tz = FixedTzOffset(0, "UTC") 

246 elif tz: 

247 tz = tz.strip() 

248 if tz.isupper(): 

249 # TODO use the awesome python TZ module? 

250 pass 

251 else: 

252 sign = tz[0] 

253 if ":" in tz: 

254 tzh, tzm = tz[1:].split(":") 

255 elif len(tz) == 4: # 'snnn' 

256 tzh, tzm = tz[1], tz[2:4] 

257 else: 

258 tzh, tzm = tz[1:3], tz[3:5] 

259 if int(tzm) >= 60: 

260 raise ValueError("timezone minute must be in 0..59") 

261 offset = int(tzm) + int(tzh) * 60 

262 if sign == "-": 

263 offset = -offset 

264 tz = FixedTzOffset(offset, tz) 

265 

266 if time_only: 

267 d = time(H, M, S, u, tzinfo=tz) 

268 else: 

269 y = int(y) 

270 if m.isdigit(): 

271 m = int(m) 

272 else: 

273 # matching is case-insensitive (re.IGNORECASE), so normalise the 

274 # captured month name before the case-sensitive lookup below 

275 m = MONTHS_MAP[m.capitalize()] 

276 d = int(d) 

277 d = datetime(y, m, d, H, M, S, u, tzinfo=tz) 

278 

279 return d 

280 

281 

282def strf_date_convert(x, _, type): 

283 is_date = any("%" + x in type for x in "aAwdbBmyYjUW") 

284 is_time = any("%" + x in type for x in "HIpMSfz") 

285 

286 if "%y" not in type and "%Y" not in type: # year not specified 

287 # Parse against the current year rather than strptime's default of 

288 # 1900, so that Feb 29 and %j day numbers resolve in the right year. 

289 x = "%s %d" % (x, datetime.today().year) 

290 type += " %Y" 

291 dt = datetime.strptime(x, type) 

292 

293 if is_date and is_time: 

294 return dt 

295 elif is_date: 

296 return dt.date() 

297 elif is_time: 

298 return dt.timetz() 

299 else: 

300 raise ValueError("Datetime not a date nor a time?") 

301 

302 

303# ref: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes 

304dt_format_to_regex = { 

305 "%a": "(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)", 

306 "%A": "(?:Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday)", 

307 "%w": "[0-6]", 

308 "%d": "[0-9]{1,2}", 

309 "%b": "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)", 

310 "%B": "(?:January|February|March|April|May|June|July|August|September|October|November|December)", 

311 "%m": "[0-9]{1,2}", 

312 "%y": "[0-9]{2}", 

313 "%Y": "[0-9]{4}", 

314 "%H": "[0-9]{1,2}", 

315 "%I": "[0-9]{1,2}", 

316 "%p": "(?:AM|PM)", 

317 "%M": "[0-9]{2}", 

318 "%S": "[0-9]{2}", 

319 "%f": "[0-9]{1,6}", 

320 # mirrors strptime: sign and minutes are required, "Z" is case-sensitive 

321 "%z": "(?:[+-][0-9]{2}:?[0-9]{2}(?::?[0-9]{2})?|(?-i:Z))", 

322 # "%Z": punt 

323 "%j": "[0-9]{1,3}", 

324 "%U": "[0-9]{1,2}", 

325 "%W": "[0-9]{1,2}", 

326} 

327 

328# Compile a regular expression pattern that matches any date/time format symbol. 

329dt_format_symbols_re = re.compile("|".join(dt_format_to_regex)) 

330 

331 

332def get_regex_for_datetime_format(format_): 

333 """ 

334 Generate a regex pattern for a given datetime format string. 

335 

336 Parameters: 

337 format_ (str): The datetime format string. 

338 

339 Returns: 

340 str: A regex pattern corresponding to the datetime format string. 

341 """ 

342 # Replace all format symbols with their regex patterns. 

343 return dt_format_symbols_re.sub(lambda m: dt_format_to_regex[m.group(0)], format_) 

344 

345 

346class TooManyFields(ValueError): 

347 pass 

348 

349 

350class RepeatedNameError(ValueError): 

351 pass 

352 

353 

354# note: {} are handled separately 

355REGEX_SAFETY = re.compile(r"([?\\.[\]()*+^$!|])") 

356 

357# allowed field types 

358ALLOWED_TYPES = set(list("nboxX%fFeEgGwWdDsSl") + ["t" + c for c in "ieahgcts"]) 

359 

360 

361def extract_format(format, extra_types): 

362 """Pull apart the format [[fill]align][sign][0][width][grouping][.precision][type]""" 

363 fill = align = None 

364 if format[0] in "<>=^": 

365 align = format[0] 

366 format = format[1:] 

367 elif len(format) > 1 and format[1] in "<>=^": 

368 fill = format[0] 

369 align = format[1] 

370 format = format[2:] 

371 

372 if format.startswith(("+", "-", " ")): 

373 format = format[1:] 

374 

375 zero = False 

376 if format and format[0] == "0": 

377 zero = True 

378 format = format[1:] 

379 

380 width = "" 

381 while format: 

382 if not format[0].isdigit(): 

383 break 

384 width += format[0] 

385 format = format[1:] 

386 

387 # Extract grouping option 

388 if format.startswith(","): 

389 format = format[1:] 

390 grouping = "," 

391 elif format.startswith("_"): 

392 format = format[1:] 

393 grouping = "_" 

394 

395 if format.startswith("."): 

396 # Precision isn't needed but we need to capture it so that 

397 # the ValueError isn't raised. 

398 format = format[1:] # drop the '.' 

399 precision = "" 

400 while format: 

401 if not format[0].isdigit(): 

402 break 

403 precision += format[0] 

404 format = format[1:] 

405 

406 # the rest is the type, if present 

407 type = format 

408 if ( 

409 type 

410 and type not in ALLOWED_TYPES 

411 and type not in extra_types 

412 and not any(k in type for k in dt_format_to_regex) 

413 ): 

414 raise ValueError("format spec %r not recognised" % type) 

415 

416 return locals() 

417 

418 

419PARSE_RE = re.compile(r"({{|}}|{[\w-]*(?:\.[\w-]+|\[[^]]+])*(?::[^}]+)?})") 

420 

421 

422class Parser(object): 

423 """Encapsulate a format string that may be used to parse other strings.""" 

424 

425 def __init__(self, format, extra_types=None, case_sensitive=False): 

426 # a mapping of a name as in {hello.world} to a regex-group compatible 

427 # name, like hello__world. It's used to prevent the transformation of 

428 # name-to-group and group to name to fail subtly, such as in: 

429 # hello_.world-> hello___world->hello._world 

430 self._group_to_name_map = {} 

431 # also store the original field name to group name mapping to allow 

432 # multiple instances of a name in the format string 

433 self._name_to_group_map = {} 

434 # and to sanity check the repeated instances store away the first 

435 # field type specification for the named field 

436 self._name_types = {} 

437 

438 self._format = format 

439 if extra_types is None: 

440 extra_types = {} 

441 self._extra_types = extra_types 

442 if case_sensitive: 

443 self._re_flags = re.DOTALL 

444 else: 

445 self._re_flags = re.IGNORECASE | re.DOTALL 

446 self._fixed_fields = [] 

447 self._named_fields = [] 

448 self._group_index = 0 

449 self._type_conversions = {} 

450 self._expression = self._generate_expression() 

451 self.__search_re = None 

452 self.__match_re = None 

453 

454 log.debug("format %r -> %r", format, self._expression) 

455 

456 def __repr__(self): 

457 if len(self._format) > 20: 

458 return "<%s %r>" % (self.__class__.__name__, self._format[:17] + "...") 

459 return "<%s %r>" % (self.__class__.__name__, self._format) 

460 

461 @property 

462 def _search_re(self): 

463 if self.__search_re is None: 

464 try: 

465 self.__search_re = re.compile(self._expression, self._re_flags) 

466 except AssertionError: 

467 # access error through sys to keep py3k and backward compat 

468 e = str(sys.exc_info()[1]) 

469 if e.endswith("this version only supports 100 named groups"): 

470 raise TooManyFields( 

471 "sorry, you are attempting to parse too many complex fields" 

472 ) 

473 return self.__search_re 

474 

475 @property 

476 def _match_re(self): 

477 if self.__match_re is None: 

478 expression = r"\A%s\Z" % self._expression 

479 try: 

480 self.__match_re = re.compile(expression, self._re_flags) 

481 except AssertionError: 

482 # access error through sys to keep py3k and backward compat 

483 e = str(sys.exc_info()[1]) 

484 if e.endswith("this version only supports 100 named groups"): 

485 raise TooManyFields( 

486 "sorry, you are attempting to parse too many complex fields" 

487 ) 

488 except re.error: 

489 raise NotImplementedError( 

490 "Group names (e.g. (?P<name>) can " 

491 "cause failure, as they are not escaped properly: '%s'" % expression 

492 ) 

493 return self.__match_re 

494 

495 @property 

496 def named_fields(self): 

497 return self._named_fields[:] 

498 

499 @property 

500 def fixed_fields(self): 

501 return self._fixed_fields[:] 

502 

503 @property 

504 def format(self): 

505 return self._format 

506 

507 def parse(self, string, evaluate_result=True): 

508 """Match my format to the string exactly. 

509 

510 Return a Result or Match instance or None if there's no match. 

511 """ 

512 m = self._match_re.match(string) 

513 if m is None: 

514 return None 

515 

516 if evaluate_result: 

517 return self.evaluate_result(m) 

518 else: 

519 return Match(self, m) 

520 

521 def search(self, string, pos=0, endpos=None, evaluate_result=True): 

522 """Search the string for my format. 

523 

524 Optionally start the search at "pos" character index and limit the 

525 search to a maximum index of endpos - equivalent to 

526 search(string[:endpos]). 

527 

528 If the ``evaluate_result`` argument is set to ``False`` a 

529 Match instance is returned instead of the actual Result instance. 

530 

531 Return either a Result instance or None if there's no match. 

532 """ 

533 if endpos is None: 

534 endpos = len(string) 

535 m = self._search_re.search(string, pos, endpos) 

536 if m is None: 

537 return None 

538 

539 if evaluate_result: 

540 return self.evaluate_result(m) 

541 else: 

542 return Match(self, m) 

543 

544 def findall( 

545 self, string, pos=0, endpos=None, extra_types=None, evaluate_result=True 

546 ): 

547 """Search "string" for all occurrences of "format". 

548 

549 Optionally start the search at "pos" character index and limit the 

550 search to a maximum index of endpos - equivalent to 

551 search(string[:endpos]). 

552 

553 Returns an iterator that holds Result or Match instances for each format match 

554 found. 

555 """ 

556 if endpos is None: 

557 endpos = len(string) 

558 return ResultIterator( 

559 self, string, pos, endpos, evaluate_result=evaluate_result 

560 ) 

561 

562 def _expand_named_fields(self, named_fields): 

563 result = {} 

564 for field, value in named_fields.items(): 

565 # split 'aaa[bbb][ccc]...' into 'aaa' and '[bbb][ccc]...' 

566 n = field.find("[") 

567 if n == -1: 

568 basename, subkeys = field, "" 

569 else: 

570 basename, subkeys = field[:n], field[n:] 

571 

572 # create nested dictionaries {'aaa': {'bbb': {'ccc': ...}}} 

573 d = result 

574 k = basename 

575 

576 if subkeys: 

577 for subkey in re.findall(r"\[[^]]+]", subkeys): 

578 d = d.setdefault(k, {}) 

579 k = subkey[1:-1] 

580 

581 # assign the value to the last key 

582 d[k] = value 

583 

584 return result 

585 

586 def evaluate_result(self, m): 

587 """Generate a Result instance for the given regex match object""" 

588 # ok, figure the fixed fields we've pulled out and type convert them 

589 fixed_fields = list(m.groups()) 

590 for n in self._fixed_fields: 

591 if n in self._type_conversions: 

592 fixed_fields[n] = self._type_conversions[n](fixed_fields[n], m) 

593 fixed_fields = tuple(fixed_fields[n] for n in self._fixed_fields) 

594 

595 # grab the named fields, converting where requested 

596 groupdict = m.groupdict() 

597 named_fields = {} 

598 name_map = {} 

599 for k in self._named_fields: 

600 korig = self._group_to_name_map[k] 

601 name_map[korig] = k 

602 if k in self._type_conversions: 

603 value = self._type_conversions[k](groupdict[k], m) 

604 else: 

605 value = groupdict[k] 

606 

607 named_fields[korig] = value 

608 

609 # now figure the match spans 

610 spans = {n: m.span(name_map[n]) for n in named_fields} 

611 spans.update((i, m.span(n + 1)) for i, n in enumerate(self._fixed_fields)) 

612 

613 # and that's our result 

614 return Result(fixed_fields, self._expand_named_fields(named_fields), spans) 

615 

616 def _regex_replace(self, match): 

617 return "\\" + match.group(1) 

618 

619 def _generate_expression(self): 

620 # turn my _format attribute into the _expression attribute 

621 e = [] 

622 for part in PARSE_RE.split(self._format): 

623 if not part: 

624 continue 

625 elif part == "{{": 

626 e.append(r"\{") 

627 elif part == "}}": 

628 e.append(r"\}") 

629 elif part[0] == "{" and part[-1] == "}": 

630 # this will be a braces-delimited field to handle 

631 e.append(self._handle_field(part)) 

632 else: 

633 # just some text to match 

634 e.append(REGEX_SAFETY.sub(self._regex_replace, part)) 

635 return "".join(e) 

636 

637 def _to_group_name(self, field): 

638 # return a version of field which can be used as capture group, even 

639 # though it might contain '.' 

640 group = field.replace(".", "_").replace("[", "_").replace("]", "_").replace("-", "_") 

641 

642 # make sure we don't collide ("a.b" colliding with "a_b") 

643 n = 1 

644 while group in self._group_to_name_map: 

645 n += 1 

646 if "." in field: 

647 group = field.replace(".", "_" * n) 

648 elif "_" in field: 

649 group = field.replace("_", "_" * n) 

650 elif "-" in field: 

651 group = field.replace("-", "_" * n) 

652 else: 

653 raise KeyError("duplicated group name %r" % (field,)) 

654 

655 # save off the mapping 

656 self._group_to_name_map[group] = field 

657 self._name_to_group_map[field] = group 

658 return group 

659 

660 def _handle_field(self, field): 

661 # first: lose the braces 

662 field = field[1:-1] 

663 

664 # now figure whether this is an anonymous or named field, and whether 

665 # there's any format specification 

666 format = "" 

667 

668 if ":" in field: 

669 name, format = field.split(":", 1) 

670 else: 

671 name = field 

672 

673 # This *should* be more flexible, but parsing complicated structures 

674 # out of the string is hard (and not necessarily useful) ... and I'm 

675 # being lazy. So for now `identifier` is "anything starting with a 

676 # letter" and digit args don't get attribute or element stuff. 

677 if name and name[0].isalpha(): 

678 if name in self._name_to_group_map: 

679 if self._name_types[name] != format: 

680 raise RepeatedNameError( 

681 'field type %r for field "%s" ' 

682 "does not match previous seen type %r" 

683 % (format, name, self._name_types[name]) 

684 ) 

685 group = self._name_to_group_map[name] 

686 # match previously-seen value 

687 return r"(?P=%s)" % group 

688 else: 

689 group = self._to_group_name(name) 

690 self._name_types[name] = format 

691 self._named_fields.append(group) 

692 # this will become a group, which must not contain dots 

693 wrap = r"(?P<%s>%%s)" % group 

694 else: 

695 self._fixed_fields.append(self._group_index) 

696 wrap = r"(%s)" 

697 group = self._group_index 

698 

699 # simplest case: no type specifier ({} or {name}) 

700 if not format: 

701 self._group_index += 1 

702 return wrap % r".+?" 

703 

704 # decode the format specification 

705 format = extract_format(format, self._extra_types) 

706 

707 # figure type conversions, if any 

708 type = format["type"] 

709 is_numeric = type and type in "n%fFegdobxEGX" 

710 conv = self._type_conversions 

711 if type in self._extra_types: 

712 type_converter = self._extra_types[type] 

713 s = getattr(type_converter, "pattern", r".+?") 

714 regex_group_count = getattr(type_converter, "regex_group_count", 0) 

715 if regex_group_count is None: 

716 regex_group_count = 0 

717 self._group_index += regex_group_count 

718 conv[group] = convert_first(type_converter) 

719 elif type == "n": 

720 s = r"\d{1,3}([,.]\d{3})*" 

721 self._group_index += 1 

722 conv[group] = int_convert(10) 

723 elif type == "b": 

724 s = r"(0[bB])?[01]+" 

725 conv[group] = int_convert(2) 

726 self._group_index += 1 

727 elif type == "o": 

728 s = r"(0[oO])?[0-7]+" 

729 conv[group] = int_convert(8) 

730 self._group_index += 1 

731 elif type in ("x", "X"): 

732 s = r"(0[xX])?[0-9a-fA-F]+" 

733 conv[group] = int_convert(16) 

734 self._group_index += 1 

735 elif type == "%": 

736 s = r"\d+(\.\d+)?%" 

737 self._group_index += 1 

738 conv[group] = percentage 

739 elif type in ("f", "F"): 

740 # precision 0 formats without a decimal point (e.g. format(20.0, ".0f") == "20") 

741 # Non-capturing group so the shared "[-+ ]?" sign prefix (added below 

742 # for numeric types) stays in front of the whole alternation. Without 

743 # it, "[-+ ]?\d*\.\d+|nan|..." would bind the sign to the first branch 

744 # only, breaking negative parsing. nan/inf match the e/g types too. 

745 if format.get("precision") == "0": 

746 s = r"(?:\d+|nan|NAN|inf|INF)" 

747 else: 

748 s = r"(?:\d*\.\d+|nan|NAN|inf|INF)" 

749 conv[group] = convert_first(float if type == "f" else Decimal) 

750 elif type in ("e", "E"): 

751 s = r"\d*\.\d+[eE][-+]?\d+|nan|NAN|[-+]?inf|[-+]?INF" 

752 conv[group] = convert_first(float) 

753 elif type in ("g", "G"): 

754 s = r"\d+(\.\d+)?([eE][-+]?\d+)?|nan|NAN|[-+]?inf|[-+]?INF" 

755 self._group_index += 2 

756 conv[group] = convert_first(float) 

757 elif type == "d": 

758 if format.get("width"): 

759 width = r"{1,%s}" % int(format["width"]) 

760 else: 

761 width = "+" 

762 s = r"[-+ ]?[0-9{g}]{w}|[-+ ]?0[xX][0-9a-fA-F{g}]{w}|[-+ ]?0[bB][01{g}]{w}|[-+ ]?0[oO][0-7{g}]{w}".format( 

763 w=width, 

764 g=format.get("grouping", ""), 

765 ) 

766 conv[group] = int_convert() 

767 # do not specify number base, determine it automatically 

768 elif any(k in type for k in dt_format_to_regex): 

769 s = get_regex_for_datetime_format(type) 

770 conv[group] = partial(strf_date_convert, type=type) 

771 # the generated pattern may contain capture groups of its own, so 

772 # account for them - otherwise the fields after this one are read 

773 # from the wrong match groups 

774 self._group_index += re.compile(s).groups 

775 elif type == "ti": 

776 s = r"(\d{4}-\d\d-\d\d)((\s+|T)%s)?(Z|\s*[-+]\d\d:?\d\d)?" % TIME_PAT 

777 n = self._group_index 

778 conv[group] = partial(date_convert, ymd=n + 1, hms=n + 4, tz=n + 7) 

779 self._group_index += 7 

780 elif type == "tg": 

781 s = r"(\d{1,2}[-/](\d{1,2}|%s)[-/]\d{4})(\s+%s)?%s?%s?" 

782 s %= (ALL_MONTHS_PAT, TIME_PAT, AM_PAT, TZ_PAT) 

783 n = self._group_index 

784 conv[group] = partial( 

785 date_convert, dmy=n + 1, hms=n + 5, am=n + 8, tz=n + 9 

786 ) 

787 self._group_index += 9 

788 elif type == "ta": 

789 s = r"((\d{1,2}|%s)[-/]\d{1,2}[-/]\d{4})(\s+%s)?%s?%s?" 

790 s %= (ALL_MONTHS_PAT, TIME_PAT, AM_PAT, TZ_PAT) 

791 n = self._group_index 

792 conv[group] = partial( 

793 date_convert, mdy=n + 1, hms=n + 5, am=n + 8, tz=n + 9 

794 ) 

795 self._group_index += 9 

796 elif type == "te": 

797 # this will allow microseconds through if they're present, but meh 

798 s = r"(%s,\s+)?(\d{1,2}\s+%s\s+\d{4})\s+%s%s" 

799 s %= (DAYS_PAT, MONTHS_PAT, TIME_PAT, TZ_PAT) 

800 n = self._group_index 

801 conv[group] = partial(date_convert, dmy=n + 3, hms=n + 5, tz=n + 8) 

802 self._group_index += 8 

803 elif type == "th": 

804 # slight flexibility here from the stock Apache format 

805 s = r"(\d{1,2}[-/]%s[-/]\d{4}):%s%s" % (MONTHS_PAT, TIME_PAT, TZ_PAT) 

806 n = self._group_index 

807 conv[group] = partial(date_convert, dmy=n + 1, hms=n + 3, tz=n + 6) 

808 self._group_index += 6 

809 elif type == "tc": 

810 s = r"(%s)\s+%s\s+(\d{1,2})\s+%s\s+(\d{4})" 

811 s %= (DAYS_PAT, MONTHS_PAT, TIME_PAT) 

812 n = self._group_index 

813 conv[group] = partial(date_convert, d_m_y=(n + 4, n + 3, n + 8), hms=n + 5) 

814 self._group_index += 8 

815 elif type == "tt": 

816 # only the AM/PM and timezone parts are optional - the time itself 

817 # is not, otherwise this type matches the empty string and yields a 

818 # bogus time(0, 0) 

819 s = r"%s%s?%s?" % (TIME_PAT, AM_PAT, TZ_PAT) 

820 n = self._group_index 

821 conv[group] = partial(date_convert, hms=n + 1, am=n + 4, tz=n + 5) 

822 self._group_index += 5 

823 elif type == "ts": 

824 s = r"%s(\s+)(\d+)(\s+)(\d{1,2}:\d{1,2}:\d{1,2})?" % MONTHS_PAT 

825 n = self._group_index 

826 conv[group] = partial(date_convert, mm=n + 1, dd=n + 3, hms=n + 5) 

827 self._group_index += 5 

828 elif type == "l": 

829 s = r"[A-Za-z]+" 

830 elif type: 

831 s = r"\%s+" % type 

832 elif format.get("precision"): 

833 if format.get("width"): 

834 s = r".{%s,%s}?" % (format["width"], format["precision"]) 

835 else: 

836 s = r".{1,%s}?" % format["precision"] 

837 elif format.get("width"): 

838 s = r".{%s,}?" % format["width"] 

839 else: 

840 s = r".+?" 

841 

842 align = format["align"] 

843 fill = format["fill"] 

844 

845 # handle some numeric-specific things like fill and sign 

846 if is_numeric: 

847 # prefix with something (align "=" trumps zero) 

848 if align == "=": 

849 # special case - align "=" acts like the zero above but with 

850 # configurable fill defaulting to "0" 

851 if not fill: 

852 fill = "0" 

853 s = r"%s*" % fill + s 

854 

855 # allow numbers to be prefixed with a sign 

856 s = r"[-+ ]?" + s 

857 

858 if not fill: 

859 fill = " " 

860 

861 # Place into a group now - this captures the value we want to keep. 

862 # Everything else from now is just padding to be stripped off 

863 if wrap: 

864 s = wrap % s 

865 self._group_index += 1 

866 

867 if format["width"]: 

868 # all we really care about is that if the format originally 

869 # specified a width then there will probably be padding - without 

870 # an explicit alignment that'll mean right alignment with spaces 

871 # padding 

872 if not align: 

873 align = ">" 

874 

875 if fill in r".\+?*[](){}^$": 

876 fill = "\\" + fill 

877 

878 # align "=" has been handled 

879 if align == "<": 

880 s = "%s%s*" % (s, fill) 

881 elif align == ">": 

882 s = "%s*%s" % (fill, s) 

883 elif align == "^": 

884 s = "%s*%s%s*" % (fill, s, fill) 

885 

886 return s 

887 

888 

889class Result(object): 

890 """The result of a parse() or search(). 

891 

892 Fixed results may be looked up using `result[index]`. 

893 Slices of fixed results may also be looked up. 

894 

895 Named results may be looked up using `result['name']`. 

896 

897 Named results may be tested for existence using `'name' in result`. 

898 """ 

899 

900 def __init__(self, fixed, named, spans): 

901 self.fixed = fixed 

902 self.named = named 

903 self.spans = spans 

904 

905 def __getitem__(self, item): 

906 if isinstance(item, (int, slice)): 

907 return self.fixed[item] 

908 return self.named[item] 

909 

910 def __repr__(self): 

911 return "<%s %r %r>" % (self.__class__.__name__, self.fixed, self.named) 

912 

913 def __contains__(self, name): 

914 return name in self.named 

915 

916 

917class Match(object): 

918 """The result of a parse() or search() if no results are generated. 

919 

920 This class is only used to expose internal used regex match objects 

921 to the user and use them for external Parser.evaluate_result calls. 

922 """ 

923 

924 def __init__(self, parser, match): 

925 self.parser = parser 

926 self.match = match 

927 

928 def evaluate_result(self): 

929 """Generate results for this Match""" 

930 return self.parser.evaluate_result(self.match) 

931 

932 

933class ResultIterator(object): 

934 """The result of a findall() operation. 

935 

936 Each element is a Result instance. 

937 """ 

938 

939 def __init__(self, parser, string, pos, endpos, evaluate_result=True): 

940 self.parser = parser 

941 self.string = string 

942 self.pos = pos 

943 self.endpos = endpos 

944 self.evaluate_result = evaluate_result 

945 

946 def __iter__(self): 

947 return self 

948 

949 def __next__(self): 

950 m = self.parser._search_re.search(self.string, self.pos, self.endpos) 

951 if m is None: 

952 raise StopIteration() 

953 self.pos = m.end() 

954 

955 if self.evaluate_result: 

956 return self.parser.evaluate_result(m) 

957 else: 

958 return Match(self.parser, m) 

959 

960 # pre-py3k compat 

961 next = __next__ 

962 

963 

964def parse(format, string, extra_types=None, evaluate_result=True, case_sensitive=False): 

965 """Using "format" attempt to pull values from "string". 

966 

967 The format must match the string contents exactly. If the value 

968 you're looking for is instead just a part of the string use 

969 search(). 

970 

971 If ``evaluate_result`` is True the return value will be an Result instance with two attributes: 

972 

973 .fixed - tuple of fixed-position values from the string 

974 .named - dict of named values from the string 

975 

976 If ``evaluate_result`` is False the return value will be a Match instance with one method: 

977 

978 .evaluate_result() - This will return a Result instance like you would get 

979 with ``evaluate_result`` set to True 

980 

981 The default behaviour is to match strings case insensitively. You may match with 

982 case by specifying case_sensitive=True. 

983 

984 If the format is invalid a ValueError will be raised. 

985 

986 See the module documentation for the use of "extra_types". 

987 

988 In the case there is no match parse() will return None. 

989 """ 

990 p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) 

991 return p.parse(string, evaluate_result=evaluate_result) 

992 

993 

994def search( 

995 format, 

996 string, 

997 pos=0, 

998 endpos=None, 

999 extra_types=None, 

1000 evaluate_result=True, 

1001 case_sensitive=False, 

1002): 

1003 """Search "string" for the first occurrence of "format". 

1004 

1005 The format may occur anywhere within the string. If 

1006 instead you wish for the format to exactly match the string 

1007 use parse(). 

1008 

1009 Optionally start the search at "pos" character index and limit the search 

1010 to a maximum index of endpos - equivalent to search(string[:endpos]). 

1011 

1012 If ``evaluate_result`` is True the return value will be an Result instance with two attributes: 

1013 

1014 .fixed - tuple of fixed-position values from the string 

1015 .named - dict of named values from the string 

1016 

1017 If ``evaluate_result`` is False the return value will be a Match instance with one method: 

1018 

1019 .evaluate_result() - This will return a Result instance like you would get 

1020 with ``evaluate_result`` set to True 

1021 

1022 The default behaviour is to match strings case insensitively. You may match with 

1023 case by specifying case_sensitive=True. 

1024 

1025 If the format is invalid a ValueError will be raised. 

1026 

1027 See the module documentation for the use of "extra_types". 

1028 

1029 In the case there is no match parse() will return None. 

1030 """ 

1031 p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) 

1032 return p.search(string, pos, endpos, evaluate_result=evaluate_result) 

1033 

1034 

1035def findall( 

1036 format, 

1037 string, 

1038 pos=0, 

1039 endpos=None, 

1040 extra_types=None, 

1041 evaluate_result=True, 

1042 case_sensitive=False, 

1043): 

1044 """Search "string" for all occurrences of "format". 

1045 

1046 You will be returned an iterator that holds Result instances 

1047 for each format match found. 

1048 

1049 Optionally start the search at "pos" character index and limit the search 

1050 to a maximum index of endpos - equivalent to search(string[:endpos]). 

1051 

1052 If ``evaluate_result`` is True each returned Result instance has two attributes: 

1053 

1054 .fixed - tuple of fixed-position values from the string 

1055 .named - dict of named values from the string 

1056 

1057 If ``evaluate_result`` is False each returned value is a Match instance with one method: 

1058 

1059 .evaluate_result() - This will return a Result instance like you would get 

1060 with ``evaluate_result`` set to True 

1061 

1062 The default behaviour is to match strings case insensitively. You may match with 

1063 case by specifying case_sensitive=True. 

1064 

1065 If the format is invalid a ValueError will be raised. 

1066 

1067 See the module documentation for the use of "extra_types". 

1068 """ 

1069 p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) 

1070 return p.findall(string, pos, endpos, evaluate_result=evaluate_result) 

1071 

1072 

1073def compile(format, extra_types=None, case_sensitive=False): 

1074 """Create a Parser instance to parse "format". 

1075 

1076 The resultant Parser has a method .parse(string) which 

1077 behaves in the same manner as parse(format, string). 

1078 

1079 The default behaviour is to match strings case insensitively. You may match with 

1080 case by specifying case_sensitive=True. 

1081 

1082 Use this function if you intend to parse many strings 

1083 with the same format. 

1084 

1085 See the module documentation for the use of "extra_types". 

1086 

1087 Returns a Parser instance. 

1088 """ 

1089 return Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) 

1090 

1091 

1092# Copyright (c) 2012-2020 Richard Jones <richard@python.org> 

1093# 

1094# Permission is hereby granted, free of charge, to any person obtaining a copy 

1095# of this software and associated documentation files (the "Software"), to deal 

1096# in the Software without restriction, including without limitation the rights 

1097# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 

1098# copies of the Software, and to permit persons to whom the Software is 

1099# furnished to do so, subject to the following conditions: 

1100# 

1101# The above copyright notice and this permission notice shall be included in 

1102# all copies or substantial portions of the Software. 

1103# 

1104# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

1105# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

1106# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

1107# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

1108# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

1109# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 

1110# SOFTWARE. 

1111 

1112# vim: set filetype=python ts=4 sw=4 et si tw=75