Refs #33476 -- Made management commands use black.

Run black on generated files, if it is available on PATH.
This commit is contained in:
Carlton Gibson 2022-02-08 12:38:43 +01:00 committed by Mariusz Felisiak
parent f9ec777a82
commit d113b5a837
10 changed files with 96 additions and 16 deletions

View File

@ -6,6 +6,7 @@ from itertools import takewhile
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError, no_translations from django.core.management.base import BaseCommand, CommandError, no_translations
from django.core.management.utils import run_formatters
from django.db import DEFAULT_DB_ALIAS, OperationalError, connections, router from django.db import DEFAULT_DB_ALIAS, OperationalError, connections, router
from django.db.migrations import Migration from django.db.migrations import Migration
from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.autodetector import MigrationAutodetector
@ -88,6 +89,7 @@ class Command(BaseCommand):
@no_translations @no_translations
def handle(self, *app_labels, **options): def handle(self, *app_labels, **options):
self.written_files = []
self.verbosity = options["verbosity"] self.verbosity = options["verbosity"]
self.interactive = options["interactive"] self.interactive = options["interactive"]
self.dry_run = options["dry_run"] self.dry_run = options["dry_run"]
@ -276,6 +278,7 @@ class Command(BaseCommand):
migration_string = writer.as_string() migration_string = writer.as_string()
with open(writer.path, "w", encoding="utf-8") as fh: with open(writer.path, "w", encoding="utf-8") as fh:
fh.write(migration_string) fh.write(migration_string)
self.written_files.append(writer.path)
elif self.verbosity == 3: elif self.verbosity == 3:
# Alternatively, makemigrations --dry-run --verbosity 3 # Alternatively, makemigrations --dry-run --verbosity 3
# will log the migrations rather than saving the file to # will log the migrations rather than saving the file to
@ -286,6 +289,7 @@ class Command(BaseCommand):
) )
) )
self.log(writer.as_string()) self.log(writer.as_string())
run_formatters(self.written_files)
def handle_merge(self, loader, conflicts): def handle_merge(self, loader, conflicts):
""" """
@ -382,6 +386,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])
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

@ -3,6 +3,7 @@ import os
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.core.management.utils import run_formatters
from django.db import DEFAULT_DB_ALIAS, connections, migrations from django.db import DEFAULT_DB_ALIAS, connections, migrations
from django.db.migrations.loader import AmbiguityError, MigrationLoader from django.db.migrations.loader import AmbiguityError, MigrationLoader
from django.db.migrations.migration import SwappableTuple from django.db.migrations.migration import SwappableTuple
@ -220,6 +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])
if self.verbosity > 0: if self.verbosity > 0:
self.stdout.write( self.stdout.write(

View File

@ -12,7 +12,7 @@ from urllib.request import build_opener
import django import django
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.core.management.utils import handle_extensions from django.core.management.utils import handle_extensions, run_formatters
from django.template import Context, Engine from django.template import Context, Engine
from django.utils import archive from django.utils import archive
from django.utils.version import get_docs_version from django.utils.version import get_docs_version
@ -80,6 +80,7 @@ class TemplateCommand(BaseCommand):
) )
def handle(self, app_or_project, name, target=None, **options): def handle(self, app_or_project, name, target=None, **options):
self.written_files = []
self.app_or_project = app_or_project self.app_or_project = app_or_project
self.a_or_an = "an" if app_or_project == "app" else "a" self.a_or_an = "an" if app_or_project == "app" else "a"
self.paths_to_remove = [] self.paths_to_remove = []
@ -200,6 +201,7 @@ class TemplateCommand(BaseCommand):
else: else:
shutil.copyfile(old_path, new_path) shutil.copyfile(old_path, new_path)
self.written_files.append(new_path)
if self.verbosity >= 2: if self.verbosity >= 2:
self.stdout.write("Creating %s" % new_path) self.stdout.write("Creating %s" % new_path)
try: try:
@ -222,6 +224,8 @@ class TemplateCommand(BaseCommand):
else: else:
shutil.rmtree(path_to_remove) shutil.rmtree(path_to_remove)
run_formatters(self.written_files)
def handle_template(self, template, subdir): def handle_template(self, template, subdir):
""" """
Determine where the app or project templates are. Determine where the app or project templates are.

View File

@ -1,5 +1,7 @@
import fnmatch import fnmatch
import os import os
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from subprocess import run from subprocess import run
@ -153,3 +155,14 @@ def is_ignored_path(path, ignore_patterns):
) )
return any(ignore(pattern) for pattern in normalize_path_patterns(ignore_patterns)) return any(ignore(pattern) for pattern in normalize_path_patterns(ignore_patterns))
def run_formatters(written_files):
"""
Run the black formatter on the specified files.
"""
if black_path := shutil.which("black"):
subprocess.run(
[black_path, "--fast", "--", *written_files],
capture_output=True,
)

View File

