Skip to content

Commit

Permalink
Merge "Implement sys_path_prepend option"
Browse files Browse the repository at this point in the history
  • Loading branch information
zzzeek authored and Gerrit Code Review committed Feb 20, 2021
2 parents 4dfcdaa + d6b0c1a commit be57ace
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 22 deletions.
9 changes: 9 additions & 0 deletions alembic/script/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import re
import shutil
import sys

from dateutil import tz

Expand All @@ -20,6 +21,8 @@
_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):

Expand Down Expand Up @@ -136,6 +139,12 @@ def from_config(cls, config):
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(
Expand Down
4 changes: 4 additions & 0 deletions alembic/templates/generic/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ script_location = ${script_location}
# 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()
Expand Down
4 changes: 4 additions & 0 deletions alembic/templates/multidb/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ script_location = ${script_location}
# 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()
Expand Down
4 changes: 4 additions & 0 deletions alembic/templates/pylons/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ script_location = ${script_location}
# 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()
Expand Down
43 changes: 21 additions & 22 deletions docs/build/front.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,14 @@ The most recent published version of this documentation should be at https://ale
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.

Expand All @@ -64,11 +52,22 @@ proceed through the usage of this command, as in::

$ /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::
Expand Down
5 changes: 5 additions & 0 deletions docs/build/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ The file generated with the "generic" configuration looks like::
# 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()
Expand Down
25 changes: 25 additions & 0 deletions docs/build/unreleased/797.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.. 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.

45 changes: 45 additions & 0 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!coding: utf-8
import os
import sys

from alembic import command
from alembic import testing
from alembic import util
Expand All @@ -13,8 +16,10 @@
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
Expand Down Expand Up @@ -143,6 +148,46 @@ def upgrade(rev, context):
)


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

Expand Down

0 comments on commit be57ace

Please sign in to comment.