1
0
mirror of https://github.com/django/django.git synced 2024-12-22 09:05:43 +00:00

Fixed #35308 -- Handled OSError when launching code formatters.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
This commit is contained in:
Jacob Walls 2024-11-29 07:04:48 -05:00 committed by GitHub
parent 978aae4334
commit 58cc91275a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 110 additions and 10 deletions

View File

@ -392,7 +392,7 @@ class Command(BaseCommand):
) )
) )
self.log(writer.as_string()) self.log(writer.as_string())
run_formatters(self.written_files) run_formatters(self.written_files, stderr=self.stderr)
@staticmethod @staticmethod
def get_relative_path(path): def get_relative_path(path):
@ -499,7 +499,7 @@ class Command(BaseCommand):
# Write the merge migrations file to the disk # Write the merge migrations file to the disk
with open(writer.path, "w", encoding="utf-8") as fh: with open(writer.path, "w", encoding="utf-8") as fh:
fh.write(writer.as_string()) fh.write(writer.as_string())
run_formatters([writer.path]) run_formatters([writer.path], stderr=self.stderr)
if self.verbosity > 0: if self.verbosity > 0:
self.log("\nCreated new merge migration %s" % writer.path) self.log("\nCreated new merge migration %s" % writer.path)
if self.scriptable: if self.scriptable:

View File

