Skip to content

Module

migration_docs

migration_docs.Migration

Migration(node, *, executor, loader, docs)

A migration and its associated docs.

Migrations are typically loaded and accessed via the parent Migrations object. When loaded, the Migration has access to core migration attributes (e.g. atomic, sql, etc) and also has attributes for every attribute collected in the documentation schema. For example, if the user configured a type attribute to be collected in .migration-docs/migration.yaml, it would be accessible as a type attribute on this object.

Source code in migration_docs/core.py
def __init__(self, node, *, executor, loader, docs):
    self._node = node
    self._executor = executor
    self._loader = loader
    self._docs = docs

app_label property

app_label

The Django app label of the migration

applied property

applied

True if the migration has been applied

atomic property

atomic

True if the migration is executed in a transaction

label property

label

The unique identifying label of the migration

name property

name

The name of the migration (e.g. 0001_initial)

operations property

operations

The raw list of migration operation objects

operations_str property

operations_str

String representations of the migration operations

__getattribute__

__getattribute__(attr)

Allows migration docs to be accessed as attributes on the Migration or the migration docs.

Doing this provides the ability for users to filter Migrations by any documented attribute.

Source code in migration_docs/core.py
def __getattribute__(self, attr):
    """
    Allows migration docs to be accessed as attributes on the Migration
    or the migration docs.

    Doing this provides the ability for users to filter Migrations
    by any documented attribute.
    """
    try:
        return object.__getattribute__(self, attr)
    except AttributeError:
        if self._docs.get(self.label) and attr in self._docs[self.label]:
            return self._docs[self.label][attr]
        elif attr in self._docs.schema:
            return None
        else:
            raise

hash

hash()

The MD5 hash of the migration file

Source code in migration_docs/core.py
@cached_property
def hash(self):
    """The MD5 hash of the migration file"""
    return hashlib.md5(inspect.getsource(inspect.getmodule(self._node)).encode()).hexdigest()

set_docs

set_docs(prompt=True, defaults=None)

Set docs about a migration

Parameters:

Name Type Description Default
prompt boolean, default=False

True if collecting data from a user.

True
defaults dict, default=None

When prompting, use these values as defaults.

None
Source code in migration_docs/core.py
def set_docs(self, prompt=True, defaults=None):
    """Set docs about a migration

    Args:
        prompt (boolean, default=False): True if collecting data from
            a user.
        defaults (dict, default=None): When prompting, use these values
            as defaults.
    """
    if self.label not in self._docs:
        self._docs[self.label] = {}
    self._docs[self.label]["_hash"] = self.hash
    self._docs[self.label]["atomic"] = self.atomic
    self._docs[self.label]["sql"] = self.sql

    if prompt:
        self._docs[self.label].update(self._docs.schema.prompt(defaults=defaults))

    self._docs.save()

sql

sql()

The raw SQL for the migration

Source code in migration_docs/core.py
@cached_property
def sql(self):
    """The raw SQL for the migration"""
    if (django.VERSION[0] >= 3 and django.VERSION[1] >= 1) or django.VERSION[0] >= 4:
        migration_sql_obj = self._loader
    else:  # pragma: no cover
        migration_sql_obj = self._executor

    try:
        sql_statements = migration_sql_obj.collect_sql([(self._node, False)])
        return "\n".join(sql_statements)
    except Exception as exc:
        return f'Error obtaining SQL - "{exc}"'

migration_docs.Migrations

Migrations(
    using: str = "default",
    loader: Union[django_migration_loader.MigrationLoader, None] = None,
    executor: Union[django_migration_executor.MigrationExecutor, None] = None,
)

Bases: FilterableUserList

A filterable and groupable list of migrations and their associated migration docs.

Source code in migration_docs/core.py
def __init__(
    self,
    using: str = "default",
    loader: Union[django_migration_loader.MigrationLoader, None] = None,
    executor: Union[django_migration_executor.MigrationExecutor, None] = None,
):
    connection = connections[using]
    self._loader = loader or django_migration_loader.MigrationLoader(
        connection, ignore_no_migrations=True
    )
    self._graph = self._loader.graph
    self._executor = django_migration_executor.MigrationExecutor(connection)
    self._docs = MigrationDocs()

    self._migrations = {
        str(node): Migration(
            node,
            executor=self._executor,
            loader=self._loader,
            docs=self._docs,
        )
        for node in self._graph.nodes.values()
    }

    # Construct a plan of migrations. Set the ``data`` as the plan so
    # that this datastructure is a list
    targets = self._graph.leaf_nodes()
    self.data = []
    seen = set()
    for target in targets:
        for migration in self._graph.forwards_plan(target):
            if migration not in seen:  # pragma: no branch
                # We don't cover the "else" branch of this statement since
                # our test models dont have complex enough migrations
                self.data.append(self._migrations[str(self._graph.nodes[migration])])
                seen.add(migration)

