Skip to content

Commit fd42cdd

Browse files
committed
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
1 parent b76d33a commit fd42cdd

9 files changed

Lines changed: 99 additions & 0 deletions

File tree

alembic/script/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ def generate_revision(
696696
self._ensure_directory(version_path)
697697

698698
path = self._rev_path(version_path, revid, message, create_date)
699+
self._ensure_directory(path.parent)
699700

700701
if not splice:
701702
for head_ in heads:

alembic/templates/async/alembic.ini.mako

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ script_location = ${script_location}
1212
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
1313
# for all available tokens
1414
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
16+
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
1517

1618
# sys.path path, will be prepended to sys.path if present.
1719
# defaults to the current working directory. for multiple paths, the path separator

alembic/templates/generic/alembic.ini.mako

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ script_location = ${script_location}
1212
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
1313
# for all available tokens
1414
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
16+
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
1517

1618
# sys.path path, will be prepended to sys.path if present.
1719
# defaults to the current working directory. for multiple paths, the path separator

alembic/templates/multidb/alembic.ini.mako

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ script_location = ${script_location}
1212
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
1313
# for all available tokens
1414
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
16+
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
1517

1618
# sys.path path, will be prepended to sys.path if present.
1719
# defaults to the current working directory. for multiple paths, the path separator

alembic/templates/pyproject/pyproject.toml.mako

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ script_location = "${script_location}"
1111
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
1212
# for all available tokens
1313
# file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s"
14+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
15+
# file_template = "%%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s"
1416

1517
# additional paths to be prepended to sys.path. defaults to the current working directory.
1618
prepend_sys_path = [

alembic/templates/pyproject_async/pyproject.toml.mako

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ script_location = "${script_location}"
1111
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
1212
# for all available tokens
1313
# file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s"
14+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
15+
# file_template = "%%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s"
1416

1517
# additional paths to be prepended to sys.path. defaults to the current working directory.
1618
prepend_sys_path = [

docs/build/tutorial.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ The all-in-one .ini file created by ``generic`` is illustrated below::
157157
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
158158
# Uncomment the line below if you want the files to be prepended with date and time
159159
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
160+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
161+
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
160162

161163
# sys.path path, will be prepended to sys.path if present.
162164
# defaults to the current working directory.
@@ -361,6 +363,20 @@ This file contains the following features:
361363
by default ``datetime.datetime.now()`` unless the ``timezone``
362364
configuration option is also used.
363365

366+
The ``file_template`` may also include directory separators to organize
367+
migration files into subdirectories. When using directory paths in
368+
``file_template``, ``recursive_version_locations`` must be set to ``true``.
369+
For example::
370+
371+
file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
372+
recursive_version_locations = true
373+
374+
This would create migration files organized by date in a structure like
375+
``versions/2024/12/26_143022_abc123_add_user_table.py``.
376+
377+
.. versionadded:: 1.18.0
378+
Support for directory paths in ``file_template``
379+
364380
* ``timezone`` - an optional timezone name (e.g. ``UTC``, ``EST5EDT``, etc.)
365381
that will be applied to the timestamp which renders inside the migration
366382
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::
625641
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
626642
# Uncomment the line below if you want the files to be prepended with date and time
627643
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
644+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
645+
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
628646

629647
# additional paths to be prepended to sys.path. defaults to the current working directory.
630648
prepend_sys_path = [

docs/build/unreleased/1774.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. change::
2+
:tags: usecase, environment
3+
:tickets: 1774
4+
5+
The ``file_template`` configuration option now supports directory paths,
6+
allowing migration files to be organized into subdirectories. When using
7+
directory separators in ``file_template`` (e.g.,
8+
``%(year)d/%(month).2d/%(day).2d_%(rev)s_%(slug)s``), Alembic will
9+
automatically create the necessary directory structure. The
10+
``recursive_version_locations`` setting must be set to ``true`` when using
11+
this feature in order for the revision files to be located for subsequent
12+
commands.

tests/test_script_production.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,64 @@ def test_no_zoneinfo_module(self):
331331
)
332332

333333

334+
class FileTemplateDirectoryTest(TestBase):
335+
"""Test file_template with directory paths."""
336+
337+
def setUp(self):
338+
self.env = staging_env()
339+
340+
def tearDown(self):
341+
clear_staging_env()
342+
343+
@testing.variation("use_recursive_version_locations", [True, False])
344+
def test_file_template_with_directory_path(
345+
self, use_recursive_version_locations
346+
):
347+
"""Test that file_template supports directory paths."""
348+
script = ScriptDirectory(
349+
self.env.dir,
350+
file_template="%(year)d/%(month).2d/" "%(day).2d_%(rev)s_%(slug)s",
351+
recursive_version_locations=bool(use_recursive_version_locations),
352+
)
353+
354+
create_date = datetime.datetime(2024, 12, 26, 14, 30, 22)
355+
with mock.patch.object(
356+
script, "_generate_create_date", return_value=create_date
357+
):
358+
generated_script = script.generate_revision(
359+
util.rev_id(), "test message"
360+
)
361+
362+
# Verify file was created in subdirectory structure
363+
# regardless of recursive_version_locations setting
364+
assert generated_script is not None
365+
expected_path = (
366+
Path(self.env.dir)
367+
/ "versions"
368+
/ "2024"
369+
/ "12"
370+
/ f"26_{generated_script.revision}_test_message.py"
371+
)
372+
eq_(Path(generated_script.path), expected_path)
373+
assert expected_path.exists()
374+
375+
# Verify the script is loadable with recursive_version_locations,
376+
# but not if it's not set
377+
script2 = ScriptDirectory(
378+
self.env.dir,
379+
file_template="%(year)d/%(month).2d/" "%(day).2d_%(rev)s_%(slug)s",
380+
recursive_version_locations=bool(use_recursive_version_locations),
381+
)
382+
if use_recursive_version_locations:
383+
assert generated_script.revision in [
384+
rev.revision for rev in script2.walk_revisions()
385+
]
386+
else:
387+
assert generated_script.revision not in [
388+
rev.revision for rev in script2.walk_revisions()
389+
]
390+
391+
334392
class RevisionCommandTest(TestBase):
335393
def setUp(self):
336394
self.env = staging_env()

0 commit comments

Comments
 (0)