Skip to content

Commit

Permalink
Merge branch 'main' into fromkeys_signature
Browse files Browse the repository at this point in the history
  • Loading branch information
rhettinger authored Apr 3, 2024
2 parents 9d54879 + ea94b3b commit 6cc49c4
Show file tree
Hide file tree
Showing 81 changed files with 964 additions and 293 deletions.
37 changes: 37 additions & 0 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,24 @@ Other constructors, all class methods:
time tuple. See also :ref:`strftime-strptime-behavior` and
:meth:`datetime.fromisoformat`.

.. versionchanged:: 3.13

If *format* specifies a day of month without a year a
:exc:`DeprecationWarning` is now emitted. This is to avoid a quadrennial
leap year bug in code seeking to parse only a month and day as the
default year used in absence of one in the format is not a leap year.
Such *format* values may raise an error as of Python 3.15. The
workaround is to always include a year in your *format*. If parsing
*date_string* values that do not have a year, explicitly add a year that
is a leap year before parsing:

.. doctest::

>>> from datetime import datetime
>>> date_string = "02/29"
>>> when = datetime.strptime(f"{date_string};1984", "%m/%d;%Y") # Avoids leap year bug.
>>> when.strftime("%B %d") # doctest: +SKIP
'February 29'


Class attributes:
Expand Down Expand Up @@ -2657,6 +2675,25 @@ Notes:
for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``,
``%W``, and ``%V``. Format ``%y`` does require a leading zero.

(10)
When parsing a month and day using :meth:`~.datetime.strptime`, always
include a year in the format. If the value you need to parse lacks a year,
append an explicit dummy leap year. Otherwise your code will raise an
exception when it encounters leap day because the default year used by the
parser is not a leap year. Users run into this bug every four years...

.. doctest::

>>> month_day = "02/29"
>>> datetime.strptime(f"{month_day};1984", "%m/%d;%Y") # No leap year bug.
datetime.datetime(1984, 2, 29, 0, 0)

.. deprecated-removed:: 3.13 3.15
:meth:`~.datetime.strptime` calls using a format string containing
a day of month without a year now emit a
:exc:`DeprecationWarning`. In 3.15 or later we may change this into
an error or change the default year to a leap year. See :gh:`70647`.

.. rubric:: Footnotes