@ -2050,6 +2050,24 @@ distribution. It enables tab-completion of ``django-admin`` and
See :doc:`/howto/custom-management-commands` for how to add customized actions. See :doc:`/howto/custom-management-commands` for how to add customized actions.
Black formatting
----------------
.. versionadded:: 4.1
The Python files created by :djadmin:`startproject`, :djadmin:`startapp`,
:djadmin:`makemigrations`, and :djadmin:`squashmigrations` are formatted using
the ``black`` command if it is present on your ``PATH``.
If you have ``black`` globally installed, but do not wish it used for the
current project, you can set the ``PATH`` explicitly::
PATH=path/to/venv/bin django-admin makemigrations
For commands using ``stdout`` you can pipe the output to ``black`` if needed::
django-admin inspectdb | black -
========================================== ==========================================
Running management commands from your code Running management commands from your code
========================================== ==========================================

View File

@ -226,6 +226,10 @@ Management Commands
* The new :option:`migrate --prune` option allows deleting nonexistent * The new :option:`migrate --prune` option allows deleting nonexistent
migrations from the ``django_migrations`` table. migrations from the ``django_migrations`` table.
* Python files created by :djadmin:`startproject`, :djadmin:`startapp`,
:djadmin:`makemigrations`, and :djadmin:`squashmigrations` are now formatted
using the ``black`` command if it is present on your ``PATH``.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -1,6 +1,6 @@
# The manage.py of the {{ project_name }} test project # The manage.py of the {{ project_name }} test project
# template context: # template context:
project_name = '{{ project_name }}' project_name = "{{ project_name }}"
project_directory = '{{ project_directory }}' project_directory = "{{ project_directory }}"
secret_key = '{{ secret_key }}' secret_key = "{{ secret_key }}"

View File

