From: Mike Bayer Date: Sat, 1 Feb 2014 17:29:03 +0000 (-0500) Subject: - Added new argument X-Git-Tag: rel_0_6_3~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=eac8e7393b9fc7d21396bd755aa94f9b55ea2739;p=thirdparty%2Fsqlalchemy%2Falembic.git - Added new argument :paramref:`.EnvironmentContext.configure.user_module_prefix`. This prefix is applied when autogenerate renders a user-defined type, which here is defined as any type that is from a module outside of the ``sqlalchemy.`` hierarchy. This prefix defaults to ``None``, in which case the :paramref:`.EnvironmentContext.configure.sqlalchemy_module_prefix` is used, thus preserving the current behavior. #171 - added new documentation sections regarding rendering of types - improved CSS so that deprecations/version changes are highlighted - cleanup of some configure paramter docs --- diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index 38e953c6..4c7b4f5e 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -204,7 +204,6 @@ def _modify_col(tname, cname, existing_nullable=None, existing_server_default=False, schema=None): - sqla_prefix = _sqlalchemy_autogenerate_prefix(autogen_context) indent = " " * 11 text = "%(prefix)salter_column(%(tname)r, %(cname)r" % { 'prefix': _alembic_autogenerate_prefix( @@ -212,7 +211,7 @@ def _modify_col(tname, cname, 'tname': tname, 'cname': cname} text += ",\n%sexisting_type=%s" % (indent, - _repr_type(sqla_prefix, existing_type, autogen_context)) + _repr_type(existing_type, autogen_context)) if server_default is not False: rendered = _render_server_default( server_default, autogen_context) @@ -220,7 +219,7 @@ def _modify_col(tname, cname, if type_ is not None: text += ",\n%stype_=%s" % (indent, - _repr_type(sqla_prefix, type_, autogen_context)) + _repr_type(type_, autogen_context)) if nullable is not None: text += ",\n%snullable=%r" % ( indent, nullable,) @@ -238,6 +237,13 @@ def _modify_col(tname, cname, text += ")" return text +def _user_autogenerate_prefix(autogen_context): + prefix = autogen_context['opts']['user_module_prefix'] + if prefix is None: + return _sqlalchemy_autogenerate_prefix(autogen_context) + else: + return prefix + def _sqlalchemy_autogenerate_prefix(autogen_context): return autogen_context['opts']['sqlalchemy_module_prefix'] or '' @@ -277,8 +283,7 @@ def _render_column(column, autogen_context): return "%(prefix)sColumn(%(name)r, %(type)s, %(kw)s)" % { 'prefix': _sqlalchemy_autogenerate_prefix(autogen_context), 'name': column.name, - 'type': _repr_type(_sqlalchemy_autogenerate_prefix(autogen_context), - column.type, autogen_context), + 'type': _repr_type(column.type, autogen_context), 'kw': ", ".join(["%s=%s" % (kwname, val) for kwname, val in opts]) } @@ -302,7 +307,7 @@ def _render_server_default(default, autogen_context): else: return None -def _repr_type(prefix, type_, autogen_context): +def _repr_type(type_, autogen_context): rendered = _user_defined_render("type", type_, autogen_context) if rendered is not False: return rendered @@ -314,7 +319,11 @@ def _repr_type(prefix, type_, autogen_context): if imports is not None: imports.add("from sqlalchemy.dialects import %s" % dname) return "%s.%r" % (dname, type_) + elif mod.startswith("sqlalchemy"): + prefix = _sqlalchemy_autogenerate_prefix(autogen_context) + return "%s%r" % (prefix, type_) else: + prefix = _user_autogenerate_prefix(autogen_context) return "%s%r" % (prefix, type_) def _render_constraint(constraint, autogen_context): diff --git a/alembic/environment.py b/alembic/environment.py index ea231e58..aa59fce9 100644 --- a/alembic/environment.py +++ b/alembic/environment.py @@ -273,6 +273,7 @@ class EnvironmentContext(object): downgrade_token="downgrades", alembic_module_prefix="op.", sqlalchemy_module_prefix="sa.", + user_module_prefix=None, **kw ): """Configure a :class:`.MigrationContext` within this @@ -469,15 +470,18 @@ class EnvironmentContext(object): .. seealso:: - ``include_schemas``, ``include_symbol`` + :ref:`autogen_render_types` + :paramref:`.EnvironmentContext.configure.include_schemas` :param include_symbol: A callable function which, given a table name and schema name (may be ``None``), returns ``True`` or ``False``, indicating if the given table should be considered in the autogenerate sweep. - .. deprecated:: 0.6.0 ``include_symbol`` is superceded by the - more generic ``include_object`` parameter. + .. deprecated:: 0.6.0 :paramref:`.EnvironmentContext.configure.include_symbol` + is superceded by the more generic + :paramref:`.EnvironmentContext.configure.include_object` + parameter. E.g.:: @@ -489,26 +493,11 @@ class EnvironmentContext(object): include_symbol = include_symbol ) - To limit autogenerate to a certain set of schemas when using the - ``include_schemas`` option:: - - def include_symbol(tablename, schema): - return schema in (None, "schema1", "schema2") - - context.configure( - # ... - include_schemas = True, - include_symbol = include_symbol - ) - - .. versionadded:: 0.3.6 - - .. versionchanged:: 0.4.0 the ``include_symbol`` callable must now - also accept a "schema" argument, which may be None. - .. seealso:: - ``include_schemas``, ``include_object`` + :paramref:`.EnvironmentContext.configure.include_schemas` + + :paramref:`.EnvironmentContext.configure.include_object` :param include_schemas: If True, autogenerate will scan across all schemas located by the SQLAlchemy @@ -522,7 +511,7 @@ class EnvironmentContext(object): .. seealso:: - ``include_symbol``, ``include_object`` + :paramref:`.EnvironmentContext.configure.include_object` :param render_item: Callable that can be used to override how any schema item, i.e. column, constraint, type, @@ -575,6 +564,20 @@ class EnvironmentContext(object): will render them using the dialect module name, i.e. ``mssql.BIT()``, ``postgresql.UUID()``. + :param user_module_prefix: When autogenerate refers to a SQLAlchemy + type (e.g. :class:`.TypeEngine`) where the module name is not + under the ``sqlalchemy`` namespace, this prefix will be used + within autogenerate, if non-``None``; if left at its default of + ``None``, the + :paramref:`.EnvironmentContext.configure.sqlalchemy_module_prefix` + is used instead. + + .. versionadded:: 0.6.3 added + :paramref:`.EnvironmentContext.configure.user_module_prefix` + + .. seealso:: + + :ref:`autogen_module_prefix` Parameters specific to individual backends: @@ -612,6 +615,7 @@ class EnvironmentContext(object): opts['downgrade_token'] = downgrade_token opts['sqlalchemy_module_prefix'] = sqlalchemy_module_prefix opts['alembic_module_prefix'] = alembic_module_prefix + opts['user_module_prefix'] = user_module_prefix if render_item is not None: opts['render_item'] = render_item if compare_type is not None: diff --git a/docs/build/_static/nature_override.css b/docs/build/_static/nature_override.css new file mode 100644 index 00000000..2d5608f3 --- /dev/null +++ b/docs/build/_static/nature_override.css @@ -0,0 +1,14 @@ +@import url("nature.css"); + + +.versionadded, .versionchanged, .deprecated { + background-color: #FFFFCC; + border: 1px solid #FFFF66; + margin-bottom: 10px; + margin-top: 10px; + padding: 7px; +} + +.versionadded > p > span, .versionchanged > p > span, .deprecated > p > span{ + font-style: italic; +} diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 4d24d96c..a3f7793e 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -5,6 +5,18 @@ Changelog .. changelog:: :version: 0.6.3 + .. change:: + :tags: feature + :tickets: 171 + + Added new argument + :paramref:`.EnvironmentContext.configure.user_module_prefix`. + This prefix is applied when autogenerate renders a user-defined type, + which here is defined as any type that is from a module outside of the + ``sqlalchemy.`` hierarchy. This prefix defaults to ``None``, in + which case the :paramref:`.EnvironmentContext.configure.sqlalchemy_module_prefix` + is used, thus preserving the current behavior. + .. change:: :tags: bug :tickets: 170 diff --git a/docs/build/conf.py b/docs/build/conf.py index 54fa6b69..586d61cf 100644 --- a/docs/build/conf.py +++ b/docs/build/conf.py @@ -118,6 +118,8 @@ pygments_style = 'sphinx' # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'nature' +html_style = "nature_override.css" + # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 3493a9f9..b224e26c 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -607,6 +607,8 @@ Autogenerate can't currently, but will *eventually* detect: like CHECK and FOREIGN KEY - these are not fully implemented. * Sequence additions, removals - not yet implemented. +.. _autogen_render_types: + Rendering Custom Types in Autogenerate -------------------------------------- @@ -622,7 +624,8 @@ it is often necessary to explicitly give the type a ``__repr__()`` that will faithfully reproduce the constructor for that type. But beyond that, it also is usually necessary to change how the enclosing module or package is rendered as well; -this is accomplished using the ``render_item`` configuration option:: +this is accomplished using the :paramref:`.EnvironmentContext.configure.render_item` +configuration option:: def render_item(type_, obj, autogen_context): """Apply custom rendering for selected items.""" @@ -648,6 +651,75 @@ this is accomplished using the ``render_item`` configuration option:: Above, we also need to make sure our ``MySpecialType`` includes an appropriate ``__repr__()`` method, which is invoked when we call it against ``"%r"``. +The callable we use for :paramref:`.EnvironmentContext.configure.render_item` +can also add imports to our migration script. The ``autogen_context`` passed in +contains an entry called ``autogen_context['imports']``, which is a Python +``set()`` for which we can add new imports. For example, if ``MySpecialType`` +were in a module called ``mymodel.types``, we can add the import for it +as we encounter the type:: + + def render_item(type_, obj, autogen_context): + """Apply custom rendering for selected items.""" + + if type_ == 'type' and isinstance(obj, MySpecialType): + # add import for this type + autogen_context['imports'].add("from mymodel import types") + return "types.%r" % obj + + # default rendering for other objects + return False + +The finished migration script will include our imports where the +``${imports}`` expression is used, producing output such as:: + + from alembic import op + import sqlalchemy as sa + from mymodel import types + + def upgrade(): + op.add_column('sometable', Column('mycolumn', types.MySpecialType())) + +.. _autogen_module_prefix: + +Controlling the Module Prefix +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :paramref:`.EnvironmentContext.configure.render_item`, note that +we deliver not just the reproduction of the type, but we can also deliver the +"module prefix", which is a module namespace from which our type can be found +within our migration script. When Alembic renders SQLAlchemy types, it will +typically use the value of :paramref:`.EnvironmentContext.configure.sqlalchemy_module_prefix`, +which defaults to ``"sa."``, to achieve this:: + + Column("my_column", sa.Integer()) + +When we use a custom type that is not within the ``sqlalchemy.`` module namespace, +by default Alembic will still use the ``"sa."`` prefix:: + + Column("my_column", sa.MyCustomType()) + +We can provide an alternate prefix here using the :paramref:`.EnvironmentContext.configure.user_module_prefix` +option:: + + + def run_migrations_online(): + # ... + + context.configure( + connection=connection, + target_metadata=target_metadata, + user_module_prefix="mymodel.types", + # ... + ) + + # ... + +Where we'd get a migration like:: + + Column("my_column", mymodel.types.MyCustomType()) + +.. versionadded:: 0.6.3 Added :paramref:`.EnvironmentContext.configure.user_module_prefix`. + Generating SQL Scripts (a.k.a. "Offline Mode") ============================================== diff --git a/tests/test_autogen_render.py b/tests/test_autogen_render.py index bd69cc33..0d3e2b1f 100644 --- a/tests/test_autogen_render.py +++ b/tests/test_autogen_render.py @@ -129,7 +129,7 @@ class AutogenRenderTest(TestCase): autogenerate.render._drop_index(idx, self.autogen_context), "op.drop_index('test_active_code_idx', table_name='test')" ) - + def test_drop_index_schema(self): """ autogenerate.render._drop_index using schema @@ -163,7 +163,7 @@ class AutogenRenderTest(TestCase): autogenerate.render._add_unique_constraint(uq, self.autogen_context), "op.create_unique_constraint('uq_test_code', 'test', ['code'])" ) - + def test_add_unique_constraint_schema(self): """ autogenerate.render._add_unique_constraint using schema @@ -196,7 +196,7 @@ class AutogenRenderTest(TestCase): autogenerate.render._drop_constraint(uq, self.autogen_context), "op.drop_constraint('uq_test_code', 'test')" ) - + def test_drop_constraint_schema(self): """ autogenerate.render._drop_constraint using schema @@ -615,17 +615,91 @@ render:primary_key\n)""" def test_render_enum(self): eq_ignore_whitespace( autogenerate.render._repr_type( - "sa.", Enum("one", "two", "three", name="myenum"), self.autogen_context), "sa.Enum('one', 'two', 'three', name='myenum')" ) eq_ignore_whitespace( autogenerate.render._repr_type( - "sa.", Enum("one", "two", "three"), self.autogen_context), "sa.Enum('one', 'two', 'three')" ) -# TODO: tests for dialect-specific type rendering + imports + def test_repr_plain_sqla_type(self): + type_ = Integer() + autogen_context = { + 'opts': { + 'sqlalchemy_module_prefix': 'sa.', + 'alembic_module_prefix': 'op.', + }, + 'dialect': mysql.dialect() + } + + eq_ignore_whitespace( + autogenerate.render._repr_type(type_, autogen_context), + "sa.Integer()" + ) + + def test_repr_user_type_user_prefix_None(self): + from sqlalchemy.types import UserDefinedType + class MyType(UserDefinedType): + def get_col_spec(self): + return "MYTYPE" + + type_ = MyType() + autogen_context = { + 'opts': { + 'sqlalchemy_module_prefix': 'sa.', + 'alembic_module_prefix': 'op.', + 'user_module_prefix': None + }, + 'dialect': mysql.dialect() + } + + eq_ignore_whitespace( + autogenerate.render._repr_type(type_, autogen_context), + "sa.MyType()" + ) + + def test_repr_user_type_user_prefix_present(self): + from sqlalchemy.types import UserDefinedType + class MyType(UserDefinedType): + def get_col_spec(self): + return "MYTYPE" + + type_ = MyType() + autogen_context = { + 'opts': { + 'sqlalchemy_module_prefix': 'sa.', + 'alembic_module_prefix': 'op.', + 'user_module_prefix': 'user.', + }, + 'dialect': mysql.dialect() + } + + eq_ignore_whitespace( + autogenerate.render._repr_type(type_, autogen_context), + "user.MyType()" + ) + + def test_repr_dialect_type(self): + from sqlalchemy.dialects.mysql import VARCHAR + + type_ = VARCHAR(20, charset='utf8', national=True) + autogen_context = { + 'opts': { + 'sqlalchemy_module_prefix': 'sa.', + 'alembic_module_prefix': 'op.', + 'user_module_prefix': None, + }, + 'imports': set(), + 'dialect': mysql.dialect() + } + eq_ignore_whitespace( + autogenerate.render._repr_type(type_, autogen_context), + "mysql.VARCHAR(charset='utf8', national=True, length=20)" + ) + eq_(autogen_context['imports'], + set(['from sqlalchemy.dialects import mysql']) + )