diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index f547ef730c..deadd1c0e1 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -24,6 +24,7 @@ from django.core.management.base import ( ) from django.core.management.color import color_style from django.utils import autoreload +from django.utils.version import PY314 def find_commands(management_dir): @@ -364,11 +365,16 @@ class ManagementUtility: # Preprocess options to extract --settings and --pythonpath. # These options could affect the commands that are available, so they # must be processed early. + if PY314: + color_kwargs = {"color": os.environ.get("DJANGO_COLORS") != "nocolor"} + else: + color_kwargs = {} parser = CommandParser( prog=self.prog_name, usage="%(prog)s subcommand [options] [args]", add_help=False, allow_abbrev=False, + **color_kwargs, ) parser.add_argument("--settings") parser.add_argument("--pythonpath") diff --git a/django/core/management/base.py b/django/core/management/base.py index 92a3abb01e..6c471acc05 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -15,6 +15,7 @@ from django.core import checks from django.core.exceptions import ImproperlyConfigured from django.core.management.color import color_style, no_style from django.db import DEFAULT_DB_ALIAS, connections +from django.utils.version import PY314 ALL_CHECKS = "__all__" @@ -301,11 +302,18 @@ class BaseCommand: parse the arguments to this command. """ kwargs.setdefault("formatter_class", DjangoHelpFormatter) + # argparse's color defaults to True on Python 3.14+. + # Respect DJANGO_COLORS=nocolor by explicitly passing color=False. + if PY314: + color_kwargs = {"color": os.environ.get("DJANGO_COLORS") != "nocolor"} + else: + color_kwargs = {} parser = CommandParser( prog="%s %s" % (os.path.basename(prog_name), subcommand), description=self.help or None, missing_args_message=getattr(self, "missing_args_message", None), called_from_command_line=getattr(self, "_called_from_command_line", None), + **color_kwargs, **kwargs, ) self.add_base_argument( diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 0c27194568..9d27ba3f47 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -38,7 +38,7 @@ from django.db.migrations.recorder import MigrationRecorder from django.test import LiveServerTestCase, SimpleTestCase, TestCase, override_settings from django.test.utils import captured_stderr, captured_stdout from django.urls import path -from django.utils.version import PY313, get_docs_version +from django.utils.version import PY313, PY314, get_docs_version from django.views.static import serve from . import urls @@ -3332,3 +3332,19 @@ class DjangoAdminSuggestions(AdminScriptTestCase): out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertNotInOutput(err, "Did you mean") + + +class AdminScriptsColorizedHelp(AdminScriptTestCase): + @unittest.skipUnless(PY314, "argparse colorized help requires Python 3.14+") + def test_help_is_colorized_on_py314(self): + # Use a command that doesn't need settings. + with mock.patch.dict(os.environ, {}, clear=False): + out, err = self.run_django_admin(["startproject", "--help"]) + # Look for ANSI SGR sequences. + self.assertRegex(out, r"\x1b\\[[0-9;]*m") + + @unittest.skipUnless(PY314, "argparse colorized help requires Python 3.14+") + def test_help_not_colorized_when_django_colors_nocolor(self): + with mock.patch.dict(os.environ, {"DJANGO_COLORS": "nocolor"}, clear=False): + out, err = self.run_django_admin(["startproject", "--help"]) + self.assertNotRegex(out, r"\x1b\\[[0-9;]*m")