2626
2727"""
2828
29+ import logging
2930import os
3031import re
31- import subprocess
3232from typing import Tuple
3333
34- from alembic .config import Config
35- from alembic .util . exc import CommandError
34+ from alembic .command import revision as mk_revision
35+ from alembic .config import CommandLine , Config as AlembicConfig
3636from alembic .runtime .migration import MigrationContext
3737from alembic .runtime .environment import EnvironmentContext
3838from alembic .script import ScriptDirectory
39+ from alembic .util .exc import CommandError
3940from sqlalchemy .engine import create_engine
4041
4142from cardinal_pythonlib .fileops import preserve_cwd
42- from cardinal_pythonlib .logs import get_brace_style_log_with_null_handler
4343
44- log = get_brace_style_log_with_null_handler (__name__ )
44+ log = logging . getLogger (__name__ )
4545
4646
4747# =============================================================================
@@ -80,7 +80,7 @@ def get_head_revision_from_alembic(
8080
8181 Arguments:
8282 alembic_config_filename:
83- config filename
83+ config filename (usually a full path to an alembic.ini file)
8484 alembic_base_dir:
8585 directory to start in, so relative paths in the config file work.
8686 version_table:
@@ -89,9 +89,9 @@ def get_head_revision_from_alembic(
8989 if alembic_base_dir is None :
9090 alembic_base_dir = os .path .dirname (alembic_config_filename )
9191 os .chdir (alembic_base_dir ) # so the directory in the config file works
92- config = Config (alembic_config_filename )
93- script = ScriptDirectory .from_config (config )
94- with EnvironmentContext (config , script , version_table = version_table ):
92+ alembic_cfg = AlembicConfig (alembic_config_filename )
93+ script = ScriptDirectory .from_config (alembic_cfg )
94+ with EnvironmentContext (alembic_cfg , script , version_table = version_table ):
9595 return script .get_current_head ()
9696
9797
@@ -123,25 +123,28 @@ def get_current_and_head_revision(
123123 :func:`get_current_revision` and :func:`get_head_revision_from_alembic`.
124124
125125 Arguments:
126- database_url: SQLAlchemy URL for the database
127- alembic_config_filename: config filename
128- alembic_base_dir: directory to start in, so relative paths in the
129- config file work.
130- version_table: table name for Alembic versions
126+ database_url:
127+ SQLAlchemy URL for the database
128+ alembic_config_filename:
129+ config filename (usually a full path to an alembic.ini file)
130+ alembic_base_dir:
131+ directory to start in, so relative paths in the config file work.
132+ version_table:
133+ table name for Alembic versions
131134 """
132135 # Where we are
133136 head_revision = get_head_revision_from_alembic (
134137 alembic_config_filename = alembic_config_filename ,
135138 alembic_base_dir = alembic_base_dir ,
136139 version_table = version_table ,
137140 )
138- log .debug ("Intended database version: {}" , head_revision )
141+ log .debug (f "Intended database version: { head_revision } " )
139142
140143 # Where we want to be
141144 current_revision = get_current_revision (
142145 database_url = database_url , version_table = version_table
143146 )
144- log .debug ("Current database version: {}" , current_revision )
147+ log .debug (f "Current database version: { current_revision } " )
145148
146149 # Are we where we want to be?
147150 return current_revision , head_revision
@@ -165,49 +168,43 @@ def upgrade_database(
165168
166169 Arguments:
167170 alembic_config_filename:
168- config filename
169-
171+ config filename (usually a full path to an alembic.ini file)
170172 db_url:
171173 Optional database URL to use, by way of override.
172-
173174 alembic_base_dir:
174175 directory to start in, so relative paths in the config file work
175-
176176 starting_revision:
177177 revision to start at (typically ``None`` to ask the database)
178-
179178 destination_revision:
180179 revision to aim for (typically ``"head"`` to migrate to the latest
181180 structure)
182-
183- version_table: table name for Alembic versions
184-
181+ version_table:
182+ table name for Alembic versions
185183 as_sql:
186184 run in "offline" mode: print the migration SQL, rather than
187185 modifying the database. See
188186 https://alembic.zzzcomputing.com/en/latest/offline.html
189-
190187 """
191188
192189 if alembic_base_dir is None :
193190 alembic_base_dir = os .path .dirname (alembic_config_filename )
194191 os .chdir (alembic_base_dir ) # so the directory in the config file works
195- config = Config (alembic_config_filename )
192+ alembic_cfg = AlembicConfig (alembic_config_filename )
196193 if db_url :
197- config .set_main_option ("sqlalchemy.url" , db_url )
198- script = ScriptDirectory .from_config (config )
194+ alembic_cfg .set_main_option ("sqlalchemy.url" , db_url )
195+ script = ScriptDirectory .from_config (alembic_cfg )
199196
200197 # noinspection PyUnusedLocal,PyProtectedMember
201198 def upgrade (rev , context ):
202199 return script ._upgrade_revs (destination_revision , rev )
203200
204201 log .info (
205- "Upgrading database to revision {!r} using Alembic" ,
206- destination_revision ,
202+ f "Upgrading database to revision { destination_revision !r} "
203+ f"using Alembic"
207204 )
208205
209206 with EnvironmentContext (
210- config ,
207+ alembic_cfg ,
211208 script ,
212209 fn = upgrade ,
213210 as_sql = as_sql ,
@@ -240,48 +237,42 @@ def downgrade_database(
240237
241238 Arguments:
242239 alembic_config_filename:
243- config filename
244-
240+ config filename (usually a full path to an alembic.ini file)
245241 db_url:
246242 Optional database URL to use, by way of override.
247-
248243 alembic_base_dir:
249244 directory to start in, so relative paths in the config file work
250-
251245 starting_revision:
252246 revision to start at (typically ``None`` to ask the database)
253-
254247 destination_revision:
255248 revision to aim for
256-
257- version_table: table name for Alembic versions
258-
249+ version_table:
250+ table name for Alembic versions
259251 as_sql:
260252 run in "offline" mode: print the migration SQL, rather than
261253 modifying the database. See
262254 https://alembic.zzzcomputing.com/en/latest/offline.html
263-
264255 """
265256
266257 if alembic_base_dir is None :
267258 alembic_base_dir = os .path .dirname (alembic_config_filename )
268259 os .chdir (alembic_base_dir ) # so the directory in the config file works
269- config = Config (alembic_config_filename )
260+ alembic_cfg = AlembicConfig (alembic_config_filename )
270261 if db_url :
271- config .set_main_option ("sqlalchemy.url" , db_url )
272- script = ScriptDirectory .from_config (config )
262+ alembic_cfg .set_main_option ("sqlalchemy.url" , db_url )
263+ script = ScriptDirectory .from_config (alembic_cfg )
273264
274265 # noinspection PyUnusedLocal,PyProtectedMember
275266 def downgrade (rev , context ):
276267 return script ._downgrade_revs (destination_revision , rev )
277268
278269 log .info (
279- "Downgrading database to revision {!r} using Alembic" ,
280- destination_revision ,
270+ f "Downgrading database to revision { destination_revision !r} "
271+ f"using Alembic"
281272 )
282273
283274 with EnvironmentContext (
284- config ,
275+ alembic_cfg ,
285276 script ,
286277 fn = downgrade ,
287278 as_sql = as_sql ,
@@ -301,6 +292,7 @@ def create_database_migration_numbered_style(
301292 alembic_versions_dir : str ,
302293 message : str ,
303294 n_sequence_chars : int = 4 ,
295+ db_url : str = None ,
304296) -> None :
305297 """
306298 Create a new Alembic migration script.
@@ -331,35 +323,43 @@ def create_database_migration_numbered_style(
331323
332324 See https://alembic.zzzcomputing.com/en/latest/autogenerate.html.
333325
334- Regarding filenames: the default ``n_sequence_chars`` of 4 is like Django
335- and gives files with names like
326+ Regarding filenames: the default ``n_sequence_chars`` of 4 is like Django
327+ and gives files with names like
336328
337- .. code-block:: none
329+ .. code-block:: none
338330
339- 0001_x.py, 0002_y.py, ...
331+ 0001_x.py, 0002_y.py, ...
340332
341- NOTE THAT TO USE A NON-STANDARD ALEMBIC VERSION TABLE, YOU MUST SPECIFY
342- THAT IN YOUR ``env.py`` (see e.g. CamCOPS).
333+ NOTE THAT TO USE A NON-STANDARD ALEMBIC VERSION TABLE, YOU MUST SPECIFY
334+ THAT IN YOUR ``env.py`` (see e.g. CamCOPS).
343335
344- Args:
345- alembic_ini_file: filename of Alembic ``alembic.ini`` file
346- alembic_versions_dir: directory in which you keep your Python scripts,
347- one per Alembic revision
348- message: message to be associated with this revision
349- n_sequence_chars: number of numerical sequence characters to use in the
350- filename/revision (see above).
336+ Args:
337+ alembic_ini_file:
338+ filename (full path) of Alembic ``alembic.ini`` file
339+ alembic_versions_dir:
340+ directory in which you keep your Python scripts, one per Alembic
341+ revision
342+ message:
343+ message to be associated with this revision
344+ n_sequence_chars:
345+ number of numerical sequence characters to use in the
346+ filename/revision (see above).
347+ db_url:
348+ Optional database URL to use, by way of override. We achieve this
349+ via a temporary config file; not ideal.
351350 """ # noqa: E501
352- file_regex = r"\d{" + str (n_sequence_chars ) + r"}_\S*\.py$"
353351
352+ # Calculate current_seq_str, new_seq_str:
353+ file_regex = r"\d{" + str (n_sequence_chars ) + r"}_\S*\.py$"
354354 _ , _ , existing_version_filenames = next (
355355 os .walk (alembic_versions_dir ), (None , None , [])
356356 )
357357 existing_version_filenames = [
358358 x for x in existing_version_filenames if re .match (file_regex , x )
359359 ]
360360 log .debug (
361- "Existing Alembic version script filenames: {!r}" ,
362- existing_version_filenames ,
361+ f "Existing Alembic version script filenames: "
362+ f" { existing_version_filenames !r } "
363363 )
364364 current_seq_strs = [
365365 x [:n_sequence_chars ] for x in existing_version_filenames
@@ -374,37 +374,29 @@ def create_database_migration_numbered_style(
374374 new_seq_str = str (new_seq_no ).zfill (n_sequence_chars )
375375
376376 log .info (
377- """
378- Generating new revision with Alembic...
379- Last revision was: {}
380- New revision will be: {}
381- [If it fails with "Can't locate revision identified by...", you might need
382- to DROP the Alembic version table (by default named 'alembic_version', but
383- you may have elected to change that in your env.py.]
384- """ ,
385- current_seq_str ,
386- new_seq_str ,
377+ f"Generating new revision with Alembic. "
378+ f"Last revision was: { current_seq_str } . "
379+ f"New revision will be: { new_seq_str } . "
380+ f"(If the process fails with \" Can't locate revision identified "
381+ f'by...", you might need to DROP the Alembic version table; by '
382+ f"default that is named { DEFAULT_ALEMBIC_VERSION_TABLE !r} , but you "
383+ f"may have elected to change that in your 'env.py' file.)"
387384 )
388385
389386 alembic_ini_dir = os .path .dirname (alembic_ini_file )
390387 os .chdir (alembic_ini_dir )
391- cmdargs = [
392- "alembic" ,
393- "-c" ,
394- alembic_ini_file ,
395- "revision" ,
396- "--autogenerate" ,
397- "-m" ,
398- message ,
399- "--rev-id" ,
400- new_seq_str ,
401- ]
402- log .info ("From directory {!r}, calling: {!r}" , alembic_ini_dir , cmdargs )
403- subprocess .call (cmdargs )
388+
389+ # https://github.com/sqlalchemy/alembic/discussions/1089
390+ namespace = CommandLine ().parser .parse_args (["revision" , "--autogenerate" ])
391+ config = AlembicConfig (alembic_ini_file , cmd_opts = namespace )
392+ if db_url :
393+ config .set_main_option ("sqlalchemy.url" , db_url )
394+
395+ mk_revision (config , message = message , autogenerate = True , rev_id = new_seq_str )
404396
405397
406398def stamp_allowing_unusual_version_table (
407- config : Config ,
399+ config : AlembicConfig ,
408400 revision : str ,
409401 sql : bool = False ,
410402 tag : str = None ,
0 commit comments