.. [#] If, that is, we ignore the effects of Relativity
Expand Down
8 changes: 6 additions & 2 deletions Include/internal/pycore_bytes_methods.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ extern PyObject *_Py_bytes_rfind(const char *str, Py_ssize_t len, PyObject *args
extern PyObject *_Py_bytes_rindex(const char *str, Py_ssize_t len, PyObject *args);
extern PyObject *_Py_bytes_count(const char *str, Py_ssize_t len, PyObject *args);
extern int _Py_bytes_contains(const char *str, Py_ssize_t len, PyObject *arg);
extern PyObject *_Py_bytes_startswith(const char *str, Py_ssize_t len, PyObject *args);
extern PyObject *_Py_bytes_endswith(const char *str, Py_ssize_t len, PyObject *args);
extern PyObject *_Py_bytes_startswith(const char *str, Py_ssize_t len,
PyObject *subobj, Py_ssize_t start,
Py_ssize_t end);
extern PyObject *_Py_bytes_endswith(const char *str, Py_ssize_t len,
PyObject *subobj, Py_ssize_t start,
Py_ssize_t end);

/* The maketrans() static method. */
extern PyObject* _Py_bytes_maketrans(Py_buffer *frm, Py_buffer *to);
Expand Down
21 changes: 20 additions & 1 deletion Lib/_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
strptime -- Calculates the time struct represented by the passed-in string
"""
import os
import time
import locale
import calendar
Expand Down Expand Up @@ -250,12 +251,30 @@ def pattern(self, format):
format = regex_chars.sub(r"\\\1", format)
whitespace_replacement = re_compile(r'\s+')
format = whitespace_replacement.sub(r'\\s+', format)
year_in_format = False
day_of_month_in_format = False
while '%' in format:
directive_index = format.index('%')+1
format_char = format[directive_index]
processed_format = "%s%s%s" % (processed_format,
format[:directive_index-1],
self[format[directive_index]])
self[format_char])
format = format[directive_index+1:]
match format_char:
case 'Y' | 'y' | 'G':
year_in_format = True
case 'd':
day_of_month_in_format = True
if day_of_month_in_format and not year_in_format:
import warnings
warnings.warn("""\
Parsing dates involving a day of month without a year specified is ambiguious
and fails to parse leap day. The default behavior will change in Python 3.15
to either always raise an exception or to use a different default year (TBD).
To avoid trouble, add a specific year to the input & format.
See https://github.com/python/cpython/issues/70647.""",
DeprecationWarning,
skip_file_prefixes=(os.path.dirname(__file__),))
return "%s%s" % (processed_format, format)

def compile(self, format):
Expand Down
9 changes: 6 additions & 3 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,16 +857,19 @@ def commonpath(paths):
drivesplits = [splitroot(p.replace(altsep, sep).lower()) for p in paths]
split_paths = [p.split(sep) for d, r, p in drivesplits]

if len({r for d, r, p in drivesplits}) != 1:
raise ValueError("Can't mix absolute and relative paths")

# Check that all drive letters or UNC paths match. The check is made only
# now otherwise type errors for mixing strings and bytes would not be
# caught.
if len({d for d, r, p in drivesplits}) != 1:
raise ValueError("Paths don't have the same drive")

drive, root, path = splitroot(paths[0].replace(altsep, sep))
if len({r for d, r, p in drivesplits}) != 1:
if drive:
raise ValueError("Can't mix absolute and relative paths")
else:
raise ValueError("Can't mix rooted and not-rooted paths")

common = path.split(sep)
common = [c for c in common if c and c != curdir]

Expand Down
13 changes: 13 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2793,6 +2793,19 @@ def test_strptime_single_digit(self):
newdate = strptime(string, format)
self.assertEqual(newdate, target, msg=reason)

def test_strptime_leap_year(self):
# GH-70647: warns if parsing a format with a day and no year.
with self.assertRaises(ValueError):
# The existing behavior that GH-70647 seeks to change.
self.theclass.strptime('02-29', '%m-%d')
with self.assertWarnsRegex(DeprecationWarning,
r'.*day of month without a year.*'):
self.theclass.strptime('03-14.159265', '%m-%d.%f')
with self._assertNotWarns(DeprecationWarning):
self.theclass.strptime('20-03-14.159265', '%y-%m-%d.%f')
with self._assertNotWarns(DeprecationWarning):
self.theclass.strptime('02-29,2024', '%m-%d,%Y')

def test_more_timetuple(self):
# This tests fields beyond those tested by the TestDate.test_timetuple.
t = self.theclass(2004, 12, 31, 6, 22, 33)
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/string_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1513,9 +1513,9 @@ def test_find_etc_raise_correct_error_messages(self):
x, None, None, None)
self.assertRaisesRegex(TypeError, r'^count\(', s.count,
x, None, None, None)
self.assertRaisesRegex(TypeError, r'^startswith\(', s.startswith,
self.assertRaisesRegex(TypeError, r'^startswith\b', s.startswith,
x, None, None, None)
self.assertRaisesRegex(TypeError, r'^endswith\(', s.endswith,
self.assertRaisesRegex(TypeError, r'^endswith\b', s.endswith,
x, None, None, None)

# issue #15534
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,10 @@ def requires_limited_api(test):
return unittest.skip('needs _testcapi and _testlimitedcapi modules')(test)
return test


TEST_MODULES_ENABLED = sysconfig.get_config_var('TEST_MODULES') == 'yes'


def requires_specialization(test):
return unittest.skipUnless(
_opcode.ENABLE_SPECIALIZATION, "requires specialization")(test)
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def test_excepthook(self):
)

def test_unraisablehook(self):
import_helper.import_module("_testcapi")
returncode, events, stderr = self.run_python("test_unraisablehook")
if returncode:
self.fail(stderr)
Expand Down
81 changes: 44 additions & 37 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
from test.support import (cpython_only, is_wasi, requires_limited_api, Py_DEBUG,
set_recursion_limit, skip_on_s390x)
set_recursion_limit, skip_on_s390x, import_helper)
try:
import _testcapi
except ImportError:
Expand Down Expand Up @@ -244,6 +244,7 @@ def test_module_not_callable_suggestion(self):
self.assertRaisesRegex(TypeError, msg, mod)


