From: Mike Bayer Date: Fri, 26 Dec 2025 17:14:49 +0000 (-0500) Subject: support paths in file_template X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=fd42cddc3eeb7f2594f50863a7e6b024f07d0b2c;p=thirdparty%2Fsqlalchemy%2Falembic.git support paths in file_template The ``file_template`` configuration option now supports directory paths, allowing migration files to be organized into subdirectories. When using directory separators in ``file_template`` (e.g., ``%(year)d/%(month).2d/%(day).2d_%(rev)s_%(slug)s``), Alembic will automatically create the necessary directory structure. The ``recursive_version_locations`` setting must be set to ``true`` when using this feature in order for the revision files to be located for subsequent commands. Fixes: #1774 Change-Id: Id68a3b0483c6519d724bbb79bbf8b471ea697c36 --- diff --git a/alembic/script/base.py b/alembic/script/base.py index 6b9692c6..f8417085 100644 --- a/alembic/script/base.py +++ b/alembic/script/base.py @@ -696,6 +696,7 @@ class ScriptDirectory: self._ensure_directory(version_path) path = self._rev_path(version_path, revid, message, create_date) + self._ensure_directory(path.parent) if not splice: for head_ in heads: diff --git a/alembic/templates/async/alembic.ini.mako b/alembic/templates/async/alembic.ini.mako index 62617c4c..02ccb0f6 100644 --- a/alembic/templates/async/alembic.ini.mako +++ b/alembic/templates/async/alembic.ini.mako @@ -12,6 +12,8 @@ script_location = ${script_location} # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file # for all available tokens # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. for multiple paths, the path separator diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index eb8c063f..0127b2af 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -12,6 +12,8 @@ script_location = ${script_location} # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file # for all available tokens # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. for multiple paths, the path separator diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index f4b65e62..76846465 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -12,6 +12,8 @@ script_location = ${script_location} # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file # for all available tokens # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. for multiple paths, the path separator diff --git a/alembic/templates/pyproject/pyproject.toml.mako b/alembic/templates/pyproject/pyproject.toml.mako index 224faec1..7edd43b0 100644 --- a/alembic/templates/pyproject/pyproject.toml.mako +++ b/alembic/templates/pyproject/pyproject.toml.mako @@ -11,6 +11,8 @@ script_location = "${script_location}" # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file # for all available tokens # file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s" +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = "%%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s" # additional paths to be prepended to sys.path. defaults to the current working directory. prepend_sys_path = [ diff --git a/alembic/templates/pyproject_async/pyproject.toml.mako b/alembic/templates/pyproject_async/pyproject.toml.mako index 224faec1..7edd43b0 100644 --- a/alembic/templates/pyproject_async/pyproject.toml.mako +++ b/alembic/templates/pyproject_async/pyproject.toml.mako @@ -11,6 +11,8 @@ script_location = "${script_location}" # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file # for all available tokens # file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s" +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = "%%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s" # additional paths to be prepended to sys.path. defaults to the current working directory. prepend_sys_path = [ diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 365842b0..60242bd6 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -157,6 +157,8 @@ The all-in-one .ini file created by ``generic`` is illustrated below:: # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + # Or organize into date-based subdirectories (requires recursive_version_locations = true) + # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. @@ -361,6 +363,20 @@ This file contains the following features: by default ``datetime.datetime.now()`` unless the ``timezone`` configuration option is also used. + The ``file_template`` may also include directory separators to organize + migration files into subdirectories. When using directory paths in + ``file_template``, ``recursive_version_locations`` must be set to ``true``. + For example:: + + file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s + recursive_version_locations = true + + This would create migration files organized by date in a structure like + ``versions/2024/12/26_143022_abc123_add_user_table.py``. + + .. versionadded:: 1.18.0 + Support for directory paths in ``file_template`` + * ``timezone`` - an optional timezone name (e.g. ``UTC``, ``EST5EDT``, etc.) that will be applied to the timestamp which renders inside the migration file's comment as well as within the filename. This option requires Python>=3.9 @@ -625,6 +641,8 @@ remains available as the absolute path to the ``pyproject.toml`` file:: # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + # Or organize into date-based subdirectories (requires recursive_version_locations = true) + # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s # additional paths to be prepended to sys.path. defaults to the current working directory. prepend_sys_path = [ diff --git a/docs/build/unreleased/1774.rst b/docs/build/unreleased/1774.rst new file mode 100644 index 00000000..acf89afa --- /dev/null +++ b/docs/build/unreleased/1774.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: usecase, environment + :tickets: 1774 + + The ``file_template`` configuration option now supports directory paths, + allowing migration files to be organized into subdirectories. When using + directory separators in ``file_template`` (e.g., + ``%(year)d/%(month).2d/%(day).2d_%(rev)s_%(slug)s``), Alembic will + automatically create the necessary directory structure. The + ``recursive_version_locations`` setting must be set to ``true`` when using + this feature in order for the revision files to be located for subsequent + commands. diff --git a/tests/test_script_production.py b/tests/test_script_production.py index b8a3c8c6..a9145773 100644 --- a/tests/test_script_production.py +++ b/tests/test_script_production.py @@ -331,6 +331,64 @@ class ScriptNamingTest(TestBase): ) +class FileTemplateDirectoryTest(TestBase): + """Test file_template with directory paths.""" + + def setUp(self): + self.env = staging_env() + + def tearDown(self): + clear_staging_env() + + @testing.variation("use_recursive_version_locations", [True, False]) + def test_file_template_with_directory_path( + self, use_recursive_version_locations + ): + """Test that file_template supports directory paths.""" + script = ScriptDirectory( + self.env.dir, + file_template="%(year)d/%(month).2d/" "%(day).2d_%(rev)s_%(slug)s", + recursive_version_locations=bool(use_recursive_version_locations), + ) + + create_date = datetime.datetime(2024, 12, 26, 14, 30, 22) + with mock.patch.object( + script, "_generate_create_date", return_value=create_date + ): + generated_script = script.generate_revision( + util.rev_id(), "test message" + ) + + # Verify file was created in subdirectory structure + # regardless of recursive_version_locations setting + assert generated_script is not None + expected_path = ( + Path(self.env.dir) + / "versions" + / "2024" + / "12" + / f"26_{generated_script.revision}_test_message.py" + ) + eq_(Path(generated_script.path), expected_path) + assert expected_path.exists() + + # Verify the script is loadable with recursive_version_locations, + # but not if it's not set + script2 = ScriptDirectory( + self.env.dir, + file_template="%(year)d/%(month).2d/" "%(day).2d_%(rev)s_%(slug)s", + recursive_version_locations=bool(use_recursive_version_locations), + ) + if use_recursive_version_locations: + assert generated_script.revision in [ + rev.revision for rev in script2.walk_revisions() + ] + else: + assert generated_script.revision not in [ + rev.revision for rev in script2.walk_revisions() + ] + + class RevisionCommandTest(TestBase): def setUp(self): self.env = staging_env()