]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Add logging for config load source in verbose mode
authorqu3vipon <sdh5813@gmail.com>
Wed, 19 Nov 2025 20:47:39 +0000 (15:47 -0500)
committerFederico Caselli <cfederico87@gmail.com>
Thu, 20 Nov 2025 20:54:05 +0000 (21:54 +0100)
### Description
I added a log message to indicate where the Alembic configuration is being loaded from (file vs in-memory), which helps when verbose mode is enabled.

I also wrote tests for both branches. Each test passes when run individually, but they fail when running the entire test suite. It seems to be related to how Alembic’s logging hierarchy interacts with the global test environment, and I'm having difficulty diagnosing the issue with my current understanding of the logging system.

I'd appreciate any guidance or suggestions from maintainers on how the logging should be captured or how tests in this area are expected to be structured.

### Checklist
This pull request is:

- [ ] A documentation / typographical error fix
- Good to go, no issue or tests are needed
- [x] A short code fix
- please include the issue number, and create an issue if none exists, which
  must include a complete example of the issue.  one line code fixes without an
  issue and demonstration will not be accepted.
- Please include: `Fixes: #<issue number>` in the commit message
- please include tests.   one line code fixes without tests will not be accepted.
- [ ] A new feature implementation
- please include the issue number, and create an issue if none exists, which must
  include a complete example of how the feature would look.
- Please include: `Fixes: #<issue number>` in the commit message
- please include tests.

**Have a nice day!**

Fixes: #1737
Closes: #1754
Pull-request: https://github.com/sqlalchemy/alembic/pull/1754
Pull-request-sha: c9cacc717998301280d87d82c0cbfe9da1aee7c8

Change-Id: I9941f68b264bdc297c6afb2d5e8af34fe0d234fa

alembic/config.py
alembic/testing/env.py
docs/build/unreleased/1737.rst [new file with mode: 0644]
tests/test_config.py

index fb8dd8a6bafe740be175f1e1b76a53608521585c..121a4459cd56fa5ab6bf174e8cb7a9946ba95b0b 100644 (file)
@@ -4,6 +4,7 @@ from argparse import ArgumentParser
 from argparse import Namespace
 from configparser import ConfigParser
 import inspect
+import logging
 import os
 from pathlib import Path
 import re
@@ -28,6 +29,9 @@ from .util import compat
 from .util.pyfiles import _preserving_path_as_str
 
 
+log = logging.getLogger(__name__)
+
+
 class Config:
     r"""Represent an Alembic configuration.
 
@@ -244,10 +248,20 @@ class Config:
             here = Path()
         self.config_args["here"] = here.as_posix()
         file_config = ConfigParser(self.config_args)
+
+        verbose = getattr(self.cmd_opts, "verbose", False)
         if self._config_file_path:
             compat.read_config_parser(file_config, [self._config_file_path])
+            if verbose:
+                log.info(
+                    "Loading config from file: %s", self._config_file_path
+                )
         else:
             file_config.add_section(self.config_ini_section)
+            if verbose:
+                log.info(
+                    "No config file provided; using in-memory default config"
+                )
         return file_config
 
     @util.memoized_property
index 72a5e42451f11d15fd401f5cee878ddb8dcea04a..ad4de783530018be713b1cf6729072752a56e7d9 100644 (file)
@@ -1,4 +1,5 @@
 import importlib.machinery
+import logging
 import os
 from pathlib import Path
 import shutil
@@ -22,7 +23,29 @@ def _get_staging_directory():
         return "scratch"
 
 
+_restore_log = None
+
+
+def _replace_logger():
+    global _restore_log
+    if _restore_log is None:
+        _restore_log = (logging.root, logging.Logger.manager)
+        logging.root = logging.RootLogger(logging.WARNING)
+        logging.Logger.root = logging.root
+        logging.Logger.manager = logging.Manager(logging.root)
+
+
+def _restore_logger():
+    global _restore_log
+
+    if _restore_log is not None:
+        logging.root, logging.Logger.manager = _restore_log
+        logging.Logger.root = logging.root
+        _restore_log = None
+
+
 def staging_env(create=True, template="generic", sourceless=False):
+    _replace_logger()
     cfg = _testing_config()
     if create:
         path = _join_path(_get_staging_directory(), "scripts")
@@ -61,6 +84,7 @@ def clear_staging_env():
 
     engines.testing_reaper.close_all()
     shutil.rmtree(_get_staging_directory(), True)
+    _restore_logger()
 
 
 def script_file_fixture(txt):
diff --git a/docs/build/unreleased/1737.rst b/docs/build/unreleased/1737.rst
new file mode 100644 (file)
index 0000000..84c4f98
--- /dev/null
@@ -0,0 +1,6 @@
+.. change::
+    :tags: feature, operations
+    :tickets: 1737
+
+    When alembic is run in "verbose" mode, alembic now logs a message to
+    indicate from which file is used to load the configuration.
index 2a1960a4eb439bbce69760d6167c902b1f697e28..0dc840f8c355901b83445730740770dd56c7accd 100644 (file)
@@ -1,3 +1,5 @@
+import io
+import logging
 import os
 import pathlib
 import sys
@@ -45,6 +47,66 @@ migrations = %(base_path)s/db/migrations
 
 
 class ConfigTest(TestBase):
+    def test_config_logging_with_file(self):
+        buf = io.StringIO()
+        handler = logging.StreamHandler(buf)
+        handler.setLevel(logging.INFO)
+
+        logger = logging.getLogger("alembic.config")
+        # logger.x=True
+        with (
+            mock.patch.object(logger, "handlers", []),
+            mock.patch.object(logger, "level", logging.NOTSET),
+        ):
+            logger.addHandler(handler)
+            logger.setLevel(logging.INFO)
+
+            cfg = _write_config_file(
+                """
+[alembic]
+script_location = %(base_path)s/db/migrations
+"""
+            )
+            test_cfg = config.Config(
+                cfg.config_file_name, config_args=dict(base_path="/tmp")
+            )
+            test_cfg.cmd_opts = mock.Mock(verbose=True)
+
+            _ = test_cfg.file_config
+
+            output = buf.getvalue()
+            assert "Loading config from file" in output
+            assert cfg.config_file_name.replace("/", os.path.sep) in output
+
+    def tearDown(self):
+        clear_staging_env()
+
+    def test_config_logging_without_file(self):
+        buf = io.StringIO()
+        handler = logging.StreamHandler(buf)
+        handler.setLevel(logging.INFO)
+
+        logger = logging.getLogger("alembic.config")
+        with (
+            mock.patch.object(logger, "handlers", []),
+            mock.patch.object(logger, "level", logging.NOTSET),
+        ):
+
+            logger.addHandler(handler)
+            logger.setLevel(logging.INFO)
+
+            test_cfg = config.Config()
+            test_cfg.cmd_opts = mock.Mock(verbose=True)
+
+            _ = test_cfg.file_config
+
+            output = buf.getvalue()
+            assert "No config file provided" in output
+            assert (
+                test_cfg.config_file_name is None
+                and test_cfg._config_file_path is None
+            )
+
     def test_config_no_file_main_option(self):
         cfg = config.Config()
         cfg.set_main_option("url", "postgresql://foo/bar")