diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 7f6571ef954576..490c32ecfc9a62 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -71,7 +71,8 @@ jobs: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.13" + python-version: "3.15" + allow-prereleases: true cache: pip cache-dependency-path: Tools/requirements-dev.txt - run: pip install -r Tools/requirements-dev.txt diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 2a53d5ff581fa2..74d364725fb129 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -20,7 +20,6 @@ from __future__ import annotations import os -import _colorize from abc import ABC, abstractmethod import ast @@ -30,6 +29,7 @@ import re import sys from typing import TYPE_CHECKING +lazy import _colorize from .render import RenderedScreen from .trace import trace diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 7a639afd74ef3c..969b257e88aa6a 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -3,10 +3,11 @@ # # All Rights Reserved """Colorful tab completion for Python prompt""" -from _colorize import ANSIColors, get_colors, get_theme import rlcompleter import keyword import types +lazy from _colorize import ANSIColors, get_colors, get_theme + class Completer(rlcompleter.Completer): """ diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 7e4dd801c84d5c..405b95682ec5bc 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -22,11 +22,11 @@ from __future__ import annotations import sys -import _colorize from contextlib import contextmanager from dataclasses import dataclass, field, fields, replace from typing import Self +lazy import _colorize from . import commands, console, input from .content import ( diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index b50426c31ead53..66e6c5c7c8e3ca 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -6,13 +6,13 @@ import token as T import tokenize import unicodedata -import _colorize from collections import deque from dataclasses import dataclass from io import StringIO from tokenize import TokenInfo as TI from typing import Iterable, Iterator, Match, NamedTuple, Self +lazy import _colorize from .types import CharBuffer, CharWidths from .trace import trace diff --git a/Lib/difflib.py b/Lib/difflib.py index 8f3cdaed9564d8..ae8b284b4d3647 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -30,10 +30,10 @@ 'Differ','IS_CHARACTER_JUNK', 'IS_LINE_JUNK', 'context_diff', 'unified_diff', 'diff_bytes', 'HtmlDiff', 'Match'] -from _colorize import can_colorize, get_theme from heapq import nlargest as _nlargest from collections import namedtuple as _namedtuple from types import GenericAlias +lazy from _colorize import can_colorize, get_theme Match = _namedtuple('Match', 'a b size') diff --git a/Lib/doctest.py b/Lib/doctest.py index 0fcfa1e3e97144..320a654f9270b2 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -106,8 +106,8 @@ def _test(): import unittest from io import StringIO, TextIOWrapper, BytesIO from collections import namedtuple -import _colorize # Used in doctests -from _colorize import ANSIColors, can_colorize +lazy import _colorize # Used in doctests +lazy from _colorize import ANSIColors, can_colorize class TestResults(namedtuple('TestResults', 'failed attempted')): diff --git a/Lib/json/tool.py b/Lib/json/tool.py index e56a601c581ae5..f7f702cab6fec1 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -7,7 +7,7 @@ import json import re import sys -from _colorize import get_theme, can_colorize +lazy from _colorize import get_theme, can_colorize # The string we are colorizing is valid JSON, diff --git a/Lib/pdb.py b/Lib/pdb.py index 4dd974b375c259..00df2229ebe8d6 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -96,11 +96,11 @@ import linecache import selectors import threading -import _colorize from contextlib import ExitStack, closing, contextmanager from types import CodeType from warnings import deprecated +lazy import _colorize try: import _pyrepl.utils diff --git a/Lib/profiling/sampling/live_collector/collector.py b/Lib/profiling/sampling/live_collector/collector.py index c03df4075277cd..a53cfc6b719a10 100644 --- a/Lib/profiling/sampling/live_collector/collector.py +++ b/Lib/profiling/sampling/live_collector/collector.py @@ -9,7 +9,7 @@ import sys import sysconfig import time -import _colorize +lazy import _colorize from ..collector import Collector, extract_lineno from ..constants import ( diff --git a/Lib/profiling/sampling/pstats_collector.py b/Lib/profiling/sampling/pstats_collector.py index 6be1d698ffaa9a..50500296c15acc 100644 --- a/Lib/profiling/sampling/pstats_collector.py +++ b/Lib/profiling/sampling/pstats_collector.py @@ -1,8 +1,8 @@ import collections import marshal import pstats +lazy from _colorize import ANSIColors -from _colorize import ANSIColors from .collector import Collector, extract_lineno from .constants import MICROSECONDS_PER_SECOND, PROFILING_MODE_CPU diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py index 9195f5ee6dd390..3b9146ac6f8661 100644 --- a/Lib/profiling/sampling/sample.py +++ b/Lib/profiling/sampling/sample.py @@ -6,7 +6,7 @@ import sysconfig import time from collections import deque -from _colorize import ANSIColors +lazy from _colorize import ANSIColors from .pstats_collector import PstatsCollector from .stack_collector import CollapsedStackCollector, FlamegraphCollector diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 8805442b69e080..074347ce3ce8a0 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -10,12 +10,12 @@ from argparse import ArgumentParser from code import InteractiveConsole from textwrap import dedent -from _colorize import get_theme, theme_no_color +lazy from _colorize import get_theme, theme_no_color from ._completer import completer -def execute(c, sql, suppress_errors=True, theme=theme_no_color): +def execute(c, sql, suppress_errors=True, theme=None): """Helper that wraps execution of SQL code. This is used both by the REPL and by direct execution from the CLI. @@ -23,6 +23,8 @@ def execute(c, sql, suppress_errors=True, theme=theme_no_color): 'c' may be a cursor or a connection. 'sql' is the SQL string to execute. """ + if theme is None: + theme = theme_no_color try: for row in c.execute(sql): diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py index 771fd46e042a41..46c9b2c1d8c9fc 100644 --- a/Lib/test/test_difflib.py +++ b/Lib/test/test_difflib.py @@ -1,5 +1,7 @@ import difflib +from test import support from test.support import findfile, force_colorized +from test.support.import_helper import ensure_lazy_imports import unittest import doctest import sys @@ -644,6 +646,12 @@ def setUpModule(): difflib.HtmlDiff._default_prefix = 0 +class LazyImportTest(unittest.TestCase): + @support.cpython_only + def test_lazy_import(self): + ensure_lazy_imports("difflib", {"_colorize"}) + + def load_tests(loader, tests, pattern): tests.addTest(doctest.DocTestSuite(difflib)) return tests diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 0a96b318b15b1c..487a750eb8b544 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -8,6 +8,7 @@ from test import support from test.support import force_colorized, force_not_colorized, os_helper +from test.support.import_helper import ensure_lazy_imports from test.support.script_helper import assert_python_ok from _colorize import get_theme @@ -334,5 +335,11 @@ class TestTool(TestMain): module = 'json.tool' +class LazyImportTest(unittest.TestCase): + @support.cpython_only + def test_lazy_import(self): + ensure_lazy_imports("json.tool", {"_colorize"}) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index db90019975521e..e4904aca2ddd11 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -22,7 +22,7 @@ from io import StringIO from test import support from test.support import has_socket_support, os_helper -from test.support.import_helper import import_module +from test.support.import_helper import ensure_lazy_imports, import_module from test.support.pty_helper import run_pty, FakeInput from test.support.script_helper import kill_python from unittest.mock import patch @@ -5306,5 +5306,11 @@ def tearDown(test): return tests +class LazyImportTest(unittest.TestCase): + @support.cpython_only + def test_lazy_import(self): + ensure_lazy_imports("pdb", {"_colorize"}) + + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 1fc0236780fa8b..b78e76d9a75127 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -7,13 +7,14 @@ import os from sqlite3.__main__ import main as cli -from test.support.import_helper import import_module +from test.support.import_helper import ensure_lazy_imports, import_module from test.support.os_helper import TESTFN, unlink from test.support.pty_helper import run_pty from test.support import ( captured_stdout, captured_stderr, captured_stdin, + cpython_only, force_not_colorized_test_class, requires_subprocess, verbose, @@ -437,6 +438,11 @@ def test_complete_no_input(self): raise +class LazyImportTest(unittest.TestCase): + @cpython_only + def test_lazy_import(self): + ensure_lazy_imports("sqlite3.__main__", {"_colorize"}) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 909808825f055e..60584bf3969e93 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -23,7 +23,7 @@ requires_subprocess, os_helper) from test.support.os_helper import TESTFN, temp_dir, unlink from test.support.script_helper import assert_python_ok, assert_python_failure, make_script -from test.support.import_helper import forget +from test.support.import_helper import ensure_lazy_imports, forget from test.support import force_not_colorized, force_not_colorized_test_class import json @@ -5543,5 +5543,11 @@ def test_suggestion_still_works_for_non_lazy_attributes(self): self.assertNotIn(b"BAR_MODULE_LOADED", stdout) +class LazyImportTest(unittest.TestCase): + @support.cpython_only + def test_lazy_import(self): + ensure_lazy_imports("traceback", {"_colorize"}) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/traceback.py b/Lib/traceback.py index 343d0e5f108c35..84547bab08dc87 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -16,9 +16,9 @@ import io import importlib.util import pathlib -import _colorize from contextlib import suppress +lazy import _colorize try: from _missing_stdlib_info import _MISSING_STDLIB_MODULE_MESSAGES @@ -32,6 +32,36 @@ 'FrameSummary', 'StackSummary', 'TracebackException', 'walk_stack', 'walk_tb', 'print_list'] + +class _ShutdownTheme: + """Empty stand-in if `_colorize` cannot be imported during late shutdown.""" + def __getattr__(self, _): return self + def __getitem__(self, _): return "" + def __format__(self, _): return "" + def __str__(self): return "" + def __add__(self, other): return other + __radd__ = __add__ + + +_shutdown_theme = _ShutdownTheme() + + +def _safe_get_theme(*, force_color=False, force_no_color=False): + try: + return _colorize.get_theme( + force_color=force_color, force_no_color=force_no_color + ) + except ImportError: + return _shutdown_theme + + +def _safe_can_colorize(*, file=None): + try: + return _colorize.can_colorize(file=file) + except ImportError: + return False + + # # Formatting and printing lists of traceback lines. # @@ -151,7 +181,7 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ def _print_exception_bltin(exc, file=None, /): if file is None: file = sys.stderr if sys.stderr is not None else sys.__stderr__ - colorize = _colorize.can_colorize(file=file) + colorize = _safe_can_colorize(file=file) return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) @@ -199,9 +229,9 @@ def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize= valuestr = _safe_string(value, 'exception') end_char = "\n" if insert_final_newline else "" if colorize: - theme = _colorize.get_theme(force_color=True).traceback + theme = _safe_get_theme(force_color=True).traceback else: - theme = _colorize.get_theme(force_no_color=True).traceback + theme = _safe_get_theme(force_no_color=True).traceback if value is None or not valuestr: line = f"{theme.type}{etype}{theme.reset}{end_char}" else: @@ -555,9 +585,9 @@ def format_frame_summary(self, frame_summary, **kwargs): if frame_summary.filename.startswith("'): filename = "" if colorize: - theme = _colorize.get_theme(force_color=True).traceback + theme = _safe_get_theme(force_color=True).traceback else: - theme = _colorize.get_theme(force_no_color=True).traceback + theme = _safe_get_theme(force_no_color=True).traceback row.append( ' File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format( theme.filename, @@ -1336,9 +1366,9 @@ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs): """ colorize = kwargs.get("colorize", False) if colorize: - theme = _colorize.get_theme(force_color=True).traceback + theme = _safe_get_theme(force_color=True).traceback else: - theme = _colorize.get_theme(force_no_color=True).traceback + theme = _safe_get_theme(force_no_color=True).traceback indent = 3 * _depth * ' ' if not self._have_exc_type: @@ -1486,9 +1516,9 @@ def _format_syntax_error(self, stype, **kwargs): # Show exactly where the problem was found. colorize = kwargs.get("colorize", False) if colorize: - theme = _colorize.get_theme(force_color=True).traceback + theme = _safe_get_theme(force_color=True).traceback else: - theme = _colorize.get_theme(force_no_color=True).traceback + theme = _safe_get_theme(force_no_color=True).traceback filename_suffix = '' if self.lineno is not None: yield ' File {}"{}"{}, line {}{}{}\n'.format( diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py index 5f22d91aebd05f..893fcba968c3ef 100644 --- a/Lib/unittest/runner.py +++ b/Lib/unittest/runner.py @@ -4,11 +4,10 @@ import time import warnings -from _colorize import get_theme - from . import result from .case import _SubTest from .signals import registerResult +lazy from _colorize import get_theme __unittest = True diff --git a/Misc/NEWS.d/next/Library/2026-05-03-17-32-24.gh-issue-144384.q-8jSr.rst b/Misc/NEWS.d/next/Library/2026-05-03-17-32-24.gh-issue-144384.q-8jSr.rst new file mode 100644 index 00000000000000..aad4b716e05372 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-03-17-32-24.gh-issue-144384.q-8jSr.rst @@ -0,0 +1 @@ +Lazily import :mod:`!_colorize`. Patch by Hugo van Kemenade.