import os
import re
import shutil
+import sys
from dateutil import tz
_default_file_template = "%(rev)s_%(slug)s"
_split_on_space_comma = re.compile(r", *|(?: +)")
+_split_on_space_comma_colon = re.compile(r", *|(?: +)|\:")
+
class ScriptDirectory(object):
if version_locations:
version_locations = _split_on_space_comma.split(version_locations)
+ prepend_sys_path = config.get_main_option("prepend_sys_path")
+ if prepend_sys_path:
+ sys.path[:0] = list(
+ _split_on_space_comma_colon.split(prepend_sys_path)
+ )
+
return ScriptDirectory(
util.coerce_resource_to_filename(script_location),
file_template=config.get_main_option(
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
Installation
============
-Installation of Alembic is typically local to a project setup and it is usually
-assumed that an approach like `virtual environments
-<https://docs.python.org/3/tutorial/venv.html>`_ are used, which would include
-that the target project also `has a setup.py script
-<https://packaging.python.org/tutorials/packaging-projects/>`_.
-
-.. note::
-
- While the ``alembic`` command line tool runs perfectly fine no matter where
- its installed, the rationale for project-local setup is that the Alembic
- command line tool runs most of its key operations through a Python file
- ``env.py`` that is established as part of a project's setup when the
- ``alembic init`` command is run for that project; the purpose of
- ``env.py`` is to establish database connectivity and optionally model
- definitions for the migration process, the latter of which in particular
- usually rely upon being able to import the modules of the project itself.
-
-
-The documentation below is **only one kind of approach to installing Alembic for a
-project**; there are many such approaches. The documentation below is
+While Alembic can be installed system wide, it's more common that it's
+installed local to a `virtual environment
+<https://docs.python.org/3/tutorial/venv.html>`_ , as it also uses libraries
+such as SQLAlchemy and database drivers that are more appropriate for
+local installations.
+
+The documentation below is **only one kind of approach to installing Alembic
+for a project**; there are many such approaches. The documentation below is
provided only for those users who otherwise have no specific project setup
chosen.
$ /path/to/your/project/.venv/bin/alembic init .
-Next, we ensure that the local project is also installed, in a development environment
-this would be in `editable mode <https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs>`_::
+The next step is **optional**. If our project itself has a ``setup.py``
+file, we can also install it in the local virtual environment in
+`editable mode <https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs>`_::
$ /path/to/your/project/.venv/bin/pip install -e .
+If we don't "install" the project locally, that's fine as well; the default
+``alembic.ini`` file includes a directive ``prepend_sys_path = .`` so that the
+local path is also in ``sys.path``. This allows us to run the ``alembic``
+command line tool from this directory without our project being "installed" in
+that environment.
+
+.. versionchanged:: 1.5.5 Fixed a long-standing issue where the ``alembic``
+ command-line tool would not preserve the default ``sys.path`` of ``.``
+ by implementing ``prepend_sys_path`` option.
+
As a final step, the `virtualenv activate <https://virtualenv.pypa.io/en/latest/userguide/#activate-script>`_
tool can be used so that the ``alembic`` command is available without any
path information, within the context of the current shell::
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
+ # sys.path path, will be prepended to sys.path if present.
+ # defaults to the current working directory.
+ # (new in 1.5.5)
+ prepend_sys_path = .
+
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
--- /dev/null
+.. change::
+ :tags: bug, environment
+ :tickets: 797
+
+ Added new config file option ``prepend_sys_path``, which is a series of
+ paths that will be prepended to sys.path; the default value in newly
+ generated alembic.ini files is ".". This fixes a long-standing issue
+ where for some reason running the alembic command line would not place the
+ local "." path in sys.path, meaning an application locally present in "."
+ and importable through normal channels, e.g. python interpreter, pytest,
+ etc. would not be located by Alembic, even though the ``env.py`` file is
+ loaded relative to the current path when ``alembic.ini`` contains a
+ relative path. To enable for existing installations, add the option to the
+ alembic.ini file as follows::
+
+ # sys.path path, will be prepended to sys.path if present.
+ # defaults to the current working directory.
+ prepend_sys_path = .
+
+ .. seealso::
+
+ :ref:`installation` - updated documentation reflecting that local
+ installation of the project is not necessary if running the Alembic cli
+ from the local path.
+
#!coding: utf-8
+import os
+import sys
+
from alembic import command
from alembic import testing
from alembic import util
from alembic.testing import is_true
from alembic.testing import mock
from alembic.testing.assertions import expect_raises_message
+from alembic.testing.env import _get_staging_directory
from alembic.testing.env import _no_sql_testing_config
from alembic.testing.env import _sqlite_file_db
+from alembic.testing.env import _sqlite_testing_config
from alembic.testing.env import clear_staging_env
from alembic.testing.env import staging_env
from alembic.testing.env import write_script
)
+class CWDTest(TestBase):
+ def setUp(self):
+ self.env = staging_env()
+ self.cfg = _sqlite_testing_config()
+
+ def tearDown(self):
+ clear_staging_env()
+
+ @testing.combinations(
+ (
+ ".",
+ ["."],
+ ),
+ ("/tmp/foo:/tmp/bar", ["/tmp/foo", "/tmp/bar"]),
+ ("/tmp/foo /tmp/bar", ["/tmp/foo", "/tmp/bar"]),
+ ("/tmp/foo,/tmp/bar", ["/tmp/foo", "/tmp/bar"]),
+ (". /tmp/foo", [".", "/tmp/foo"]),
+ )
+ def test_sys_path_prepend(self, config_value, expected):
+ self.cfg.set_main_option("prepend_sys_path", config_value)
+
+ script = ScriptDirectory.from_config(self.cfg)
+ env = EnvironmentContext(self.cfg, script)
+
+ target = os.path.abspath(_get_staging_directory())
+
+ def assert_(heads, context):
+ eq_(
+ [os.path.abspath(p) for p in sys.path[0 : len(expected)]],
+ [os.path.abspath(p) for p in expected],
+ )
+ return []
+
+ p = [p for p in sys.path if os.path.abspath(p) != target]
+ with mock.patch.object(sys, "path", p):
+ env.configure(url="sqlite://", fn=assert_)
+ with env:
+ script.run_env()
+
+
class MigrationTransactionTest(TestBase):
__backend__ = True