Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2629,7 +2629,10 @@ differences between platforms in handling of unsupported format specifiers.
``%G``, ``%u`` and ``%V`` were added.

.. versionadded:: 3.12
``%:z`` was added.
``%:z`` was added for :meth:`~.datetime.strftime`

.. versionadded:: next
``%:z`` was added for :meth:`~.datetime.strptime`

Technical Detail
^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -2724,12 +2727,18 @@ Notes:
When the ``%z`` directive is provided to the :meth:`~.datetime.strptime` method,
the UTC offsets can have a colon as a separator between hours, minutes
and seconds.
For example, ``'+01:00:00'`` will be parsed as an offset of one hour.
In addition, providing ``'Z'`` is identical to ``'+00:00'``.
For example, both ``'+010000'`` and ``'+01:00:00'`` will be parsed as an offset
of one hour. In addition, providing ``'Z'`` is identical to ``'+00:00'``.

``%:z``
Behaves exactly as ``%z``, but has a colon separator added between
hours, minutes and seconds.
When used with :meth:`~.datetime.strftime`, behaves exactly as ``%z``,
except that a colon separator is added between hours, minutes and seconds.

When used with :meth:`~.datetime.strptime`, the UTC offset is *required*
to have a colon as a separator between hours, minutes and seconds.
For example, ``'+01:00:00'`` (but *not* ``'+010000'``) will be parsed as
an offset of one hour. In addition, providing ``'Z'`` is identical to
``'+00:00'``.

``%Z``
In :meth:`~.datetime.strftime`, ``%Z`` is replaced by an empty string if
Expand Down
43 changes: 27 additions & 16 deletions Lib/_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,9 @@ def __init__(self, locale_time=None):
# W is set below by using 'U'
'y': r"(?P<y>\d\d)",
'Y': r"(?P<Y>\d\d\d\d)",
# See gh-121237: "z" must support colons for backwards compatibility.
'z': r"(?P<z>([+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?)|(?-i:Z))?",
':z': r"(?P<colon_z>([+-]\d\d:[0-5]\d(:[0-5]\d(\.\d{1,6})?)?)|(?-i:Z))?",
'A': self.__seqToRE(self.locale_time.f_weekday, 'A'),
'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
'B': self.__seqToRE(_fixmonths(self.locale_time.f_month[1:]), 'B'),
Expand Down Expand Up @@ -459,16 +461,16 @@ def pattern(self, format):
year_in_format = False
day_of_month_in_format = False
def repl(m):
format_char = m[1]
match format_char:
directive = m.group()[1:] # exclude `%` symbol
match directive:
case 'Y' | 'y' | 'G':
nonlocal year_in_format
year_in_format = True
case 'd':
nonlocal day_of_month_in_format
day_of_month_in_format = True
return self[format_char]
format = re_sub(r'%[-_0^#]*[0-9]*([OE]?\\?.?)', repl, format)
return self[directive]
format = re_sub(r'%[-_0^#]*[0-9]*([OE]?[:\\]?.?)', repl, format)
if day_of_month_in_format and not year_in_format:
import warnings
warnings.warn("""\
Expand Down Expand Up @@ -555,8 +557,17 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
raise ValueError("time data %r does not match format %r" %
(data_string, format))
if len(data_string) != found.end():
raise ValueError("unconverted data remains: %s" %
data_string[found.end():])
rest = data_string[found.end():]
# Specific check for '%:z' directive
if (
"colon_z" in found.re.groupindex
and found.group("colon_z") is not None
and rest[0] != ":"
):
raise ValueError(
f"Missing colon in %:z before '{rest}', got '{data_string}'"
)
raise ValueError("unconverted data remains: %s" % rest)

iso_year = year = None
month = day = 1
Expand Down Expand Up @@ -616,18 +627,18 @@ def parse_int(s):
hour = parse_int(found_dict['I'])
ampm = found_dict.get('p', '').lower()
# If there was no AM/PM indicator, we'll treat this like AM
if ampm in ('', locale_time.am_pm[0]):
# We're in AM so the hour is correct unless we're
# looking at 12 midnight.
# 12 midnight == 12 AM == hour 0
if hour == 12:
hour = 0
elif ampm == locale_time.am_pm[1]:
if ampm == locale_time.am_pm[1]:
# We're in PM so we need to add 12 to the hour unless
# we're looking at 12 noon.
# 12 noon == 12 PM == hour 12
if hour != 12:
hour += 12
else:
# We're in AM so the hour is correct unless we're
# looking at 12 midnight.
# 12 midnight == 12 AM == hour 0
if hour == 12:
hour = 0
elif group_key == 'M':
minute = parse_int(found_dict['M'])
elif group_key == 'S':
Expand Down Expand Up @@ -662,8 +673,8 @@ def parse_int(s):
week_of_year_start = 0
elif group_key == 'V':
iso_week = int(found_dict['V'])
elif group_key == 'z':
z = found_dict['z']
elif group_key in ('z', 'colon_z'):
z = found_dict[group_key]
if z:
if z == 'Z':
gmtoff = 0
Expand All @@ -672,7 +683,7 @@ def parse_int(s):
z = z[:3] + z[4:]
if len(z) > 5:
if z[5] != ':':
msg = f"Inconsistent use of : in {found_dict['z']}"
msg = f"Inconsistent use of : in {found_dict[group_key]}"
raise ValueError(msg)
z = z[:5] + z[6:]
hours = int(z[1:3])
Expand Down
28 changes: 26 additions & 2 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2907,6 +2907,12 @@ def test_strptime(self):
strptime("-00:02:01.000003", "%z").utcoffset(),
-timedelta(minutes=2, seconds=1, microseconds=3)
)
self.assertEqual(strptime("+01:07", "%:z").utcoffset(),
1 * HOUR + 7 * MINUTE)
self.assertEqual(strptime("-10:02", "%:z").utcoffset(),
-(10 * HOUR + 2 * MINUTE))
self.assertEqual(strptime("-00:00:01.00001", "%:z").utcoffset(),
-timedelta(seconds=1, microseconds=10))
# Only local timezone and UTC are supported
for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'),
(-_time.timezone, _time.tzname[0])):
Expand Down Expand Up @@ -2936,6 +2942,16 @@ def test_strptime(self):
with self.assertRaises(ValueError): strptime("-000", "%z")
with self.assertRaises(ValueError): strptime("z", "%z")

def test_strptime_ampm(self):
dt = datetime(1999, 3, 17, 0, 44, 55, 2)
for hour in range(0, 24):
with self.subTest(hour=hour):
new_dt = dt.replace(hour=hour)
dt_str = new_dt.strftime("%I %p")

self.assertEqual(self.theclass.strptime(dt_str, "%I %p").hour,
hour)

def test_strptime_single_digit(self):
# bpo-34903: Check that single digit dates and times are allowed.

Expand Down Expand Up @@ -2985,7 +3001,7 @@ def test_strptime_leap_year(self):
self.theclass.strptime('02-29,2024', '%m-%d,%Y')

def test_strptime_z_empty(self):
for directive in ('z',):
for directive in ('z', ':z'):
string = '2025-04-25 11:42:47'
format = f'%Y-%m-%d %H:%M:%S%{directive}'
target = self.theclass(2025, 4, 25, 11, 42, 47)
Expand Down Expand Up @@ -4053,6 +4069,12 @@ def test_strptime_tz(self):
strptime("-00:02:01.000003", "%z").utcoffset(),
-timedelta(minutes=2, seconds=1, microseconds=3)
)
self.assertEqual(strptime("+01:07", "%:z").utcoffset(),
1 * HOUR + 7 * MINUTE)
self.assertEqual(strptime("-10:02", "%:z").utcoffset(),
-(10 * HOUR + 2 * MINUTE))
self.assertEqual(strptime("-00:00:01.00001", "%:z").utcoffset(),
-timedelta(seconds=1, microseconds=10))
# Only local timezone and UTC are supported
for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'),
(-_time.timezone, _time.tzname[0])):
Expand Down Expand Up @@ -4082,9 +4104,11 @@ def test_strptime_tz(self):
self.assertEqual(strptime("UTC", "%Z").tzinfo, None)

def test_strptime_errors(self):
for tzstr in ("-2400", "-000", "z"):
for tzstr in ("-2400", "-000", "z", "24:00"):
with self.assertRaises(ValueError):
self.theclass.strptime(tzstr, "%z")
with self.assertRaises(ValueError):
self.theclass.strptime(tzstr, "%:z")

def test_strptime_single_digit(self):
# bpo-34903: Check that single digit times are allowed.
Expand Down
65 changes: 39 additions & 26 deletions Lib/test/test_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,37 +406,50 @@ def test_offset(self):
(*_, offset), _, offset_fraction = _strptime._strptime("-013030.000001", "%z")
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
self.assertEqual(offset_fraction, -1)
(*_, offset), _, offset_fraction = _strptime._strptime("+01:00", "%z")
self.assertEqual(offset, one_hour)
self.assertEqual(offset_fraction, 0)
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30", "%z")
self.assertEqual(offset, -(one_hour + half_hour))
self.assertEqual(offset_fraction, 0)
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", "%z")
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
self.assertEqual(offset_fraction, 0)
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", "%z")
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
self.assertEqual(offset_fraction, -1)
(*_, offset), _, offset_fraction = _strptime._strptime("+01:30:30.001", "%z")
self.assertEqual(offset, one_hour + half_hour + half_minute)
self.assertEqual(offset_fraction, 1000)
(*_, offset), _, offset_fraction = _strptime._strptime("Z", "%z")
self.assertEqual(offset, 0)
self.assertEqual(offset_fraction, 0)

cases = [
("+01:00", one_hour, 0),
("-01:30", -(one_hour + half_hour), 0),
("-01:30:30", -(one_hour + half_hour + half_minute), 0),
("-01:30:30.000001", -(one_hour + half_hour + half_minute), -1),
("+01:30:30.001", +(one_hour + half_hour + half_minute), 1000),
("Z", 0, 0),
]
for directive in ("%z", "%:z"):
for offset_str, expected_offset, expected_fraction in cases:
with self.subTest(offset_str=offset_str, directive=directive):
(*_, offset), _, offset_fraction = _strptime._strptime(
offset_str, directive
)
self.assertEqual(offset, expected_offset)
self.assertEqual(offset_fraction, expected_fraction)

def test_bad_offset(self):
with self.assertRaises(ValueError):
_strptime._strptime("-01:30:30.", "%z")
with self.assertRaises(ValueError):
_strptime._strptime("-0130:30", "%z")
with self.assertRaises(ValueError):
_strptime._strptime("-01:30:30.1234567", "%z")
with self.assertRaises(ValueError):
_strptime._strptime("-01:30:30:123456", "%z")
error_cases_any_z = [
"-01:30:30.", # Decimal point not followed with digits
"-01:30:30.1234567", # Too many digits after decimal point
"-01:30:30:123456", # Colon as decimal separator
"-0130:30", # Incorrect use of colons
]
for directive in ("%z", "%:z"):
for timestr in error_cases_any_z:
with self.subTest(timestr=timestr, directive=directive):
with self.assertRaises(ValueError):
_strptime._strptime(timestr, directive)

required_colons_cases = ["-013030", "+0130", "-01:3030.123456"]
for timestr in required_colons_cases:
with self.subTest(timestr=timestr):
with self.assertRaises(ValueError):
_strptime._strptime(timestr, "%:z")

with self.assertRaises(ValueError) as err:
_strptime._strptime("-01:3030", "%z")
self.assertEqual("Inconsistent use of : in -01:3030", str(err.exception))
with self.assertRaises(ValueError) as err:
_strptime._strptime("-01:3030", "%:z")
self.assertEqual("Missing colon in %:z before '30', got '-01:3030'",
str(err.exception))

@skip_if_buggy_ucrt_strfptime
def test_timezone(self):
Expand Down
24 changes: 20 additions & 4 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -3126,11 +3126,27 @@ JIT_DEPS = \
$(srcdir)/Tools/jit/*.py \
$(srcdir)/Python/executor_cases.c.h \
pyconfig.h

jit_stencils.h: $(JIT_DEPS)

ifneq ($(filter aarch64-apple-darwin%,$(HOST_GNU_TYPE)),)
JIT_STENCIL_HEADER := jit_stencils-aarch64-apple-darwin.h
else ifneq ($(filter x86_64-apple-darwin%,$(HOST_GNU_TYPE)),)
JIT_STENCIL_HEADER := jit_stencils-x86_64-apple-darwin.h
else ifeq ($(HOST_GNU_TYPE), aarch64-pc-windows-msvc)
JIT_STENCIL_HEADER := jit_stencils-aarch64-pc-windows-msvc.h
else ifeq ($(HOST_GNU_TYPE), i686-pc-windows-msvc)
JIT_STENCIL_HEADER := jit_stencils-i686-pc-windows-msvc.h
else ifeq ($(HOST_GNU_TYPE), x86_64-pc-windows-msvc)
JIT_STENCIL_HEADER := jit_stencils-x86_64-pc-windows-msvc.h
else ifneq ($(filter aarch64-%-linux-gnu,$(HOST_GNU_TYPE)),)
JIT_STENCIL_HEADER := jit_stencils-$(HOST_GNU_TYPE).h
else ifneq ($(filter x86_64-%-linux-gnu,$(HOST_GNU_TYPE)),)
JIT_STENCIL_HEADER := jit_stencils-$(HOST_GNU_TYPE).h
endif

jit_stencils.h $(JIT_STENCIL_HEADER): $(JIT_DEPS)
@REGEN_JIT_COMMAND@

Python/jit.o: $(srcdir)/Python/jit.c @JIT_STENCILS_H@
Python/jit.o: $(srcdir)/Python/jit.c jit_stencils.h $(JIT_STENCIL_HEADER)
$(CC) -c $(PY_CORE_CFLAGS) -o $@ $<

.PHONY: regen-jit
Expand Down Expand Up @@ -3228,7 +3244,7 @@ clean-retain-profile: pycremoval
-rm -rf Python/deepfreeze
-rm -f Python/frozen_modules/*.h
-rm -f Python/frozen_modules/MANIFEST
-rm -f jit_stencils.h
-rm -f jit_stencils*.h
-find build -type f -a ! -name '*.gc??' -exec rm -f {} ';'
-rm -f Include/pydtrace_probes.h
-rm -f profile-gen-stamp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Support ``%:z`` directive for :meth:`datetime.datetime.strptime`,
:meth:`datetime.time.strptime` and :func:`time.strptime`.
Patch by Lucas Esposito and Semyon Moroz.
1 change: 1 addition & 0 deletions Modules/socketmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4191,6 +4191,7 @@ sock_recvfrom(PyObject *self, PyObject *args)
}

ret = PyTuple_Pack(2, buf, addr);
Py_DECREF(buf);

finally:
Py_XDECREF(addr);
Expand Down
17 changes: 12 additions & 5 deletions Tools/jit/_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,38 +551,45 @@ def get_target(host: str) -> _COFF32 | _COFF64 | _ELF | _MachO:
optimizer: type[_optimizers.Optimizer]
target: _COFF32 | _COFF64 | _ELF | _MachO
if re.fullmatch(r"aarch64-apple-darwin.*", host):
host = "aarch64-apple-darwin"
condition = "defined(__aarch64__) && defined(__APPLE__)"
optimizer = _optimizers.OptimizerAArch64
target = _MachO(host, condition, optimizer=optimizer)
elif re.fullmatch(r"aarch64-pc-windows-msvc", host):
args = ["-fms-runtime-lib=dll", "-fplt"]
host = "aarch64-pc-windows-msvc"
condition = "defined(_M_ARM64)"
args = ["-fms-runtime-lib=dll", "-fplt"]
optimizer = _optimizers.OptimizerAArch64
target = _COFF64(host, condition, args=args, optimizer=optimizer)
elif re.fullmatch(r"aarch64-.*-linux-gnu", host):
host = "aarch64-unknown-linux-gnu"
condition = "defined(__aarch64__) && defined(__linux__)"
# -mno-outline-atomics: Keep intrinsics from being emitted.
args = ["-fpic", "-mno-outline-atomics"]
condition = "defined(__aarch64__) && defined(__linux__)"
optimizer = _optimizers.OptimizerAArch64
target = _ELF(host, condition, args=args, optimizer=optimizer)
elif re.fullmatch(r"i686-pc-windows-msvc", host):
host = "i686-pc-windows-msvc"
condition = "defined(_M_IX86)"
# -Wno-ignored-attributes: __attribute__((preserve_none)) is not supported here.
args = ["-DPy_NO_ENABLE_SHARED", "-Wno-ignored-attributes"]
optimizer = _optimizers.OptimizerX86
condition = "defined(_M_IX86)"
target = _COFF32(host, condition, args=args, optimizer=optimizer)
elif re.fullmatch(r"x86_64-apple-darwin.*", host):
host = "x86_64-apple-darwin"
condition = "defined(__x86_64__) && defined(__APPLE__)"
optimizer = _optimizers.OptimizerX86
target = _MachO(host, condition, optimizer=optimizer)
elif re.fullmatch(r"x86_64-pc-windows-msvc", host):
args = ["-fms-runtime-lib=dll"]
host = "x86_64-pc-windows-msvc"
condition = "defined(_M_X64)"
args = ["-fms-runtime-lib=dll"]
optimizer = _optimizers.OptimizerX86
target = _COFF64(host, condition, args=args, optimizer=optimizer)
elif re.fullmatch(r"x86_64-.*-linux-gnu", host):
args = ["-fno-pic", "-mcmodel=medium", "-mlarge-data-threshold=0"]
host = "x86_64-unknown-linux-gnu"
condition = "defined(__x86_64__) && defined(__linux__)"
args = ["-fno-pic", "-mcmodel=medium", "-mlarge-data-threshold=0"]
optimizer = _optimizers.OptimizerX86
target = _ELF(host, condition, args=args, optimizer=optimizer)
else:
Expand Down
Loading
Loading