@ -41,6 +41,8 @@ custom_templates_dir = os.path.join(os.path.dirname(__file__), "custom_templates
SYSTEM_CHECK_MSG = "System check identified no issues" SYSTEM_CHECK_MSG = "System check identified no issues"
HAS_BLACK = shutil.which("black")
class AdminScriptTestCase(SimpleTestCase): class AdminScriptTestCase(SimpleTestCase):
def setUp(self): def setUp(self):
@ -732,7 +734,10 @@ class DjangoAdminSettingsDirectory(AdminScriptTestCase):
with open(os.path.join(app_path, "apps.py")) as f: with open(os.path.join(app_path, "apps.py")) as f:
content = f.read() content = f.read()
self.assertIn("class SettingsTestConfig(AppConfig)", content) self.assertIn("class SettingsTestConfig(AppConfig)", content)
self.assertIn("name = 'settings_test'", content) self.assertIn(
'name = "settings_test"' if HAS_BLACK else "name = 'settings_test'",
content,
)
def test_setup_environ_custom_template(self): def test_setup_environ_custom_template(self):
"directory: startapp creates the correct directory with a custom template" "directory: startapp creates the correct directory with a custom template"
@ -754,7 +759,7 @@ class DjangoAdminSettingsDirectory(AdminScriptTestCase):
with open(os.path.join(app_path, "apps.py"), encoding="utf8") as f: with open(os.path.join(app_path, "apps.py"), encoding="utf8") as f:
content = f.read() content = f.read()
self.assertIn("class こんにちはConfig(AppConfig)", content) self.assertIn("class こんにちはConfig(AppConfig)", content)
self.assertIn("name = 'こんにちは'", content) self.assertIn('name = "こんにちは"' if HAS_BLACK else "name = 'こんにちは'", content)
def test_builtin_command(self): def test_builtin_command(self):
""" """
@ -2614,8 +2619,8 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
test_manage_py = os.path.join(testproject_dir, "manage.py") test_manage_py = os.path.join(testproject_dir, "manage.py")
with open(test_manage_py) as fp: with open(test_manage_py) as fp:
content = fp.read() content = fp.read()
self.assertIn("project_name = 'another_project'", content) self.assertIn('project_name = "another_project"', content)
self.assertIn("project_directory = '%s'" % testproject_dir, content) self.assertIn('project_directory = "%s"' % testproject_dir, content)
def test_no_escaping_of_project_variables(self): def test_no_escaping_of_project_variables(self):
"Make sure template context variables are not html escaped" "Make sure template context variables are not html escaped"
@ -2880,11 +2885,15 @@ class StartApp(AdminScriptTestCase):
with open(os.path.join(app_path, "apps.py")) as f: with open(os.path.join(app_path, "apps.py")) as f:
content = f.read() content = f.read()
self.assertIn("class NewAppConfig(AppConfig)", content) self.assertIn("class NewAppConfig(AppConfig)", content)
if HAS_BLACK:
test_str = 'default_auto_field = "django.db.models.BigAutoField"'
else:
test_str = "default_auto_field = 'django.db.models.BigAutoField'"
self.assertIn(test_str, content)
self.assertIn( self.assertIn(
"default_auto_field = 'django.db.models.BigAutoField'", 'name = "new_app"' if HAS_BLACK else "name = 'new_app'",
content, content,
) )
self.assertIn("name = 'new_app'", content)
class DiffSettings(AdminScriptTestCase): class DiffSettings(AdminScriptTestCase):

View File

@ -2,6 +2,7 @@ import datetime
import importlib import importlib
import io import io
import os import os
import shutil
import sys import sys
from unittest import mock from unittest import mock
@ -28,6 +29,8 @@ from .models import UnicodeModel, UnserializableModel
from .routers import TestRouter from .routers import TestRouter
from .test_base import MigrationTestBase from .test_base import MigrationTestBase
HAS_BLACK = shutil.which("black")
class MigrateTests(MigrationTestBase): class MigrateTests(MigrationTestBase):
""" """
@ -1524,8 +1527,12 @@ class MakeMigrationsTests(MigrationTestBase):
# Remove all whitespace to check for empty dependencies and operations # Remove all whitespace to check for empty dependencies and operations
content = content.replace(" ", "") content = content.replace(" ", "")
self.assertIn("dependencies=[\n]", content) self.assertIn(
self.assertIn("operations=[\n]", content) "dependencies=[]" if HAS_BLACK else "dependencies=[\n]", content
)
self.assertIn(
"operations=[]" if HAS_BLACK else "operations=[\n]", content
)
@override_settings(MIGRATION_MODULES={"migrations": None}) @override_settings(MIGRATION_MODULES={"migrations": None})
def test_makemigrations_disabled_migrations_for_app(self): def test_makemigrations_disabled_migrations_for_app(self):
@ -1661,6 +1668,13 @@ class MakeMigrationsTests(MigrationTestBase):
"0003_merge_0002_conflicting_second_0002_second.py", "0003_merge_0002_conflicting_second_0002_second.py",
) )
self.assertIs(os.path.exists(merge_file), True) self.assertIs(os.path.exists(merge_file), True)
with open(merge_file, encoding="utf-8") as fp:
content = fp.read()
if HAS_BLACK:
target_str = '("migrations", "0002_conflicting_second")'
else:
target_str = "('migrations', '0002_conflicting_second')"
self.assertIn(target_str, content)
self.assertIn("Created new merge migration %s" % merge_file, out.getvalue()) self.assertIn("Created new merge migration %s" % merge_file, out.getvalue())
@mock.patch("django.db.migrations.utils.datetime") @mock.patch("django.db.migrations.utils.datetime")
@ -2252,7 +2266,9 @@ class MakeMigrationsTests(MigrationTestBase):
# generate an initial migration # generate an initial migration
migration_name_0001 = "my_initial_migration" migration_name_0001 = "my_initial_migration"
content = cmd("0001", migration_name_0001) content = cmd("0001", migration_name_0001)
self.assertIn("dependencies=[\n]", content) self.assertIn(
"dependencies=[]" if HAS_BLACK else "dependencies=[\n]", content
)
# importlib caches os.listdir() on some platforms like macOS # importlib caches os.listdir() on some platforms like macOS
# (#23850). # (#23850).
@ -2262,11 +2278,15 @@ class MakeMigrationsTests(MigrationTestBase):
# generate an empty migration # generate an empty migration
migration_name_0002 = "my_custom_migration" migration_name_0002 = "my_custom_migration"
content = cmd("0002", migration_name_0002, "--empty") content = cmd("0002", migration_name_0002, "--empty")
if HAS_BLACK:
template_str = 'dependencies=[\n("migrations","0001_%s"),\n]'
else:
template_str = "dependencies=[\n('migrations','0001_%s'),\n]"
self.assertIn( self.assertIn(
"dependencies=[\n('migrations','0001_%s'),\n]" % migration_name_0001, template_str % migration_name_0001,
content, content,
) )
self.assertIn("operations=[\n]", content) self.assertIn("operations=[]" if HAS_BLACK else "operations=[\n]", content)
def test_makemigrations_with_invalid_custom_name(self): def test_makemigrations_with_invalid_custom_name(self):
msg = "The migration name must be a valid Python identifier." msg = "The migration name must be a valid Python identifier."
@ -2606,7 +2626,11 @@ class SquashMigrationsTests(MigrationTestBase):
) )
with open(squashed_migration_file, encoding="utf-8") as fp: with open(squashed_migration_file, encoding="utf-8") as fp:
content = fp.read() content = fp.read()
self.assertIn(" ('migrations', '0001_initial')", content) if HAS_BLACK:
test_str = ' ("migrations", "0001_initial")'
else:
test_str = " ('migrations', '0001_initial')"
self.assertIn(test_str, content)
self.assertNotIn("initial = True", content) self.assertNotIn("initial = True", content)
out = out.getvalue() out = out.getvalue()
self.assertNotIn(" - 0001_initial", out) self.assertNotIn(" - 0001_initial", out)

View File

@ -3,6 +3,7 @@ asgiref >= 3.4.1
argon2-cffi >= 16.1.0 argon2-cffi >= 16.1.0
backports.zoneinfo; python_version < '3.9' backports.zoneinfo; python_version < '3.9'
bcrypt bcrypt
black
docutils docutils
geoip2 geoip2
jinja2 >= 2.9.2 jinja2 >= 2.9.2