]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Added quiet option to command line
authorCaselIT <cfederico87@gmail.com>
Sun, 2 Apr 2023 10:21:00 +0000 (12:21 +0200)
committerFederico Caselli <cfederico87@gmail.com>
Fri, 28 Apr 2023 21:00:04 +0000 (21:00 +0000)
Added quiet option to the command line, using the ``-q/--quiet``
option. This flag will prevent alembic from logging anything
to stdout.

Fixes: #1109
Change-Id: I7d9fac05d93e07efaefd87a582a7e785891798ef

13 files changed:
.github/workflows/run-on-pr.yaml
.github/workflows/run-test.yaml
alembic/__init__.py
alembic/command.py
alembic/config.py
alembic/script/base.py
alembic/script/write_hooks.py
alembic/util/__init__.py
alembic/util/langhelpers.py
alembic/util/messaging.py
docs/build/unreleased/1109.rst [new file with mode: 0644]
tests/test_command.py
tests/test_script_production.py

index 360a6cf227c509f1641a4b1694075f2a00e3f504..f3a4691ec5ec31ce532aa4e0c0e5eca81f78d10d 100644 (file)
@@ -25,7 +25,7 @@ jobs:
         os:
           - "ubuntu-latest"
         python-version:
-          - "3.10"
+          - "3.11"
         sqlalchemy:
           - sqla13
           - sqla14
@@ -61,7 +61,7 @@ jobs:
         os:
           - "ubuntu-latest"
         python-version:
-          - "3.10"
+          - "3.11"
 
       fail-fast: false
 
index 3ff5e510802585a090f4f0471da181061675aa37..cddf78ba74f3ed66848995e3a67fc26923d0abae 100644 (file)
@@ -34,6 +34,7 @@ jobs:
           - "3.8"
           - "3.9"
           - "3.10"
+          - "3.11"
         sqlalchemy:
           - sqla13
           - sqla14
@@ -71,6 +72,7 @@ jobs:
         python-version:
           - "3.9"
           - "3.10"
+          - "3.11"
 
       fail-fast: false
 
index b069e1a6ec3f2afbf7c6950e5c5c585fb3cd1fd9..4c6b97b19be441d6162259bdce554a5d56031e27 100644 (file)
@@ -3,4 +3,4 @@ import sys
 from . import context
 from . import op
 
-__version__ = "1.10.5"
+__version__ = "1.11.0"
index a8e56571f162c7f767ca3ac5f5a8f050245dde23..a015be398fc6f0440aa79c9d84be6e7da71329b7 100644 (file)
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
     from .runtime.environment import ProcessRevisionDirectiveFn
 
 
