From: qu3vipon Date: Wed, 19 Nov 2025 20:47:39 +0000 (-0500) Subject: Add logging for config load source in verbose mode X-Git-Tag: rel_1_18_0~14^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e1f72ffad7f1b4fa92d20c1d499f8c175ff72672;p=thirdparty%2Fsqlalchemy%2Falembic.git Add logging for config load source in verbose mode ### 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: #` 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: #` 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 --- diff --git a/alembic/config.py b/alembic/config.py index fb8dd8a6..121a4459 100644 --- a/alembic/config.py +++ b/alembic/config.py @@ -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 diff --git a/alembic/testing/env.py b/alembic/testing/env.py index 72a5e424..ad4de783 100644 --- a/alembic/testing/env.py +++ b/alembic/testing/env.py @@ -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 index 00000000..84c4f980 --- /dev/null +++ b/docs/build/unreleased/1737.rst @@ -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. diff --git a/tests/test_config.py b/tests/test_config.py index 2a1960a4..0dc840f8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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")