Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib-nested-django-apps, mapping migration files from nested django apps #248

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion django_migration_linter/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from appdirs import user_cache_dir

__version__ = "4.1.0"
__version__ = "4.2.0"

DEFAULT_CACHE_PATH = user_cache_dir("django-migration-linter", version=__version__)

Expand Down
13 changes: 13 additions & 0 deletions django_migration_linter/management/commands/lintmigrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ def add_arguments(self, parser: CommandParser) -> None:
nargs="?",
help="if specified, only migrations listed in the file will be considered",
)
parser.add_argument(
"--resolve-nested-apps",
action="store_true",
help=(
"if specified, read_migrations_list migration files and"
" _gather_migrations_git file names will be indexed by their file"
" system prefix, relative to 'settings.BASE_DIR', which should"
" match the loaded django appconfig's path. on IndexError"
" falls-through to normal folder-named must be app_label based"
" app_config mapping."
),
)
cache_group = parser.add_mutually_exclusive_group(required=False)
cache_group.add_argument(
"--cache-path",
Expand Down Expand Up @@ -181,6 +193,7 @@ def handle(self, *args, **options):
migration_name=options["migration_name"],
git_commit_id=options["git_commit_id"],
migrations_file_path=options["include_migrations_from"],
resolve_nested_apps=options["resolve_nested_apps"],
)
linter.print_summary()
if linter.has_errors:
Expand Down
100 changes: 77 additions & 23 deletions django_migration_linter/migration_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.db import DEFAULT_DB_ALIAS, ProgrammingError, connections
from django.db.migrations import Migration, RunPython, RunSQL
from django.db.migrations.operations.base import Operation
from django.apps import apps