-def list_templates(config):
+def list_templates(config: Config):
     """List available templates.
 
     :param config: a :class:`.Config` object.
@@ -69,28 +69,32 @@ def init(
         raise util.CommandError("No such template %r" % template)
 
     if not os.access(directory, os.F_OK):
-        util.status(
-            "Creating directory %s" % os.path.abspath(directory),
-            os.makedirs,
-            directory,
-        )
+        with util.status(
+            f"Creating directory {os.path.abspath(directory)!r}",
+            **config.messaging_opts,
+        ):
+            os.makedirs(directory)
 
     versions = os.path.join(directory, "versions")
-    util.status(
-        "Creating directory %s" % os.path.abspath(versions),
-        os.makedirs,
-        versions,
-    )
+    with util.status(
+        f"Creating directory {os.path.abspath(versions)!r}",
+        **config.messaging_opts,
+    ):
+        os.makedirs(versions)
 
     script = ScriptDirectory(directory)
 
+    config_file: str | None = None
     for file_ in os.listdir(template_dir):
         file_path = os.path.join(template_dir, file_)
         if file_ == "alembic.ini.mako":
             assert config.config_file_name is not None
             config_file = os.path.abspath(config.config_file_name)
             if os.access(config_file, os.F_OK):
-                util.msg("File %s already exists, skipping" % config_file)
+                util.msg(
+                    f"File {config_file!r} already exists, skipping",
+                    **config.messaging_opts,
+                )
             else:
                 script._generate_template(
                     file_path, config_file, script_location=directory
@@ -104,12 +108,15 @@ def init(
             os.path.join(os.path.abspath(directory), "__init__.py"),
             os.path.join(os.path.abspath(versions), "__init__.py"),
         ]:
-            file_ = util.status("Adding %s" % path, open, path, "w")
-            file_.close()  # type:ignore[attr-defined]
+            with util.status("Adding {path!r}", **config.messaging_opts):
+                with open(path, "w"):
+                    pass
 
+    assert config_file is not None
     util.msg(
         "Please edit configuration/connection/logging "
-        "settings in %r before proceeding." % config_file
+        f"settings in {config_file!r} before proceeding.",
+        **config.messaging_opts,
     )
 
 
index 338769b29fa576c121362f25892a755be8668d0a..2968f0c0b740376523983519b2f2584b14f83f22 100644 (file)
@@ -6,12 +6,16 @@ from configparser import ConfigParser
 import inspect
 import os
 import sys
-from typing import Dict
+from typing import Any
+from typing import cast
+from typing import Mapping
 from typing import Optional
 from typing import overload
 from typing import TextIO
 from typing import Union
 
+from typing_extensions import TypedDict
+
 from . import __version__
 from . import command
 from . import util
@@ -99,7 +103,7 @@ class Config:
         output_buffer: Optional[TextIO] = None,
         stdout: TextIO = sys.stdout,
         cmd_opts: Optional[Namespace] = None,
-        config_args: util.immutabledict = util.immutabledict(),
+        config_args: Mapping[str, Any] = util.immutabledict(),
         attributes: Optional[dict] = None,
     ) -> None:
         """Construct a new :class:`.Config`"""
@@ -162,6 +166,8 @@ class Config:
         those arguments will formatted against the provided text,
         otherwise we simply output the provided text verbatim.
 
+        This is a no-op when the``quiet`` messaging option is enabled.
+
         e.g.::
 
             >>> config.print_stdout('Some text %s', 'arg')
@@ -174,7 +180,7 @@ class Config:
         else:
             output = str(text)
 
-        util.write_outstream(self.stdout, output, "\n")
+        util.write_outstream(self.stdout, output, "\n", **self.messaging_opts)
 
     @util.memoized_property
     def file_config(self):
@@ -213,14 +219,14 @@ class Config:
 
     @overload
     def get_section(
-        self, name: str, default: Dict[str, str]
-    ) -> Dict[str, str]:
+        self, name: str, default: Mapping[str, str]
+    ) -> Mapping[str, str]:
         ...
 
     @overload
     def get_section(
-        self, name: str, default: Optional[Dict[str, str]] = ...
-    ) -> Optional[Dict[str, str]]:
+        self, name: str, default: Optional[Mapping[str, str]] = ...
+    ) -> Optional[Mapping[str, str]]:
         ...
 
     def get_section(self, name: str, default=None):
@@ -311,6 +317,20 @@ class Config:
         """
         return self.get_section_option(self.config_ini_section, name, default)
 
+    @util.memoized_property
+    def messaging_opts(self) -> MessagingOptions:
+        """The messaging options."""
+        return cast(
+            MessagingOptions,
+            util.immutabledict(
+                {"quiet": getattr(self.cmd_opts, "quiet", False)}
+            ),
+        )
+
+
+class MessagingOptions(TypedDict, total=False):
+    quiet: bool
+
 
 class CommandLine:
     def __init__(self, prog: Optional[str] = None) -> None:
@@ -512,6 +532,12 @@ class CommandLine:
             action="store_true",
             help="Raise a full stack trace on error",
         )
