diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | alembic/__init__.py | 2 | ||||
-rw-r--r-- | alembic/compat.py | 21 | ||||
-rw-r--r-- | alembic/script.py | 22 | ||||
-rw-r--r-- | alembic/util.py | 16 | ||||
-rw-r--r-- | docs/build/changelog.rst | 19 | ||||
-rw-r--r-- | tests/__init__.py | 13 | ||||
-rw-r--r-- | tests/test_versioning.py | 12 |
8 files changed, 89 insertions, 17 deletions
@@ -1,4 +1,5 @@ *.pyc +*.pyo build/ dist/ docs/build/output/ diff --git a/alembic/__init__.py b/alembic/__init__.py index d73b16b..13031b0 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,6 +1,6 @@ from os import path -__version__ = '0.6.2' +__version__ = '0.6.3' package_dir = path.abspath(path.dirname(__file__)) diff --git a/alembic/compat.py b/alembic/compat.py index 0e6162a..aac0560 100644 --- a/alembic/compat.py +++ b/alembic/compat.py @@ -45,21 +45,28 @@ if py2k: if py33: from importlib import machinery - def load_module(module_id, path): - return machinery.SourceFileLoader(module_id, path).load_module() + def load_module_py(module_id, path): + return machinery.SourceFileLoader(module_id, path).load_module(module_id) + + def load_module_pyc(module_id, path): + return machinery.SourcelessFileLoader(module_id, path).load_module(module_id) + else: import imp - def load_module(module_id, path): - fp = open(path, 'rb') - try: + def load_module_py(module_id, path): + with open(path, 'rb') as fp: mod = imp.load_source(module_id, path, fp) if py2k: source_encoding = parse_encoding(fp) if source_encoding: mod._alembic_source_encoding = source_encoding return mod - finally: - fp.close() + + def load_module_pyc(module_id, path): + with open(path, 'rb') as fp: + mod = imp.load_compiled(module_id, path, fp) + # no source encoding here + return mod try: exec_ = getattr(compat_builtins, 'exec') diff --git a/alembic/script.py b/alembic/script.py index 34fd150..e816d2b 100644 --- a/alembic/script.py +++ b/alembic/script.py @@ -4,7 +4,7 @@ import re import shutil from . import util -_rev_file = re.compile(r'.*\.py$') +_rev_file = re.compile(r'(.*\.py)(c|o)?$') _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+') @@ -463,9 +463,27 @@ class Script(object): @classmethod def _from_filename(cls, dir_, filename): - if not _rev_file.match(filename): + py_match = _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 is_o or is_c: + py_exists = os.path.exists(os.path.join(dir_, py_filename)) + pyc_exists = os.path.exists(os.path.join(dir_, py_filename + "c")) + + # prefer .py over .pyc because we'd like to get the + # source encoding; prefer .pyc over .pyo because we'd like to + # have the docstrings which a -OO file would not have + if py_exists or is_o and pyc_exists: + return None + module = util.load_python_file(dir_, filename) + if not hasattr(module, "revision"): # attempt to get the revision id from the script name, # this for legacy only diff --git a/alembic/util.py b/alembic/util.py index 6a5f8df..93b6b76 100644 --- a/alembic/util.py +++ b/alembic/util.py @@ -10,7 +10,7 @@ from mako.template import Template from sqlalchemy.engine import url from sqlalchemy import __version__ -from .compat import callable, exec_, load_module, binary_type +from .compat import callable, exec_, load_module_py, load_module_pyc, binary_type class CommandError(Exception): pass @@ -196,10 +196,20 @@ def load_python_file(dir_, filename): module_id = re.sub(r'\W', "_", filename) path = os.path.join(dir_, filename) - module = load_module(module_id, path) + _, ext = os.path.splitext(filename) + if ext == ".py": + module = load_module_py(module_id, path) + elif ext in (".pyc", ".pyo"): + module = load_module_pyc(module_id, path) del sys.modules[module_id] return module +def simple_pyc_file_from_path(path): + if sys.flags.optimize: + return path + "o" # e.g. .pyo + else: + return path + "c" # e.g. .pyc + def pyc_file_from_path(path): """Given a python source path, locate the .pyc. @@ -213,7 +223,7 @@ def pyc_file_from_path(path): if has3147: return imp.cache_from_source(path) else: - return path + "c" + return simple_pyc_file_from_path(path) def rev_id(): val = int(uuid.uuid4()) % 100000000000000 diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 937d6a0..a216737 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -2,6 +2,25 @@ ========== Changelog ========== +.. changelog:: + :version: 0.6.3 + + .. change:: + :tags: feature + :tickets: 163 + + The :class:`.ScriptDirectory` system that loads migration files + from a ``versions/`` directory now supports so-called + "sourceless" operation, where the ``.py`` files are not present + and instead ``.pyc`` or ``.pyo`` files are directly present where + the ``.py`` files should be. Note that while Python 3.3 has a + new system of locating ``.pyc``/``.pyo`` files within a directory + called ``__pycache__`` (e.g. PEP-3147), PEP-3147 maintains + support for the "source-less imports" use case, where the + ``.pyc``/``.pyo`` are in present in the "old" location, e.g. next + to the ``.py`` file; this is the usage that's supported even when + running Python3.3. + .. changelog:: :version: 0.6.2 diff --git a/tests/__init__.py b/tests/__init__.py index 904ee76..ad5b033 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -318,7 +318,7 @@ def clear_staging_env(): shutil.rmtree(staging_directory, True) -def write_script(scriptdir, rev_id, content, encoding='ascii'): +def write_script(scriptdir, rev_id, content, encoding='ascii', sourceless=False): old = scriptdir._revision_map[rev_id] path = old.path @@ -338,6 +338,17 @@ def write_script(scriptdir, rev_id, content, encoding='ascii'): scriptdir._revision_map[script.revision] = script script.nextrev = old.nextrev + if sourceless: + # note that if -O is set, you'd see pyo files here, + # the pyc util function looks at sys.flags.optimize to handle this + assert os.access(pyc_path, os.F_OK) + # look for a non-pep3147 path here. + # if not present, need to copy from __pycache__ + simple_pyc_path = util.simple_pyc_file_from_path(path) + if not os.access(simple_pyc_path, os.F_OK): + shutil.copyfile(pyc_path, simple_pyc_path) + os.unlink(path) + def three_rev_fixture(cfg): a = util.rev_id() diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 91163c9..a4be95f 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -8,6 +8,8 @@ from . import clear_staging_env, staging_env, \ assert_raises_message class VersioningTest(unittest.TestCase): + sourceless = False + def test_001_revisions(self): global a, b, c a = util.rev_id() @@ -28,7 +30,7 @@ class VersioningTest(unittest.TestCase): def downgrade(): op.execute("DROP TABLE foo") - """ % a) + """ % a, sourceless=self.sourceless) script.generate_revision(b, None, refresh=True) write_script(script, b, """ @@ -43,7 +45,7 @@ class VersioningTest(unittest.TestCase): def downgrade(): op.execute("DROP TABLE bar") - """ % (b, a)) + """ % (b, a), sourceless=self.sourceless) script.generate_revision(c, None, refresh=True) write_script(script, c, """ @@ -58,7 +60,7 @@ class VersioningTest(unittest.TestCase): def downgrade(): op.execute("DROP TABLE bat") - """ % (c, b)) + """ % (c, b), sourceless=self.sourceless) def test_002_upgrade(self): @@ -182,3 +184,7 @@ class VersionNameTemplateTest(unittest.TestCase): """) + +class SourcelessVersioningTest(VersioningTest): + sourceless = True + |