Support apps with custom label.

This commit is contained in:
Oleg Samokhin authored and David Wobrock committed 2025-04-21 16:43:45 +02:00
1 parent 572ab55cb0
commit 0ee05a2b30
10 files changed
+125 -22

No files matched your search

+6 -3
View File
@@ -1,8 +1,11 @@
## x.y.z ## 6.0.0
Feature:
- **Breaking change**: Handle custom Django app label when gathering migrations from git reference. (#262)
This means that, an app that was previously referenced by its folder name, will now be referenced by its Django app label.
Miscellaneous: Miscellaneous:
- Add py.typed file (#303)
- Add py.typed file
## 5.2.0 ## 5.2.0
+40 -15
View File
@@ -7,8 +7,9 @@ import logging
import os import os
import re import re
from enum import Enum, unique from enum import Enum, unique
from importlib.util import find_spec
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from typing import Callable, Iterable from typing import Callable, Dict, Iterable
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
@@ -389,7 +390,6 @@ class MigrationLinter:
def _gather_migrations_git( def _gather_migrations_git(
self, git_commit_id: str, migrations_list: list[tuple[str, str]] | None = None self, git_commit_id: str, migrations_list: list[tuple[str, str]] | None = None
) -> Iterable[Migration]: ) -> Iterable[Migration]:
migrations = []
# Get changes since specified commit # Get changes since specified commit
git_diff_command = [ git_diff_command = [
"git", "git",
@@ -406,25 +406,50 @@ class MigrationLinter:
stderr=PIPE, stderr=PIPE,
cwd=self.django_path, cwd=self.django_path,
) )
diskpath_and_migration: Dict[str, Migration] = {}
for migration in self._gather_all_migrations():
spec = find_spec(migration.__module__)
if spec:
diskpath_and_migration[str(spec.origin)] = migration
migrations = []
for line in map( for line in map(
clean_bytes_to_str, diff_process.stdout.readlines() # type: ignore clean_bytes_to_str, diff_process.stdout.readlines() # type: ignore
): ):
# Only gather lines that include added migrations # Only gather lines that include added migrations.
if self.is_migration_file(line): if self.is_migration_file(line):
app_label, name = split_migration_path(line) # Find the migration objects with similar path.
if migrations_list is None or (app_label, name) in migrations_list: suitable_migrations = [
if (app_label, name) in self.migration_loader.disk_migrations: migration
migration = self.migration_loader.disk_migrations[ for path, migration in diskpath_and_migration.items()
app_label, name if path.endswith(line)
] ]
if suitable_migrations:
migration = suitable_migrations[0]
if (
migrations_list is None
or (migration.app_label, migration.name) in migrations_list
):
migrations.append(migration) migrations.append(migration)
else: if len(suitable_migrations) > 1:
logger.info( # If multiple migration founds, we chose one randomly, but need
"Found migration file (%s, %s) " # to alert that it's not very precise.
"that is not present in loaded migration.", logger.warning(
app_label, "Found multiple migration files matching altered "
name, "file path %s. Choose (%s, %s) opportunistically.",
line,
migration.app_label,
migration.name,
) )
else:
app_label, name = split_migration_path(line)
logger.info(
"Found migration file (%s, %s) "
"that is not present in loaded migration.",
app_label,
name,
)
diff_process.wait() diff_process.wait()
if diff_process.returncode != 0: if diff_process.returncode != 0:
+2
View File
@@ -19,3 +19,5 @@ DATA_MIGRATIONS = "app_data_migrations"
MAKE_NOT_NULL_WITH_DJANGO_DEFAULT = "app_make_not_null_with_django_default" MAKE_NOT_NULL_WITH_DJANGO_DEFAULT = "app_make_not_null_with_django_default"
MAKE_NOT_NULL_WITH_LIB_DEFAULT = "app_make_not_null_with_lib_default" MAKE_NOT_NULL_WITH_LIB_DEFAULT = "app_make_not_null_with_lib_default"
CREATE_INDEX_EXCLUSIVE = "app_create_index_exclusive" CREATE_INDEX_EXCLUSIVE = "app_create_index_exclusive"
CUSTOM_APP_NAME_DIRECTORY = "app_with_custom_name"
CUSTOM_APP_LABEL = "my_custom_name"
+30 -4
View File
@@ -34,22 +34,32 @@ class BaseBackwardCompatibilityDetection:
linter = self._launch_linter(app, commit_id) linter = self._launch_linter(app, commit_id)
self.assertTrue(linter.has_errors) self.assertTrue(linter.has_errors)
self.assertNotEqual(linter.nb_valid + linter.nb_erroneous, 0) self.assertNotEqual(linter.nb_valid + linter.nb_erroneous, 0)
self.assertGreater(linter.nb_total, 0)
def _test_linter_finds_no_errors(self, app=None, commit_id=None): def _test_linter_finds_no_errors(self, app=None, commit_id=None):
linter = self._launch_linter(app, commit_id) linter = self._launch_linter(app, commit_id)
self.assertFalse(linter.has_errors) self.assertFalse(linter.has_errors)
self.assertNotEqual(linter.nb_valid + linter.nb_erroneous, 0) self.assertNotEqual(linter.nb_valid + linter.nb_erroneous, 0)
self.assertGreater(linter.nb_total, 0)
def _test_linter_linted_no_migration(self, app=None, commit_id=None):
linter = self._launch_linter(app, commit_id)
self.assertEqual(linter.nb_valid, 0)
self.assertEqual(linter.nb_erroneous, 0)
self.assertEqual(linter.nb_total, 0)
def _launch_linter(self, app=None, commit_id=None): def _launch_linter(self, app=None, commit_id=None):
linter = MigrationLinter( linter = self._get_linter()
linter.lint_all_migrations(app_label=app, git_commit_id=commit_id)
return linter
def _get_linter(self):
return MigrationLinter(
self.test_project_path, self.test_project_path,
database=self.database, database=self.database,
no_cache=True, no_cache=True,
) )
linter.lint_all_migrations(app_label=app, git_commit_id=commit_id)
return linter
# *** Tests ***
def test_create_table_with_not_null_column(self): def test_create_table_with_not_null_column(self):
app = fixtures.CREATE_TABLE_WITH_NOT_NULL_COLUMN app = fixtures.CREATE_TABLE_WITH_NOT_NULL_COLUMN
self._test_linter_finds_no_errors(app) self._test_linter_finds_no_errors(app)
@@ -121,6 +131,22 @@ class BaseBackwardCompatibilityDetection:
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
linter.get_sql("app_unique_together", "0003") linter.get_sql("app_unique_together", "0003")
def test_custom_named_app(self):
# Folder name is not found.
app = fixtures.CUSTOM_APP_NAME_DIRECTORY
self._test_linter_linted_no_migration(app)
# Django app name is found.
app = fixtures.CUSTOM_APP_LABEL
self._test_linter_finds_no_errors(app)
# And with git ref:
app = fixtures.CUSTOM_APP_NAME_DIRECTORY
self._test_linter_linted_no_migration(app, commit_id="v0.1.4")
app = fixtures.CUSTOM_APP_LABEL
self._test_linter_finds_no_errors(app, commit_id="v0.1.4")
class SqliteBackwardCompatibilityDetectionTestCase( class SqliteBackwardCompatibilityDetectionTestCase(
BaseBackwardCompatibilityDetection, unittest.TestCase BaseBackwardCompatibilityDetection, unittest.TestCase
Whitespace-only changes.
@@ -0,0 +1,8 @@
from __future__ import annotations
from django.apps import AppConfig
class DefaultConfig(AppConfig):
name = "tests.test_project.app_with_custom_name"
label = "my_custom_name"
@@ -0,0 +1,30 @@
# Generated by Django 2.2 on 2019-07-29 21:21
from __future__ import annotations
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="A",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("int_field", models.IntegerField()),
("char_field", models.CharField(max_length=255)),
],
)
]
Whitespace-only changes.
@@ -0,0 +1,8 @@
from __future__ import annotations
from django.db import models
class A(models.Model):
int_field = models.IntegerField()
char_field = models.CharField(max_length=255)
+1
View File
@@ -59,6 +59,7 @@ INSTALLED_APPS = [
"tests.test_project.app_make_not_null_with_django_default", "tests.test_project.app_make_not_null_with_django_default",
"tests.test_project.app_make_not_null_with_lib_default", "tests.test_project.app_make_not_null_with_lib_default",
"tests.test_project.app_create_index_exclusive", "tests.test_project.app_create_index_exclusive",
"tests.test_project.app_with_custom_name.apps.DefaultConfig",
] ]
if django.VERSION[0] >= 5: if django.VERSION[0] >= 5: