diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index eddc98824d48106..3b34445f019c36c 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -707,9 +707,10 @@ attributes (see :ref:`import-mod-attrs` for module attributes): Retrieving source code ---------------------- -.. function:: getdoc(object, *, inherit_class_doc=True, fallback_to_class_doc=True) +.. function:: getdoc(object, *, inherit_class_doc=True, fallback_to_class_doc=True, dedent=True) - Get the documentation string for an object, cleaned up with :func:`cleandoc`. + Get the documentation string for an object, cleaned up with :func:`cleandoc` + (with the same meaning of *dedent*). If the documentation string for an object is not provided: * if the object is a class and *inherit_class_doc* is true (by default), @@ -730,6 +731,9 @@ Retrieving source code Documentation strings on :class:`~functools.cached_property` objects are now inherited if not overridden. + .. versionchanged:: next + Added the *dedent* parameter. + .. function:: getcomments(object) @@ -791,15 +795,23 @@ Retrieving source code former. -.. function:: cleandoc(doc) +.. function:: cleandoc(doc, *, dedent=True) Clean up indentation from docstrings that are indented to line up with blocks of code. All leading whitespace is removed from the first line. Any leading whitespace - that can be uniformly removed from the second line onwards is removed. Empty - lines at the beginning and end are subsequently removed. Also, all tabs are - expanded to spaces. + that can be uniformly removed from the second line onwards is removed, unless + *dedent* is false. Empty lines at the beginning and end are subsequently + removed. Also, all tabs are expanded to spaces. + + Since Python 3.13 the compiler removes the indentation of docstrings, so + *dedent* only affects documentation strings which are not written as + docstrings in the source code, like those generated by Argument Clinic, + where the indentation is meaningful. + + .. versionchanged:: next + Added the *dedent* parameter. .. _inspect-signature-object: diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst index fe64daa3291d670..2a31213560c92d3 100644 --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -236,8 +236,8 @@ underlying :class:`Popen` interface can be used directly. .. attribute:: returncode - Exit status of the child process. If the process exited due to a - signal, this will be the negative signal number. + Exit status of the child process, an integer. If the process + exited due to a signal, this will be the negative signal number. .. attribute:: cmd diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 893154246ae4d81..f3b5383658b5ac4 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -1656,6 +1656,32 @@ The :mod:`!test.support.os_helper` module provides support for os tests. wrapped with a wait loop that checks for the existence of the file. +.. decorator:: with_source_date_epoch(*, epoch=123456789) + + A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH` + environment variable set to *epoch*. + + +.. decorator:: without_source_date_epoch + + A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH` + environment variable unset. + + +.. class:: SourceDateEpochTestMeta + + Metaclass wrapping all test methods of the class with + :func:`with_source_date_epoch` if the *source_date_epoch* keyword class + argument is true, or with :func:`without_source_date_epoch` otherwise. + For example:: + + class TestsWithSourceEpoch(Tests, + metaclass=SourceDateEpochTestMeta, + source_date_epoch=True): + pass + + + :mod:`!test.support.import_helper` --- Utilities for import tests ================================================================= diff --git a/Lib/inspect.py b/Lib/inspect.py index 2a14e43b66f2fac..3f8991c79652d3f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -793,12 +793,14 @@ def _getowndoc(obj): except AttributeError: return None -def getdoc(object, *, fallback_to_class_doc=True, inherit_class_doc=True): +def getdoc(object, *, fallback_to_class_doc=True, inherit_class_doc=True, + dedent=True): """Get the documentation string for an object. All tabs are expanded to spaces. To clean up docstrings that are indented to line up with blocks of code, any whitespace than can be - uniformly removed from the second line onwards is removed.""" + uniformly removed from the second line onwards is removed, unless + dedent is false.""" if fallback_to_class_doc: try: doc = object.__doc__ @@ -813,22 +815,23 @@ def getdoc(object, *, fallback_to_class_doc=True, inherit_class_doc=True): return None if not isinstance(doc, str): return None - return cleandoc(doc) + return cleandoc(doc, dedent=dedent) -def cleandoc(doc): +def cleandoc(doc, *, dedent=True): """Clean up indentation from docstrings. Any whitespace that can be uniformly removed from the second line - onwards is removed.""" + onwards is removed, unless dedent is false.""" lines = doc.expandtabs().split('\n') # Find minimum indentation of any non-blank lines after first line. margin = sys.maxsize - for line in lines[1:]: - content = len(line.lstrip(' ')) - if content: - indent = len(line) - content - margin = min(margin, indent) + if dedent: + for line in lines[1:]: + content = len(line.lstrip(' ')) + if content: + indent = len(line) - content + margin = min(margin, indent) # Remove indentation. if lines: lines[0] = lines[0].lstrip(' ') diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 72974af26bee64c..3cba08b83af6811 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -130,9 +130,12 @@ def pathdirs(): return dirs def _getdoc(object): + # Docstrings written in the source are dedented by the compiler; the + # indentation of generated docstrings is meaningful. return inspect.getdoc(object, fallback_to_class_doc=False, - inherit_class_doc=False) + inherit_class_doc=False, + dedent=False) def getdoc(object): """Get the doc string or comments for an object.""" diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 054860a19c74b6d..a14fede00c391c9 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -143,7 +143,7 @@ def __init__(self, returncode, cmd, output=None, stderr=None): self.stderr = stderr def __str__(self): - if self.returncode and self.returncode < 0: + if isinstance(self.returncode, int) and self.returncode < 0: try: return "Command %r died with %r." % ( self.cmd, signal.Signals(-self.returncode)) @@ -151,8 +151,8 @@ def __str__(self): return "Command %r died with unknown signal %d." % ( self.cmd, -self.returncode) else: - return "Command %r returned non-zero exit status %d." % ( - self.cmd, self.returncode) + return (f"Command {self.cmd!r} returned non-zero " + f"exit status {self.returncode}.") @property def stdout(self): diff --git a/Lib/test/dtracedata/call_stack.stp b/Lib/test/dtracedata/call_stack.stp index 54082c202f66aa4..d4455fc8489af20 100644 --- a/Lib/test/dtracedata/call_stack.stp +++ b/Lib/test/dtracedata/call_stack.stp @@ -10,7 +10,7 @@ function basename:string(path:string) return last_token; } -probe process.mark("function__entry") +probe @PYTHON_SYSTEMTAP_PROBE@("function__entry") { funcname = user_string($arg2); @@ -19,7 +19,8 @@ probe process.mark("function__entry") } } -probe process.mark("function__entry"), process.mark("function__return") +probe @PYTHON_SYSTEMTAP_PROBE@("function__entry"), + @PYTHON_SYSTEMTAP_PROBE@("function__return") { filename = user_string($arg1); funcname = user_string($arg2); @@ -31,7 +32,7 @@ probe process.mark("function__entry"), process.mark("function__return") } } -probe process.mark("function__return") +probe @PYTHON_SYSTEMTAP_PROBE@("function__return") { funcname = user_string($arg2); diff --git a/Lib/test/dtracedata/gc.stp b/Lib/test/dtracedata/gc.stp index 162c6d3a2209b98..11d2715e6c721a8 100644 --- a/Lib/test/dtracedata/gc.stp +++ b/Lib/test/dtracedata/gc.stp @@ -1,6 +1,6 @@ global tracing -probe process.mark("function__entry") +probe @PYTHON_SYSTEMTAP_PROBE@("function__entry") { funcname = user_string($arg2); @@ -9,14 +9,15 @@ probe process.mark("function__entry") } } -probe process.mark("gc__start"), process.mark("gc__done") +probe @PYTHON_SYSTEMTAP_PROBE@("gc__start"), + @PYTHON_SYSTEMTAP_PROBE@("gc__done") { if (tracing) { printf("%d\t%s:%ld\n", gettimeofday_us(), $$name, $arg1); } } -probe process.mark("function__return") +probe @PYTHON_SYSTEMTAP_PROBE@("function__return") { funcname = user_string($arg2); diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py index daf6060940e97f0..e1e2e69cb3d8334 100644 --- a/Lib/test/support/os_helper.py +++ b/Lib/test/support/os_helper.py @@ -1,6 +1,7 @@ import collections.abc import contextlib import errno +import functools import logging import os import re @@ -806,6 +807,48 @@ def __exit__(self, *ignore_exc): os.environ = self._environ +def without_source_date_epoch(fxn): + """Runs function with SOURCE_DATE_EPOCH unset.""" + @functools.wraps(fxn) + def wrapper(*args, **kwargs): + with EnvironmentVarGuard() as env: + env.unset('SOURCE_DATE_EPOCH') + return fxn(*args, **kwargs) + return wrapper + + +_MISSING = sentinel("MISSING") + +def with_source_date_epoch(fxn=_MISSING, *, epoch=123456789): + """Runs function with SOURCE_DATE_EPOCH set to *epoch*.""" + if fxn is _MISSING: + return functools.partial(with_source_date_epoch, epoch=epoch) + + @functools.wraps(fxn) + def wrapper(*args, **kwargs): + with EnvironmentVarGuard() as env: + env['SOURCE_DATE_EPOCH'] = str(epoch) + return fxn(*args, **kwargs) + return wrapper + + +# Run tests with SOURCE_DATE_EPOCH set or unset explicitly. +class SourceDateEpochTestMeta(type(unittest.TestCase)): + def __new__(mcls, name, bases, dct, *, source_date_epoch): + cls = super().__new__(mcls, name, bases, dct) + + for attr in dir(cls): + if attr.startswith('test_'): + meth = getattr(cls, attr) + if source_date_epoch: + wrapper = with_source_date_epoch(meth) + else: + wrapper = without_source_date_epoch(meth) + setattr(cls, attr, wrapper) + + return cls + + try: if support.MS_WINDOWS: import ctypes diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py index 95dcb4ef9fdc202..9a6ca2be1624c6c 100644 --- a/Lib/test/test_compileall.py +++ b/Lib/test/test_compileall.py @@ -28,8 +28,8 @@ from test import support from test.support import os_helper from test.support import script_helper -from test.test_py_compile import without_source_date_epoch -from test.test_py_compile import SourceDateEpochTestMeta +from test.support.os_helper import without_source_date_epoch +from test.support.os_helper import SourceDateEpochTestMeta from test.support.os_helper import FakePath diff --git a/Lib/test/test_dtrace.py b/Lib/test/test_dtrace.py index 30731b8f90ac14d..4967a18053057b3 100644 --- a/Lib/test/test_dtrace.py +++ b/Lib/test/test_dtrace.py @@ -6,11 +6,13 @@ import subprocess import sys import sysconfig +import tempfile import types import unittest from test import support from test.support import findfile, MS_WINDOWS +from test.support import os_helper if not support.has_subprocess_support: @@ -25,6 +27,31 @@ def abspath(filename): return os.path.abspath(findfile(filename, subdir="dtracedata")) +def get_probe_binary(): + binary = sys.executable + if sysconfig.get_config_var("Py_ENABLE_SHARED"): + lib_dir = sysconfig.get_config_var("LIBDIR") + if not lib_dir or sysconfig.is_python_build(): + lib_dir = os.path.abspath(os.path.dirname(sys.executable)) + + lib_names = [] + for name in ( + sysconfig.get_config_var("INSTSONAME"), + sysconfig.get_config_var("LDLIBRARY"), + ): + if name and name not in lib_names: + lib_names.append(name) + + if lib_dir: + for name in lib_names: + libpython_path = os.path.join(lib_dir, name) + if os.path.exists(libpython_path): + binary = libpython_path + break + + return binary + + def normalize_trace_output(output): """Normalize DTrace output for comparison. @@ -180,6 +207,45 @@ class DTraceBackend(TraceBackend): class SystemTapBackend(TraceBackend): EXTENSION = ".stp" COMMAND = ["stap", "-g"] + PROBE_PLACEHOLDER = "@PYTHON_SYSTEMTAP_PROBE@" + + @staticmethod + def quote_systemtap_string(value): + return value.replace("\\", "\\\\").replace('"', '\\"') + + def python_probe(self): + executable = self.quote_systemtap_string(sys.executable) + probe_binary = get_probe_binary() + if probe_binary == sys.executable: + return f'process("{executable}").mark' + + # Python built with --enable-shared + probe_binary = self.quote_systemtap_string(probe_binary) + return f'process("{executable}").library("{probe_binary}").mark' + + def render_script(self, filename): + with open(filename) as fp: + script = fp.read() + + return script.replace(self.PROBE_PLACEHOLDER, self.python_probe()) + + def trace(self, script_file, subcommand=None, *, timeout=None, + check_returncode=False): + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", suffix=self.EXTENSION, delete=False + ) as script: + script.write(self.render_script(script_file)) + generated_script_file = script.name + + try: + return super().trace( + generated_script_file, + subcommand, + timeout=timeout, + check_returncode=check_returncode, + ) + finally: + os_helper.unlink(generated_script_file) class BPFTraceBackend(TraceBackend): @@ -273,7 +339,7 @@ def run_case(self, name, optimize_python=None): python_flags.extend(["-O"] * optimize_python) subcommand = [sys.executable] + python_flags + [python_file] - program = self.PROGRAMS[name].format(python=sys.executable) + program = self.PROGRAMS[name].format(python=get_probe_binary()) try: proc = create_process_group( @@ -312,7 +378,7 @@ def run_case(self, name, optimize_python=None): def assert_usable(self): # Check if bpftrace is available and can attach to USDT probes - program = f'usdt:{sys.executable}:python:function__entry {{ printf("probe: success\\n"); exit(); }}' + program = f'usdt:{get_probe_binary()}:python:function__entry {{ printf("probe: success\\n"); exit(); }}' try: proc = create_process_group( ["bpftrace", "-e", program, "-c", @@ -455,28 +521,7 @@ def get_readelf_version(): return int(match.group(1)), int(match.group(2)) def get_readelf_output(self): - binary = sys.executable - if sysconfig.get_config_var("Py_ENABLE_SHARED"): - lib_dir = sysconfig.get_config_var("LIBDIR") - if not lib_dir or sysconfig.is_python_build(): - lib_dir = os.path.abspath(os.path.dirname(sys.executable)) - - lib_names = [] - for name in ( - sysconfig.get_config_var("INSTSONAME"), - sysconfig.get_config_var("LDLIBRARY"), - ): - if name and name not in lib_names: - lib_names.append(name) - - if lib_dir: - for name in lib_names: - libpython_path = os.path.join(lib_dir, name) - if os.path.exists(libpython_path): - binary = libpython_path - break - - return run_readelf(["readelf", "-n", binary]) + return run_readelf(["readelf", "-n", get_probe_binary()]) def test_check_probes(self): readelf_output = self.get_readelf_output() diff --git a/Lib/test/test_importlib/source/test_file_loader.py b/Lib/test/test_importlib/source/test_file_loader.py index e4bd850f3514ff1..ca2cc045fa674e1 100644 --- a/Lib/test/test_importlib/source/test_file_loader.py +++ b/Lib/test/test_importlib/source/test_file_loader.py @@ -5,21 +5,18 @@ machinery = util.import_importlib('importlib.machinery') importlib_util = util.import_importlib('importlib.util') -import errno import marshal import os import py_compile -import shutil import stat import sys import types import unittest -import warnings -from test.support.import_helper import make_legacy_pyc, unload +from test.support.import_helper import make_legacy_pyc -from test.test_py_compile import without_source_date_epoch -from test.test_py_compile import SourceDateEpochTestMeta +from test.support.os_helper import without_source_date_epoch +from test.support.os_helper import SourceDateEpochTestMeta class SimpleTest: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 5153e5eb9a4ff8b..844811692df2d6d 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -781,6 +781,22 @@ def test_cleandoc(self): with self.subTest(i=i): self.assertEqual(func(input), expected) + def test_cleandoc_no_dedent(self): + func = inspect.cleandoc + self.assertEqual(func('An\n indented\n docstring.', dedent=False), + 'An\n indented\n docstring.') + # Everything else that cleandoc() does still applies. + self.assertEqual(func(' An\n\n\tindented\n\n', dedent=False), + 'An\n\n indented') + + def test_getdoc_no_dedent(self): + class C: + pass + # Written as a docstring, it would be dedented by the compiler. + C.__doc__ = 'Summary.\n\n param\n description' + self.assertEqual(inspect.getdoc(C, dedent=False), C.__doc__) + self.assertEqual(inspect.getdoc(C), 'Summary.\n\nparam\n description') + @cpython_only def test_c_cleandoc(self): try: diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index b4265e6a0b458db..616e5d0c7cf971c 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -1,4 +1,3 @@ -import functools import importlib.util import os import py_compile @@ -11,43 +10,7 @@ from test import support from test.support import os_helper, script_helper - - -def without_source_date_epoch(fxn): - """Runs function with SOURCE_DATE_EPOCH unset.""" - @functools.wraps(fxn) - def wrapper(*args, **kwargs): - with os_helper.EnvironmentVarGuard() as env: - env.unset('SOURCE_DATE_EPOCH') - return fxn(*args, **kwargs) - return wrapper - - -def with_source_date_epoch(fxn): - """Runs function with SOURCE_DATE_EPOCH set.""" - @functools.wraps(fxn) - def wrapper(*args, **kwargs): - with os_helper.EnvironmentVarGuard() as env: - env['SOURCE_DATE_EPOCH'] = '123456789' - return fxn(*args, **kwargs) - return wrapper - - -# Run tests with SOURCE_DATE_EPOCH set or unset explicitly. -class SourceDateEpochTestMeta(type(unittest.TestCase)): - def __new__(mcls, name, bases, dct, *, source_date_epoch): - cls = super().__new__(mcls, name, bases, dct) - - for attr in dir(cls): - if attr.startswith('test_'): - meth = getattr(cls, attr) - if source_date_epoch: - wrapper = with_source_date_epoch(meth) - else: - wrapper = without_source_date_epoch(meth) - setattr(cls, attr, wrapper) - - return cls +from test.support.os_helper import SourceDateEpochTestMeta class PyCompileTestsBase: diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 6d30d267cd5ad29..6ba440053089161 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -170,21 +170,20 @@ def test_randomize(self): ns = self.parse_args([opt]) self.assertTrue(ns.randomize) - with os_helper.EnvironmentVarGuard() as env: - # with SOURCE_DATE_EPOCH - env['SOURCE_DATE_EPOCH'] = '1697839080' - ns = self.parse_args(['--randomize']) - regrtest = main.Regrtest(ns) - self.assertFalse(regrtest.randomize) - self.assertIsInstance(regrtest.random_seed, str) - self.assertEqual(regrtest.random_seed, '1697839080') - - # without SOURCE_DATE_EPOCH - del env['SOURCE_DATE_EPOCH'] - ns = self.parse_args(['--randomize']) - regrtest = main.Regrtest(ns) - self.assertTrue(regrtest.randomize) - self.assertIsInstance(regrtest.random_seed, int) + @os_helper.with_source_date_epoch(epoch=1697839080) + def test_randomize_with_source_date_epoch(self): + ns = self.parse_args(['--randomize']) + regrtest = main.Regrtest(ns) + self.assertFalse(regrtest.randomize) + self.assertIsInstance(regrtest.random_seed, str) + self.assertEqual(regrtest.random_seed, '1697839080') + + @os_helper.without_source_date_epoch + def test_randomize_without_source_date_epoch(self): + ns = self.parse_args(['--randomize']) + regrtest = main.Regrtest(ns) + self.assertTrue(regrtest.randomize) + self.assertIsInstance(regrtest.random_seed, int) def test_no_randomize(self): ns = self.parse_args([]) diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index 4fd14d98b0324c0..d1840e97d0f2f7c 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -2449,6 +2449,16 @@ def test_CalledProcessError_str(self): err = subprocess.CalledProcessError(-9876543, "fake cmd") self.assertEqual(str(err), "Command 'fake cmd' died with unknown signal 9876543.") + # returncode which is not an integer, which happens for example when + # Popen is mocked: str() must not fail + for returncode in (None, "2", 2.5, [2]): + with self.subTest(returncode=returncode): + err = subprocess.CalledProcessError(returncode, "fake cmd") + self.assertEqual( + str(err), + f"Command 'fake cmd' returned non-zero " + f"exit status {returncode}.") + def test_preexec(self): # DISCLAIMER: Setting environment variables is *not* a good use # of a preexec_fn. This is merely a test. diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 83f2eef6b6f8ab6..e9974d6c05648bb 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -22,14 +22,15 @@ from random import randint, random, randbytes from test import archiver_tests -from test.support import script_helper, os_helper +from test.support import script_helper from test.support import ( findfile, requires_zlib, requires_bz2, requires_lzma, requires_zstd, captured_stdout, captured_stderr, requires_subprocess, cpython_only, gc_collect ) from test.support.os_helper import ( - TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath + TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath, + with_source_date_epoch, without_source_date_epoch, ) from test.support.import_helper import ensure_lazy_imports from test.support.warnings_helper import check_no_resource_warning @@ -1961,6 +1962,7 @@ def test_repack_file_entry_before_first_file(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_bytes_before_removed_files(self): """Should preserve if there are bytes before stale local file entries.""" @@ -2005,6 +2007,7 @@ def test_repack_bytes_before_removed_files(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_bytes_after_removed_files(self): """Should keep extra bytes if there are bytes after stale local file entries.""" @@ -2048,6 +2051,7 @@ def test_repack_bytes_after_removed_files(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_bytes_between_removed_files(self): """Should strip only local file entries before random bytes.""" @@ -2252,6 +2256,7 @@ def test_repack_removed_partial(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_removed_bytes_between_files(self): """Should not remove bytes between local file entries.""" @@ -4004,29 +4009,24 @@ def test_writestr_extended_local_header_issue1202(self): zinfo.flag_bits |= zipfile._MASK_USE_DATA_DESCRIPTOR # Include an extended local header. orig_zip.writestr(zinfo, data) + @with_source_date_epoch(epoch=1735715999) def test_write_with_source_date_epoch(self): - with os_helper.EnvironmentVarGuard() as env: - # Set the SOURCE_DATE_EPOCH environment variable to a specific timestamp - env['SOURCE_DATE_EPOCH'] = "1735715999" - - with zipfile.ZipFile(TESTFN, "w") as zf: - zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") - with zipfile.ZipFile(TESTFN, "r") as zf: - zip_info = zf.getinfo("test_source_date_epoch.txt") - expected_utc = (2025, 1, 1, 7, 19, 58) - self.assertEqual(zip_info.date_time, expected_utc) + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_source_date_epoch.txt") + expected_utc = (2025, 1, 1, 7, 19, 58) + self.assertEqual(zip_info.date_time, expected_utc) + @without_source_date_epoch def test_write_without_source_date_epoch(self): - with os_helper.EnvironmentVarGuard() as env: - del env['SOURCE_DATE_EPOCH'] - - with zipfile.ZipFile(TESTFN, "w") as zf: - zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH") + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH") - with zipfile.ZipFile(TESTFN, "r") as zf: - zip_info = zf.getinfo("test_no_source_date_epoch.txt") - self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2) + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_no_source_date_epoch.txt") + self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2) def assertTimestampAlmostEqual(self, time1, time2, tolerance): import datetime diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 6c353b4ff850626..764bb9b1e9246f6 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -620,7 +620,6 @@ def _decodeExtra(self, filename_crc): if up_unicode_name: self.filename = _sanitize_filename(up_unicode_name) else: - import warnings warnings.warn("Empty unicode path extra field (0x7075)", stacklevel=2) except struct.error as e: raise BadZipFile("Corrupt unicode path extra field (0x7075)") from e @@ -2153,7 +2152,6 @@ def comment(self, comment): raise TypeError("comment: expected bytes, got %s" % type(comment).__name__) # check for valid comment length if len(comment) > ZIP_MAX_COMMENT: - import warnings warnings.warn('Archive comment is too long; truncating to %d bytes' % ZIP_MAX_COMMENT, stacklevel=2) comment = comment[:ZIP_MAX_COMMENT] @@ -2244,7 +2242,6 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False): if (zinfo._end_offset is not None and zef_file.tell() + zinfo.compress_size > zinfo._end_offset): if zinfo._end_offset == zinfo.header_offset: - import warnings warnings.warn( f"Overlapped entries: {zinfo.orig_filename!r} " f"(possible zip bomb)", @@ -2494,7 +2491,6 @@ def _extract_member(self, member, targetpath, pwd): def _writecheck(self, zinfo): """Check for errors before writing a file to the archive.""" if zinfo.filename in self.NameToInfo: - import warnings warnings.warn('Duplicate name: %r' % zinfo.filename, stacklevel=3) if self.mode not in ('w', 'x', 'a'): raise ValueError("write() requires mode 'w', 'x', or 'a'") diff --git a/Misc/NEWS.d/next/Library/2026-07-22-12-00-00.gh-issue-153970.Kq7Wn3.rst b/Misc/NEWS.d/next/Library/2026-07-22-12-00-00.gh-issue-153970.Kq7Wn3.rst new file mode 100644 index 000000000000000..def943b46b59420 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-12-00-00.gh-issue-153970.Kq7Wn3.rst @@ -0,0 +1,3 @@ +Calling :func:`str` on a :exc:`subprocess.CalledProcessError` no longer +raises :exc:`TypeError` when its :attr:`!returncode` is not an integer, such +as ``None``. diff --git a/Misc/NEWS.d/next/Library/2026-08-05-16-30-12.gh-issue-155236.Kd7pQr.rst b/Misc/NEWS.d/next/Library/2026-08-05-16-30-12.gh-issue-155236.Kd7pQr.rst new file mode 100644 index 000000000000000..7e8a7a3c280a6d2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-05-16-30-12.gh-issue-155236.Kd7pQr.rst @@ -0,0 +1,4 @@ +Add the *dedent* parameter in :func:`inspect.cleandoc` and +:func:`inspect.getdoc`. +:mod:`pydoc` no longer dedents documentation strings, so the indentation +of the parameter descriptions generated by Argument Clinic is preserved. diff --git a/Python/instrumentation.c b/Python/instrumentation.c index 0af2070b5cd983a..806d3fbf5d6b192 100644 --- a/Python/instrumentation.c +++ b/Python/instrumentation.c @@ -989,8 +989,12 @@ call_one_instrument( if (res == NULL) { return -1; } + if (res == &_PyInstrumentation_DISABLE) { + assert(_Py_IsImmortal(res)); + return 1; + } Py_DECREF(res); - return (res == &_PyInstrumentation_DISABLE); + return 0; } static const int8_t MOST_SIGNIFICANT_BITS[16] = {