|
1 | 1 | """PyUnit testing against strptime""" |
2 | 2 |
|
3 | 3 | import unittest |
| 4 | +from unittest import mock |
| 5 | +from contextlib import nullcontext |
4 | 6 | import datetime |
5 | 7 | import time |
6 | 8 | import locale |
|
9 | 11 | import platform |
10 | 12 | import sys |
11 | 13 | from test import support |
12 | | -from test.support import skip_if_buggy_ucrt_strfptime, run_with_locales |
| 14 | +from test.support import ( |
| 15 | + skip_if_buggy_ucrt_strfptime, run_with_locales, warnings_helper, |
| 16 | +) |
| 17 | +from test.support.hypothesis_helper import hypothesis |
13 | 18 | from datetime import date as datetime_date |
14 | 19 |
|
15 | 20 | import _strptime |
16 | 21 |
|
17 | 22 |
|
| 23 | +_strptime_formats = hypothesis.strategies.lists( |
| 24 | + hypothesis.strategies.sampled_from(( |
| 25 | + '%Y', '%y', '%m', '%d', '%H', '%M', '%S', '%f', '%b', '%B', |
| 26 | + '%a', '%A', '%w', '%u', '%I', '%p', '%k', '%l', '%P', |
| 27 | + '%z', '%:z', '%Z', '%F', '%D', '%T', '%R', '%e', '%n', '%t', |
| 28 | + '%j', '%U', '%W', '%G', '%V', '%%', |
| 29 | + '-', '/', ':', '.', ' ', '\t', '\n', '\x1c', 'T', 'Z', '年', |
| 30 | + )), max_size=8, |
| 31 | +).map(''.join) |
| 32 | + |
| 33 | + |
18 | 34 | class PublicStrptimeTests(unittest.TestCase): |
| 35 | + def assert_matches_python(self, text, fmt, *, accelerated=False): |
| 36 | + methods = ( |
| 37 | + (time.strptime, '_strptime_time', None), |
| 38 | + (datetime.date.strptime, '_strptime_datetime_date', datetime.date), |
| 39 | + (datetime.time.strptime, '_strptime_datetime_time', datetime.time), |
| 40 | + (datetime.datetime.strptime, '_strptime_datetime_datetime', |
| 41 | + datetime.datetime), |
| 42 | + ) |
| 43 | + for parse, fallback, cls in methods: |
| 44 | + message = f'{parse.__qualname__}({text!r}, {fmt!r})' |
| 45 | + reference = getattr(_strptime, fallback) |
| 46 | + args = (text, fmt) if cls is None else (cls, text, fmt) |
| 47 | + try: |
| 48 | + expected = reference(*args) |
| 49 | + except (ValueError, re.PatternError, OverflowError) as error: |
| 50 | + self.assertFalse(accelerated, message) |
| 51 | + with self.assertRaises(type(error), msg=message) as caught: |
| 52 | + parse(text, fmt) |
| 53 | + self.assertEqual(str(caught.exception), str(error), message) |
| 54 | + else: |
| 55 | + # Guard against accidentally falling back for every input. |
| 56 | + # This mock can be removed if implementation changes make |
| 57 | + # patching the fallback impractical; keep the comparisons. |
| 58 | + guard = mock.patch.object( |
| 59 | + _strptime, fallback, autospec=True, |
| 60 | + side_effect=AssertionError('used the Python parser'), |
| 61 | + ) if accelerated else nullcontext() |
| 62 | + with guard: |
| 63 | + actual = parse(text, fmt) |
| 64 | + self.assertEqual(actual, expected, message) |
| 65 | + if isinstance(actual, time.struct_time): |
| 66 | + self.assertEqual(actual.tm_zone, expected.tm_zone, message) |
| 67 | + self.assertEqual(actual.tm_gmtoff, expected.tm_gmtoff, |
| 68 | + message) |
| 69 | + elif isinstance(actual, (datetime.time, datetime.datetime)): |
| 70 | + self.assertEqual(actual.tzname(), expected.tzname(), message) |
| 71 | + |
| 72 | + @support.cpython_only |
| 73 | + @support.run_with_locale('LC_TIME', 'C') |
| 74 | + def test_accelerated_formats(self): |
| 75 | + for text, fmt in ( |
| 76 | + ('2024-02-29 12:34:56.123456', '%Y-%m-%d %H:%M:%S.%f'), |
| 77 | + ('Jan 02 2024', '%b %d %Y'), |
| 78 | + ('Tuesday 2024 January 02', '%A %Y %B %d'), |
| 79 | + ('Tue 2024-01-02', '%a %Y-%m-%d'), |
| 80 | + ('12 PM', '%I %p'), ('12', '%I'), (' 1 pm', '%l %P'), |
| 81 | + (' 1', '%k'), ('2024 2', '%Y %e'), |
| 82 | + ('+01:02:03.456789', '%:z'), ('-000001.5', '%z'), |
| 83 | + ('UTC', '%Z'), ('+0530 gMt', '%z %Z'), |
| 84 | + ('2024-02-29 12:34:56', '%F %T'), |
| 85 | + ('02/29/24 12:34', '%D %R'), |
| 86 | + ('2024\t02\n29', '%Y%t%m%n%d'), |
| 87 | + ('2023 366', '%Y %j'), |
| 88 | + ('2024 00 0', '%Y %W %w'), |
| 89 | + ('2024 01 1', '%Y %U %u'), |
| 90 | + ('2020-W53-7', '%G-W%V-%u'), |
| 91 | + ): |
| 92 | + self.assert_matches_python(text, fmt, accelerated=True) |
| 93 | + |
| 94 | + # Missing-year formats deliberately exercise the deprecated behavior. |
| 95 | + @support.run_with_locale('LC_TIME', 'C') |
| 96 | + @warnings_helper.ignore_warnings(category=DeprecationWarning) |
| 97 | + @hypothesis.given(value=hypothesis.strategies.datetimes(), |
| 98 | + fmt=_strptime_formats) |
| 99 | + @hypothesis.example(value=datetime.datetime(2024, 2, 29, 13, 24, 35), |
| 100 | + fmt='%Y%m%d%H%M%S') |
| 101 | + @hypothesis.example(value=datetime.datetime(2021, 1, 1), fmt='%G-W%V-%u') |
| 102 | + @hypothesis.example(value=datetime.datetime(2024, 2, 29, 13, 24, 35), |
| 103 | + fmt='%a %d %B %Y %I:%M:%S %p') |
| 104 | + def test_strftime_output_matches_python(self, value, fmt): |
| 105 | + try: |
| 106 | + text = value.strftime(fmt) |
| 107 | + except ValueError: |
| 108 | + # Some strptime directives are unsupported by the platform's |
| 109 | + # strftime. Arbitrary input tests still exercise their parsing. |
| 110 | + hypothesis.reject() |
| 111 | + self.assert_matches_python(text, fmt) |
| 112 | + |
| 113 | + @support.run_with_locale('LC_TIME', 'C') |
| 114 | + @warnings_helper.ignore_warnings(category=DeprecationWarning) |
| 115 | + @hypothesis.given(text=hypothesis.strategies.text(max_size=80), |
| 116 | + fmt=_strptime_formats) |
| 117 | + @hypothesis.example(text='2024\x1c 112', fmt='%Y\x1c%d%m') |
| 118 | + @hypothesis.example(text='34\x1c 123', fmt='%M%n%H%S') |
| 119 | + @hypothesis.example(text='2023 366', fmt='%Y %j') |
| 120 | + @hypothesis.example(text='2024 01 02 1', fmt='%Y %U %W %u') |
| 121 | + @hypothesis.example(text='+01:0203', fmt='%z') |
| 122 | + @hypothesis.example(text='1 pM', fmt='%I %p') |
| 123 | + def test_arbitrary_input_matches_python(self, text, fmt): |
| 124 | + self.assert_matches_python(text, fmt) |
| 125 | + |
19 | 126 | def test_inferred_weekday(self): |
20 | 127 | for text, fmt in ( |
21 | 128 | ('0001-01-01', '%Y-%m-%d'), |
@@ -134,34 +241,6 @@ def test_locale_names(self): |
134 | 241 | for fmt in ('%b %d %Y', '%B %d %Y', '%a %Y-%m-%d', '%A %Y-%m-%d'): |
135 | 242 | self.assert_matches_python(value.strftime(fmt), fmt) |
136 | 243 |
|
137 | | - def assert_matches_python(self, text, fmt): |
138 | | - methods = ( |
139 | | - (time.strptime, _strptime._strptime_time), |
140 | | - (datetime.date.strptime, |
141 | | - lambda s, f: _strptime._strptime_datetime_date(datetime.date, s, f)), |
142 | | - (datetime.time.strptime, |
143 | | - lambda s, f: _strptime._strptime_datetime_time(datetime.time, s, f)), |
144 | | - (datetime.datetime.strptime, |
145 | | - lambda s, f: _strptime._strptime_datetime_datetime( |
146 | | - datetime.datetime, s, f)), |
147 | | - ) |
148 | | - for parse, reference in methods: |
149 | | - with self.subTest(method=parse, text=text, format=fmt): |
150 | | - try: |
151 | | - expected = reference(text, fmt) |
152 | | - except (ValueError, re.PatternError) as error: |
153 | | - with self.assertRaises(type(error)) as caught: |
154 | | - parse(text, fmt) |
155 | | - self.assertEqual(str(caught.exception), str(error)) |
156 | | - else: |
157 | | - actual = parse(text, fmt) |
158 | | - self.assertEqual(actual, expected) |
159 | | - if isinstance(actual, time.struct_time): |
160 | | - self.assertEqual(actual.tm_zone, expected.tm_zone) |
161 | | - self.assertEqual(actual.tm_gmtoff, expected.tm_gmtoff) |
162 | | - elif isinstance(actual, (datetime.time, datetime.datetime)): |
163 | | - self.assertEqual(actual.tzname(), expected.tzname()) |
164 | | - |
165 | 244 | @support.run_with_locale('LC_TIME', 'C') |
166 | 245 | def test_names_and_weekdays(self): |
167 | 246 | for month in range(1, 13): |
|
0 commit comments