excess_docs property

excess_docs

Return additional docs

__getitem__

__getitem__(i)

Allow accessing by list index or migration label

Source code in migration_docs/core.py
def __getitem__(self, i):
    """Allow accessing by list index or migration label"""
    if isinstance(i, int):
        return self.data[i]
    else:
        return self._migrations[i]

bootstrap_docs

bootstrap_docs()

Bootstraps all migration docs to empty values.

Source code in migration_docs/core.py
def bootstrap_docs(self):
    """Bootstraps all migration docs to empty values."""
    self._docs = MigrationDocs(data={str(node): None for node in self})
    self._docs.save()

filter_by_missing_docs

filter_by_missing_docs()

Filter migration docs by ones that are missing

Source code in migration_docs/core.py
def filter_by_missing_docs(self):
    """Filter migration docs by ones that are missing"""
    return self.intersect("label", set(self._migrations) - set(self._docs))

filter_by_stale_docs

filter_by_stale_docs()

Filter migration docs by ones that are stale

Source code in migration_docs/core.py
def filter_by_stale_docs(self):
    """Filter migration docs by ones that are stale"""
    labels = [
        migration
        for migration, docs in self._docs.items()
        if docs is not None
        and migration in self._migrations
        and docs["_hash"] != self._migrations[migration].hash
    ]
    return self.intersect("label", labels)

prune_excess_docs

prune_excess_docs()

Remove additional docs

Source code in migration_docs/core.py
def prune_excess_docs(self):
    """Remove additional docs"""
    for label in self.excess_docs:
        del self._docs[label]

    self._docs.save()

migration_docs.bootstrap

bootstrap(msg: Callable = _pretty_msg) -> None

Bootstrap migration docs with filler values when integrating docs with a project for the first time.

Parameters:

Name Type Description Default
msg Callable

A message printer for showing messages to the user.

_pretty_msg

Raises:

Type Description
RuntimeError

When migration docs have already been synced

Source code in migration_docs/core.py
def bootstrap(msg: Callable = _pretty_msg) -> None:
    """
    Bootstrap migration docs with filler values when integrating docs
    with a project for the first time.

    Args:
        msg: A message printer for showing messages to the user.

    Raises:
        RuntimeError: When migration docs have already been synced
    """
    if MigrationDocs():
        raise RuntimeError("Cannot bootstrap when migration docs have already been synced.")

    Migrations().bootstrap_docs()

    msg("django-migration-docs: Docs successfully bootstrapped.")

migration_docs.check

check(msg: Callable = _pretty_msg) -> bool

Check migration notes. Return False if any of the conditions hold true: - There are migrations without docs. - There are documented migrations that no longer exist. - There are stale migration docs.

Parameters:

Name Type Description Default
msg Callable

A message printer for showing messages to the user.

_pretty_msg

Returns:

Type Description
bool

True when the migration docs are up to date, False otherwise.

Source code in migration_docs/core.py
def check(msg: Callable = _pretty_msg) -> bool:
    """
    Check migration notes. Return False if any of the conditions hold true:
    - There are migrations without docs.
    - There are documented migrations that no longer exist.
    - There are stale migration docs.

    Args:
        msg: A message printer for showing messages to the user.

    Returns:
        `True` when the migration docs are up to date, `False` otherwise.
    """
    migrations = Migrations()
    missing_docs = migrations.filter_by_missing_docs()
    stale_docs = migrations.filter_by_stale_docs()
    excess_docs = migrations.excess_docs

    if missing_docs:
        msg(
            f"django-migration-docs: Found no docs for {len(missing_docs)}" " migration(s).",
            fg="red",
        )

    if stale_docs:
        msg(
            f"django-migration-docs: Found {len(stale_docs)} stale" " migration doc(s).",
            fg="red",
        )

    if excess_docs:
        msg(
            f"django-migration-docs: Found docs for {len(excess_docs)}" " deleted migration(s).",
            fg="red",
        )

    if missing_docs or stale_docs or excess_docs:
        msg(
            'django-migration-docs: Run "manage.py migration_docs sync" to' " fix errors.",
            fg="red",
        )
        return False
    else:
        msg("django-migration-docs: Migration docs are up to date.")
        return True

migration_docs.show

show(
    app_labels: Union[List[str], None] = None,
    unapplied: bool = False,
    style: str = "default",
) -> str

Shows migration docs to the user

Parameters:

Name Type Description Default
app_labels Union[List[str], None]

App labels to limit the shown migrations to.

None
unapplied bool

Only show unapplied migrations.

False
style str

The style to use when rendering. Corresponds to a Jinja template stored in .migration-docs/{style}_show.tpl.

'default'

Returns:

Type Description
str

The rendered migration list.

Source code in migration_docs/core.py
def show(
    app_labels: Union[List[str], None] = None, unapplied: bool = False, style: str = "default"
) -> str:
    """Shows migration docs to the user

    Args:
        app_labels: App labels to limit the shown migrations to.
        unapplied: Only show unapplied migrations.
        style: The style to use when rendering. Corresponds to a Jinja template stored in
            `.migration-docs/{style}_show.tpl`.

    Returns:
        The rendered migration list.
    """
    migrations = Migrations()

    if app_labels:
        migrations = migrations.intersect("app_label", app_labels)

    if unapplied:
        migrations = migrations.filter("applied", False)

    env = jinja2.Environment(
        loader=jinja2.FileSystemLoader(_get_migration_docs_file_root()),
        trim_blocks=True,
    )
    template_file = "show.tpl" if style == "default" else f"show_{style}.tpl"
    try:
        template = env.get_template(template_file)
    except jinja2.exceptions.TemplateNotFound:
        if style == "default":
            # Use the default migration template if the user didn't provide one
            template = jinja2.Template(DEFAULT_MIGRATION_TEMPLATE, trim_blocks=True)
        else:
            raise

    rendered = template.render(migrations=migrations, app_labels=app_labels, unapplied=unapplied)

    return rendered

migration_docs.sync

sync(msg: Callable = _pretty_msg) -> None

Sync new migrations with the migration docs and prune migrations that no longer exist.

Parameters:

Name Type Description Default
msg Callable

A message printer for showing messages to the user.

_pretty_msg
Source code in migration_docs/core.py
def sync(msg: Callable = _pretty_msg) -> None:
    """
    Sync new migrations with the migration docs and prune migrations that
    no longer exist.

    Args:
        msg: A message printer for showing messages to the user.
    """
    # Run any configured pre-sync hooks
    pre_sync_hooks = getattr(settings, "MIGRATION_DOCS_PRE_SYNC_HOOKS", [])
    if pre_sync_hooks:
        msg("django-migration-docs: Running pre-sync hooks...")
        for pre_sync_hook in pre_sync_hooks:
            msg(pre_sync_hook, fg="yellow")
            utils.shell(pre_sync_hook)

    migrations = Migrations()
    missing_docs = migrations.filter_by_missing_docs()
    stale_docs = migrations.filter_by_stale_docs()
    excess_docs = migrations.excess_docs

    # Collect information for new migrations
    if missing_docs:
        msg(
            "django-migration-docs: Found no docs for"
            f" {len(missing_docs)} migration(s). Please enter"
            " more information."
        )
        for migration in missing_docs:
            msg(f"{migration.label}:", fg="yellow")
            migration.set_docs()

    # Update any stale documentation
    if stale_docs:
        msg(
            f"django-migration-docs: Found {len(stale_docs)} stale"
            " migration doc(s). Docs updated automatically."
        )
        for migration in stale_docs:
            migration.set_docs(prompt=False)

    # Delete old migrations
    if excess_docs:
        msg(
            f"django-migration-docs: Found docs for {len(excess_docs)}"
            " deleted migration(s). Docs were removed."
        )
        migrations.prune_excess_docs()

    msg("django-migration-docs: Successfully synced migration docs.")

migration_docs.update

update(migrations: List[str], msg: Callable = _pretty_msg) -> None

Update migration docs for specific migrations.

Parameters:

Name Type Description Default
migrations List[str]

A list of migration labels to update (e.g. users.0001_initial).

required
msg Callable

A message printer for showing messages to the user.

_pretty_msg
Source code in migration_docs/core.py
def update(migrations: List[str], msg: Callable = _pretty_msg) -> None:
    """
    Update migration docs for specific migrations.

    Args:
        migrations: A list of migration labels to update (e.g. users.0001_initial).
        msg: A message printer for showing messages to the user.
    """
    migration_objs = Migrations()
    for migration in migrations:
        msg(f"{migration}:", fg="yellow")
        try:
            migration_objs[migration].set_docs()
        except KeyError:
            msg(f'Migration with label "{migration}" does not exist.', fg="red")