langroid 0.16.5__py3-none-any.whl → 0.16.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. langroid/agent/md_tool_message_grammar.py +455 -0
  2. langroid/agent/tools/code_file_tool_parse.py +150 -0
  3. langroid/agent/tools/code_file_tool_pyparsing.py +194 -0
  4. langroid/agent/tools/code_file_tool_pyparsing2.py +199 -0
  5. langroid/agent/tools/formatted_model_custom.py +150 -0
  6. langroid/agent/tools/formatted_model_custom2.py +168 -0
  7. langroid/agent/tools/formatted_model_custom3.py +279 -0
  8. langroid/agent/tools/formatted_model_custom4.py +395 -0
  9. langroid/agent/tools/formatted_model_jinja.py +133 -0
  10. langroid/agent/tools/formatted_model_jinja.py-e +122 -0
  11. langroid/agent/tools/formatted_model_jinja2.py +145 -0
  12. langroid/agent/tools/formatted_model_jinja2.py-e +135 -0
  13. langroid/agent/tools/formatted_model_lark.py +0 -0
  14. langroid/agent/tools/formatted_model_lark2.py +168 -0
  15. langroid/agent/tools/formatted_model_parse.py +105 -0
  16. langroid/agent/tools/formatted_model_parse.py-e +98 -0
  17. langroid/agent/tools/formatted_model_parse2.py +113 -0
  18. langroid/agent/tools/formatted_model_parse2.py-e +109 -0
  19. langroid/agent/tools/formatted_model_parse3.py +114 -0
  20. langroid/agent/tools/formatted_model_parse3.py-e +110 -0
  21. langroid/agent/tools/formatted_model_parsimon.py +194 -0
  22. langroid/agent/tools/formatted_model_parsimon.py-e +186 -0
  23. langroid/agent/tools/formatted_model_pyparsing.py +169 -0
  24. langroid/agent/tools/formatted_model_pyparsing.py-e +149 -0
  25. langroid/agent/tools/formatted_model_pyparsing2.py +159 -0
  26. langroid/agent/tools/formatted_model_pyparsing2.py-e +143 -0
  27. langroid/agent/tools/formatted_model_pyparsing3.py +133 -0
  28. langroid/agent/tools/formatted_model_pyparsing3.py-e +121 -0
  29. langroid/agent/tools/formatted_model_pyparsing4.py +213 -0
  30. langroid/agent/tools/formatted_model_pyparsing4.py-e +176 -0
  31. langroid/agent/tools/formatted_model_pyparsing5.py +173 -0
  32. langroid/agent/tools/formatted_model_pyparsing5.py-e +142 -0
  33. langroid/agent/tools/formatted_model_regex.py +246 -0
  34. langroid/agent/tools/formatted_model_regex.py-e +248 -0
  35. langroid/agent/tools/formatted_model_regex2.py +250 -0
  36. langroid/agent/tools/formatted_model_regex2.py-e +253 -0
  37. langroid/agent/tools/formatted_model_tatsu.py +172 -0
  38. langroid/agent/tools/formatted_model_tatsu.py-e +160 -0
  39. langroid/agent/tools/formatted_model_template.py +217 -0
  40. langroid/agent/tools/formatted_model_template.py-e +200 -0
  41. langroid/agent/tools/formatted_model_xml.py +178 -0
  42. langroid/agent/tools/formatted_model_xml2.py +178 -0
  43. langroid/agent/tools/formatted_model_xml3.py +132 -0
  44. langroid/agent/tools/formatted_model_xml4.py +130 -0
  45. langroid/agent/tools/formatted_model_xml5.py +130 -0
  46. langroid/agent/tools/formatted_model_xml6.py +113 -0
  47. langroid/agent/tools/formatted_model_xml7.py +117 -0
  48. langroid/agent/tools/formatted_model_xml8.py +164 -0
  49. langroid/agent/tools/generic_tool.py +165 -0
  50. langroid/agent/tools/generic_tool_tatsu.py +275 -0
  51. langroid/agent/tools/grammar_based_model.py +132 -0
  52. langroid/agent/tools/grammar_based_model.py-e +128 -0
  53. langroid/agent/tools/grammar_based_model_lark.py +156 -0
  54. langroid/agent/tools/grammar_based_model_lark.py-e +153 -0
  55. langroid/agent/tools/grammar_based_model_parse.py +86 -0
  56. langroid/agent/tools/grammar_based_model_parse.py-e +80 -0
  57. langroid/agent/tools/grammar_based_model_parsimonious.py +129 -0
  58. langroid/agent/tools/grammar_based_model_parsimonious.py-e +120 -0
  59. langroid/agent/tools/grammar_based_model_pyparsing.py +105 -0
  60. langroid/agent/tools/grammar_based_model_pyparsing.py-e +103 -0
  61. langroid/agent/tools/grammar_based_model_regex.py +139 -0
  62. langroid/agent/tools/grammar_based_model_regex.py-e +130 -0
  63. langroid/agent/tools/grammar_based_model_regex2.py +124 -0
  64. langroid/agent/tools/grammar_based_model_regex2.py-e +116 -0
  65. langroid/agent/tools/grammar_based_model_tatsu.py +80 -0
  66. langroid/agent/tools/grammar_based_model_tatsu.py-e +77 -0
  67. langroid/agent/tools/lark_earley_example.py +135 -0
  68. langroid/agent/tools/lark_earley_example.py-e +117 -0
  69. langroid/agent/tools/lark_example.py +72 -0
  70. langroid/agent/tools/parse_example.py +76 -0
  71. langroid/agent/tools/parse_example2.py +87 -0
  72. langroid/agent/tools/parse_example3.py +42 -0
  73. langroid/agent/tools/parse_test.py +791 -0
  74. langroid/agent/xml_tool_message.py +106 -0
  75. langroid/language_models/openai_gpt.py +6 -1
  76. {langroid-0.16.5.dist-info → langroid-0.16.7.dist-info}/METADATA +1 -1
  77. {langroid-0.16.5.dist-info → langroid-0.16.7.dist-info}/RECORD +80 -6
  78. pyproject.toml +1 -1
  79. {langroid-0.16.5.dist-info → langroid-0.16.7.dist-info}/LICENSE +0 -0
  80. {langroid-0.16.5.dist-info → langroid-0.16.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,791 @@
1
+ # coding: utf-8
2
+ import sys
3
+ from datetime import date, datetime, time
4
+
5
+ import parse
6
+ import pytest
7
+
8
+
9
+ def test_no_match():
10
+ # string does not match format
11
+ assert parse.parse("{{hello}}", "hello") is None
12
+
13
+
14
+ def test_nothing():
15
+ # do no actual parsing
16
+ r = parse.parse("{{hello}}", "{hello}")
17
+ assert r.fixed == ()
18
+ assert r.named == {}
19
+
20
+
21
+ def test_no_evaluate_result():
22
+ # pull a fixed value out of string
23
+ match = parse.parse("hello {}", "hello world", evaluate_result=False)
24
+ r = match.evaluate_result()
25
+ assert r.fixed == ("world",)
26
+
27
+
28
+ def test_regular_expression():
29
+ # match an actual regular expression
30
+ s = r"^(hello\s[wW]{}!+.*)$"
31
+ e = s.replace("{}", "orld")
32
+ r = parse.parse(s, e)
33
+ assert r.fixed == ("orld",)
34
+ e = s.replace("{}", ".*?")
35
+ r = parse.parse(s, e)
36
+ assert r.fixed == (".*?",)
37
+
38
+
39
+ def test_question_mark():
40
+ # issue9: make sure a ? in the parse string is handled correctly
41
+ r = parse.parse('"{}"?', '"teststr"?')
42
+ assert r[0] == "teststr"
43
+
44
+
45
+ def test_pipe():
46
+ # issue22: make sure a | in the parse string is handled correctly
47
+ r = parse.parse("| {}", "| teststr")
48
+ assert r[0] == "teststr"
49
+
50
+
51
+ def test_unicode():
52
+ # issue29: make sure unicode is parsable
53
+ r = parse.parse("{}", "t€ststr")
54
+ assert r[0] == "t€ststr"
55
+
56
+
57
+ def test_hexadecimal():
58
+ # issue42: make sure bare hexadecimal isn't matched as "digits"
59
+ r = parse.parse("{:d}", "abcdef")
60
+ assert r is None
61
+
62
+
63
+ def test_fixed():
64
+ # pull a fixed value out of string
65
+ r = parse.parse("hello {}", "hello world")
66
+ assert r.fixed == ("world",)
67
+
68
+
69
+ def test_left():
70
+ # pull left-aligned text out of string
71
+ r = parse.parse("{:<} world", "hello world")
72
+ assert r.fixed == ("hello",)
73
+
74
+
75
+ def test_right():
76
+ # pull right-aligned text out of string
77
+ r = parse.parse("hello {:>}", "hello world ")
78
+ assert r.fixed == ("world",)
79
+
80
+
81
+ def test_center():
82
+ # pull center-aligned text out of string
83
+ r = parse.parse("hello {:^} world", "hello there world")
84
+ assert r.fixed == ("there",)
85
+
86
+
87
+ def test_center_named():
88
+ # pull center-aligned text out of string
89
+ r = parse.parse("hello {blah:^} world", "hello there world")
90
+ assert r.named == {"blah": "there"}
91
+
92
+
93
+ def test_typed():
94
+ # pull a named, typed values out of string
95
+ r = parse.parse("hello {:d} {:w}", "hello 12 people")
96
+ assert r.fixed == (12, "people")
97
+ r = parse.parse("hello {:w} {:w}", "hello 12 people")
98
+ assert r.fixed == ("12", "people")
99
+
100
+
101
+ def test_sign():
102
+ # sign is ignored
103
+ r = parse.parse("Pi = {:.7f}", "Pi = 3.1415926")
104
+ assert r.fixed == (3.1415926,)
105
+ r = parse.parse("Pi = {:+.7f}", "Pi = 3.1415926")
106
+ assert r.fixed == (3.1415926,)
107
+ r = parse.parse("Pi = {:-.7f}", "Pi = 3.1415926")
108
+ assert r.fixed == (3.1415926,)
109
+ r = parse.parse("Pi = {: .7f}", "Pi = 3.1415926")
110
+ assert r.fixed == (3.1415926,)
111
+
112
+
113
+ def test_precision():
114
+ # pull a float out of a string
115
+ r = parse.parse("Pi = {:.7f}", "Pi = 3.1415926")
116
+ assert r.fixed == (3.1415926,)
117
+ r = parse.parse("Pi/10 = {:8.5f}", "Pi/10 = 0.31415")
118
+ assert r.fixed == (0.31415,)
119
+ # float may have not leading zero
120
+ r = parse.parse("Pi/10 = {:8.5f}", "Pi/10 = .31415")
121
+ assert r.fixed == (0.31415,)
122
+ r = parse.parse("Pi/10 = {:8.5f}", "Pi/10 = -.31415")
123
+ assert r.fixed == (-0.31415,)
124
+
125
+
126
+ def test_custom_type():
127
+ # use a custom type
128
+ r = parse.parse(
129
+ "{:shouty} {:spam}",
130
+ "hello world",
131
+ {"shouty": lambda s: s.upper(), "spam": lambda s: "".join(reversed(s))},
132
+ )
133
+ assert r.fixed == ("HELLO", "dlrow")
134
+ r = parse.parse("{:d}", "12", {"d": lambda s: int(s) * 2})
135
+ assert r.fixed == (24,)
136
+ r = parse.parse("{:d}", "12")
137
+ assert r.fixed == (12,)
138
+
139
+
140
+ def test_typed_fail():
141
+ # pull a named, typed values out of string
142
+ assert parse.parse("hello {:d} {:w}", "hello people 12") is None
143
+
144
+
145
+ def test_named():
146
+ # pull a named value out of string
147
+ r = parse.parse("hello {name}", "hello world")
148
+ assert r.named == {"name": "world"}
149
+
150
+
151
+ def test_named_repeated():
152
+ # test a name may be repeated
153
+ r = parse.parse("{n} {n}", "x x")
154
+ assert r.named == {"n": "x"}
155
+
156
+
157
+ def test_named_repeated_type():
158
+ # test a name may be repeated with type conversion
159
+ r = parse.parse("{n:d} {n:d}", "1 1")
160
+ assert r.named == {"n": 1}
161
+
162
+
163
+ def test_named_repeated_fail_value():
164
+ # test repeated name fails if value mismatches
165
+ r = parse.parse("{n} {n}", "x y")
166
+ assert r is None
167
+
168
+
169
+ def test_named_repeated_type_fail_value():
170
+ # test repeated name with type conversion fails if value mismatches
171
+ r = parse.parse("{n:d} {n:d}", "1 2")
172
+ assert r is None
173
+
174
+
175
+ def test_named_repeated_type_mismatch():
176
+ # test repeated name with mismatched type
177
+ with pytest.raises(parse.RepeatedNameError):
178
+ parse.compile("{n:d} {n:w}")
179
+
180
+
181
+ def test_mixed():
182
+ # pull a fixed and named values out of string
183
+ r = parse.parse("hello {} {name} {} {spam}", "hello world and other beings")
184
+ assert r.fixed == ("world", "other")
185
+ assert r.named == {"name": "and", "spam": "beings"}
186
+
187
+
188
+ def test_named_typed():
189
+ # pull a named, typed values out of string
190
+ r = parse.parse("hello {number:d} {things}", "hello 12 people")
191
+ assert r.named == {"number": 12, "things": "people"}
192
+ r = parse.parse("hello {number:w} {things}", "hello 12 people")
193
+ assert r.named == {"number": "12", "things": "people"}
194
+
195
+
196
+ def test_named_aligned_typed():
197
+ # pull a named, typed values out of string
198
+ r = parse.parse("hello {number:<d} {things}", "hello 12 people")
199
+ assert r.named == {"number": 12, "things": "people"}
200
+ r = parse.parse("hello {number:>d} {things}", "hello 12 people")
201
+ assert r.named == {"number": 12, "things": "people"}
202
+ r = parse.parse("hello {number:^d} {things}", "hello 12 people")
203
+ assert r.named == {"number": 12, "things": "people"}
204
+
205
+
206
+ def test_multiline():
207
+ r = parse.parse("hello\n{}\nworld", "hello\nthere\nworld")
208
+ assert r.fixed[0] == "there"
209
+
210
+
211
+ def test_spans():
212
+ # test the string sections our fields come from
213
+ string = "hello world"
214
+ r = parse.parse("hello {}", string)
215
+ assert r.spans == {0: (6, 11)}
216
+ start, end = r.spans[0]
217
+ assert string[start:end] == r.fixed[0]
218
+
219
+ string = "hello world"
220
+ r = parse.parse("hello {:>}", string)
221
+ assert r.spans == {0: (10, 15)}
222
+ start, end = r.spans[0]
223
+ assert string[start:end] == r.fixed[0]
224
+
225
+ string = "hello 0x12 world"
226
+ r = parse.parse("hello {val:x} world", string)
227
+ assert r.spans == {"val": (6, 10)}
228
+ start, end = r.spans["val"]
229
+ assert string[start:end] == "0x%x" % r.named["val"]
230
+
231
+ string = "hello world and other beings"
232
+ r = parse.parse("hello {} {name} {} {spam}", string)
233
+ assert r.spans == {0: (6, 11), "name": (12, 15), 1: (16, 21), "spam": (22, 28)}
234
+
235
+
236
+ def test_numbers():
237
+ # pull a numbers out of a string
238
+ def y(fmt, s, e, str_equals=False):
239
+ p = parse.compile(fmt)
240
+ r = p.parse(s)
241
+ assert r is not None
242
+ r = r.fixed[0]
243
+ if str_equals:
244
+ assert str(r) == str(e)
245
+ else:
246
+ assert r == e
247
+
248
+ def n(fmt, s, e):
249
+ assert parse.parse(fmt, s) is None
250
+
251
+ y("a {:d} b", "a 0 b", 0)
252
+ y("a {:d} b", "a 12 b", 12)
253
+ y("a {:5d} b", "a 12 b", 12)
254
+ y("a {:5d} b", "a -12 b", -12)
255
+ y("a {:d} b", "a -12 b", -12)
256
+ y("a {:d} b", "a +12 b", 12)
257
+ y("a {:d} b", "a 12 b", 12)
258
+ y("a {:d} b", "a 0b1000 b", 8)
259
+ y("a {:d} b", "a 0o1000 b", 512)
260
+ y("a {:d} b", "a 0x1000 b", 4096)
261
+ y("a {:d} b", "a 0xabcdef b", 0xABCDEF)
262
+
263
+ y("a {:%} b", "a 100% b", 1)
264
+ y("a {:%} b", "a 50% b", 0.5)
265
+ y("a {:%} b", "a 50.1% b", 0.501)
266
+
267
+ y("a {:n} b", "a 100 b", 100)
268
+ y("a {:n} b", "a 1,000 b", 1000)
269
+ y("a {:n} b", "a 1.000 b", 1000)
270
+ y("a {:n} b", "a -1,000 b", -1000)
271
+ y("a {:n} b", "a 10,000 b", 10000)
272
+ y("a {:n} b", "a 100,000 b", 100000)
273
+ n("a {:n} b", "a 100,00 b", None)
274
+ y("a {:n} b", "a 100.000 b", 100000)
275
+ y("a {:n} b", "a 1.000.000 b", 1000000)
276
+
277
+ y("a {:f} b", "a 12.0 b", 12.0)
278
+ y("a {:f} b", "a -12.1 b", -12.1)
279
+ y("a {:f} b", "a +12.1 b", 12.1)
280
+ y("a {:f} b", "a .121 b", 0.121)
281
+ y("a {:f} b", "a -.121 b", -0.121)
282
+ n("a {:f} b", "a 12 b", None)
283
+
284
+ y("a {:e} b", "a 1.0e10 b", 1.0e10)
285
+ y("a {:e} b", "a .0e10 b", 0.0e10)
286
+ y("a {:e} b", "a 1.0E10 b", 1.0e10)
287
+ y("a {:e} b", "a 1.10000e10 b", 1.1e10)
288
+ y("a {:e} b", "a 1.0e-10 b", 1.0e-10)
289
+ y("a {:e} b", "a 1.0e+10 b", 1.0e10)
290
+ # can't actually test this one on values 'cos nan != nan
291
+ y("a {:e} b", "a nan b", float("nan"), str_equals=True)
292
+ y("a {:e} b", "a NAN b", float("nan"), str_equals=True)
293
+ y("a {:e} b", "a inf b", float("inf"))
294
+ y("a {:e} b", "a +inf b", float("inf"))
295
+ y("a {:e} b", "a -inf b", float("-inf"))
296
+ y("a {:e} b", "a INF b", float("inf"))
297
+ y("a {:e} b", "a +INF b", float("inf"))
298
+ y("a {:e} b", "a -INF b", float("-inf"))
299
+
300
+ y("a {:g} b", "a 1 b", 1)
301
+ y("a {:g} b", "a 1e10 b", 1e10)
302
+ y("a {:g} b", "a 1.0e10 b", 1.0e10)
303
+ y("a {:g} b", "a 1.0E10 b", 1.0e10)
304
+
305
+ y("a {:b} b", "a 1000 b", 8)
306
+ y("a {:b} b", "a 0b1000 b", 8)
307
+ y("a {:o} b", "a 12345670 b", int("12345670", 8))
308
+ y("a {:o} b", "a 0o12345670 b", int("12345670", 8))
309
+ y("a {:x} b", "a 1234567890abcdef b", 0x1234567890ABCDEF)
310
+ y("a {:x} b", "a 1234567890ABCDEF b", 0x1234567890ABCDEF)
311
+ y("a {:x} b", "a 0x1234567890abcdef b", 0x1234567890ABCDEF)
312
+ y("a {:x} b", "a 0x1234567890ABCDEF b", 0x1234567890ABCDEF)
313
+
314
+ y("a {:05d} b", "a 00001 b", 1)
315
+ y("a {:05d} b", "a -00001 b", -1)
316
+ y("a {:05d} b", "a +00001 b", 1)
317
+ y("a {:02d} b", "a 10 b", 10)
318
+
319
+ y("a {:=d} b", "a 000012 b", 12)
320
+ y("a {:x=5d} b", "a xxx12 b", 12)
321
+ y("a {:x=5d} b", "a -xxx12 b", -12)
322
+
323
+ # Test that hex numbers that ambiguously start with 0b / 0B are parsed correctly
324
+ # See issue #65 (https://github.com/r1chardj0n3s/parse/issues/65)
325
+ y("a {:x} b", "a 0B b", 0xB)
326
+ y("a {:x} b", "a 0B1 b", 0xB1)
327
+ y("a {:x} b", "a 0b b", 0xB)
328
+ y("a {:x} b", "a 0b1 b", 0xB1)
329
+
330
+ # Test that number signs are understood correctly
331
+ y("a {:d} b", "a -0o10 b", -8)
332
+ y("a {:d} b", "a -0b1010 b", -10)
333
+ y("a {:d} b", "a -0x1010 b", -0x1010)
334
+ y("a {:o} b", "a -10 b", -8)
335
+ y("a {:b} b", "a -1010 b", -10)
336
+ y("a {:x} b", "a -1010 b", -0x1010)
337
+ y("a {:d} b", "a +0o10 b", 8)
338
+ y("a {:d} b", "a +0b1010 b", 10)
339
+ y("a {:d} b", "a +0x1010 b", 0x1010)
340
+ y("a {:o} b", "a +10 b", 8)
341
+ y("a {:b} b", "a +1010 b", 10)
342
+ y("a {:x} b", "a +1010 b", 0x1010)
343
+
344
+
345
+ def test_two_datetimes():
346
+ r = parse.parse("a {:ti} {:ti} b", "a 1997-07-16 2012-08-01 b")
347
+ assert len(r.fixed) == 2
348
+ assert r[0] == datetime(1997, 7, 16)
349
+ assert r[1] == datetime(2012, 8, 1)
350
+
351
+
352
+ def test_flexible_datetimes():
353
+ r = parse.parse("a {:%Y-%m-%d} b", "a 1997-07-16 b")
354
+ assert len(r.fixed) == 1
355
+ assert r[0] == date(1997, 7, 16)
356
+
357
+ r = parse.parse("a {:%Y-%b-%d} b", "a 1997-Feb-16 b")
358
+ assert len(r.fixed) == 1
359
+ assert r[0] == date(1997, 2, 16)
360
+
361
+ r = parse.parse("a {:%Y-%b-%d} {:d} b", "a 1997-Feb-16 8 b")
362
+ assert len(r.fixed) == 2
363
+ assert r[0] == date(1997, 2, 16)
364
+
365
+ r = parse.parse("a {my_date:%Y-%b-%d} {num:d} b", "a 1997-Feb-16 8 b")
366
+ assert (r.named["my_date"]) == date(1997, 2, 16)
367
+ assert (r.named["num"]) == 8
368
+
369
+ r = parse.parse("a {:%Y-%B-%d} b", "a 1997-February-16 b")
370
+ assert r[0] == date(1997, 2, 16)
371
+
372
+ r = parse.parse("a {:%Y%m%d} b", "a 19970716 b")
373
+ assert r[0] == date(1997, 7, 16)
374
+
375
+
376
+ def test_flexible_datetime_with_colon():
377
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S}", "2023-11-21 13:23:27")
378
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27)
379
+
380
+
381
+ def test_datetime_with_various_subsecond_precision():
382
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S.%f}", "2023-11-21 13:23:27.123456")
383
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27, 123456)
384
+
385
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S.%f}", "2023-11-21 13:23:27.12345")
386
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27, 123450)
387
+
388
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S.%f}", "2023-11-21 13:23:27.1234")
389
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27, 123400)
390
+
391
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S.%f}", "2023-11-21 13:23:27.123")
392
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27, 123000)
393
+
394
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S.%f}", "2023-11-21 13:23:27.0")
395
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27, 0)
396
+
397
+
398
+ @pytest.mark.skipif(
399
+ sys.version_info[0] < 3, reason="Python 3+ required for timezone support"
400
+ )
401
+ def test_flexible_datetime_with_timezone():
402
+ from datetime import timezone
403
+
404
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S %z}", "2023-11-21 13:23:27 +0000")
405
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27, tzinfo=timezone.utc)
406
+
407
+
408
+ @pytest.mark.skipif(
409
+ sys.version_info[0] < 3, reason="Python 3+ required for timezone support"
410
+ )
411
+ def test_flexible_datetime_with_timezone_that_has_colons():
412
+ from datetime import timezone
413
+
414
+ r = parse.parse("{dt:%Y-%m-%d %H:%M:%S %z}", "2023-11-21 13:23:27 +00:00:00")
415
+ assert r.named["dt"] == datetime(2023, 11, 21, 13, 23, 27, tzinfo=timezone.utc)
416
+
417
+
418
+ def test_flexible_time():
419
+ r = parse.parse("a {time:%H:%M:%S} b", "a 13:23:27 b")
420
+ assert r.named["time"] == time(13, 23, 27)
421
+
422
+
423
+ def test_flexible_time_no_hour():
424
+ r = parse.parse("a {time:%M:%S} b", "a 23:27 b")
425
+ assert r.named["time"] == time(0, 23, 27)
426
+
427
+
428
+ def test_flexible_time_ms():
429
+ r = parse.parse("a {time:%M:%S:%f} b", "a 23:27:123456 b")
430
+ assert r.named["time"] == time(0, 23, 27, 123456)
431
+
432
+
433
+ def test_flexible_dates_single_digit():
434
+ r = parse.parse("{dt:%Y/%m/%d}", "2023/1/1")
435
+ assert r.named["dt"] == date(2023, 1, 1)
436
+
437
+
438
+ def test_flexible_dates_j():
439
+ r = parse.parse("{dt:%Y/%j}", "2023/9")
440
+ assert r.named["dt"] == date(2023, 1, 9)
441
+
442
+ r = parse.parse("{dt:%Y/%j}", "2023/009")
443
+ assert r.named["dt"] == date(2023, 1, 9)
444
+
445
+
446
+ def test_flexible_dates_year_current_year_inferred():
447
+ r = parse.parse("{dt:%j}", "9")
448
+ assert r.named["dt"] == date(datetime.today().year, 1, 9)
449
+
450
+
451
+ def test_datetimes():
452
+ def y(fmt, s, e, tz=None):
453
+ p = parse.compile(fmt)
454
+ r = p.parse(s)
455
+ assert r is not None
456
+ r = r.fixed[0]
457
+ assert r == e
458
+ assert tz is None or r.tzinfo == tz
459
+
460
+ utc = parse.FixedTzOffset(0, "UTC")
461
+ assert repr(utc) == "<FixedTzOffset UTC 0:00:00>"
462
+ aest = parse.FixedTzOffset(10 * 60, "+1000")
463
+ tz60 = parse.FixedTzOffset(60, "+01:00")
464
+
465
+ # ISO 8660 variants
466
+ # YYYY-MM-DD (eg 1997-07-16)
467
+ y("a {:ti} b", "a 1997-07-16 b", datetime(1997, 7, 16))
468
+
469
+ # YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
470
+ y("a {:ti} b", "a 1997-07-16 19:20 b", datetime(1997, 7, 16, 19, 20, 0))
471
+ y("a {:ti} b", "a 1997-07-16T19:20 b", datetime(1997, 7, 16, 19, 20, 0))
472
+ y(
473
+ "a {:ti} b",
474
+ "a 1997-07-16T19:20Z b",
475
+ datetime(1997, 7, 16, 19, 20, tzinfo=utc),
476
+ )
477
+ y(
478
+ "a {:ti} b",
479
+ "a 1997-07-16T19:20+0100 b",
480
+ datetime(1997, 7, 16, 19, 20, tzinfo=tz60),
481
+ )
482
+ y(
483
+ "a {:ti} b",
484
+ "a 1997-07-16T19:20+01:00 b",
485
+ datetime(1997, 7, 16, 19, 20, tzinfo=tz60),
486
+ )
487
+ y(
488
+ "a {:ti} b",
489
+ "a 1997-07-16T19:20 +01:00 b",
490
+ datetime(1997, 7, 16, 19, 20, tzinfo=tz60),
491
+ )
492
+
493
+ # YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
494
+ y("a {:ti} b", "a 1997-07-16 19:20:30 b", datetime(1997, 7, 16, 19, 20, 30))
495
+ y("a {:ti} b", "a 1997-07-16T19:20:30 b", datetime(1997, 7, 16, 19, 20, 30))
496
+ y(
497
+ "a {:ti} b",
498
+ "a 1997-07-16T19:20:30Z b",
499
+ datetime(1997, 7, 16, 19, 20, 30, tzinfo=utc),
500
+ )
501
+ y(
502
+ "a {:ti} b",
503
+ "a 1997-07-16T19:20:30+01:00 b",
504
+ datetime(1997, 7, 16, 19, 20, 30, tzinfo=tz60),
505
+ )
506
+ y(
507
+ "a {:ti} b",
508
+ "a 1997-07-16T19:20:30 +01:00 b",
509
+ datetime(1997, 7, 16, 19, 20, 30, tzinfo=tz60),
510
+ )
511
+
512
+ # YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
513
+ y(
514
+ "a {:ti} b",
515
+ "a 1997-07-16 19:20:30.500000 b",
516
+ datetime(1997, 7, 16, 19, 20, 30, 500000),
517
+ )
518
+ y(
519
+ "a {:ti} b",
520
+ "a 1997-07-16T19:20:30.500000 b",
521
+ datetime(1997, 7, 16, 19, 20, 30, 500000),
522
+ )
523
+ y(
524
+ "a {:ti} b",
525
+ "a 1997-07-16T19:20:30.5Z b",
526
+ datetime(1997, 7, 16, 19, 20, 30, 500000, tzinfo=utc),
527
+ )
528
+ y(
529
+ "a {:ti} b",
530
+ "a 1997-07-16T19:20:30.5+01:00 b",
531
+ datetime(1997, 7, 16, 19, 20, 30, 500000, tzinfo=tz60),
532
+ )
533
+
534
+ aest_d = datetime(2011, 11, 21, 10, 21, 36, tzinfo=aest)
535
+ dt = datetime(2011, 11, 21, 10, 21, 36)
536
+ dt00 = datetime(2011, 11, 21, 10, 21)
537
+ d = datetime(2011, 11, 21)
538
+
539
+ # te RFC2822 e-mail format datetime
540
+ y("a {:te} b", "a Mon, 21 Nov 2011 10:21:36 +1000 b", aest_d)
541
+ y("a {:te} b", "a Mon, 21 Nov 2011 10:21:36 +10:00 b", aest_d)
542
+ y("a {:te} b", "a 21 Nov 2011 10:21:36 +1000 b", aest_d)
543
+
544
+ # tg global (day/month) format datetime
545
+ y("a {:tg} b", "a 21/11/2011 10:21:36 AM +1000 b", aest_d)
546
+ y("a {:tg} b", "a 21/11/2011 10:21:36 AM +10:00 b", aest_d)
547
+ y("a {:tg} b", "a 21-11-2011 10:21:36 AM +1000 b", aest_d)
548
+ y("a {:tg} b", "a 21/11/2011 10:21:36 +1000 b", aest_d)
549
+ y("a {:tg} b", "a 21/11/2011 10:21:36 b", dt)
550
+ y("a {:tg} b", "a 21/11/2011 10:21 b", dt00)
551
+ y("a {:tg} b", "a 21-11-2011 b", d)
552
+ y("a {:tg} b", "a 21-Nov-2011 10:21:36 AM +1000 b", aest_d)
553
+ y("a {:tg} b", "a 21-November-2011 10:21:36 AM +1000 b", aest_d)
554
+
555
+ # ta US (month/day) format datetime
556
+ y("a {:ta} b", "a 11/21/2011 10:21:36 AM +1000 b", aest_d)
557
+ y("a {:ta} b", "a 11/21/2011 10:21:36 AM +10:00 b", aest_d)
558
+ y("a {:ta} b", "a 11-21-2011 10:21:36 AM +1000 b", aest_d)
559
+ y("a {:ta} b", "a 11/21/2011 10:21:36 +1000 b", aest_d)
560
+ y("a {:ta} b", "a 11/21/2011 10:21:36 b", dt)
561
+ y("a {:ta} b", "a 11/21/2011 10:21 b", dt00)
562
+ y("a {:ta} b", "a 11-21-2011 b", d)
563
+ y("a {:ta} b", "a Nov-21-2011 10:21:36 AM +1000 b", aest_d)
564
+ y("a {:ta} b", "a November-21-2011 10:21:36 AM +1000 b", aest_d)
565
+ y("a {:ta} b", "a November-21-2011 b", d)
566
+
567
+ # ts Linux System log format datetime
568
+ y(
569
+ "a {:ts} b",
570
+ "a Nov 21 10:21:36 b",
571
+ datetime(datetime.today().year, 11, 21, 10, 21, 36),
572
+ )
573
+ y(
574
+ "a {:ts} b",
575
+ "a Nov 1 10:21:36 b",
576
+ datetime(datetime.today().year, 11, 1, 10, 21, 36),
577
+ )
578
+ y(
579
+ "a {:ts} b",
580
+ "a Nov 1 03:21:36 b",
581
+ datetime(datetime.today().year, 11, 1, 3, 21, 36),
582
+ )
583
+
584
+ # th HTTP log format date/time datetime
585
+ y("a {:th} b", "a 21/Nov/2011:10:21:36 +1000 b", aest_d)
586
+ y("a {:th} b", "a 21/Nov/2011:10:21:36 +10:00 b", aest_d)
587
+
588
+ d = datetime(2011, 11, 21, 10, 21, 36)
589
+
590
+ # tc ctime() format datetime
591
+ y("a {:tc} b", "a Mon Nov 21 10:21:36 2011 b", d)
592
+
593
+ t530 = parse.FixedTzOffset(-5 * 60 - 30, "-5:30")
594
+ t830 = parse.FixedTzOffset(-8 * 60 - 30, "-8:30")
595
+
596
+ # tt Time time
597
+ y("a {:tt} b", "a 10:21:36 AM +1000 b", time(10, 21, 36, tzinfo=aest))
598
+ y("a {:tt} b", "a 10:21:36 AM +10:00 b", time(10, 21, 36, tzinfo=aest))
599
+ y("a {:tt} b", "a 10:21:36 AM b", time(10, 21, 36))
600
+ y("a {:tt} b", "a 10:21:36 PM b", time(22, 21, 36))
601
+ y("a {:tt} b", "a 10:21:36 b", time(10, 21, 36))
602
+ y("a {:tt} b", "a 10:21 b", time(10, 21))
603
+ y("a {:tt} b", "a 10:21:36 PM -5:30 b", time(22, 21, 36, tzinfo=t530))
604
+ y("a {:tt} b", "a 10:21:36 PM -530 b", time(22, 21, 36, tzinfo=t530))
605
+ y("a {:tt} b", "a 10:21:36 PM -05:30 b", time(22, 21, 36, tzinfo=t530))
606
+ y("a {:tt} b", "a 10:21:36 PM -0530 b", time(22, 21, 36, tzinfo=t530))
607
+ y("a {:tt} b", "a 10:21:36 PM -08:30 b", time(22, 21, 36, tzinfo=t830))
608
+ y("a {:tt} b", "a 10:21:36 PM -0830 b", time(22, 21, 36, tzinfo=t830))
609
+
610
+
611
+ def test_datetime_group_count():
612
+ # test we increment the group count correctly for datetimes
613
+ r = parse.parse("{:ti} {}", "1972-01-01 spam")
614
+ assert r.fixed[1] == "spam"
615
+ r = parse.parse("{:tg} {}", "1-1-1972 spam")
616
+ assert r.fixed[1] == "spam"
617
+ r = parse.parse("{:ta} {}", "1-1-1972 spam")
618
+ assert r.fixed[1] == "spam"
619
+ r = parse.parse("{:th} {}", "21/Nov/2011:10:21:36 +1000 spam")
620
+ assert r.fixed[1] == "spam"
621
+ r = parse.parse("{:te} {}", "21 Nov 2011 10:21:36 +1000 spam")
622
+ assert r.fixed[1] == "spam"
623
+ r = parse.parse("{:tc} {}", "Mon Nov 21 10:21:36 2011 spam")
624
+ assert r.fixed[1] == "spam"
625
+ r = parse.parse("{:tt} {}", "10:21 spam")
626
+ assert r.fixed[1] == "spam"
627
+
628
+
629
+ def test_mixed_types():
630
+ # stress-test: pull one of everything out of a string
631
+ r = parse.parse(
632
+ """
633
+ letters: {:w}
634
+ non-letters: {:W}
635
+ whitespace: "{:s}"
636
+ non-whitespace: \t{:S}\n
637
+ digits: {:d} {:d}
638
+ non-digits: {:D}
639
+ numbers with thousands: {:n}
640
+ fixed-point: {:f}
641
+ floating-point: {:e}
642
+ general numbers: {:g} {:g}
643
+ binary: {:b}
644
+ octal: {:o}
645
+ hex: {:x}
646
+ ISO 8601 e.g. {:ti}
647
+ RFC2822 e.g. {:te}
648
+ Global e.g. {:tg}
649
+ US e.g. {:ta}
650
+ ctime() e.g. {:tc}
651
+ HTTP e.g. {:th}
652
+ time: {:tt}
653
+ final value: {}
654
+ """,
655
+ """
656
+ letters: abcdef_GHIJLK
657
+ non-letters: !@#%$ *^%
658
+ whitespace: " \t\n"
659
+ non-whitespace: \tabc\n
660
+ digits: 12345 0b1011011
661
+ non-digits: abcdef
662
+ numbers with thousands: 1,000
663
+ fixed-point: 100.2345
664
+ floating-point: 1.1e-10
665
+ general numbers: 1 1.1
666
+ binary: 0b1000
667
+ octal: 0o1000
668
+ hex: 0x1000
669
+ ISO 8601 e.g. 1972-01-20T10:21:36Z
670
+ RFC2822 e.g. Mon, 20 Jan 1972 10:21:36 +1000
671
+ Global e.g. 20/1/1972 10:21:36 AM +1:00
672
+ US e.g. 1/20/1972 10:21:36 PM +10:30
673
+ ctime() e.g. Sun Sep 16 01:03:52 1973
674
+ HTTP e.g. 21/Nov/2011:00:07:11 +0000
675
+ time: 10:21:36 PM -5:30
676
+ final value: spam
677
+ """,
678
+ )
679
+ assert r is not None
680
+ assert r.fixed[22] == "spam"
681
+
682
+
683
+ def test_mixed_type_variant():
684
+ r = parse.parse(
685
+ """
686
+ letters: {:w}
687
+ non-letters: {:W}
688
+ whitespace: "{:s}"
689
+ non-whitespace: \t{:S}\n
690
+ digits: {:d}
691
+ non-digits: {:D}
692
+ numbers with thousands: {:n}
693
+ fixed-point: {:f}
694
+ floating-point: {:e}
695
+ general numbers: {:g} {:g}
696
+ binary: {:b}
697
+ octal: {:o}
698
+ hex: {:x}
699
+ ISO 8601 e.g. {:ti}
700
+ RFC2822 e.g. {:te}
701
+ Global e.g. {:tg}
702
+ US e.g. {:ta}
703
+ ctime() e.g. {:tc}
704
+ HTTP e.g. {:th}
705
+ time: {:tt}
706
+ final value: {}
707
+ """,
708
+ """
709
+ letters: abcdef_GHIJLK
710
+ non-letters: !@#%$ *^%
711
+ whitespace: " \t\n"
712
+ non-whitespace: \tabc\n
713
+ digits: 0xabcdef
714
+ non-digits: abcdef
715
+ numbers with thousands: 1.000.000
716
+ fixed-point: 0.00001
717
+ floating-point: NAN
718
+ general numbers: 1.1e10 nan
719
+ binary: 0B1000
720
+ octal: 0O1000
721
+ hex: 0X1000
722
+ ISO 8601 e.g. 1972-01-20T10:21:36Z
723
+ RFC2822 e.g. Mon, 20 Jan 1972 10:21:36 +1000
724
+ Global e.g. 20/1/1972 10:21:36 AM +1:00
725
+ US e.g. 1/20/1972 10:21:36 PM +10:30
726
+ ctime() e.g. Sun Sep 16 01:03:52 1973
727
+ HTTP e.g. 21/Nov/2011:00:07:11 +0000
728
+ time: 10:21:36 PM -5:30
729
+ final value: spam
730
+ """,
731
+ )
732
+ assert r is not None
733
+ assert r.fixed[21] == "spam"
734
+
735
+
736
+ @pytest.mark.skipif(
737
+ sys.version_info >= (3, 5),
738
+ reason="Python 3.5 removed the limit of 100 named groups in a regular expression",
739
+ )
740
+ def test_too_many_fields():
741
+ # Python 3.5 removed the limit of 100 named groups in a regular expression,
742
+ # so only test for the exception if the limit exists.
743
+ p = parse.compile("{:ti}" * 15)
744
+ with pytest.raises(parse.TooManyFields):
745
+ p.parse("")
746
+
747
+
748
+ def test_letters():
749
+ res = parse.parse("{:l}", "")
750
+ assert res is None
751
+ res = parse.parse("{:l}", "sPaM")
752
+ assert res.fixed == ("sPaM",)
753
+ res = parse.parse("{:l}", "sP4M")
754
+ assert res is None
755
+ res = parse.parse("{:l}", "sP_M")
756
+ assert res is None
757
+
758
+
759
+ def test_strftime_strptime_roundtrip():
760
+ dt = datetime.now()
761
+ fmt = "_".join([k for k in parse.dt_format_to_regex if k != "%z"])
762
+ s = dt.strftime(fmt)
763
+ [res] = parse.parse("{:" + fmt + "}", s)
764
+ assert res == dt
765
+
766
+
767
+ def test_parser_format():
768
+ parser = parse.compile("hello {}")
769
+ assert parser.format.format("world") == "hello world"
770
+ with pytest.raises(AttributeError):
771
+ parser.format = "hi {}"
772
+
773
+
774
+ def test_hyphen_inside_field_name():
775
+ # https://github.com/r1chardj0n3s/parse/issues/86
776
+ # https://github.com/python-openapi/openapi-core/issues/672
777
+ template = "/local/sub/{user-id}/duration"
778
+ assert parse.Parser(template).named_fields == ["user_id"]
779
+ string = "https://dummy_server.com/local/sub/1647222638/duration"
780
+ result = parse.search(template, string)
781
+ assert result["user-id"] == "1647222638"
782
+
783
+
784
+ def test_hyphen_inside_field_name_collision_handling():
785
+ template = "/foo/{user-id}/{user_id}/{user.id}/bar/"
786
+ assert parse.Parser(template).named_fields == ["user_id", "user__id", "user___id"]
787
+ string = "/foo/1/2/3/bar/"
788
+ result = parse.search(template, string)
789
+ assert result["user-id"] == "1"
790
+ assert result["user_id"] == "2"
791
+ assert result["user.id"] == "3"