diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 192b22ff7540034..c11e9c068bed3bd 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3758,6 +3758,10 @@ def test_fromisoformat_fails_datetime(self): '2009-04-19T12:30:45-00:90:00', # Time zone field out from range '2009-04-19T12:30:45-00:00:90', # Time zone field out from range '2020-2020', # Ambiguous 9-char date portion + '2009-04-19T12:30:45.+05:00', # Empty fraction before offset + '2009-04-19T12:30:45.-05:00', # Empty fraction before offset + '2009-04-19T12:30:45.Z', # Empty fraction before Z + '2009-04-19T12:30:45,+05:00', # Empty fraction (comma) before offset ] for bad_str in bad_strs: @@ -5034,6 +5038,10 @@ def test_fromisoformat_fails(self): '24:01:00.000000', # Has non-zero minutes on 24:00 '12:30:45+00:90:00', # Time zone field out from range '12:30:45+00:00:90', # Time zone field out from range + '12:30:45.+05:00', # Empty fraction before offset + '12:30:45.-05:00', # Empty fraction before offset + '12:30:45.Z', # Empty fraction before Z + '12:30:45,+05:00', # Empty fraction (comma) before offset ] for bad_str in bad_strs: diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py index b2150b621516bae..95dcb4ef9fdc202 100644 --- a/Lib/test/test_compileall.py +++ b/Lib/test/test_compileall.py @@ -554,6 +554,24 @@ def temporary_pycache_prefix(self): finally: sys.pycache_prefix = old_prefix + @contextlib.contextmanager + def no_pycache_prefix(self): + """Ignore any ambient pycache prefix for the duration of the test. + + Some tests assume bytecode is written next to the source in a + __pycache__ directory. When the test suite is run with + PYTHONPYCACHEPREFIX set, neutralize it both in this process (used by + cache_from_source) and in any spawned subprocesses. + """ + old_prefix = sys.pycache_prefix + sys.pycache_prefix = None + try: + with os_helper.EnvironmentVarGuard() as env: + env.unset('PYTHONPYCACHEPREFIX') + yield + finally: + sys.pycache_prefix = old_prefix + def _get_run_args(self, args): return [*support.optim_args_from_interpreter_flags(), '-S', '-m', 'compileall', @@ -650,15 +668,16 @@ def test_legacy_paths(self): def test_multiple_runs(self): # Bug 8527 reported that multiple calls produced empty # __pycache__/__pycache__ directories. - self.assertRunOK('-q', self.pkgdir) - # Verify the __pycache__ directory contents. - self.assertTrue(os.path.exists(self.pkgdir_cachedir)) - cachecachedir = os.path.join(self.pkgdir_cachedir, '__pycache__') - self.assertFalse(os.path.exists(cachecachedir)) - # Call compileall again. - self.assertRunOK('-q', self.pkgdir) - self.assertTrue(os.path.exists(self.pkgdir_cachedir)) - self.assertFalse(os.path.exists(cachecachedir)) + with self.no_pycache_prefix(): + self.assertRunOK('-q', self.pkgdir) + # Verify the __pycache__ directory contents. + self.assertTrue(os.path.exists(self.pkgdir_cachedir)) + cachecachedir = os.path.join(self.pkgdir_cachedir, '__pycache__') + self.assertFalse(os.path.exists(cachecachedir)) + # Call compileall again. + self.assertRunOK('-q', self.pkgdir) + self.assertTrue(os.path.exists(self.pkgdir_cachedir)) + self.assertFalse(os.path.exists(cachecachedir)) @without_source_date_epoch # timestamp invalidation test def test_force(self): @@ -731,10 +750,13 @@ def test_symlink_loop(self): script_helper.make_pkg(pkg) os.symlink('.', os.path.join(pkg, 'evil')) os.symlink('.', os.path.join(pkg, 'evil2')) - self.assertRunOK('-q', self.pkgdir) - self.assertCompiled(os.path.join( - self.pkgdir, 'spam', 'evil', 'evil2', '__init__.py' - )) + # This relies on the __pycache__ layout (shared across the symlinked + # paths), so neutralize any ambient PYTHONPYCACHEPREFIX. + with self.no_pycache_prefix(): + self.assertRunOK('-q', self.pkgdir) + self.assertCompiled(os.path.join( + self.pkgdir, 'spam', 'evil', 'evil2', '__init__.py' + )) def test_quiet(self): noisy = self.assertRunOK(self.pkgdir) @@ -821,13 +843,16 @@ def test_include_on_stdin(self): f2 = script_helper.make_script(self.pkgdir, 'f2', '') f3 = script_helper.make_script(self.pkgdir, 'f3', '') f4 = script_helper.make_script(self.pkgdir, 'f4', '') - p = script_helper.spawn_python(*(self._get_run_args(()) + ['-i', '-'])) - p.stdin.write((f3+os.linesep).encode('ascii')) - script_helper.kill_python(p) - self.assertNotCompiled(f1) - self.assertNotCompiled(f2) - self.assertCompiled(f3) - self.assertNotCompiled(f4) + # spawn_python() runs with -E, ignoring PYTHONPYCACHEPREFIX, so make + # cache_from_source() in this process agree by neutralizing it too. + with self.no_pycache_prefix(): + p = script_helper.spawn_python(*(self._get_run_args(()) + ['-i', '-'])) + p.stdin.write((f3+os.linesep).encode('ascii')) + script_helper.kill_python(p) + self.assertNotCompiled(f1) + self.assertNotCompiled(f2) + self.assertCompiled(f3) + self.assertNotCompiled(f4) def test_compiles_as_much_as_possible(self): bingfn = script_helper.make_script(self.pkgdir, 'bing', 'syntax(error') diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index a18f83e23908f9c..5015b053ff29fac 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -2332,27 +2332,36 @@ def test_textbox_8bit_fill_last_cell(self): def test_textbox_unicode(self): # Like test_textbox_8bit, but characters are entered as strings -- the # way do_command() receives get_wch() input -- rather than integer - # bytes. Each string is used only if encodable in the current locale. + # bytes. Each string is used only if encodable in the current locale; + # a narrow build stores one byte per cell, so multi-byte characters + # additionally need a wide build. for text in ['abc', 'héšλ', 'café', 'naïve ¤', 'soupçon €Š', 'дякую єі']: - if self._encodable(text): - with self.subTest(text=text): - box, win = self._make_textbox(1, 12) - for ch in text: - box.do_command(ch) - self.assertEqual(box.gather(), text + ' ') + if not self._encodable(text): + continue + if not WIDE_BUILD and len(text.encode(self.stdscr.encoding)) != len(text): + continue + with self.subTest(text=text): + box, win = self._make_textbox(1, 12) + for ch in text: + box.do_command(ch) + self.assertEqual(box.gather(), text + ' ') def test_textbox_unicode_insert_mode(self): # Like test_textbox_8bit_insert, but the character is entered as a string - # (get_wch() input). Each string is used only if encodable. + # (get_wch() input). Each string is used only if encodable; multi-byte + # characters additionally need a wide build (one byte per cell otherwise). for text in ['abcd', 'aβλc', 'aéàc', 'a¤½c', 'a€Šc', 'aдві']: - if self._encodable(text): - with self.subTest(text=text): - box, win = self._make_textbox(1, 10, insert_mode=True) - for ch in text[0] + text[2:]: # all but the 2nd character - box.do_command(ch) - win.move(0, 1) - box.do_command(text[1]) # insert it at position 1 - self.assertEqual(box.gather(), text + ' ') + if not self._encodable(text): + continue + if not WIDE_BUILD and len(text.encode(self.stdscr.encoding)) != len(text): + continue + with self.subTest(text=text): + box, win = self._make_textbox(1, 10, insert_mode=True) + for ch in text[0] + text[2:]: # all but the 2nd character + box.do_command(ch) + win.move(0, 1) + box.do_command(text[1]) # insert it at position 1 + self.assertEqual(box.gather(), text + ' ') @requires_wide_build def test_textbox_combining(self): diff --git a/Lib/test/test_future_stmt/test_future.py b/Lib/test/test_future_stmt/test_future.py index faa3a2bfe121dc6..acd8d76dc90a293 100644 --- a/Lib/test/test_future_stmt/test_future.py +++ b/Lib/test/test_future_stmt/test_future.py @@ -3,7 +3,7 @@ import __future__ import ast import unittest -from test.support import import_helper +from test.support import force_not_colorized, import_helper from test.support.script_helper import spawn_python, kill_python from textwrap import dedent import os @@ -176,6 +176,7 @@ def test_unicode_literals_exec(self): exec("from __future__ import unicode_literals; x = ''", {}, scope) self.assertIsInstance(scope["x"], str) + @force_not_colorized def test_syntactical_future_repl(self): p = spawn_python('-i') p.stdin.write(b"from __future__ import barry_as_FLUFL\n") diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 9f3df8010d32339..9f6dec7d1c5802c 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -1669,6 +1669,11 @@ def _clean(self): unlink(self.source) def setUp(self): + # These tests assume bytecode is written next to the source in a + # local __pycache__ directory, so neutralize any pycache prefix (e.g. + # when the test suite is run with PYTHONPYCACHEPREFIX set). + self._orig_pycache_prefix = sys.pycache_prefix + sys.pycache_prefix = None self.source = TESTFN + '.py' self._clean() with open(self.source, 'w', encoding='utf-8') as fp: @@ -1680,6 +1685,7 @@ def tearDown(self): assert sys.path[0] == os.curdir, 'Unexpected sys.path[0]' del sys.path[0] self._clean() + sys.pycache_prefix = self._orig_pycache_prefix @skip_if_dont_write_bytecode def test_import_pyc_path(self): diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py index a926a7a4d408afc..8ec2dcd69e58b63 100644 --- a/Lib/test/test_importlib/test_util.py +++ b/Lib/test/test_importlib/test_util.py @@ -328,6 +328,17 @@ class PEP3147Tests: tag = sys.implementation.cache_tag + def setUp(self): + # Most of these tests assume the default (unset) pycache prefix, so + # clear it for the duration of the test (e.g. when the test suite is + # run with PYTHONPYCACHEPREFIX set). Tests that need a specific prefix + # set their own via util.temporary_pycache_prefix(). + self._orig_pycache_prefix = sys.pycache_prefix + sys.pycache_prefix = None + + def tearDown(self): + sys.pycache_prefix = self._orig_pycache_prefix + @unittest.skipIf(sys.implementation.cache_tag is None, 'requires sys.implementation.cache_tag not be None') def test_cache_from_source(self): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 495702badc3a7f1..5153e5eb9a4ff8b 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -7,6 +7,7 @@ import functools import gc import importlib +import importlib.util import inspect import io import linecache @@ -6519,6 +6520,19 @@ class TestModuleCLI(unittest.TestCase): NO_SOURCE_ERROR = "No source code available for defining module" NO_SOURCE_TARGET_ERROR = "Failed to retrieve source code for given target" + @staticmethod + def _expected_cached(module): + # assert_python_ok() runs the subprocess in isolated mode (-I), which + # ignores PYTHONPYCACHEPREFIX, so compute the expected cached path the + # same way (i.e. without any pycache prefix) to stay independent of the + # environment the test suite is run in. Modules without a cached path + # (e.g. frozen modules such as ntpath/importlib.machinery on Windows) + # report None, so preserve that. + if module.__spec__.cached is None: + return None + with support.swap_attr(sys, 'pycache_prefix', None): + return importlib.util.cache_from_source(module.__spec__.origin) + def test_only_source(self): module = importlib.import_module('unittest') rc, out, err = assert_python_ok('-m', 'inspect', @@ -6576,12 +6590,13 @@ def test_details_option_with_package(self): args = support.optim_args_from_interpreter_flags() rc, out, err = assert_python_ok(*args, '-m', 'inspect', module_name, '--details') + cached = self._expected_cached(module) # Full rendering check on the expected output expected_lines = [ f"Target: {module.__name__}", # No aliasing f"Origin: {module.__spec__.origin}", f"Source: {module.__file__}", - f"Cached: {module.__spec__.cached}", # None is still displayed + f"Cached: {cached}", # None is still displayed f"Loader: {_clean_object_ids(repr(module.__spec__.loader))}", f"Submodule search paths: {module.__path__}", "", @@ -6619,13 +6634,14 @@ def test_details_option_with_data_target(self): args = support.optim_args_from_interpreter_flags() rc, out, err = assert_python_ok(*args, '-m', 'inspect', cli_target, '--details') + cached = self._expected_cached(module) # Full rendering check on the expected output # The error is only informational when reading source details expected_lines = [ f"Target: {cli_target}", # No aliasing f"Origin: {module.__spec__.origin}", f"Source: {module.__file__}", - f"Cached: {module.__spec__.cached}", # None is still displayed + f"Cached: {cached}", # None is still displayed self.NO_SOURCE_TARGET_ERROR, "", ] @@ -6644,12 +6660,13 @@ def test_details_option_with_aliased_target(self): args = support.optim_args_from_interpreter_flags() rc, out, err = assert_python_ok(*args, '-m', 'inspect', cli_target, '--details') + cached = self._expected_cached(module) # Full rendering check on the expected output expected_lines = [ f'Target: {defining_target} (looked up as "{cli_target}")', f"Origin: {module.__spec__.origin}", f"Source: {module.__file__}", - f"Cached: {module.__spec__.cached}", # None is still displayed + f"Cached: {cached}", # None is still displayed f"Line: {inspect.findsource(target)[1]}", "", ] diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index 09113e983fcd94d..b4265e6a0b458db 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -186,21 +186,24 @@ def test_source_date_epoch(self): def test_double_dot_no_clobber(self): # http://bugs.python.org/issue22966 # py_compile foo.bar.py -> __pycache__/foo.cpython-34.pyc - weird_path = os.path.join(self.directory, 'foo.bar.py') - cache_path = importlib.util.cache_from_source(weird_path) - pyc_path = weird_path + 'c' - head, tail = os.path.split(cache_path) - penultimate_tail = os.path.basename(head) - self.assertEqual( - os.path.join(penultimate_tail, tail), - os.path.join( - '__pycache__', - 'foo.bar.{}.pyc'.format(sys.implementation.cache_tag))) - with open(weird_path, 'w') as file: - file.write('x = 123\n') - py_compile.compile(weird_path) - self.assertTrue(os.path.exists(cache_path)) - self.assertFalse(os.path.exists(pyc_path)) + # This test asserts the default __pycache__ layout, so neutralize any + # pycache prefix (e.g. when run with PYTHONPYCACHEPREFIX set). + with support.swap_attr(sys, 'pycache_prefix', None): + weird_path = os.path.join(self.directory, 'foo.bar.py') + cache_path = importlib.util.cache_from_source(weird_path) + pyc_path = weird_path + 'c' + head, tail = os.path.split(cache_path) + penultimate_tail = os.path.basename(head) + self.assertEqual( + os.path.join(penultimate_tail, tail), + os.path.join( + '__pycache__', + 'foo.bar.{}.pyc'.format(sys.implementation.cache_tag))) + with open(weird_path, 'w') as file: + file.write('x = 123\n') + py_compile.compile(weird_path) + self.assertTrue(os.path.exists(cache_path)) + self.assertFalse(os.path.exists(pyc_path)) @unittest.skipIf(sys.implementation.cache_tag is None, 'requires sys.implementation.cache_tag is not None') @@ -307,7 +310,13 @@ def test_with_files(self): self.assertEqual(rc, 0) self.assertEqual(stdout, b'') self.assertEqual(stderr, b'') - self.assertTrue(os.path.exists(self.cache_path)) + # pycompilecmd() runs the interpreter in isolated mode (-I), which + # ignores PYTHONPYCACHEPREFIX, so the bytecode is written next to the + # source. Compute the expected cache path the same way. + with support.swap_attr(sys, 'pycache_prefix', None): + cache_path = importlib.util.cache_from_source( + self.source_path, optimization='' if __debug__ else 1) + self.assertTrue(os.path.exists(cache_path)) def test_bad_syntax(self): bad_syntax = os.path.join(os.path.dirname(__file__), diff --git a/Misc/NEWS.d/next/Library/2026-06-25-07-08-17.gh-issue-152157.dt5Ef0.rst b/Misc/NEWS.d/next/Library/2026-06-25-07-08-17.gh-issue-152157.dt5Ef0.rst new file mode 100644 index 000000000000000..00e83b4af6be7a9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-25-07-08-17.gh-issue-152157.dt5Ef0.rst @@ -0,0 +1,3 @@ +The C implementations of :meth:`~datetime.datetime.fromisoformat` and :meth:`~datetime.time.fromisoformat` +now reject a decimal separator that is not followed by any +fractional digit before a timezone designator. diff --git a/Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst b/Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst new file mode 100644 index 000000000000000..9858415018ae1ba --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst @@ -0,0 +1,5 @@ +Fix several tests in ``test.test_inspect``, ``test.test_import``, +``test.test_importlib``, ``test.test_py_compile`` and +``test.test_compileall`` that failed when the test suite was run with +:envvar:`PYTHONPYCACHEPREFIX` set. These tests now neutralize the pycache +prefix where they assume the default ``__pycache__`` bytecode layout. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index fd8d95d05c933e0..cd02b298b406e6a 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1034,7 +1034,16 @@ parse_hh_mm_ss_ff(const char *tstr, const char *tstr_end, int *hour, has_separator = (c == ':'); } - if (p >= p_end) { + if (c == '.' || c == ',') { + if (i < 2) { + return -3; // Decimal mark on hour or minute + } + if (p >= p_end) { + return -3; // Decimal mark not followed by any digit + } + break; + } + else if (p >= p_end) { return c != '\0'; } else if (has_separator && (c == ':')) { @@ -1043,14 +1052,10 @@ parse_hh_mm_ss_ff(const char *tstr, const char *tstr_end, int *hour, } continue; } - else if (c == '.' || c == ',') { - if (i < 2) { - return -3; // Decimal mark on hour or minute - } - break; - } else if (!has_separator) { + else if (!has_separator) { --p; - } else { + } + else { return -4; // Malformed time separator } } diff --git a/Tools/requirements-hypothesis.txt b/Tools/requirements-hypothesis.txt index 8ecf796ec7343a0..aff7e3b0e224b9c 100644 --- a/Tools/requirements-hypothesis.txt +++ b/Tools/requirements-hypothesis.txt @@ -1,4 +1,4 @@ # Requirements file for hypothesis that # we use to run our property-based tests in CI. -hypothesis==6.151.9 +hypothesis==6.155.3