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
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
1from __future__ import absolute_import
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
14__version__ = "1.22.3"
15__all__ = ["parse", "search", "findall", "with_pattern"]
17log = logging.getLogger(__name__)
20def with_pattern(pattern, regex_group_count=None):
21 r"""Attach a regular expression pattern matcher to a custom type converter
22 function.
24 This annotates the type converter with the :attr:`pattern` attribute.
26 EXAMPLE:
27 >>> import parse
28 >>> @parse.with_pattern(r"\d+")
29 ... def parse_number(text):
30 ... return int(text)
32 is equivalent to:
34 >>> def parse_number(text):
35 ... return int(text)
36 >>> parse_number.pattern = r"\d+"
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 """
43 def decorator(func):
44 func.pattern = pattern
45 func.regex_group_count = regex_group_count
46 return func
48 return decorator
51class int_convert:
52 """Convert a string to an integer.
54 The string may start with a sign.
56 It may be of a base other than 2, 8, 10 or 16.
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.
62 It may also have other non-numeric characters that we can ignore.
63 """
65 CHARS = "0123456789abcdefghijklmnopqrstuvwxyz"
67 def __init__(self, base=None):
68 self.base = base
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
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
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
96 chars = int_convert.CHARS[:base]
97 string = re.sub("[^%s]" % chars, "", string.lower())
98 return sign * int(string, base)
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 """
106 def __init__(self, converter):
107 self.converter = converter
109 def __call__(self, string, match):
110 return self.converter(string)
113def percentage(string, match):
114 return float(string[:-1]) / 100.0
117class FixedTzOffset(tzinfo):
118 """Fixed offset in minutes east from UTC."""
120 ZERO = timedelta(0)
122 def __init__(self, offset, name):
123 self._offset = timedelta(minutes=offset)
124 self._name = name
126 def __repr__(self):
127 return "<%s %s %s>" % (self.__class__.__name__, self._name, self._offset)
129 def utcoffset(self, dt):
130 return self._offset
132 def tzname(self, dt):
133 return self._name
135 def dst(self, dt):
136 return self.ZERO
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
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)"
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
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)
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
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)
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)
279 return d
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")
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)
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?")
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}
328# Compile a regular expression pattern that matches any date/time format symbol.
329dt_format_symbols_re = re.compile("|".join(dt_format_to_regex))
332def get_regex_for_datetime_format(format_):
333 """
334 Generate a regex pattern for a given datetime format string.
336 Parameters:
337 format_ (str): The datetime format string.
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_)
346class TooManyFields(ValueError):
347 pass
350class RepeatedNameError(ValueError):
351 pass
354# note: {} are handled separately
355REGEX_SAFETY = re.compile(r"([?\\.[\]()*+^$!|])")
357# allowed field types
358ALLOWED_TYPES = set(list("nboxX%fFeEgGwWdDsSl") + ["t" + c for c in "ieahgcts"])
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:]
372 if format.startswith(("+", "-", " ")):
373 format = format[1:]
375 zero = False
376 if format and format[0] == "0":
377 zero = True
378 format = format[1:]
380 width = ""
381 while format:
382 if not format[0].isdigit():
383 break
384 width += format[0]
385 format = format[1:]
387 # Extract grouping option
388 if format.startswith(","):
389 format = format[1:]
390 grouping = ","
391 elif format.startswith("_"):
392 format = format[1:]
393 grouping = "_"
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:]
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)
416 return locals()
419PARSE_RE = re.compile(r"({{|}}|{[\w-]*(?:\.[\w-]+|\[[^]]+])*(?::[^}]+)?})")
422class Parser(object):
423 """Encapsulate a format string that may be used to parse other strings."""
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 = {}
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
454 log.debug("format %r -> %r", format, self._expression)
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)
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
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
495 @property
496 def named_fields(self):
497 return self._named_fields[:]
499 @property
500 def fixed_fields(self):
501 return self._fixed_fields[:]
503 @property
504 def format(self):
505 return self._format
507 def parse(self, string, evaluate_result=True):
508 """Match my format to the string exactly.
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
516 if evaluate_result:
517 return self.evaluate_result(m)
518 else:
519 return Match(self, m)
521 def search(self, string, pos=0, endpos=None, evaluate_result=True):
522 """Search the string for my format.
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]).
528 If the ``evaluate_result`` argument is set to ``False`` a
529 Match instance is returned instead of the actual Result instance.
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
539 if evaluate_result:
540 return self.evaluate_result(m)
541 else:
542 return Match(self, m)
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".
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]).
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 )
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:]
572 # create nested dictionaries {'aaa': {'bbb': {'ccc': ...}}}
573 d = result
574 k = basename
576 if subkeys:
577 for subkey in re.findall(r"\[[^]]+]", subkeys):
578 d = d.setdefault(k, {})
579 k = subkey[1:-1]
581 # assign the value to the last key
582 d[k] = value
584 return result
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)
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]
607 named_fields[korig] = value
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))
613 # and that's our result
614 return Result(fixed_fields, self._expand_named_fields(named_fields), spans)
616 def _regex_replace(self, match):
617 return "\\" + match.group(1)
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)
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("-", "_")
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,))
655 # save off the mapping
656 self._group_to_name_map[group] = field
657 self._name_to_group_map[field] = group
658 return group
660 def _handle_field(self, field):
661 # first: lose the braces
662 field = field[1:-1]
664 # now figure whether this is an anonymous or named field, and whether
665 # there's any format specification
666 format = ""
668 if ":" in field:
669 name, format = field.split(":", 1)
670 else:
671 name = field
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
699 # simplest case: no type specifier ({} or {name})
700 if not format:
701 self._group_index += 1
702 return wrap % r".+?"
704 # decode the format specification
705 format = extract_format(format, self._extra_types)
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".+?"
842 align = format["align"]
843 fill = format["fill"]
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
855 # allow numbers to be prefixed with a sign
856 s = r"[-+ ]?" + s
858 if not fill:
859 fill = " "
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
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 = ">"
875 if fill in r".\+?*[](){}^$":
876 fill = "\\" + fill
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)
886 return s
889class Result(object):
890 """The result of a parse() or search().
892 Fixed results may be looked up using `result[index]`.
893 Slices of fixed results may also be looked up.
895 Named results may be looked up using `result['name']`.
897 Named results may be tested for existence using `'name' in result`.
898 """
900 def __init__(self, fixed, named, spans):
901 self.fixed = fixed
902 self.named = named
903 self.spans = spans
905 def __getitem__(self, item):
906 if isinstance(item, (int, slice)):
907 return self.fixed[item]
908 return self.named[item]
910 def __repr__(self):
911 return "<%s %r %r>" % (self.__class__.__name__, self.fixed, self.named)
913 def __contains__(self, name):
914 return name in self.named
917class Match(object):
918 """The result of a parse() or search() if no results are generated.
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 """
924 def __init__(self, parser, match):
925 self.parser = parser
926 self.match = match
928 def evaluate_result(self):
929 """Generate results for this Match"""
930 return self.parser.evaluate_result(self.match)
933class ResultIterator(object):
934 """The result of a findall() operation.
936 Each element is a Result instance.
937 """
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
946 def __iter__(self):
947 return self
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()
955 if self.evaluate_result:
956 return self.parser.evaluate_result(m)
957 else:
958 return Match(self.parser, m)
960 # pre-py3k compat
961 next = __next__
964def parse(format, string, extra_types=None, evaluate_result=True, case_sensitive=False):
965 """Using "format" attempt to pull values from "string".
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().
971 If ``evaluate_result`` is True the return value will be an Result instance with two attributes:
973 .fixed - tuple of fixed-position values from the string
974 .named - dict of named values from the string
976 If ``evaluate_result`` is False the return value will be a Match instance with one method:
978 .evaluate_result() - This will return a Result instance like you would get
979 with ``evaluate_result`` set to True
981 The default behaviour is to match strings case insensitively. You may match with
982 case by specifying case_sensitive=True.
984 If the format is invalid a ValueError will be raised.
986 See the module documentation for the use of "extra_types".
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)
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".
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().
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]).
1012 If ``evaluate_result`` is True the return value will be an Result instance with two attributes:
1014 .fixed - tuple of fixed-position values from the string
1015 .named - dict of named values from the string
1017 If ``evaluate_result`` is False the return value will be a Match instance with one method:
1019 .evaluate_result() - This will return a Result instance like you would get
1020 with ``evaluate_result`` set to True
1022 The default behaviour is to match strings case insensitively. You may match with
1023 case by specifying case_sensitive=True.
1025 If the format is invalid a ValueError will be raised.
1027 See the module documentation for the use of "extra_types".
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)
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".
1046 You will be returned an iterator that holds Result instances
1047 for each format match found.
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]).
1052 If ``evaluate_result`` is True each returned Result instance has two attributes:
1054 .fixed - tuple of fixed-position values from the string
1055 .named - dict of named values from the string
1057 If ``evaluate_result`` is False each returned value is a Match instance with one method:
1059 .evaluate_result() - This will return a Result instance like you would get
1060 with ``evaluate_result`` set to True
1062 The default behaviour is to match strings case insensitively. You may match with
1063 case by specifying case_sensitive=True.
1065 If the format is invalid a ValueError will be raised.
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)
1073def compile(format, extra_types=None, case_sensitive=False):
1074 """Create a Parser instance to parse "format".
1076 The resultant Parser has a method .parse(string) which
1077 behaves in the same manner as parse(format, string).
1079 The default behaviour is to match strings case insensitively. You may match with
1080 case by specifying case_sensitive=True.
1082 Use this function if you intend to parse many strings
1083 with the same format.
1085 See the module documentation for the use of "extra_types".
1087 Returns a Parser instance.
1088 """
1089 return Parser(format, extra_types=extra_types, case_sensitive=case_sensitive)
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.
1112# vim: set filetype=python ts=4 sw=4 et si tw=75