+        parser.add_argument(
+            "-q",
+            "--quiet",
+            action="store_true",
+            help="Do not log to std output.",
+        )
         subparsers = parser.add_subparsers()
 
         positional_translations = {command.stamp: {"revision": "revisions"}}
@@ -568,7 +594,7 @@ class CommandLine:
             if options.raiseerr:
                 raise
             else:
-                util.err(str(e))
+                util.err(str(e), **config.messaging_opts)
 
     def main(self, argv=None):
         options = self.parser.parse_args(argv)
index b6858b59acb13471960428c1cbfc59147f8cb8d9..244086589f930086fa375593759a2fc6ed6f27ca 100644 (file)
@@ -9,9 +9,9 @@ import sys
 from types import ModuleType
 from typing import Any
 from typing import cast
-from typing import Dict
 from typing import Iterator
 from typing import List
+from typing import Mapping
 from typing import Optional
 from typing import Sequence
 from typing import Set
@@ -27,6 +27,7 @@ from ..util import not_none
 
 if TYPE_CHECKING:
     from ..config import Config
+    from ..config import MessagingOptions
     from ..runtime.migration import RevisionStep
     from ..runtime.migration import StampStep
     from ..script.revision import Revision
@@ -79,8 +80,11 @@ class ScriptDirectory:
         sourceless: bool = False,
         output_encoding: str = "utf-8",
         timezone: Optional[str] = None,
-        hook_config: Optional[Dict[str, str]] = None,
+        hook_config: Optional[Mapping[str, str]] = None,
         recursive_version_locations: bool = False,
+        messaging_opts: MessagingOptions = cast(
+            "MessagingOptions", util.EMPTY_DICT
+        ),
     ) -> None:
         self.dir = dir
         self.file_template = file_template
@@ -92,6 +96,7 @@ class ScriptDirectory:
         self.timezone = timezone
         self.hook_config = hook_config
         self.recursive_version_locations = recursive_version_locations
+        self.messaging_opts = messaging_opts
 
         if not os.access(dir, os.F_OK):
             raise util.CommandError(
@@ -225,6 +230,7 @@ class ScriptDirectory:
             timezone=config.get_main_option("timezone"),
             hook_config=config.get_section("post_write_hooks", {}),
             recursive_version_locations=rvl,
+            messaging_opts=config.messaging_opts,
         )
 
     @contextmanager
@@ -580,24 +586,24 @@ class ScriptDirectory:
         return os.path.abspath(os.path.join(self.dir, "env.py"))
 
     def _generate_template(self, src: str, dest: str, **kw: Any) -> None:
-        util.status(
-            "Generating %s" % os.path.abspath(dest),
-            util.template_to_file,
-            src,
-            dest,
-            self.output_encoding,
-            **kw,
-        )
+        with util.status(
+            f"Generating {os.path.abspath(dest)}", **self.messaging_opts
+        ):
+            util.template_to_file(src, dest, self.output_encoding, **kw)
 
     def _copy_file(self, src: str, dest: str) -> None:
-        util.status(
-            "Generating %s" % os.path.abspath(dest), shutil.copy, src, dest
-        )
+        with util.status(
+            f"Generating {os.path.abspath(dest)}", **self.messaging_opts
+        ):
+            shutil.copy(src, dest)
 
     def _ensure_directory(self, path: str) -> None:
         path = os.path.abspath(path)
         if not os.path.exists(path):
-            util.status("Creating directory %s" % path, os.makedirs, path)
+            with util.status(
+                f"Creating directory {path}", **self.messaging_opts
+            ):
+                os.makedirs(path)
 
     def _generate_create_date(self) -> datetime.datetime:
         if self.timezone is not None:
index 8bc7ac1c172a14b8084e3d5e71ba92e463af01e8..d37555d7979a0a3520a8e662004e71083a34ea22 100644 (file)
@@ -7,6 +7,7 @@ from typing import Any
 from typing import Callable
 from typing import Dict
 from typing import List
