Commits

Mike Bayer committed 623c7e7

- Altered the support for "sourceless" migration files (e.g. only
.pyc or .pyo present) so that the flag "sourceless=true" needs to
be in alembic.ini for this behavior to take effect.
fixes #163

Comments (0)

Files changed (8)

alembic/script.py

 import shutil
 from . import util
 
-_rev_file = re.compile(r'(.*\.py)(c|o)?$')
+_sourceless_rev_file = re.compile(r'(.*\.py)(c|o)?$')
+_only_source_rev_file = re.compile(r'(.*\.py)$')
 _legacy_rev = re.compile(r'([a-f0-9]+)\.py$')
 _mod_def_re = re.compile(r'(upgrade|downgrade)_([a-z0-9]+)')
 _slug_re = re.compile(r'\w+')
 
     """
     def __init__(self, dir, file_template=_default_file_template,
-                    truncate_slug_length=40):
+                    truncate_slug_length=40,
+                    sourceless=False):
         self.dir = dir
         self.versions = os.path.join(self.dir, 'versions')
         self.file_template = file_template
         self.truncate_slug_length = truncate_slug_length or 40
+        self.sourceless = sourceless
 
         if not os.access(dir, os.F_OK):
             raise util.CommandError("Path doesn't exist: %r.  Please use "
                     file_template=config.get_main_option(
                                         'file_template',
                                         _default_file_template),
-                    truncate_slug_length=truncate_slug_length
+                    truncate_slug_length=truncate_slug_length,
+                    sourceless=config.get_main_option("sourceless") == "true"
                     )
 
     def walk_revisions(self, base="base", head="head"):
     def _revision_map(self):
         map_ = {}
         for file_ in os.listdir(self.versions):
-            script = Script._from_filename(self.versions, file_)
+            script = Script._from_filename(self, self.versions, file_)
             if script is None:
                 continue
             if script.revision in map_:
             **kw
         )
         if refresh:
-            script = Script._from_path(path)
+            script = Script._from_path(self, path)
             self._revision_map[script.revision] = script
             if script.down_revision:
                 self._revision_map[script.down_revision].\
                         self.doc)
 
     @classmethod
-    def _from_path(cls, path):
+    def _from_path(cls, scriptdir, path):
         dir_, filename = os.path.split(path)
-        return cls._from_filename(dir_, filename)
+        return cls._from_filename(scriptdir, dir_, filename)
 
     @classmethod
-    def _from_filename(cls, dir_, filename):
-        py_match = _rev_file.match(filename)
+    def _from_filename(cls, scriptdir, dir_, filename):
+        if scriptdir.sourceless:
+            py_match = _sourceless_rev_file.match(filename)
+        else:
+            py_match = _only_source_rev_file.match(filename)
 
         if not py_match:
             return None
 
         py_filename = py_match.group(1)
-        is_c = py_match.group(2) == 'c'
-        is_o = py_match.group(2) == 'o'
+
+        if scriptdir.sourceless:
+            is_c = py_match.group(2) == 'c'
+            is_o = py_match.group(2) == 'o'
+        else:
+            is_c = is_o = False
 
         if is_o or is_c:
             py_exists = os.path.exists(os.path.join(dir_, py_filename))

alembic/templates/generic/alembic.ini.mako

 # the 'revision' command, regardless of autogenerate
 # revision_environment = false
 
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
 sqlalchemy.url = driver://user:pass@localhost/dbname
 
 

alembic/templates/multidb/alembic.ini.mako

 # the 'revision' command, regardless of autogenerate
 # revision_environment = false
 
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
 databases = engine1, engine2
 
 [engine1]

alembic/templates/pylons/alembic.ini.mako

 # the 'revision' command, regardless of autogenerate
 # revision_environment = false
 
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
 pylons_config_file = ./development.ini
 
 # that's it !

docs/build/changelog.rst

     :version: 0.6.4
 
     .. change::
+      :tags: feature
+      :tickets: 163
+
+      Altered the support for "sourceless" migration files (e.g. only
+      .pyc or .pyo present) so that the flag "sourceless=true" needs to
+      be in alembic.ini for this behavior to take effect.
+
+    .. change::
       :tags: bug, mssql
       :tickets: 185
 

docs/build/tutorial.rst

     # the 'revision' command, regardless of autogenerate
     # revision_environment = false
 
+    # set to 'true' to allow .pyc and .pyo files without
+    # a source .py file to be detected as revisions in the
+    # versions/ directory
+    # sourceless = false
+
     sqlalchemy.url = driver://user:pass@localhost/dbname
 
     # Logging configuration
       ``%%(minute).2d``, ``%%(second).2d`` - components of the create date
       as returned by ``datetime.datetime.now()``
 
-    .. versionadded:: 0.3.6 - added date parameters to ``file_template``.
+  .. versionadded:: 0.3.6 - added date parameters to ``file_template``.
 
 * ``truncate_slug_length`` - defaults to 40, the max number of characters
   to include in the "slug" field.
 
-    .. versionadded:: 0.6.1 - added ``truncate_slug_length`` configuration
+  .. versionadded:: 0.6.1 - added ``truncate_slug_length`` configuration
 
 * ``sqlalchemy.url`` - A URL to connect to the database via SQLAlchemy.  This key is in fact
   only referenced within the ``env.py`` file that is specific to the "generic" configuration;
   of the file.
 * ``revision_environment`` - this is a flag which when set to the value 'true', will indicate
   that the migration environment script ``env.py`` should be run unconditionally when
-  generating new revision files (new in 0.3.3).
+  generating new revision files
+
+  .. versionadded:: 0.3.3
+
+* ``sourceless`` - when set to 'true', revision files that only exist as .pyc
+  or .pyo files in the versions directory will be used as versions, allowing
+  "sourceless" versioning folders.  When left at the default of 'false',
+  only .py files are consumed as version files.
+
+  .. versionadded:: 0.6.4
+
 * ``[loggers]``, ``[handlers]``, ``[formatters]``, ``[logger_*]``, ``[handler_*]``,
   ``[formatter_*]`` - these sections are all part of Python's standard logging configuration,
   the mechanics of which are documented at `Configuration File Format <http://docs.python.org/library/logging.config.html#configuration-file-format>`_.

tests/__init__.py

     with open(path, 'w') as f:
         f.write(txt)
 
-def _sqlite_testing_config():
+def _sqlite_testing_config(sourceless=False):
     dir_ = os.path.join(staging_directory, 'scripts')
     return _write_config_file("""
 [alembic]
 script_location = %s
 sqlalchemy.url = sqlite:///%s/foo.db
