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
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
1# common.py
2from .core import *
3from .helpers import DelimitedList, any_open_tag, any_close_tag
4from datetime import datetime, timedelta
5import sys
7PY_310_OR_LATER = sys.version_info >= (3, 10)
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:
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`
26 Parse actions:
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`
36 Examples:
38 .. testcode::
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 ''')
50 .. testoutput::
51 :options: +NORMALIZE_WHITESPACE
54 # any int or real number, returned as the appropriate type
55 100
56 [100]
58 -100
59 [-100]
61 +100
62 [100]
64 3.14159
65 [3.14159]
67 6.02e23
68 [6.02e+23]
70 1e-12
71 [1e-12]
73 .. testcode::
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 ''')
85 .. testoutput::
86 :options: +NORMALIZE_WHITESPACE
89 # any int or real number, returned as float
90 100
91 [100.0]
93 -100
94 [-100.0]
96 +100
97 [100.0]
99 3.14159
100 [3.14159]
102 6.02e23
103 [6.02e+23]
105 1e-12
106 [1e-12]
108 .. testcode::
110 pyparsing_common.hex_integer.run_tests('''
111 # hex numbers
112 100
113 FF
114 ''')
116 .. testoutput::
117 :options: +NORMALIZE_WHITESPACE
120 # hex numbers
121 100
122 [256]
124 FF
125 [255]
127 .. testcode::
129 pyparsing_common.fraction.run_tests('''
130 # fractions
131 1/2
132 -3/4
133 ''')
135 .. testoutput::
136 :options: +NORMALIZE_WHITESPACE
139 # fractions
140 1/2
141 [0.5]
143 -3/4
144 [-0.75]
146 .. testcode::
148 pyparsing_common.mixed_integer.run_tests('''
149 # mixed fractions
150 1
151 1/2
152 -3/4
153 1-3/4
154 ''')
156 .. testoutput::
157 :options: +NORMALIZE_WHITESPACE
160 # mixed fractions
161 1
162 [1]
164 1/2
165 [0.5]
167 -3/4
168 [-0.75]
170 1-3/4
171 [1.75]
172 .. testcode::
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 ''')
181 .. testoutput::
182 :options: +NORMALIZE_WHITESPACE
185 # uuid
186 12345678-1234-5678-1234-567812345678
187 [UUID('12345678-1234-5678-1234-567812345678')]
188 """
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]
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]
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"""
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"""
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"""
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])
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)
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"""
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"""
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"""
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"""
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"""
274 identifier = Word(identchars, identbodychars).set_name("identifier")
275 """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')"""
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``)"
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)"
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)"
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
312 Params -
313 - fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%d"``)
315 Example:
317 .. testcode::
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"))
323 prints:
325 .. testoutput::
327 [datetime.date(1999, 12, 31)]
328 """
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))
336 return cvt_fn
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`
343 Params -
344 - fmt - format to be passed to :class:`datetime.strptime` (default= ``"%Y-%m-%dT%H:%M:%S.%f"``)
346 Example:
348 .. testcode::
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"))
354 prints:
356 .. testoutput::
358 [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)]
359 """
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))
367 return cvt_fn
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``)"
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 ``' '``"
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`.
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.
388 Omitted fields will default to fields from Jan 1, 00:00:00.
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
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."
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."
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``)"
423 _html_stripper = any_open_tag.suppress() | any_close_tag.suppress()
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
429 Example:
431 .. testcode::
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)
440 Prints:
442 .. testoutput::
444 More info at the pyparsing wiki page
445 """
446 return pyparsing_common._html_stripper.transform_string(tokens[0])
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."""
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]
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]
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)
526 .. versionchanged:: 3.1.0
527 ``url`` named group added
528 """
529 # fmt: on
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
543_builtin_exprs = [
544 v for v in vars(pyparsing_common).values() if isinstance(v, ParserElement)
545]