Skip to content

Commit

Permalink
Merge branch 'main' into pythongh-114847
Browse files Browse the repository at this point in the history
  • Loading branch information
barneygale committed Apr 3, 2024
2 parents 8b3b808 + 345194d commit 01127e3
Show file tree
Hide file tree
Showing 98 changed files with 1,551 additions and 614 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
15 changes: 10 additions & 5 deletions Doc/library/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1880,8 +1880,8 @@ Loading and running tests
Python identifiers) will be loaded.

All test modules must be importable from the top level of the project. If
the start directory is not the top level directory then the top level
directory must be specified separately.
the start directory is not the top level directory then *top_level_dir*
must be specified separately.

If importing a module fails, for example due to a syntax error, then
this will be recorded as a single error and discovery will continue. If
Expand All @@ -1901,9 +1901,11 @@ Loading and running tests
package.

The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. *top_level_dir* is stored so
``load_tests`` does not need to pass this argument in to
``loader.discover()``.
packages can continue discovery themselves.

*top_level_dir* is stored internally, and used as a default to any
nested calls to ``discover()``. That is, if a package's ``load_tests``
calls ``loader.discover()``, it does not need to pass this argument.

*start_dir* can be a dotted module name as well as a directory.

Expand All @@ -1930,6 +1932,9 @@ Loading and running tests
*start_dir* can not be a :term:`namespace packages <namespace package>`.
It has been broken since Python 3.7 and Python 3.11 officially remove it.

.. versionchanged:: 3.13
*top_level_dir* is only stored for the duration of *discover* call.


The following attributes of a :class:`TestLoader` can be configured either by
subclassing or assignment on an instance:
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
6 changes: 3 additions & 3 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1038,9 +1038,9 @@ def __repr__(self):
return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})'

@classmethod
def fromkeys(cls, iterable, *args):
'Create a ChainMap with a single dict created from the iterable.'
return cls(dict.fromkeys(iterable, *args))
def fromkeys(cls, iterable, value=None, /):
'Create a new ChainMap with keys from iterable and values set to value.'
return cls(dict.fromkeys(iterable, value))

def copy(self):
'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]'
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
8 changes: 4 additions & 4 deletions Lib/test/support/interpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def __str__(self):

def create():
"""Return a new (idle) Python interpreter."""
id = _interpreters.create(isolated=True)
id = _interpreters.create(reqrefs=True)
return Interpreter(id)


Expand Down Expand Up @@ -109,13 +109,13 @@ def __new__(cls, id, /):
assert hasattr(self, '_ownsref')
except KeyError:
# This may raise InterpreterNotFoundError:
_interpreters._incref(id)
_interpreters.incref(id)
try:
self = super().__new__(cls)
self._id = id
self._ownsref = True
except BaseException:
_interpreters._deccref(id)
_interpreters.decref(id)
raise
_known[id] = self
return self
Expand All @@ -142,7 +142,7 @@ def _decref(self):
return
self._ownsref = False
try:
_interpreters._decref(self.id)
_interpreters.decref(self.id)
except InterpreterNotFoundError:
pass

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test__xxsubinterpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def f():
def test_create_daemon_thread(self):
with self.subTest('isolated'):
expected = 'spam spam spam spam spam'
subinterp = interpreters.create(isolated=True)
subinterp = interpreters.create('isolated')
script, file = _captured_script(f"""
import threading
def f():
Expand All @@ -604,7 +604,7 @@ def f():
self.assertEqual(out, expected)

with self.subTest('not isolated'):
subinterp = interpreters.create(isolated=False)
subinterp = interpreters.create('legacy')
script, file = _captured_script("""
import threading
def f():
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
Loading

0 comments on commit 01127e3

Please sign in to comment.