+sourceless = %s
 
 [loggers]
 keys = root
 [formatter_generic]
 format = %%(levelname)-5.5s [%%(name)s] %%(message)s
 datefmt = %%H:%%M:%%S
-    """ % (dir_, dir_))
+    """ % (dir_, dir_, "true" if sourceless else "false"))
 
 
 def _no_sql_testing_config(dialect="postgresql", directives=""):
     pyc_path = util.pyc_file_from_path(path)
     if os.access(pyc_path, os.F_OK):
         os.unlink(pyc_path)
-    script = Script._from_path(path)
+    script = Script._from_path(scriptdir, path)
     old = scriptdir._revision_map[script.revision]
     if old.down_revision != script.down_revision:
         raise Exception("Can't change down_revision "

tests/test_versioning.py

     @classmethod
     def setup_class(cls):
         cls.env = staging_env(sourceless=cls.sourceless)
-        cls.cfg = _sqlite_testing_config()
+        cls.cfg = _sqlite_testing_config(sourceless=cls.sourceless)
 
     @classmethod
     def teardown_class(cls):
 class SourcelessVersioningTest(VersioningTest):
     sourceless = True
 
+class SourcelessNeedsFlagTest(unittest.TestCase):
+    def setUp(self):
+        self.env = staging_env(sourceless=False)
+        self.cfg = _sqlite_testing_config()
+
+    def tearDown(self):
+        clear_staging_env()
+
+    def test_needs_flag(self):
+        a = util.rev_id()
+
+        script = ScriptDirectory.from_config(self.cfg)
+        script.generate_revision(a, None, refresh=True)
+        write_script(script, a, """
+    revision = '%s'
+    down_revision = None
+
+    from alembic import op
+
+    def upgrade():
+        op.execute("CREATE TABLE foo(id integer)")
+
+    def downgrade():
+        op.execute("DROP TABLE foo")
+
+    """ % a, sourceless=True)
+
+        script = ScriptDirectory.from_config(self.cfg)
+        eq_(script.get_heads(), [])
+
+        self.cfg.set_main_option("sourceless", "true")
+        script = ScriptDirectory.from_config(self.cfg)
+        eq_(script.get_heads(), [a])