@ -121,7 +121,7 @@ class Command(BaseCommand):
) )
with open(writer.path, "w", encoding="utf-8") as fh: with open(writer.path, "w", encoding="utf-8") as fh:
fh.write(migration_file_string) fh.write(migration_file_string)
run_formatters([writer.path]) run_formatters([writer.path], stderr=self.stderr)
if verbosity > 0: if verbosity > 0:
self.stdout.write( self.stdout.write(

View File

@ -221,7 +221,7 @@ class Command(BaseCommand):
) )
with open(writer.path, "w", encoding="utf-8") as fh: with open(writer.path, "w", encoding="utf-8") as fh:
fh.write(writer.as_string()) fh.write(writer.as_string())
run_formatters([writer.path]) run_formatters([writer.path], stderr=self.stderr)
if self.verbosity > 0: if self.verbosity > 0:
self.stdout.write( self.stdout.write(

View File

@ -229,7 +229,7 @@ class TemplateCommand(BaseCommand):
else: else:
shutil.rmtree(path_to_remove) shutil.rmtree(path_to_remove)
run_formatters([top_dir], **formatter_paths) run_formatters([top_dir], **formatter_paths, stderr=self.stderr)
def handle_template(self, template, subdir): def handle_template(self, template, subdir):
""" """

View File

@ -2,6 +2,8 @@ import fnmatch
import os import os
import shutil import shutil
import subprocess import subprocess
import sys
import traceback
from pathlib import Path from pathlib import Path
from subprocess import run from subprocess import run
@ -161,7 +163,7 @@ def find_formatters():
return {"black_path": shutil.which("black")} return {"black_path": shutil.which("black")}
def run_formatters(written_files, black_path=(sentinel := object())): def run_formatters(written_files, black_path=(sentinel := object()), stderr=sys.stderr):
""" """
Run the black formatter on the specified files. Run the black formatter on the specified files.
""" """
@ -169,7 +171,11 @@ def run_formatters(written_files, black_path=(sentinel := object())):
if black_path is sentinel: if black_path is sentinel:
black_path = shutil.which("black") black_path = shutil.which("black")
if black_path: if black_path:
subprocess.run( try:
[black_path, "--fast", "--", *written_files], subprocess.run(
capture_output=True, [black_path, "--fast", "--", *written_files],
) capture_output=True,
)
except OSError:
stderr.write("Formatters failed to launch:")
traceback.print_exc(file=stderr)

View File

@ -16,6 +16,8 @@ import unittest
from io import StringIO from io import StringIO
from unittest import mock from unittest import mock
from user_commands.utils import AssertFormatterFailureCaughtContext
from django import conf, get_version from django import conf, get_version
from django.conf import settings from django.conf import settings
from django.core.management import ( from django.core.management import (
@ -2943,6 +2945,16 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
expected_mode, expected_mode,
) )
def test_failure_to_format_code(self):
with AssertFormatterFailureCaughtContext(self) as ctx:
call_command(
"startapp",
"mynewapp",
directory=self.test_dir,
stdout=ctx.stdout,
stderr=ctx.stderr,
)
class StartApp(AdminScriptTestCase): class StartApp(AdminScriptTestCase):
def test_invalid_name(self): def test_invalid_name(self):

View File

@ -4,7 +4,10 @@ import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from importlib import import_module from importlib import import_module
from user_commands.utils import AssertFormatterFailureCaughtContext
from django.apps import apps from django.apps import apps
from django.core.management import call_command
from django.db import connection, connections, migrations, models from django.db import connection, connections, migrations, models
from django.db.migrations.migration import Migration from django.db.migrations.migration import Migration
from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.optimizer import MigrationOptimizer
@ -168,6 +171,15 @@ class MigrationTestBase(TransactionTestCase):
def assertFKNotExists(self, table, columns, to): def assertFKNotExists(self, table, columns, to):
return self.assertFKExists(table, columns, to, False) return self.assertFKExists(table, columns, to, False)
def assertFormatterFailureCaught(
self, *args, module="migrations.test_migrations", **kwargs
):
with (
self.temporary_migration_module(module=module),
AssertFormatterFailureCaughtContext(self) as ctx,
):
call_command(*args, stdout=ctx.stdout, stderr=ctx.stderr, **kwargs)
@contextmanager @contextmanager
def temporary_migration_module(self, app_label="migrations", module=None): def temporary_migration_module(self, app_label="migrations", module=None):
""" """

View File

@ -2265,6 +2265,19 @@ class MakeMigrationsTests(MigrationTestBase):
self.assertEqual(out.getvalue(), f"{merge_file}\n") self.assertEqual(out.getvalue(), f"{merge_file}\n")
self.assertIn(f"Created new merge migration {merge_file}", err.getvalue()) self.assertIn(f"Created new merge migration {merge_file}", err.getvalue())
def test_makemigrations_failure_to_format_code(self):
self.assertFormatterFailureCaught("makemigrations", "migrations")
def test_merge_makemigrations_failure_to_format_code(self):
self.assertFormatterFailureCaught("makemigrations", "migrations", empty=True)
self.assertFormatterFailureCaught(
"makemigrations",
"migrations",
merge=True,
interactive=False,
module="migrations.test_migrations_conflict",
)
def test_makemigrations_migrations_modules_path_not_exist(self): def test_makemigrations_migrations_modules_path_not_exist(self):
""" """
makemigrations creates migrations when specifying a custom location makemigrations creates migrations when specifying a custom location
@ -3069,6 +3082,11 @@ class SquashMigrationsTests(MigrationTestBase):
+ black_warning, + black_warning,
) )
def test_failure_to_format_code(self):
self.assertFormatterFailureCaught(
"squashmigrations", "migrations", "0002", interactive=False
)
class AppLabelErrorTests(TestCase): class AppLabelErrorTests(TestCase):
""" """
@ -3302,6 +3320,9 @@ class OptimizeMigrationTests(MigrationTestBase):
with self.assertRaisesMessage(CommandError, msg): with self.assertRaisesMessage(CommandError, msg):
call_command("optimizemigration", "migrations", "nonexistent") call_command("optimizemigration", "migrations", "nonexistent")
def test_failure_to_format_code(self):
self.assertFormatterFailureCaught("optimizemigration", "migrations", "0001")
class CustomMigrationCommandTests(MigrationTestBase): class CustomMigrationCommandTests(MigrationTestBase):
@override_settings( @override_settings(

View File

@ -0,0 +1 @@
# This file is not executable, to simulate a broken `black` install.

View File

@ -1,6 +1,8 @@
import os import os
import sys
from argparse import ArgumentDefaultsHelpFormatter from argparse import ArgumentDefaultsHelpFormatter
from io import StringIO from io import StringIO
from pathlib import Path
from unittest import mock from unittest import mock
from admin_scripts.tests import AdminScriptTestCase from admin_scripts.tests import AdminScriptTestCase
@ -15,6 +17,7 @@ from django.core.management.utils import (
is_ignored_path, is_ignored_path,
normalize_path_patterns, normalize_path_patterns,
popen_wrapper, popen_wrapper,
run_formatters,
) )
from django.db import connection from django.db import connection
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
@ -22,6 +25,7 @@ from django.test.utils import captured_stderr, extend_sys_path
from django.utils import translation from django.utils import translation
from .management.commands import dance from .management.commands import dance
from .utils import AssertFormatterFailureCaughtContext
# A minimal set of apps to avoid system checks running on all apps. # A minimal set of apps to avoid system checks running on all apps.
@ -535,3 +539,24 @@ class UtilsTests(SimpleTestCase):
def test_normalize_path_patterns_truncates_wildcard_base(self): def test_normalize_path_patterns_truncates_wildcard_base(self):
expected = [os.path.normcase(p) for p in ["foo/bar", "bar/*/"]] expected = [os.path.normcase(p) for p in ["foo/bar", "bar/*/"]]
self.assertEqual(normalize_path_patterns(["foo/bar/*", "bar/*/"]), expected) self.assertEqual(normalize_path_patterns(["foo/bar/*", "bar/*/"]), expected)
def test_run_formatters_handles_oserror_for_black_path(self):
cases = [
(FileNotFoundError, "nonexistent"),
(
OSError if sys.platform == "win32" else PermissionError,
str(Path(__file__).parent / "test_files" / "black"),
),
]
for exception, location in cases:
with (
self.subTest(exception.__qualname__),
AssertFormatterFailureCaughtContext(
self, shutil_which_result=location
) as ctx,
):
run_formatters([], stderr=ctx.stderr)
parsed_error = ctx.stderr.getvalue()
self.assertIn(exception.__qualname__, parsed_error)
if sys.platform != "win32":
self.assertIn(location, parsed_error)

View File

@ -0,0 +1,23 @@
from io import StringIO
from unittest import mock
class AssertFormatterFailureCaughtContext:
def __init__(self, test, shutil_which_result="nonexistent"):
self.stdout = StringIO()
self.stderr = StringIO()
self.test = test
self.shutil_which_result = shutil_which_result
def __enter__(self):
self.mocker = mock.patch(
"django.core.management.utils.shutil.which",
return_value=self.shutil_which_result,
)
self.mocker.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.mocker.stop()
self.test.assertIn("Formatters failed to launch", self.stderr.getvalue())