Skip to content
Merged
2 changes: 1 addition & 1 deletion Doc/c-api/complex.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ rather than dereferencing them through pointers.

Please note, that these functions are :term:`soft deprecated` since Python
3.15. Avoid using this API in a new code to do complex arithmetic: either use
the `Number Protocol <number>`_ API or use native complex types, like
the :ref:`Number Protocol <number>` API or use native complex types, like
:c:expr:`double complex`.


Expand Down
2 changes: 1 addition & 1 deletion Doc/extending/first-extension-module.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Then, create ``meson.build`` containing the following:

.. note::

See `meson-python documentation <meson-python>`_ for details on
See the `meson-python documentation <meson-python_>`_ for details on
configuration.

Now, build install the *project in the current directory* (``.``) via ``pip``:
Expand Down
20 changes: 19 additions & 1 deletion Doc/library/shlex.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ The :mod:`!shlex` module defines the following functions:
.. versionadded:: 3.8


.. function:: quote(s)
.. function:: quote(s, *, force=False)

Return a shell-escaped version of the string *s*. The returned value is a
string that can safely be used as one token in a shell command line, for
cases where you cannot use a list.

If *force* is :const:`True`, then *s* is unconditionally quoted,
even if it is already safe for a shell without being quoted.

.. _shlex-quote-warning:

.. warning::
Expand Down Expand Up @@ -91,8 +94,23 @@ The :mod:`!shlex` module defines the following functions:
>>> command
['ls', '-l', 'somefile; rm -rf ~']

The *force* keyword can be used to produce consistent behavior when
escaping multiple strings:

>>> from shlex import quote
>>> filenames = ['my first file', 'file2', 'file 3']
>>> filenames_some_escaped = [quote(f) for f in filenames]
>>> filenames_some_escaped
["'my first file'", 'file2', "'file 3'"]
>>> filenames_all_escaped = [quote(f, force=True) for f in filenames]
>>> filenames_all_escaped
["'my first file'", "'file2'", "'file 3'"]

.. versionadded:: 3.3

.. versionchanged:: next
The *force* keyword was added.

The :mod:`!shlex` module defines the following class:


Expand Down
14 changes: 10 additions & 4 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2616,7 +2616,9 @@ expression support in the :mod:`re` module).
:func:`re.split`). Splitting an empty string with a specified separator
returns ``['']``.

For example::
For example:

.. doctest::

>>> '1,2,3'.split(',')
['1', '2', '3']
Expand All @@ -2634,7 +2636,9 @@ expression support in the :mod:`re` module).
string or a string consisting of just whitespace with a ``None`` separator
returns ``[]``.

For example::
For example:

.. doctest::

>>> '1 2 3'.split()
['1', '2', '3']
Expand All @@ -2646,7 +2650,9 @@ expression support in the :mod:`re` module).
If *sep* is not specified or is ``None`` and *maxsplit* is ``0``, only
leading runs of consecutive whitespace are considered.

For example::
For example:

.. doctest::

>>> "".split(None, 0)
[]
Expand All @@ -2655,7 +2661,7 @@ expression support in the :mod:`re` module).
>>> " foo ".split(maxsplit=0)
['foo ']

See also :meth:`join`.
See also :meth:`join` and :meth:`rsplit`.


.. index::
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ os
process via a pidfd. Available on Linux 5.6+.
(Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.)

shlex
-----

* Add keyword-only parameter *force* to :func:`shlex.quote` to force quoting
a string, even if it is already safe for a shell without being quoted.
(Contributed by Jay Berry in :gh:`148846`.)

xml
---

