From ec0e784f91b551c654f0962431cc31091926792d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 4 Jan 2025 03:29:53 +0000 Subject: [PATCH 1/2] Fixed #36056 -- Made OutputWrapper a virtual subclass of TextIOBase. This fixes the ignored exception in self._out.flush() from django.core.management.base.OutputWrapper: `ValueError: I/O operation on closed file.` --- django/core/management/base.py | 5 ++++- tests/user_commands/tests.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/django/core/management/base.py b/django/core/management/base.py index ba38ae17482b..12b4b42f6147 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -142,7 +142,7 @@ def add_arguments(self, actions): super().add_arguments(self._reordered_actions(actions)) -class OutputWrapper(TextIOBase): +class OutputWrapper: """ Wrapper around stdout/stderr """ @@ -181,6 +181,9 @@ def write(self, msg="", style_func=None, ending=None): self._out.write(style_func(msg)) +TextIOBase.register(OutputWrapper) + + class BaseCommand: """ The base class from which all management commands ultimately diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 2add272c1024..acc338685e0a 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -1,7 +1,7 @@ import os import sys from argparse import ArgumentDefaultsHelpFormatter -from io import StringIO +from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path from unittest import mock @@ -11,6 +11,7 @@ from django.core import management from django.core.checks import Tags from django.core.management import BaseCommand, CommandError, find_commands +from django.core.management.base import OutputWrapper from django.core.management.utils import ( find_command, get_random_secret_key, @@ -28,6 +29,29 @@ from .utils import AssertFormatterFailureCaughtContext +class OutputWrapperTests(SimpleTestCase): + def test_unhandled_exceptions(self): + cases = [ + StringIO("Hello world"), + TextIOWrapper(BytesIO(b"Hello world")), + ] + for out in cases: + with self.subTest(out=out): + wrapper = OutputWrapper(out) + out.close() + + unraisable_exceptions = [] + + def unraisablehook(unraisable): + unraisable_exceptions.append(unraisable) + sys.__unraisablehook__(unraisable) + + with mock.patch.object(sys, "unraisablehook", unraisablehook): + del wrapper + + self.assertEqual(unraisable_exceptions, []) + + # A minimal set of apps to avoid system checks running on all apps. @override_settings( INSTALLED_APPS=[ From 51df0dff3c4f28016185a9e876ee5b3420712f99 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 4 Jan 2025 03:30:40 +0000 Subject: [PATCH 2/2] Fixed #36057 -- Enabled test runner to debug chained exceptions with `--pdb` on Python 3.13+. --- django/test/runner.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/django/test/runner.py b/django/test/runner.py index a52c52fe2189..b83cd373433b 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -30,7 +30,7 @@ from django.test.utils import teardown_databases as _teardown_databases from django.test.utils import teardown_test_environment from django.utils.datastructures import OrderedSet -from django.utils.version import PY312 +from django.utils.version import PY312, PY313 try: import ipdb as pdb @@ -126,7 +126,10 @@ def debug(self, error): self.buffer = False exc_type, exc_value, traceback = error print("\nOpening PDB: %r" % exc_value) - pdb.post_mortem(traceback) + if PY313: + pdb.post_mortem(exc_value) + else: + pdb.post_mortem(traceback) class DummyList: