Skip to content

Commit e532a7e

Browse files
committed
organize into a "plugin" directory structure
we attempt to move autogen functions into independent units that are more obviously pluggable, and we add support for arbitrary "plugin" entrypoints that could add more pluggable units into autogenerate or anywhere else Change-Id: Id606a76dc6d12a308028f6cfdad690e0e63a43e5
1 parent c94816e commit e532a7e

30 files changed

Lines changed: 3001 additions & 957 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ repos:
77
- id: black
88

99
- repo: https://github.com/sqlalchemyorg/zimports
10-
rev: v0.6.2
10+
rev: v0.7.0
1111
hooks:
1212
- id: zimports
1313
args:

alembic/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from . import context
22
from . import op
3+
from .runtime import plugins
34

4-
__version__ = "1.17.3"
5+
6+
__version__ = "1.18.0"

alembic/autogenerate/api.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import contextlib
4+
import logging
45
from typing import Any
56
from typing import Dict
67
from typing import Iterator
@@ -17,11 +18,9 @@
1718
from . import render
1819
from .. import util
1920
from ..operations import ops
21+
from ..runtime.plugins import Plugin
2022
from ..util import sqla_compat
2123

22-
"""Provide the 'autogenerate' feature which can produce migration operations
23-
automatically."""
24-
2524
if TYPE_CHECKING:
2625
from sqlalchemy.engine import Connection
2726
from sqlalchemy.engine import Dialect
@@ -42,6 +41,10 @@
4241
from ..script.base import Script
4342
from ..script.base import ScriptDirectory
4443
from ..script.revision import _GetRevArg
44+
from ..util import PriorityDispatcher
45+
46+
47+
log = logging.getLogger(__name__)
4548

4649

4750
def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any:
@@ -304,7 +307,7 @@ class AutogenContext:
304307
305308
"""
306309

307-
dialect: Optional[Dialect] = None
310+
dialect: Dialect
308311
"""The :class:`~sqlalchemy.engine.Dialect` object currently in use.
309312
310313
This is normally obtained from the
@@ -326,9 +329,11 @@ class AutogenContext:
326329
327330
"""
328331

329-
migration_context: MigrationContext = None # type: ignore[assignment]
332+
migration_context: MigrationContext
330333
"""The :class:`.MigrationContext` established by the ``env.py`` script."""
331334

335+
comparators: PriorityDispatcher
336+
332337
def __init__(
333338
self,
334339
migration_context: MigrationContext,
@@ -346,6 +351,19 @@ def __init__(
346351
"the database for schema information"
347352
)
348353

354+
# branch off from the "global" comparators. This collection
355+
# is empty in Alembic except that it is populated by third party
356+
# extensions that don't use the plugin system. so we will build
357+
# off of whatever is in there.
358+
if autogenerate:
359+
self.comparators = compare.comparators.branch()
360+
Plugin.populate_autogenerate_priority_dispatch(
361+
self.comparators,
362+
include_plugins=migration_context.opts.get(
363+
"autogenerate_plugins", ["alembic.autogenerate.*"]
364+
),
365+
)
366+
349367
if opts is None:
350368
opts = migration_context.opts
351369

@@ -380,9 +398,8 @@ def __init__(
380398
self._name_filters = name_filters
381399

382400
self.migration_context = migration_context
383-
if self.migration_context is not None:
384-
self.connection = self.migration_context.bind
385-
self.dialect = self.migration_context.dialect
401+
self.connection = self.migration_context.bind
402+
self.dialect = self.migration_context.dialect
386403

387404
self.imports = set()
388405
self.opts: Dict[str, Any] = opts
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import TYPE_CHECKING
5+
6+
from . import comments
7+
from . import constraints
8+
from . import schema
9+
from . import server_defaults
10+
from . import tables
11+
from . import types
12+
from ... import util
13+
from ...runtime.plugins import Plugin
14+
15+
if TYPE_CHECKING:
16+
from ..api import AutogenContext
17+
from ...operations.ops import MigrationScript
18+
from ...operations.ops import UpgradeOps
19+
20+
21+
log = logging.getLogger(__name__)
22+
23+
comparators = util.PriorityDispatcher()
24+
"""global registry which alembic keeps empty, but copies when creating
25+
a new AutogenContext.
26+
27+
This is to support a variety of third party plugins that hook their autogen
28+
functionality onto this collection.
29+
30+
"""
31+
32+
33+
def _populate_migration_script(
34+
autogen_context: AutogenContext, migration_script: MigrationScript
35+
) -> None:
36+
upgrade_ops = migration_script.upgrade_ops_list[-1]
37+
downgrade_ops = migration_script.downgrade_ops_list[-1]
38+
39+
_produce_net_changes(autogen_context, upgrade_ops)
40+
upgrade_ops.reverse_into(downgrade_ops)
41+
42+
43+
def _produce_net_changes(
44+
autogen_context: AutogenContext, upgrade_ops: UpgradeOps
45+
) -> None:
46+
assert autogen_context.dialect is not None
47+
48+
autogen_context.comparators.dispatch(
49+
"autogenerate", qualifier=autogen_context.dialect.name
50+
)(autogen_context, upgrade_ops)
51+
52+
53+
Plugin.setup_plugin_from_module(schema, "alembic.autogenerate.schemas")
54+
Plugin.setup_plugin_from_module(tables, "alembic.autogenerate.tables")
55+
Plugin.setup_plugin_from_module(types, "alembic.autogenerate.types")
56+
Plugin.setup_plugin_from_module(
57+
constraints, "alembic.autogenerate.constraints"
58+
)
59+
Plugin.setup_plugin_from_module(
60+
server_defaults, "alembic.autogenerate.defaults"
61+
)
62+
Plugin.setup_plugin_from_module(comments, "alembic.autogenerate.comments")
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import Any
5+
from typing import Optional
6+
from typing import TYPE_CHECKING
7+
from typing import Union
8+
9+
from ...operations import ops
10+
from ...util import PriorityDispatchResult
11+
12+
if TYPE_CHECKING:
13+
14+
from sqlalchemy.sql.elements import quoted_name
15+
from sqlalchemy.sql.schema import Column
16+
from sqlalchemy.sql.schema import Table
17+
18+
from ..api import AutogenContext
19+
from ...operations.ops import AlterColumnOp
20+
from ...operations.ops import ModifyTableOps
21+
from ...runtime.plugins import Plugin
22+
23+
log = logging.getLogger(__name__)
24+
25+
26+
def _compare_column_comment(
27+
autogen_context: AutogenContext,
28+
alter_column_op: AlterColumnOp,
29+
schema: Optional[str],
30+
tname: Union[quoted_name, str],
31+
cname: quoted_name,
32+
conn_col: Column[Any],
33+
metadata_col: Column[Any],
34+
) -> PriorityDispatchResult:
35+
assert autogen_context.dialect is not None
36+
if not autogen_context.dialect.supports_comments:
37+
return PriorityDispatchResult.CONTINUE
38+
39+
metadata_comment = metadata_col.comment
40+
conn_col_comment = conn_col.comment
41+
if conn_col_comment is None and metadata_comment is None:
42+
return PriorityDispatchResult.CONTINUE
43+
44+
alter_column_op.existing_comment = conn_col_comment
45+
46+
if conn_col_comment != metadata_comment:
47+
alter_column_op.modify_comment = metadata_comment
48+
log.info("Detected column comment '%s.%s'", tname, cname)
49+
50+
return PriorityDispatchResult.STOP
51+
else:
52+
return PriorityDispatchResult.CONTINUE
53+
54+
55+
def _compare_table_comment(
56+
autogen_context: AutogenContext,
57+
modify_table_ops: ModifyTableOps,
58+
schema: Optional[str],
59+
tname: Union[quoted_name, str],
60+
conn_table: Optional[Table],
61+
metadata_table: Optional[Table],
62+
) -> PriorityDispatchResult:
63+
assert autogen_context.dialect is not None
64+
if not autogen_context.dialect.supports_comments:
65+
return PriorityDispatchResult.CONTINUE
66+
67+
# if we're doing CREATE TABLE, comments will be created inline
68+
# with the create_table op.
69+
if conn_table is None or metadata_table is None:
70+
return PriorityDispatchResult.CONTINUE
71+
72+
if conn_table.comment is None and metadata_table.comment is None:
73+
return PriorityDispatchResult.CONTINUE
74+
75+
if metadata_table.comment is None and conn_table.comment is not None:
76+
modify_table_ops.ops.append(
77+
ops.DropTableCommentOp(
78+
tname, existing_comment=conn_table.comment, schema=schema
79+
)
80+
)
81+
return PriorityDispatchResult.STOP
82+
elif metadata_table.comment != conn_table.comment:
83+
modify_table_ops.ops.append(
84+
ops.CreateTableCommentOp(
85+
tname,
86+
metadata_table.comment,
87+
existing_comment=conn_table.comment,
88+
schema=schema,
89+
)
90+
)
91+
return PriorityDispatchResult.STOP
92+
93+
return PriorityDispatchResult.CONTINUE
94+
95+
96+
def setup(plugin: Plugin) -> None:
97+
plugin.add_autogenerate_comparator(
98+
_compare_column_comment,
99+
"column",
100+
"comments",
101+
)
102+
plugin.add_autogenerate_comparator(
103+
_compare_table_comment,
104+
"table",
105+
"comments",
106+
)

0 commit comments

Comments
 (0)