Expand Down
11 changes: 10 additions & 1 deletion Lib/logging/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,16 @@ def __init__(self, filename, when='h', interval=1, backupCount=0,
# path object (see Issue #27493), but self.baseFilename will be a string
filename = self.baseFilename
if os.path.exists(filename):
t = int(os.stat(filename).st_mtime)
# Use the minimum of file creation and modification time as
# the base of the rollover calculation
stat_result = os.stat(filename)
# Use st_birthtime whenever it is available or use st_ctime
# instead otherwise
try:
creation_time = stat_result.st_birthtime
except AttributeError:
creation_time = stat_result.st_ctime
t = int(min(creation_time, stat_result.st_mtime))
else:
t = int(time.time())
self.rolloverAt = self.computeRollover(t)
Expand Down
14 changes: 10 additions & 4 deletions Lib/shlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,12 @@ def join(split_command):
return ' '.join(quote(arg) for arg in split_command)


def quote(s):
"""Return a shell-escaped version of the string *s*."""
def quote(s, *, force=False):
"""Return a shell-escaped version of the string *s*.

If *force* is *True*, then *s* is unconditionally quoted,
even if it is already safe for a shell without being quoted.
"""
if not s:
return "''"

Expand All @@ -329,8 +333,10 @@ def quote(s):
safe_chars = (b'%+,-./0123456789:=@'
b'ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
b'abcdefghijklmnopqrstuvwxyz')
# No quoting is needed if `s` is an ASCII string consisting only of `safe_chars`
if s.isascii() and not s.encode().translate(None, delete=safe_chars):
# No quoting is needed if we are not forcing quoting
# and `s` is an ASCII string consisting only of `safe_chars`.
if (not force
and s.isascii() and not s.encode().translate(None, delete=safe_chars)):
return s

# use single quotes, and put single quotes into double quotes
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"has_fork_support", "requires_fork",
"has_subprocess_support", "requires_subprocess",
"has_socket_support", "requires_working_socket",
"has_st_birthtime",
"has_remote_subprocess_debugging", "requires_remote_subprocess_debugging",
"anticipate_failure", "load_package_tests", "detect_api_mismatch",
"check__all__", "skip_if_buggy_ucrt_strfptime",
Expand Down Expand Up @@ -620,6 +621,10 @@ def skip_wasi_stack_overflow():
or is_android
)

# At the moment, st_birthtime attribute is only supported on Windows,
# MacOS and FreeBSD.
has_st_birthtime = sys.platform.startswith(("win", "freebsd", "darwin"))

def requires_fork():
return unittest.skipUnless(has_fork_support, "requires working os.fork()")

Expand Down
26 changes: 25 additions & 1 deletion Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ def test_invalid_identifier(self):
ast.fix_missing_locations(m)
with self.assertRaises(TypeError) as cm:
compile(m, "<test>", "exec")
self.assertIn("identifier must be of type str", str(cm.exception))
self.assertIn("expecting a string object", str(cm.exception))

def test_invalid_constant(self):
for invalid_constant in int, (1, 2, int), frozenset((1, 2, int)):
Expand Down Expand Up @@ -1081,6 +1081,30 @@ def test_none_checks(self) -> None:
for node, attr, source in tests:
self.assert_none_check(node, attr, source)

def test_required_field_messages(self):
binop = ast.BinOp(
left=ast.Constant(value=2),
right=ast.Constant(value=2),
op=ast.Add(),
)
expr_without_position = ast.Expression(body=binop)
expr_with_wrong_body = ast.Expression(body=[binop])

with self.assertRaisesRegex(TypeError, "required field") as cm:
compile(expr_without_position, "<test>", "eval")
with self.assertRaisesRegex(
TypeError,
"field 'body' was expecting node of type 'expr', got 'list'",
):
compile(expr_with_wrong_body, "<test>", "eval")

constant = ast.parse("u'test'", mode="eval")
constant.body.kind = 0xFF
with self.assertRaisesRegex(
TypeError, "field 'kind' was expecting a string or bytes object"
):
compile(constant, "<test>", "eval")

def test_repr(self) -> None:
snapshots = AST_REPR_DATA_FILE.read_text().split("\n")
for test, snapshot in zip(ast_repr_get_test_cases(), snapshots, strict=True):
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_deque.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,22 @@ def test_index(self):
else:
self.assertEqual(d.index(element, start, stop), target)

# Test stop argument
for elem in d:
index = d.index(elem)
self.assertEqual(
index,
d.index(elem, 0),
)
self.assertEqual(
index,
d.index(elem, 0, len(d)),
)
self.assertEqual(
index,
d.index(elem, 0, len(d) + 100),
)

# Test large start argument
d = deque(range(0, 10000, 10))
for step in range(100):
Expand Down
24 changes: 24 additions & 0 deletions Lib/test/test_free_threading/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ def copy_loop():

threading_helper.run_concurrently([mutate, copy_loop])

def test_index_race_in_ac(self):
# gh-150750: There was a c_default specified as `Py_SIZE(self)`,
# it was used without a critical section.

d = deque(range(100))

def index():
for _ in range(10000):
try:
d.index(50)
except ValueError:
pass

def mutate():
for _ in range(10000):
d.append(0)
d.clear()
d.extend(range(100))
d.appendleft(-1)

threading_helper.run_concurrently(
[index, *[mutate for _ in range(3)]],
)


if __name__ == "__main__":
unittest.main()
50 changes: 50 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -6615,6 +6615,56 @@ def test_rollover(self):
print(tf.read())
self.assertTrue(found, msg=msg)