+from typing import Mapping
 from typing import Optional
 from typing import Union
 
@@ -41,7 +42,7 @@ def register(name: str) -> Callable:
 
 
 def _invoke(
-    name: str, revision: str, options: Dict[str, Union[str, int]]
+    name: str, revision: str, options: Mapping[str, Union[str, int]]
 ) -> Any:
     """Invokes the formatter registered for the given name.
 
@@ -61,7 +62,7 @@ def _invoke(
         return hook(revision, options)
 
 
-def _run_hooks(path: str, hook_config: Dict[str, str]) -> None:
+def _run_hooks(path: str, hook_config: Mapping[str, str]) -> None:
     """Invoke hooks for a generated revision."""
 
     from .base import _split_on_space_comma
@@ -84,14 +85,8 @@ def _run_hooks(path: str, hook_config: Dict[str, str]) -> None:
                 "Key %s.type is required for post write hook %r" % (name, name)
             ) from ke
         else:
-            util.status(
-                'Running post write hook "%s"' % name,
-                _invoke,
-                type_,
-                path,
-                opts,
-                newline=True,
-            )
+            with util.status("Running post write hook {name!r}", newline=True):
+                _invoke(type_, path, opts)
 
 
 def _parse_cmdline_options(cmdline_options_str: str, path: str) -> List[str]:
index c81d4317ad37bd386137e280b6d32f0b33d79d75..8f684abcff8fe6a5db058640ac7c47d02c195d4a 100644 (file)
@@ -5,6 +5,7 @@ from .langhelpers import _with_legacy_names
 from .langhelpers import asbool
 from .langhelpers import dedupe_tuple
 from .langhelpers import Dispatcher
+from .langhelpers import EMPTY_DICT
 from .langhelpers import immutabledict
 from .langhelpers import memoized_property
 from .langhelpers import ModuleClsProxy
index 8203358e94f19fc1277260c7295fe9a73cdd285f..f62bc1933a6150fcd01f41f3dc8207c64eb3c7b6 100644 (file)
@@ -7,6 +7,7 @@ from typing import Any
 from typing import Callable
 from typing import Dict
 from typing import List
+from typing import Mapping
 from typing import Optional
 from typing import overload
 from typing import Sequence
@@ -25,6 +26,7 @@ from sqlalchemy.util import unique_list  # noqa
 from .compat import inspect_getfullargspec
 
 
+EMPTY_DICT: Mapping[Any, Any] = immutabledict()
 _T = TypeVar("_T")
 
 
index 7d9d090a774d4cc090d93c44415fbeb4d8e8523c..35592c0ec9a83f327661ac7435ba908be108b969 100644 (file)
@@ -1,11 +1,10 @@
 from __future__ import annotations
 
 from collections.abc import Iterable
+from contextlib import contextmanager
 import logging
 import sys
 import textwrap
-from typing import Any
-from typing import Callable
 from typing import Optional
 from typing import TextIO
 from typing import Union
@@ -34,7 +33,11 @@ except (ImportError, OSError):
     TERMWIDTH = None
 
 
-def write_outstream(stream: TextIO, *text) -> None:
+def write_outstream(
+    stream: TextIO, *text: Union[str, bytes], quiet: bool = False
+) -> None:
+    if quiet:
+        return
     encoding = getattr(stream, "encoding", "ascii") or "ascii"
     for t in text:
         if not isinstance(t, bytes):
@@ -49,21 +52,23 @@ def write_outstream(stream: TextIO, *text) -> None:
             break
 
 
-def status(_statmsg: str, fn: Callable, *arg, **kw) -> Any:
-    newline = kw.pop("newline", False)
-    msg(_statmsg + " ...", newline, True)
+@contextmanager
+def status(status_msg: str, newline: bool = False, quiet: bool = False):
+    msg(status_msg + " ...", newline, flush=True, quiet=quiet)
     try:
-        ret = fn(*arg, **kw)
-        write_outstream(sys.stdout, "  done\n")
-        return ret
+        yield
     except:
-        write_outstream(sys.stdout, "  FAILED\n")
+        if not quiet:
+            write_outstream(sys.stdout, "  FAILED\n")
         raise
+    else:
+        if not quiet:
+            write_outstream(sys.stdout, "  done\n")
 
 
-def err(message: str):
+def err(message: str, quiet: bool = False):
     log.error(message)
-    msg("FAILED: %s" % message)
+    msg(f"FAILED: {message}", quiet=quiet)
     sys.exit(-1)
 
 
@@ -76,7 +81,11 @@ def warn(msg: str, stacklevel: int = 2) -> None:
     warnings.warn(msg, UserWarning, stacklevel=stacklevel)
 
 
-def msg(msg: str, newline: bool = True, flush: bool = False) -> None:
+def msg(
+    msg: str, newline: bool = True, flush: bool = False, quiet: bool = False
+) -> None:
+    if quiet:
+        return
     if TERMWIDTH is None:
         write_outstream(sys.stdout, msg)
         if newline:
diff --git a/docs/build/unreleased/1109.rst b/docs/build/unreleased/1109.rst
new file mode 100644 (file)
index 0000000..b6c0718
--- /dev/null
@@ -0,0 +1,6 @@
+.. change::
+    :tags: usecase, commands
+
+    Added quiet option to the command line, using the ``-q/--quiet``
+    option. This flag will prevent alembic from logging anything
+    to stdout.
index 5ec3567927a482cf61098f23cc88743afaeeba5a..0937930ea916a897fb9aff9b205cb45cd9fc7abd 100644 (file)
@@ -1221,14 +1221,16 @@ class CommandLineTest(TestBase):
                     mock.call(
                         os.path.abspath(os.path.join(path, "__init__.py")), "w"
                     ),
-                    mock.call().close(),
+                    mock.call().__enter__(),
+                    mock.call().__exit__(None, None, None),
                     mock.call(
                         os.path.abspath(
                             os.path.join(path, "versions", "__init__.py")
                         ),
                         "w",
                     ),
-                    mock.call().close(),
+                    mock.call().__enter__(),
+                    mock.call().__exit__(None, None, None),
                 ],
             )
 
index 151b3b88d1e3c7c1244b2b5f22c55f943e0a9fe6..d50f7f50c7097c6d7e8a0c8d40ab2a7a09ceefd8 100644 (file)
@@ -1,5 +1,6 @@
 import datetime
 import os
+from pathlib import Path
 import re
 from unittest.mock import patch
 
@@ -1333,28 +1334,23 @@ class NormPathTest(TestBase):
                 ).replace("/", ":NORM:"),
             )
 
-    def test_script_location_muliple(self):
+    def test_script_location_multiple(self):
         config = _multi_dir_testing_config()
 
         script = ScriptDirectory.from_config(config)
 
-        def normpath(path):
+        def _normpath(path):
             return path.replace("/", ":NORM:")
 
-        normpath = mock.Mock(side_effect=normpath)
+        normpath = mock.Mock(side_effect=_normpath)
 
         with mock.patch("os.path.normpath", normpath):
+            sd = Path(_get_staging_directory()).as_posix()
             eq_(
                 script._version_locations,
                 [
-                    os.path.abspath(
-                        os.path.join(_get_staging_directory(), "model1/")
-                    ).replace("/", ":NORM:"),
-                    os.path.abspath(
-                        os.path.join(_get_staging_directory(), "model2/")
-                    ).replace("/", ":NORM:"),
-                    os.path.abspath(
-                        os.path.join(_get_staging_directory(), "model3/")
-                    ).replace("/", ":NORM:"),
+                    _normpath(os.path.abspath(sd + "/model1/")),
+                    _normpath(os.path.abspath(sd + "/model2/")),
+                    _normpath(os.path.abspath(sd + "/model3/")),
                 ],
             )