├── .github ├── FUNDING.yml └── workflows │ └── test-deploy.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── django_migrations_formatter ├── __init__.py └── apps.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── settings.py └── tests.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [MarkusH] 2 | -------------------------------------------------------------------------------- /.github/workflows/test-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test & Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "[0-9]+.[0-9]+.[0-9]+" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | name: "Test Python ${{ matrix.python-version }} & Django ${{ matrix.django-version }}" 17 | strategy: 18 | matrix: 19 | django-version: ["2.2.0", "3.2.0", "4.0.0"] 20 | python-version: ["3.8", "3.9", "3.10"] 21 | exclude: 22 | # 2.2 23 | - django-version: "2.2.0" 24 | python-version: "3.10" 25 | fail-fast: false 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - uses: actions/cache@v2 33 | name: Configure pip caching 34 | with: 35 | path: ~/.cache/pip 36 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pip- 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install -U pip 42 | python -m pip install Django~=${{ matrix.django-version }} 43 | python -m pip install '.[black,isort,test]' 44 | - name: Run tests 45 | run: | 46 | coverage run "$(command -v django-admin)" test --pythonpath . -v 2 --settings=tests.settings 47 | coverage report 48 | - name: Upload coverage to codecov.io 49 | uses: codecov/codecov-action@v1 50 | with: 51 | token: ${{ secrets.CODECOV_TOKEN }} 52 | deploy: 53 | if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') }} 54 | runs-on: ubuntu-latest 55 | needs: [test] 56 | steps: 57 | - uses: actions/checkout@v2 58 | - name: Set up Python 59 | uses: actions/setup-python@v2 60 | with: 61 | python-version: "3.9" 62 | - uses: actions/cache@v2 63 | name: Configure pip caching 64 | with: 65 | path: ~/.cache/pip 66 | key: ${{ runner.os }}-publish-pip-${{ hashFiles('setup.py') }} 67 | restore-keys: | 68 | ${{ runner.os }}-publish-pip- 69 | - name: Install dependencies 70 | run: | 71 | pip install -U pip 72 | python -m pip install setuptools wheel twine 73 | - name: Publish 74 | env: 75 | TWINE_USERNAME: __token__ 76 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 77 | run: | 78 | python setup.py sdist bdist_wheel 79 | twine upload dist/* 80 | - name: Create Release 81 | uses: actions/create-release@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | tag_name: ${{ github.ref }} 86 | release_name: Release ${{ github.ref }} 87 | draft: false 88 | prerelease: false 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.py[cod] 3 | *$py.class 4 | __pycache__ 5 | build 6 | dist 7 | .eggs 8 | .DS_Store 9 | .tox 10 | .vscode 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.1.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: "22.1.0" 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/PyCQA/flake8 13 | rev: "4.0.1" 14 | hooks: 15 | - id: flake8 16 | - repo: https://github.com/PyCQA/isort 17 | rev: "5.10.1" 18 | hooks: 19 | - id: isort 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Under development 4 | 5 | - Remove support for Python 3.7 6 | 7 | ## 1.0.0 (2022-01-30) 8 | 9 | - Removed support for Python 3.6. 10 | 11 | - Removed support for Django 3.0 and 3.1. 12 | 13 | - A minimum version of `black>=22.1.0` is required. 14 | 15 | - Added the `black` and `isort` installation extras. One can now automatically 16 | install black and isort with `python -m pip install 17 | "django-migrations-formatter[black,isort]"`. 18 | 19 | ## 0.1.5 (2021-10-06) 20 | 21 | * Add explicit support for Django 4.0 and Python 3.10 22 | 23 | ## 0.1.4 (2021-04-06) 24 | 25 | * Add explicit support for Django 3.2 26 | 27 | ## 0.1.3 (2021-04-02) 28 | 29 | * Another shot at a single-file GitHub workflow 30 | 31 | ## 0.1.2 (2021-04-02) 32 | 33 | * Fix release building 34 | 35 | ## 0.1.1 (2021-04-02) 36 | 37 | * Use [pre-commit.ci](https://results.pre-commit.ci/repo/github/351587462) for linting 38 | 39 | * Use a single workflow file 40 | 41 | ## 0.1.0 (2021-03-26) 42 | 43 | * Initial version. Django Migration files are automatically formatted with 44 | black and isort of those tools are installed. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Markus Holtermann. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-migrations-formatter 2 | 3 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/MarkusH/django-migrations-formatter/Test%20&%20Deploy/main?style=for-the-badge)](https://github.com/MarkusH/django-migrations-formatter/actions?query=branch%3Amain+event%3Apush) 4 | [![Codecov branch](https://img.shields.io/codecov/c/gh/MarkusH/django-migrations-formatter/main?style=for-the-badge)](https://app.codecov.io/gh/MarkusH/django-migrations-formatter/branch/main) 5 | [![Version](https://img.shields.io/pypi/v/django-migrations-formatter?label=Version&style=for-the-badge)](https://pypi.org/project/django-migrations-formatter/) 6 | ![License](https://img.shields.io/pypi/l/django-migrations-formatter?style=for-the-badge) 7 | ![Python Versions](https://img.shields.io/pypi/pyversions/django-migrations-formatter?label=Python&style=for-the-badge) 8 | ![Django Versions](https://img.shields.io/pypi/djversions/django-migrations-formatter?color=%230C4B33&label=Django&style=for-the-badge) 9 | 10 | This Django library will format Django migrations using 11 | [black](https://pypi.org/project/black/) and [isort](https://pypi.org/project/isort/). 12 | 13 | ## Installation 14 | 15 | Start by installing `django-migrations-formatter` from PyPI: 16 | 17 | ```console 18 | (env)$ python -m pip install django-migrations-formatter 19 | ``` 20 | 21 | You will also need to make sure to have `black` and/or `isort` installed. 22 | Without them, this library doesn't provide any value. For ease of use, you can 23 | install either of them by including them as "extras" during the installation. 24 | 25 | ```console 26 | (env)$ python -m pip install "django-migrations-formatter[black,isort]" 27 | ``` 28 | 29 | Then you need to add `django_migrations_formatter.apps.MigrationsFormatter` to 30 | your `INSTALLED_APPS`: 31 | 32 | ```python 33 | INSTALLED_APPS = [ 34 | ..., 35 | 'django_migrations_formatter.apps.MigrationsFormatter', 36 | ] 37 | ``` 38 | 39 | ## Contributing 40 | 41 | The project uses [black](https://pypi.org/project/black/) and 42 | [isort](https://pypi.org/project/isort/) for formatting its code. 43 | [flake8](https://pypi.org/project/flake8/) is used for linting. All these are 44 | combined into [pre-commit](https://pre-commit.com/) to run before each commit 45 | and push. To set it up: 46 | 47 | ```console 48 | (env)$ python -m pip install '.[black,dev,isort,test]' 49 | (env)$ pre-commit install -t pre-commit -t pre-push --install-hooks 50 | ``` 51 | 52 | To run the unit tests: 53 | 54 | ```console 55 | (env)$ django-admin test --pythonpath . -v 2 --settings=tests.settings 56 | ``` 57 | 58 | If you spot an problem, please [open an issue](https://github.com/MarkusH/django-migrations-formatter/issues/new) 59 | on GitHub. 60 | -------------------------------------------------------------------------------- /django_migrations_formatter/__init__.py: -------------------------------------------------------------------------------- 1 | from django.db.migrations.writer import MigrationWriter 2 | 3 | try: 4 | import black 5 | import black.const 6 | except ImportError: # pragma: no cover 7 | BLACK_INSTALLED = False 8 | else: 9 | BLACK_INSTALLED = True 10 | 11 | try: 12 | import isort 13 | except ImportError: # pragma: no cover 14 | ISORT_INSTALLED = False 15 | else: 16 | ISORT_INSTALLED = True 17 | 18 | 19 | def format_black(self, content): 20 | config_file = black.find_pyproject_toml((self.basedir,)) 21 | if config_file: 22 | config = black.parse_pyproject_toml(config_file) 23 | else: 24 | config = {} 25 | versions = config.get("target_version", []) 26 | mode = black.Mode( 27 | target_versions={black.TargetVersion[val.upper()] for val in versions}, 28 | line_length=config.get("line_length") or black.const.DEFAULT_LINE_LENGTH, 29 | string_normalization=not config.get("skip_string_normalization"), 30 | is_pyi=bool(config.get("pyi")), 31 | ) 32 | return black.format_str(content, mode=mode) 33 | 34 | 35 | def format_isort(self, content): 36 | return isort.code(content) 37 | 38 | 39 | def as_string(self): 40 | content = self._as_string() 41 | if self._black_installed: 42 | content = self._format_black(content) 43 | if self._isort_installed: 44 | content = self._format_isort(content) 45 | return content 46 | 47 | 48 | def patch_migration_writer(): 49 | MigrationWriter._as_string = MigrationWriter.as_string 50 | MigrationWriter._black_installed = BLACK_INSTALLED 51 | MigrationWriter._isort_installed = ISORT_INSTALLED 52 | MigrationWriter._format_black = format_black 53 | MigrationWriter._format_isort = format_isort 54 | MigrationWriter.as_string = as_string 55 | -------------------------------------------------------------------------------- /django_migrations_formatter/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from . import patch_migration_writer 4 | 5 | 6 | class MigrationsFormatter(AppConfig): 7 | name = "django_migrations_formatter" 8 | 9 | def ready(self) -> None: 10 | patch_migration_writer() 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6,<7"] 3 | 4 | [tool.black] 5 | target-version = ['py38'] 6 | 7 | [tool.coverage.run] 8 | branch = true 9 | source = ["django_migrations_formatter/"] 10 | 11 | [tool.coverage.report] 12 | show_missing = true 13 | fail_under = 100 14 | 15 | [tool.isort] 16 | combine_as_imports = true 17 | known_first_party = ["django_migrations_formatter", "tests"] 18 | profile = "black" 19 | 20 | [tool.setuptools_scm] 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as f: 4 | long_description = f.read() 5 | 6 | setuptools.setup( 7 | name="django-migrations-formatter", 8 | author="Markus Holtermann", 9 | author_email="info@markusholtermann.eu", 10 | description="A Django library to automatically format your migrations.", 11 | license="BSD", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/MarkusH/django-migrations-formatter", 15 | project_urls={ 16 | "CI": "https://github.com/MarkusH/django-migrations-formatter/actions", # noqa 17 | "Changelog": "https://github.com/MarkusH/django-migrations-formatter/blob/main/CHANGELOG.md", # noqa 18 | "Issues": "https://github.com/MarkusH/django-migrations-formatter/issues", # noqa 19 | }, 20 | packages=setuptools.find_packages( 21 | exclude=["*.tests", "*.tests.*", "tests.*", "tests"] 22 | ), 23 | include_package_data=True, 24 | extras_require={ 25 | "black": [ 26 | "black>=22.1.0", 27 | ], 28 | "dev": ["pre-commit"], 29 | "isort": [ 30 | "isort", 31 | ], 32 | "test": [ 33 | "coverage[toml]>=6,<7", 34 | "Django", 35 | ], 36 | }, 37 | setup_requires=["setuptools_scm>=6,<7"], 38 | use_scm_version=True, 39 | classifiers=[ 40 | "Development Status :: 3 - Alpha", 41 | "Environment :: Web Environment", 42 | "Framework :: Django", 43 | "Framework :: Django :: 2.2", 44 | "Framework :: Django :: 3.2", 45 | "Framework :: Django :: 4.0", 46 | "Intended Audience :: Developers", 47 | "License :: OSI Approved :: BSD License", 48 | "Operating System :: OS Independent", 49 | "Programming Language :: Python", 50 | "Programming Language :: Python :: 3", 51 | "Programming Language :: Python :: 3.8", 52 | "Programming Language :: Python :: 3.9", 53 | "Programming Language :: Python :: 3.10", 54 | ], 55 | python_requires=">=3.8", 56 | ) 57 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkusH/django-migrations-formatter/daaebc632ea015ba4cda4e40b6f52c751dfa422a/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = "not-so-secret" 2 | DEBUG = True 3 | 4 | INSTALLED_APPS = ["django_migrations_formatter.apps.MigrationsFormatter"] 5 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import textwrap 3 | from unittest import mock 4 | 5 | from django.db import migrations, models 6 | from django.db.migrations.writer import MigrationWriter 7 | from django.test import SimpleTestCase 8 | 9 | 10 | class TestMigrationWriter(SimpleTestCase): 11 | def get_migration(self): 12 | fields = { 13 | "charfield": models.DateTimeField(default=datetime.datetime.utcnow), 14 | "datetimefield": models.DateTimeField(default=datetime.datetime.utcnow), 15 | } 16 | 17 | options = { 18 | "verbose_name": "My model", 19 | "verbose_name_plural": "My models", 20 | } 21 | 22 | migration = type( 23 | "Migration", 24 | (migrations.Migration,), 25 | { 26 | "app_label": "django_migrations_formatter", 27 | "dependencies": [("testapp", "some_other_one")], 28 | "operations": [ 29 | migrations.CreateModel( 30 | "MyModel", tuple(fields.items()), options, (models.Model,) 31 | ), 32 | migrations.CreateModel( 33 | "MyModel2", tuple(fields.items()), bases=(models.Model,) 34 | ), 35 | migrations.CreateModel( 36 | name="MyModel3", 37 | fields=tuple(fields.items()), 38 | options=options, 39 | bases=(models.Model,), 40 | ), 41 | migrations.DeleteModel("MyModel"), 42 | migrations.AddField( 43 | "OtherModel", "datetimefield", fields["datetimefield"] 44 | ), 45 | ], 46 | }, 47 | ) 48 | 49 | return migration 50 | 51 | def assertInOutput(self, expected, output): 52 | if expected not in output: 53 | print("Expected this:") 54 | print(expected) 55 | print("to be in:") 56 | print(output) 57 | self.fail("Failed") 58 | 59 | def test_black_and_isort(self): 60 | writer = MigrationWriter(self.get_migration()) 61 | output = writer.as_string() 62 | expected = textwrap.dedent( 63 | """ 64 | import datetime 65 | 66 | from django.db import migrations, models 67 | 68 | 69 | class Migration(migrations.Migration): 70 | 71 | dependencies = [ 72 | ("testapp", "some_other_one"), 73 | ] 74 | 75 | operations = [ 76 | migrations.CreateModel( 77 | name="MyModel", 78 | fields=[ 79 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 80 | ( 81 | "datetimefield", 82 | models.DateTimeField(default=datetime.datetime.utcnow), 83 | ), 84 | ], 85 | options={ 86 | "verbose_name": "My model", 87 | "verbose_name_plural": "My models", 88 | }, 89 | ), 90 | migrations.CreateModel( 91 | name="MyModel2", 92 | fields=[ 93 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 94 | ( 95 | "datetimefield", 96 | models.DateTimeField(default=datetime.datetime.utcnow), 97 | ), 98 | ], 99 | ), 100 | migrations.CreateModel( 101 | name="MyModel3", 102 | fields=[ 103 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 104 | ( 105 | "datetimefield", 106 | models.DateTimeField(default=datetime.datetime.utcnow), 107 | ), 108 | ], 109 | options={ 110 | "verbose_name": "My model", 111 | "verbose_name_plural": "My models", 112 | }, 113 | ), 114 | migrations.DeleteModel( 115 | name="MyModel", 116 | ), 117 | migrations.AddField( 118 | model_name="OtherModel", 119 | name="datetimefield", 120 | field=models.DateTimeField(default=datetime.datetime.utcnow), 121 | ), 122 | ] 123 | """ # noqa 124 | ) 125 | self.assertInOutput(expected, output) 126 | 127 | def test_only_black(self): 128 | writer = MigrationWriter(self.get_migration()) 129 | writer._isort_installed = False 130 | with mock.patch( 131 | "black.parse_pyproject_toml", return_value={"line_length": 100} 132 | ): 133 | # We only call `parse_pyproject_toml()` when we found a config file. 134 | # In that case, we mock the config here to be different to the 135 | # default one (e.g. 100 vs 88 chars per line) 136 | output = writer.as_string() 137 | expected = textwrap.dedent( 138 | """ 139 | import datetime 140 | from django.db import migrations, models 141 | 142 | 143 | class Migration(migrations.Migration): 144 | 145 | dependencies = [ 146 | ("testapp", "some_other_one"), 147 | ] 148 | 149 | operations = [ 150 | migrations.CreateModel( 151 | name="MyModel", 152 | fields=[ 153 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 154 | ("datetimefield", models.DateTimeField(default=datetime.datetime.utcnow)), 155 | ], 156 | options={ 157 | "verbose_name": "My model", 158 | "verbose_name_plural": "My models", 159 | }, 160 | ), 161 | migrations.CreateModel( 162 | name="MyModel2", 163 | fields=[ 164 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 165 | ("datetimefield", models.DateTimeField(default=datetime.datetime.utcnow)), 166 | ], 167 | ), 168 | migrations.CreateModel( 169 | name="MyModel3", 170 | fields=[ 171 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 172 | ("datetimefield", models.DateTimeField(default=datetime.datetime.utcnow)), 173 | ], 174 | options={ 175 | "verbose_name": "My model", 176 | "verbose_name_plural": "My models", 177 | }, 178 | ), 179 | migrations.DeleteModel( 180 | name="MyModel", 181 | ), 182 | migrations.AddField( 183 | model_name="OtherModel", 184 | name="datetimefield", 185 | field=models.DateTimeField(default=datetime.datetime.utcnow), 186 | ), 187 | ] 188 | """ # noqa 189 | ) 190 | self.assertInOutput(expected, output) 191 | 192 | def test_only_black_without_config(self): 193 | writer = MigrationWriter(self.get_migration()) 194 | writer._isort_installed = False 195 | with mock.patch("black.find_pyproject_toml", return_value=None): 196 | output = writer.as_string() 197 | expected = textwrap.dedent( 198 | """ 199 | import datetime 200 | from django.db import migrations, models 201 | 202 | 203 | class Migration(migrations.Migration): 204 | 205 | dependencies = [ 206 | ("testapp", "some_other_one"), 207 | ] 208 | 209 | operations = [ 210 | migrations.CreateModel( 211 | name="MyModel", 212 | fields=[ 213 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 214 | ( 215 | "datetimefield", 216 | models.DateTimeField(default=datetime.datetime.utcnow), 217 | ), 218 | ], 219 | options={ 220 | "verbose_name": "My model", 221 | "verbose_name_plural": "My models", 222 | }, 223 | ), 224 | migrations.CreateModel( 225 | name="MyModel2", 226 | fields=[ 227 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 228 | ( 229 | "datetimefield", 230 | models.DateTimeField(default=datetime.datetime.utcnow), 231 | ), 232 | ], 233 | ), 234 | migrations.CreateModel( 235 | name="MyModel3", 236 | fields=[ 237 | ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), 238 | ( 239 | "datetimefield", 240 | models.DateTimeField(default=datetime.datetime.utcnow), 241 | ), 242 | ], 243 | options={ 244 | "verbose_name": "My model", 245 | "verbose_name_plural": "My models", 246 | }, 247 | ), 248 | migrations.DeleteModel( 249 | name="MyModel", 250 | ), 251 | migrations.AddField( 252 | model_name="OtherModel", 253 | name="datetimefield", 254 | field=models.DateTimeField(default=datetime.datetime.utcnow), 255 | ), 256 | ] 257 | """ # noqa 258 | ) 259 | self.assertInOutput(expected, output) 260 | 261 | def test_only_isort(self): 262 | writer = MigrationWriter(self.get_migration()) 263 | writer._black_installed = False 264 | output = writer.as_string() 265 | expected = textwrap.dedent( 266 | """ 267 | import datetime 268 | 269 | from django.db import migrations, models 270 | 271 | 272 | class Migration(migrations.Migration): 273 | 274 | dependencies = [ 275 | ('testapp', 'some_other_one'), 276 | ] 277 | 278 | operations = [ 279 | migrations.CreateModel( 280 | name='MyModel', 281 | fields=[ 282 | ('charfield', models.DateTimeField(default=datetime.datetime.utcnow)), 283 | ('datetimefield', models.DateTimeField(default=datetime.datetime.utcnow)), 284 | ], 285 | options={ 286 | 'verbose_name': 'My model', 287 | 'verbose_name_plural': 'My models', 288 | }, 289 | ), 290 | migrations.CreateModel( 291 | name='MyModel2', 292 | fields=[ 293 | ('charfield', models.DateTimeField(default=datetime.datetime.utcnow)), 294 | ('datetimefield', models.DateTimeField(default=datetime.datetime.utcnow)), 295 | ], 296 | ), 297 | migrations.CreateModel( 298 | name='MyModel3', 299 | fields=[ 300 | ('charfield', models.DateTimeField(default=datetime.datetime.utcnow)), 301 | ('datetimefield', models.DateTimeField(default=datetime.datetime.utcnow)), 302 | ], 303 | options={ 304 | 'verbose_name': 'My model', 305 | 'verbose_name_plural': 'My models', 306 | }, 307 | ), 308 | migrations.DeleteModel( 309 | name='MyModel', 310 | ), 311 | migrations.AddField( 312 | model_name='OtherModel', 313 | name='datetimefield', 314 | field=models.DateTimeField(default=datetime.datetime.utcnow), 315 | ), 316 | ] 317 | """ # noqa 318 | ) 319 | self.assertInOutput(expected, output) 320 | 321 | def test_neither_installed(self): 322 | writer = MigrationWriter(self.get_migration()) 323 | writer._black_installed = False 324 | writer._isort_installed = False 325 | output = writer.as_string() 326 | expected = textwrap.dedent( 327 | """ 328 | import datetime 329 | from django.db import migrations, models 330 | 331 | 332 | class Migration(migrations.Migration): 333 | 334 | dependencies = [ 335 | ('testapp', 'some_other_one'), 336 | ] 337 | 338 | operations = [ 339 | migrations.CreateModel( 340 | name='MyModel', 341 | fields=[ 342 | ('charfield', models.DateTimeField(default=datetime.datetime.utcnow)), 343 | ('datetimefield', models.DateTimeField(default=datetime.datetime.utcnow)), 344 | ], 345 | options={ 346 | 'verbose_name': 'My model', 347 | 'verbose_name_plural': 'My models', 348 | }, 349 | ), 350 | migrations.CreateModel( 351 | name='MyModel2', 352 | fields=[ 353 | ('charfield', models.DateTimeField(default=datetime.datetime.utcnow)), 354 | ('datetimefield', models.DateTimeField(default=datetime.datetime.utcnow)), 355 | ], 356 | ), 357 | migrations.CreateModel( 358 | name='MyModel3', 359 | fields=[ 360 | ('charfield', models.DateTimeField(default=datetime.datetime.utcnow)), 361 | ('datetimefield', models.DateTimeField(default=datetime.datetime.utcnow)), 362 | ], 363 | options={ 364 | 'verbose_name': 'My model', 365 | 'verbose_name_plural': 'My models', 366 | }, 367 | ), 368 | migrations.DeleteModel( 369 | name='MyModel', 370 | ), 371 | migrations.AddField( 372 | model_name='OtherModel', 373 | name='datetimefield', 374 | field=models.DateTimeField(default=datetime.datetime.utcnow), 375 | ), 376 | ] 377 | """ # noqa 378 | ) 379 | self.assertInOutput(expected, output) 380 | --------------------------------------------------------------------------------