]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Implement sys_path_prepend option
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 20 Feb 2021 20:48:58 +0000 (15:48 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 20 Feb 2021 21:32:36 +0000 (16:32 -0500)
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.

Change-Id: If8f4279bd862acca44d46f4b1ab90b0a18098af3
Fixes: #797
alembic/script/base.py
alembic/templates/generic/alembic.ini.mako
alembic/templates/multidb/alembic.ini.mako
alembic/templates/pylons/alembic.ini.mako
docs/build/front.rst
docs/build/tutorial.rst
docs/build/unreleased/797.rst [new file with mode: 0644]
tests/test_environment.py

index a9b5705d20437d93d2a7ef0f5c06b3cc74af3949..3f618643521f47fa588f1c04472d83035abb810a 100644 (file)
@@ -3,6 +3,7 @@ import datetime
 import os
 import re
 import shutil
+import sys
 
 from dateutil import tz
 
@@ -20,6 +21,8 @@ _slug_re = re.compile(r"\w+")
 _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):
 
@@ -136,6 +139,12 @@ 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(
index 281794fb0fc31c2ba04041c0675294f987af2149..bf7e5d1c59631a51d641146f1549966c60f2d01f 100644 (file)
@@ -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()
index 0b0919e0655e878f8f762fc8a80728d833d4d6a6..ec3c5193448f2593c07bb86c2c01e3dad32a4227 100644 (file)
@@ -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()
index 70fead0b1bcfacfcaa2dd1ef74bc50af46631b39..c37397d80d177b98ae8da5c707098e9e26c11882 100644 (file)
@@ -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()
index 09d7b6bcb90a7d09bc22f880dfcf5694697b6a72..a56a85fba97ad4bdfb57028e79669ceb9a57acba 100644 (file)
@@ -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.
 
@@ -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::
index 487eb394c5613c4d1af1d0b2fd546f121a26699d..54da11124d1b4b3d5a4bdde6db543fdda425092f 100644 (file)
@@ -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()
diff --git a/docs/build/unreleased/797.rst b/docs/build/unreleased/797.rst
new file mode 100644 (file)
index 0000000..9f01e41
--- /dev/null
@@ -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.
+
index 63de6cd8f06a64b8f6fe243fe10a5c30c12c9205..e90d6fb0df6490df693b1bc904177cc3d0be484e 100644 (file)
@@ -1,4 +1,7 @@
 #!coding: utf-8
+import os
+import sys
+
 from alembic import command
 from alembic import testing
 from alembic import util
@@ -13,8 +16,10 @@ from alembic.testing import is_false
 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
@@ -143,6 +148,46 @@ def downgrade():
             )
 
 
+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