@unittest.skipIf(_testcapi is None, "requires _testcapi")
class TestCallingConventions(unittest.TestCase):
"""Test calling using various C calling conventions (METH_*) from Python
Expand Down Expand Up @@ -441,6 +442,7 @@ def static_method():

NULL_OR_EMPTY = object()


class FastCallTests(unittest.TestCase):
"""Test calling using various callables from C
"""
Expand Down Expand Up @@ -484,49 +486,51 @@ class FastCallTests(unittest.TestCase):
]

# Add all the calling conventions and variants of C callables
_instance = _testcapi.MethInstance()
for obj, expected_self in (
(_testcapi, _testcapi), # module-level function
(_instance, _instance), # bound method
(_testcapi.MethClass, _testcapi.MethClass), # class method on class
(_testcapi.MethClass(), _testcapi.MethClass), # class method on inst.
(_testcapi.MethStatic, None), # static method
):
CALLS_POSARGS.extend([
(obj.meth_varargs, (1, 2), (expected_self, (1, 2))),
(obj.meth_varargs_keywords,
(1, 2), (expected_self, (1, 2), NULL_OR_EMPTY)),
(obj.meth_fastcall, (1, 2), (expected_self, (1, 2))),
(obj.meth_fastcall, (), (expected_self, ())),
(obj.meth_fastcall_keywords,
(1, 2), (expected_self, (1, 2), NULL_OR_EMPTY)),
(obj.meth_fastcall_keywords,
(), (expected_self, (), NULL_OR_EMPTY)),
(obj.meth_noargs, (), expected_self),
(obj.meth_o, (123, ), (expected_self, 123)),
])

CALLS_KWARGS.extend([
(obj.meth_varargs_keywords,
(1, 2), {'x': 'y'}, (expected_self, (1, 2), {'x': 'y'})),
(obj.meth_varargs_keywords,
(), {'x': 'y'}, (expected_self, (), {'x': 'y'})),
(obj.meth_varargs_keywords,
(1, 2), {}, (expected_self, (1, 2), NULL_OR_EMPTY)),
(obj.meth_fastcall_keywords,
(1, 2), {'x': 'y'}, (expected_self, (1, 2), {'x': 'y'})),
(obj.meth_fastcall_keywords,
(), {'x': 'y'}, (expected_self, (), {'x': 'y'})),
(obj.meth_fastcall_keywords,
(1, 2), {}, (expected_self, (1, 2), NULL_OR_EMPTY)),
])
if _testcapi:
_instance = _testcapi.MethInstance()
for obj, expected_self in (
(_testcapi, _testcapi), # module-level function
(_instance, _instance), # bound method
(_testcapi.MethClass, _testcapi.MethClass), # class method on class
(_testcapi.MethClass(), _testcapi.MethClass), # class method on inst.
(_testcapi.MethStatic, None), # static method
):
CALLS_POSARGS.extend([
(obj.meth_varargs, (1, 2), (expected_self, (1, 2))),
(obj.meth_varargs_keywords,
(1, 2), (expected_self, (1, 2), NULL_OR_EMPTY)),
(obj.meth_fastcall, (1, 2), (expected_self, (1, 2))),
(obj.meth_fastcall, (), (expected_self, ())),
(obj.meth_fastcall_keywords,
(1, 2), (expected_self, (1, 2), NULL_OR_EMPTY)),
(obj.meth_fastcall_keywords,
(), (expected_self, (), NULL_OR_EMPTY)),
(obj.meth_noargs, (), expected_self),
(obj.meth_o, (123, ), (expected_self, 123)),
])

CALLS_KWARGS.extend([
(obj.meth_varargs_keywords,
(1, 2), {'x': 'y'}, (expected_self, (1, 2), {'x': 'y'})),
(obj.meth_varargs_keywords,
(), {'x': 'y'}, (expected_self, (), {'x': 'y'})),
(obj.meth_varargs_keywords,
(1, 2), {}, (expected_self, (1, 2), NULL_OR_EMPTY)),
(obj.meth_fastcall_keywords,
(1, 2), {'x': 'y'}, (expected_self, (1, 2), {'x': 'y'})),
(obj.meth_fastcall_keywords,
(), {'x': 'y'}, (expected_self, (), {'x': 'y'})),
(obj.meth_fastcall_keywords,
(1, 2), {}, (expected_self, (1, 2), NULL_OR_EMPTY)),
])

def check_result(self, result, expected):
if isinstance(expected, tuple) and expected[-1] is NULL_OR_EMPTY:
if result[-1] in ({}, None):
expected = (*expected[:-1], result[-1])
self.assertEqual(result, expected)

@unittest.skipIf(_testcapi is None, "requires _testcapi")
def test_vectorcall_dict(self):
# Test PyObject_VectorcallDict()

Expand All @@ -546,6 +550,7 @@ def test_vectorcall_dict(self):
result = _testcapi.pyobject_fastcalldict(func, args, kwargs)
self.check_result(result, expected)

@unittest.skipIf(_testcapi is None, "requires _testcapi")
def test_vectorcall(self):
# Test PyObject_Vectorcall()

Expand Down Expand Up @@ -610,6 +615,7 @@ def testfunction_kw(self, *, kw):
ADAPTIVE_WARMUP_DELAY = 2


@unittest.skipIf(_testcapi is None, "requires _testcapi")
class TestPEP590(unittest.TestCase):

def test_method_descriptor_flag(self):
Expand Down Expand Up @@ -1022,6 +1028,7 @@ class TestRecursion(unittest.TestCase):

@skip_on_s390x
@unittest.skipIf(is_wasi and Py_DEBUG, "requires deep stack")
@unittest.skipIf(_testcapi is None, "requires _testcapi")
def test_super_deep(self):

def recurse(n):
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/test_capi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import os
import unittest
from test.support import load_package_tests
from test.support import TEST_MODULES_ENABLED


if not TEST_MODULES_ENABLED:
raise unittest.SkipTest("requires test modules")


def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)
7 changes: 3 additions & 4 deletions Lib/test/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,8 @@
check_impl_detail, requires_debug_ranges,
gc_collect)
from test.support.script_helper import assert_python_ok
from test.support import threading_helper
from test.support.bytecode_helper import (BytecodeTestCase,
instructions_with_positions)
from test.support import threading_helper, import_helper
from test.support.bytecode_helper import instructions_with_positions
from opcode import opmap, opname
COPY_FREE_VARS = opmap['COPY_FREE_VARS']

Expand Down Expand Up @@ -176,7 +175,7 @@ class CodeTest(unittest.TestCase):

@cpython_only
def test_newempty(self):
import _testcapi
_testcapi = import_helper.import_module("_testcapi")
co = _testcapi.code_newempty("filename", "funcname", 15)
self.assertEqual(co.co_filename, "filename")
self.assertEqual(co.co_name, "funcname")
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_coroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
from test.support import import_helper
from test.support import warnings_helper
from test.support.script_helper import assert_python_ok
try:
import _testcapi
except ImportError:
_testcapi = None


class AsyncYieldFrom:
Expand Down Expand Up @@ -2445,6 +2449,7 @@ def test_unawaited_warning_during_shutdown(self):


@support.cpython_only
@unittest.skipIf(_testcapi is None, "requires _testcapi")
class CAPITest(unittest.TestCase):

def test_tp_await_1(self):
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_ctypes/test_as_parameter.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import _ctypes_test
import ctypes
import unittest
from ctypes import (Structure, CDLL, CFUNCTYPE,
POINTER, pointer, byref,
c_short, c_int, c_long, c_longlong,
c_byte, c_wchar, c_float, c_double,
ArgumentError)
from test.support import import_helper
_ctypes_test = import_helper.import_module("_ctypes_test")


dll = CDLL(_ctypes_test.__file__)
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_ctypes/test_bitfields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import _ctypes_test
import os
import unittest
from ctypes import (CDLL, Structure, sizeof, POINTER, byref, alignment,
Expand All @@ -7,6 +6,8 @@
c_uint32, c_uint64,
c_short, c_ushort, c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong)
from test import support
from test.support import import_helper
_ctypes_test = import_helper.import_module("_ctypes_test")


class BITS(Structure):
Expand Down
Loading

0 comments on commit 6cc49c4

Please sign in to comment.