From acf83cc297dc2f1fe7a32bdcddf5f81690b84a20 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Fri, 19 Jun 2026 23:04:08 +0200 Subject: [PATCH 1/6] Moved ui logging setup into own setup/bootstrap function. --- beets/__init__.py | 15 ++++++ beets/ui/__init__.py | 111 +++++++++++++++++++++++-------------------- 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 64ba2e589c..bc9ad6d535 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -13,12 +13,18 @@ # included in all copies or substantial portions of the Software. +from __future__ import annotations + from sys import stderr +from typing import TYPE_CHECKING import confuse from .util.deprecation import deprecate_imports +if TYPE_CHECKING: + from .logging import Logger + __version__ = "2.12.0" __author__ = "Adrian Sampson " @@ -46,5 +52,14 @@ def read(self, user: bool = True, defaults: bool = True) -> None: except confuse.ConfigReadError as err: stderr.write(f"configuration `import` failed: {err.reason}") + def log_sources(self, log: Logger) -> None: + """Log all configuration sources in priority order.""" + + log.debug("configuration sources (highest → lowest priority):") + for source in self.sources: + log.debug( + "{} {}", type(source).__name__, getattr(source, "filename", "") + ) + config = IncludeLazyConfig("beets", __name__) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index b5ce2e1daa..1d0515a8fe 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -56,13 +56,6 @@ log = logging.getLogger("beets") -if not log.handlers: - handler = logging.StreamHandler() - handler.setFormatter( - logging.LegacyFormatter("%(legacy_prefix)s%(message)s") - ) - log.addHandler(handler) -log.propagate = False # Don't propagate to root handler. # Encoding utilities. @@ -767,14 +760,11 @@ def parse_subcommand(self, args): # The main entry point and bootstrapping. -def _setup( - options: optparse.Values, -) -> tuple[list[Subcommand], library.Library]: +def _setup() -> tuple[list[Subcommand], library.Library]: """Prepare and global state and updates it with command line options. Returns a list of subcommands, a list of plugins, and a library instance. """ - config = _configure(options) plugins.load_plugins() @@ -790,43 +780,6 @@ def _setup( return subcommands, lib -def _configure(options): - """Amend the global configuration object with command line options.""" - # Add any additional config files specified with --config. This - # special handling lets specified plugins get loaded before we - # finish parsing the command line. - if getattr(options, "config", None) is not None: - overlay_path = options.config - del options.config - config.set_file(overlay_path) - else: - overlay_path = None - config.set_args(options) - - # Configure the logger. - if config["verbose"].get(int): - log.set_global_level(logging.DEBUG) - else: - log.set_global_level(logging.INFO) - - if overlay_path: - log.debug( - "overlaying configuration: {}", util.displayable_path(overlay_path) - ) - - config_path = config.user_config_path() - if os.path.isfile(config_path): - log.debug("user configuration: {}", util.displayable_path(config_path)) - else: - log.debug( - "no user configuration found at {}", - util.displayable_path(config_path), - ) - - log.debug("data directory: {}", util.displayable_path(config.config_dir())) - return config - - def _ensure_db_directory_exists(path): if path == b":memory:": # in memory db return @@ -929,9 +882,11 @@ def parse_csl_callback( options, subargs = parser.parse_global_options(args) - # Special case for the `config --edit` command: bypass _setup so - # that an invalid configuration does not prevent the editor from - # starting. + # Defer config errors such that logging is available early for error reporting + # also allows `config --edit` command to bypass on config errors + deferred_error = _bootstrap_config(options) + _bootstrap_logging() + if ( subargs and subargs[0] == "config" @@ -941,7 +896,10 @@ def parse_csl_callback( return config_edit(options) - subcommands, lib = _setup(options) + if deferred_error: + raise deferred_error + + subcommands, lib = _setup() parser.add_subcommand(*subcommands) subcommand, suboptions, subargs = parser.parse_subcommand(subargs) @@ -952,6 +910,55 @@ def parse_csl_callback( return None +def _bootstrap_config(options: optparse.Values) -> confuse.ConfigError | None: + """Apply CLI to config, initialize logging, return error as value if any.""" + + # Apply config overlay (deferred error to allow setting up logging) + deferred_error: confuse.ConfigError | None = None + try: + # For some reason we need to materialize before adding another config + # file otherwise the sources order is inverted... + config.read() # FIXME + if overlay_path := getattr(options, "config", None): + config.set_file(overlay_path) + except confuse.ConfigError as e: + deferred_error = e + + # Even if the earlier config loading fails we seperatly try to set the config + # values the cli options + try: + config.set_args(options) + except confuse.ConfigError as e: + deferred_error = e + + return deferred_error + + +def _bootstrap_logging(): + if not log.handlers: + handler = logging.StreamHandler() + handler.setFormatter( + logging.LegacyFormatter("%(legacy_prefix)s%(message)s") + ) + log.addHandler(handler) + + # Verbosity level set via cli + # --verbose + if config["verbose"].get(int): + log.set_global_level(logging.DEBUG) + else: + log.set_global_level(logging.INFO) + + # List configuration sources for users convinence + log.debug("configuration sources (highest → lowest priority):") + for source in config.sources[1:]: + log.debug( + "{} {}", type(source).__name__, getattr(source, "filename", "") + ) + + log.debug("data directory: {}", util.displayable_path(config.config_dir())) + + def main(args: list[str] | None = None) -> None: """Run the main command-line interface for beets. Includes top-level exception handlers that print friendly error messages. From 5cd8b59c34b5b90908cbc2f3adbfd42a62b4a217 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Fri, 19 Jun 2026 23:05:13 +0200 Subject: [PATCH 2/6] Moved early logging error on windows into place where the error will propagate and log properly. --- beets/ui/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 1d0515a8fe..f83b679fe7 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -896,6 +896,14 @@ def parse_csl_callback( return config_edit(options) + if "AppData\\Local\\Microsoft\\WindowsApps" in sys.exec_prefix: + raise UserError( + "beets is unable to use the Microsoft Store version of " + "Python. Please install Python from https://python.org.\n" + "More details can be found here " + "https://beets.readthedocs.io/en/stable/guides/main.html" + ) + if deferred_error: raise deferred_error @@ -963,14 +971,6 @@ def main(args: list[str] | None = None) -> None: """Run the main command-line interface for beets. Includes top-level exception handlers that print friendly error messages. """ - if "AppData\\Local\\Microsoft\\WindowsApps" in sys.exec_prefix: - log.error( - "error: beets is unable to use the Microsoft Store version of " - "Python. Please install Python from https://python.org.\n" - "error: More details can be found here " - "https://beets.readthedocs.io/en/stable/guides/main.html" - ) - sys.exit(1) try: _raw_main(args) except UserError as exc: From 1949d0f224be00e0bd4aaca68556b01474766178 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Mon, 22 Jun 2026 21:39:55 +0200 Subject: [PATCH 3/6] Ensure default config is read, even on config error. --- beets/ui/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index f83b679fe7..7cae98a6e0 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -921,7 +921,6 @@ def parse_csl_callback( def _bootstrap_config(options: optparse.Values) -> confuse.ConfigError | None: """Apply CLI to config, initialize logging, return error as value if any.""" - # Apply config overlay (deferred error to allow setting up logging) deferred_error: confuse.ConfigError | None = None try: # For some reason we need to materialize before adding another config @@ -931,6 +930,9 @@ def _bootstrap_config(options: optparse.Values) -> confuse.ConfigError | None: config.set_file(overlay_path) except confuse.ConfigError as e: deferred_error = e + # Ensure defaults are loaded even when user config is broken, + # so that logging and other subsystems can read basic settings. + config.read(user=False, defaults=True) # Even if the earlier config loading fails we seperatly try to set the config # values the cli options @@ -950,20 +952,18 @@ def _bootstrap_logging(): ) log.addHandler(handler) - # Verbosity level set via cli - # --verbose + # Verbosity level set via cli --verbose. if config["verbose"].get(int): log.set_global_level(logging.DEBUG) else: log.set_global_level(logging.INFO) - # List configuration sources for users convinence + # List configuration sources for user convenience. log.debug("configuration sources (highest → lowest priority):") for source in config.sources[1:]: log.debug( "{} {}", type(source).__name__, getattr(source, "filename", "") ) - log.debug("data directory: {}", util.displayable_path(config.config_dir())) From 420b64bf01d859e00246a1e28995a4b271327304 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Tue, 23 Jun 2026 12:22:52 +0200 Subject: [PATCH 4/6] Short round of reviews, typo and use logging function. --- beets/ui/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 7cae98a6e0..d3fd6ded82 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -934,8 +934,8 @@ def _bootstrap_config(options: optparse.Values) -> confuse.ConfigError | None: # so that logging and other subsystems can read basic settings. config.read(user=False, defaults=True) - # Even if the earlier config loading fails we seperatly try to set the config - # values the cli options + # Even if the earlier config loading fails we seperatly try to set the + # cli options try: config.set_args(options) except confuse.ConfigError as e: @@ -959,11 +959,7 @@ def _bootstrap_logging(): log.set_global_level(logging.INFO) # List configuration sources for user convenience. - log.debug("configuration sources (highest → lowest priority):") - for source in config.sources[1:]: - log.debug( - "{} {}", type(source).__name__, getattr(source, "filename", "") - ) + config.log_sources(log) log.debug("data directory: {}", util.displayable_path(config.config_dir())) From d20b418ed6b1ca77310cfa781d1d7864c52f131e Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Wed, 24 Jun 2026 17:27:48 +0200 Subject: [PATCH 5/6] Enhanced comment. See thread https://github.com/beetbox/beets/pull/6755#discussion_r3444580358 --- beets/__init__.py | 3 ++- beets/ui/__init__.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index bc9ad6d535..8c61a7f97d 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -56,7 +56,8 @@ def log_sources(self, log: Logger) -> None: """Log all configuration sources in priority order.""" log.debug("configuration sources (highest → lowest priority):") - for source in self.sources: + # skip first source as it is always the root source/empty + for source in self.sources[1:]: log.debug( "{} {}", type(source).__name__, getattr(source, "filename", "") ) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index d3fd6ded82..4aa501f6f6 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -923,9 +923,11 @@ def _bootstrap_config(options: optparse.Values) -> confuse.ConfigError | None: deferred_error: confuse.ConfigError | None = None try: - # For some reason we need to materialize before adding another config - # file otherwise the sources order is inverted... - config.read() # FIXME + # Explicit read() so we own error handling (a broken user config file would + # otherwise surface on the first implicit config access e.g. config["verbose"]. + # It also ensures confuse's source list is populated before set_file() adds the + # --config overlay. + config.read() if overlay_path := getattr(options, "config", None): config.set_file(overlay_path) except confuse.ConfigError as e: From 68202bfe4206025646573479a99011eaed367c9f Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Wed, 24 Jun 2026 18:56:40 +0200 Subject: [PATCH 6/6] Added proposal for logging modes to docs. This is intended for review and decision making at the moment. --- docs/reference/index.rst | 1 + docs/reference/logging.rst | 176 +++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 docs/reference/logging.rst diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 1f04366597..edb7856245 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -12,3 +12,4 @@ started with beets as a new user, though, you may want to read the config pathformat query + logging diff --git a/docs/reference/logging.rst b/docs/reference/logging.rst new file mode 100644 index 0000000000..7c7d77564a --- /dev/null +++ b/docs/reference/logging.rst @@ -0,0 +1,176 @@ +Logging +======= + +Beets implements a flexible logging solution and ships multiple built-in modes. +You can select a mode from the command line for quick control, or define and +customize modes in the configuration file for more advanced setups. + +Use the ``--logging `` option to select a logging mode for a single +invocation of any beets command. For example, to run the import command in debug +mode, you could do: + +.. code-block:: console + + beets --logging debug import music/ + +.. note:: + + The command-line option overrides any logging mode configured in the + configuration file. + +If you want to persist a logging mode across multiple invocations of beets, you +can define a mode in the configuration file. For example, to set the default +logging mode to ``debug``, you could add the following to your configuration +file: + +.. code-block:: yaml + :caption: config.yaml + + logging: debug + +.. note:: + + Invalid logging modes will be ignored, and beets will fall back to the + default mode. + +Build-in modes +-------------- + +Beets ships with several built-in logging modes. The following table summarizes +the available modes and their characteristics: + +.. conf:: legacy + + The default mode for ``beets: + TODO Add a real output + +.. conf:: verbose + + The default logging mode for ``beets>=v3.0.0``. + + This mode explains what beets is doing in a more explicit way than + ``legacy``, including key decisions such as matching, tagging, and + file operations, without exposing full internal debug information. + + .. code-block:: console + + [] : + TODO Add a real output + + + This mode aims to be the recommended default for users on beets 3.0+ as it improves transparency while keeping output readable. + +.. conf:: quiet + + Suppresses informational output and only displays warnings and errors. + + This mode is useful when running beets in scripts or when you want to minimize console output while still being notified of important issues. + + .. code-block:: console + + [] : + TODO Add a real output + +.. conf:: debug + + Enables verbose diagnostic logging for both core functionality and plugins. + + This mode includes timestamps, log levels, and logger names, making it + useful for debugging and development. + + .. code-block:: console + +