@unittest.skipUnless(support.has_st_birthtime,
"st_birthtime not available or supported by Python on this OS")
def test_rollover_based_on_st_birthtime_only(self):
def add_record(message: str) -> None:
fh = logging.handlers.TimedRotatingFileHandler(
self.fn, when='S', interval=4, encoding="utf-8", backupCount=1)
fmt = logging.Formatter('%(asctime)s %(message)s')
fh.setFormatter(fmt)
record = logging.makeLogRecord({'msg': message})
fh.emit(record)
fh.close()

add_record('testing - initial')
self.assertLogFile(self.fn)
# Sleep a little over the half of rollover time - and this value
# must be over 2 seconds, since this is the mtime resolution on
# FAT32 filesystems.
time.sleep(2.1)
add_record('testing - update before rollover to renew the st_mtime')
time.sleep(2.1) # a little over the half of rollover time
add_record('testing - new record supposedly in the new file after rollover')

# At this point, the log file should be rotated if the rotation
# is based on creation time but should be not if it's based on
# creation time.
found = False
now = datetime.datetime.now()
GO_BACK = 5 # seconds
for secs in range(GO_BACK):
prev = now - datetime.timedelta(seconds=secs)
fn = self.fn + prev.strftime(".%Y-%m-%d_%H-%M-%S")
found = os.path.exists(fn)
if found:
self.rmfiles.append(fn)
break
msg = 'No rotated files found, went back %d seconds' % GO_BACK
if not found:
# print additional diagnostics
dn, fn = os.path.split(self.fn)
files = [f for f in os.listdir(dn) if f.startswith(fn)]
print('Test time: %s' % now.strftime("%Y-%m-%d %H-%M-%S"), file=sys.stderr)
print('The only matching files are: %s' % files, file=sys.stderr)
for f in files:
print('Contents of %s:' % f)
path = os.path.join(dn, f)
print(os.stat(path))
with open(path, 'r') as tf:
print(tf.read())
self.assertTrue(found, msg=msg)

def test_rollover_at_midnight(self, weekly=False):
os_helper.unlink(self.fn)
now = datetime.datetime.now()
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_shlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ def testQuote(self):
self.assertRaises(TypeError, shlex.quote, 42)
self.assertRaises(TypeError, shlex.quote, b"abc")

def testForceQuote(self):
self.assertEqual(shlex.quote("spam"), "spam")
self.assertEqual(shlex.quote("spam", force=False), "spam")
self.assertEqual(shlex.quote("spam", force=True), "'spam'")
self.assertEqual(shlex.quote("spam eggs", force=False), "'spam eggs'")
self.assertEqual(shlex.quote("spam eggs", force=True), "'spam eggs'")
self.assertEqual(shlex.quote("two's-complement", force=False), "'two'\"'\"'s-complement'")

def testJoin(self):
for split_command, command in [
(['a ', 'b'], "'a ' b"),
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,18 @@ def test_blob_set_empty_slice(self):
self.blob[0:0] = b""
self.assertEqual(self.blob[:], self.data)

def test_blob_set_empty_slice_wrong_type(self):
with self.assertRaises(TypeError):
self.blob[5:5] = None

def test_blob_set_empty_slice_wrong_size(self):
with self.assertRaisesRegex(IndexError, "wrong size"):
self.blob[5:5] = b"123"

def test_blob_set_empty_slice_correct(self):
self.blob[5:5] = b""
self.assertEqual(self.blob[:], self.data)

def test_blob_set_slice_with_skip(self):
self.blob[0:10:2] = b"12345"
actual = self.cx.execute("select b from test").fetchone()[0]
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,7 @@ Owen Martin
Sidney San Martín
Westley Martínez
Sébastien Martini
Iván Márton
Roger Masse
Nick Mathewson
Simon Mathieu
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Produce more meaningful messages when compiling AST objects with wrong field
values. Patch by Batuhan Taskaya.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
A bug has been fixed that made the ``TimedRotatingFileHandler`` use the
MTIME attribute of the configured log file to to detect whether it has to be
rotated yet or not. In cases when the file was changed within the rotation
period the value of the MTIME was also updated to the current time and as a
result the rotation never happened. The file creation time (CTIME) is used
instead that makes the rotation file modification independent.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add keyword-only parameter *force* to :func:`shlex.quote` to force quoting
a string, even if it is already safe for a shell without being quoted.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a race condition in :meth:`collections.deque.index` with free-threading.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :class:`sqlite3.Blob` slice assignment to raise
:exc:`TypeError` and :exc:`IndexError` for type and size mismatches
respectively, even when the target slice is empty.
Loading
Loading