from .cache import Cache
from .constants import (
Expand All @@ -24,7 +25,7 @@
from .operations import IgnoreMigration
from .sql_analyser import analyse_sql_statements, get_sql_analyser_class
from .sql_analyser.base import Issue
from .utils import clean_bytes_to_str, get_migration_abspath, split_migration_path
from .utils import clean_bytes_to_str, get_migration_abspath, split_migration_path, split_migration_prefix

logger = logging.getLogger("django_migration_linter")

Expand All @@ -41,6 +42,17 @@ def values() -> list[str]:
return list(map(lambda c: c.value, MessageType))



_appconfigs_relpath_index = None
def appconfigs_relpath_index():
global _appconfigs_relpath_index
if _appconfigs_relpath_index is None:
_appconfigs_relpath_index = {
os.path.relpath(v.path, settings.BASE_DIR): k
for k,v in apps.app_configs.items()
}
return _appconfigs_relpath_index

class MigrationLinter:
def __init__(
self,
Expand Down Expand Up @@ -118,11 +130,12 @@ def lint_all_migrations(
migration_name: str | None = None,
git_commit_id: str | None = None,
migrations_file_path: str | None = None,
resolve_nested_apps: bool | None = False,
) -> None:
# Collect migrations.
migrations_list = self.read_migrations_list(migrations_file_path)
migrations_list = self.read_migrations_list(migrations_file_path, resolve_nested_apps)
if git_commit_id:
migrations = self._gather_migrations_git(git_commit_id, migrations_list)
migrations = self._gather_migrations_git(git_commit_id, migrations_list, resolve_nested_apps)
else:
migrations = self._gather_all_migrations(migrations_list)

Expand Down Expand Up @@ -338,9 +351,35 @@ def is_migration_file(filename: str) -> bool:
and "__init__" not in filename
)


def _resolve_nested_apps(migration_file: str) -> tuple[str, str] | None:
prefix, name = split_migration_prefix(migration_file)
if prefix in appconfigs_relpath_index():
app_label = apps.app_configs[appconfigs_relpath_index()[prefix]].label
return (app_label, name, None)

return (None, None, prefix)

def _resolve_nested_apps_or_split_migration_path(
migration_file: str,
resolve_nested_apps: bool | None = False
) -> tuple[str, str]:
if resolve_nested_apps:
app_label, name, prefix = MigrationLinter._resolve_nested_apps(migration_file)
if app_label is not None:
return (app_label, name)

logger.warning(
"IndexError: key %r not in relative django.apps.apps.app_configs[].path list, assuming app_label is parent folder name of ./migrations",
prefix
)

return split_migration_path(migration_file)

@classmethod
def read_migrations_list(
cls, migrations_file_path: str | None
cls, migrations_file_path: str | None,
resolve_nested_apps: bool | None = False
) -> list[tuple[str, str]] | None:
"""
Returning an empty list is different from returning None here.
Expand All @@ -355,8 +394,9 @@ def read_migrations_list(
with open(migrations_file_path) as file:
for line in file:
if cls.is_migration_file(line):
app_label, name = split_migration_path(line)
migrations.append((app_label, name))
migrations.append(
cls._resolve_nested_apps_or_split_migration_path(line, resolve_nested_apps)
)
except OSError:
logger.exception("Migrations list path not found %s", migrations_file_path)
raise Exception("Error while reading migrations list file")
Expand All @@ -368,8 +408,35 @@ def read_migrations_list(
)
return migrations

def _gather_migrations_git__inner(
self, line: str,
migrations_list: list[tuple[str, str]] | None = None,
resolve_nested_apps: bool | None = False
) -> tuple[str, str]:
# Only gather lines that include added migrations
if self.is_migration_file(line):
if resolve_nested_apps:
if not line.startswith(os.path.basename(self.django_path)):
line = os.path.join(os.path.basename(self.django_path), line)
app_label, name = MigrationLinter._resolve_nested_apps_or_split_migration_path(line, resolve_nested_apps)
if migrations_list is None or (app_label, name) in migrations_list:
if (app_label, name) in self.migration_loader.disk_migrations:
migration = self.migration_loader.disk_migrations[
app_label, name
]
return migration
else:
logger.info(
"Found migration file (%s, %s) "
"that is not present in loaded migration.",
app_label,
name,
)
return None

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,
resolve_nested_apps: bool | None = False
) -> Iterable[Migration]:
migrations = []
# Get changes since specified commit
Expand All @@ -381,22 +448,9 @@ def _gather_migrations_git(
for line in map(
clean_bytes_to_str, diff_process.stdout.readlines() # type: ignore
):
# Only gather lines that include added migrations
if self.is_migration_file(line):
app_label, name = split_migration_path(line)
if migrations_list is None or (app_label, name) in migrations_list:
if (app_label, name) in self.migration_loader.disk_migrations:
migration = self.migration_loader.disk_migrations[
app_label, name
]
migrations.append(migration)
else:
logger.info(
"Found migration file (%s, %s) "
"that is not present in loaded migration.",
app_label,
name,
)
migration = self._gather_migrations_git__inner(line, migrations_list, resolve_nested_apps)
if migration is not None:
migrations.append(migration)
diff_process.wait()

if diff_process.returncode != 0:
Expand Down
15 changes: 15 additions & 0 deletions django_migration_linter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def split_path(path: str) -> list[str]:
return decomposed_path


def join_path(components: tuple[str, ...], sep:str=os.path.sep) -> str:
if components[0] == sep:
components[0] = ''
return sep.join(components)


def split_migration_path(migration_path: str) -> tuple[str, str]:
from django.db.migrations.loader import MIGRATIONS_MODULE_NAME

Expand All @@ -32,6 +38,15 @@ def split_migration_path(migration_path: str) -> tuple[str, str]:
return decomposed_path[i - 1], os.path.splitext(decomposed_path[i + 1])[0]
return "", ""

def split_migration_prefix(migration_path: str) -> tuple[str, str]:
from django.db.migrations.loader import MIGRATIONS_MODULE_NAME

decomposed_path = split_path(migration_path)
for i, p in enumerate(decomposed_path):
if p == MIGRATIONS_MODULE_NAME:
return join_path(decomposed_path[:i]), os.path.splitext(decomposed_path[i + 1])[0]
return "", ""


def clean_bytes_to_str(byte_input: bytes) -> str:
return byte_input.decode("utf-8").strip()
Expand Down
Empty file.
Empty file.
6 changes: 6 additions & 0 deletions tests/test_project/app_nested/app_subapp/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AppSubappConfig(AppConfig):
name = "tests.test_project.app_nested.app_subapp"
label = "app_nested_app_subapp"
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 2.1.4 on 2019-03-19 21:08

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",
),
),
("null_field", models.IntegerField(null=True)),
],
)
]
16 changes: 16 additions & 0 deletions tests/test_project/app_nested/app_subapp/migrations/0002_foo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 2.1.4 on 2019-03-19 21:08

from __future__ import annotations

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("app_nested_app_subapp", "0001_initial")]

operations = [
migrations.AddField(
model_name="a", name="new_null_field", field=models.IntegerField(null=True)
)
]
Empty file.
8 changes: 8 additions & 0 deletions tests/test_project/app_nested/app_subapp/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from __future__ import annotations

from django.db import models


class A(models.Model):
null_field = models.IntegerField(null=True)
new_null_field = models.IntegerField(null=True)
6 changes: 6 additions & 0 deletions tests/test_project/app_nested/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AppNestedConfig(AppConfig):
name = "tests.test_project.app_nested"
label = "app_nested"
30 changes: 30 additions & 0 deletions tests/test_project/app_nested/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 2.1.4 on 2019-03-19 21:08

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",
),
),
("null_field", models.IntegerField(null=True)),
],
)
]
16 changes: 16 additions & 0 deletions tests/test_project/app_nested/migrations/0002_foo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 2.1.4 on 2019-03-19 21:08

from __future__ import annotations

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("app_nested", "0001_initial")]

operations = [
migrations.AddField(
model_name="a", name="new_null_field", field=models.IntegerField(null=True)
)
]
Empty file.
8 changes: 8 additions & 0 deletions tests/test_project/app_nested/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from __future__ import annotations

from django.db import models


class A(models.Model):
null_field = models.IntegerField(null=True)
new_null_field = models.IntegerField(null=True)
2 changes: 2 additions & 0 deletions tests/test_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"tests.test_project.app_data_migrations",
"tests.test_project.app_make_not_null_with_django_default",
"tests.test_project.app_make_not_null_with_lib_default",
"tests.test_project.app_nested",
"tests.test_project.app_nested.app_subapp",
]

MIDDLEWARE = [
Expand Down
Loading