From 15be14c9a5ce9d0d52a769f16d53a749c41e449b 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 e15f5a87a..f7333c5ad 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -1,9 +1,15 @@ +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 " @@ -31,5 +37,14 @@ class IncludeLazyConfig(confuse.LazyConfig): 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 cdb61e7b6..1feb716a1 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -42,13 +42,6 @@ if sys.platform == "win32": 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. @@ -753,14 +746,11 @@ optparse.Option.ALWAYS_TYPED_ACTIONS += ("callback",) # 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() @@ -776,43 +766,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 @@ -915,9 +868,11 @@ def _raw_main(args: list[str] | None) -> None: 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" @@ -927,7 +882,10 @@ def _raw_main(args: list[str] | None) -> None: 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) @@ -938,6 +896,55 @@ def _raw_main(args: list[str] | None) -> None: 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 44f8445303933e8da5b5d89ce766bcc69fb6e746 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 1feb716a1..9bfcc5c08 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -882,6 +882,14 @@ def _raw_main(args: list[str] | None) -> None: 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 @@ -949,14 +957,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 3c01762ae65985c71e445e383546fbc05c79c113 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 9bfcc5c08..087265df4 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -907,7 +907,6 @@ def _raw_main(args: list[str] | None) -> 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 @@ -917,6 +916,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 @@ -936,20 +938,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 289e7e3fe7c6139a142671c6435258fa76102c62 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 087265df4..b98f88eae 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -920,8 +920,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: @@ -945,11 +945,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 222d1320035964ae4a2e30daa8d62354df23210c 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 f7333c5ad..343959c6c 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -41,7 +41,8 @@ class IncludeLazyConfig(confuse.LazyConfig): """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 b98f88eae..7b874f55c 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -909,9 +909,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 a5d17fcc58591c0256081d2de010d3e85f29ea7b Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Mon, 29 Jun 2026 11:36:06 +0200 Subject: [PATCH 6/6] Minor fixup: added typehint and improved docstring --- beets/ui/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 7b874f55c..85e22f873 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -905,7 +905,7 @@ def _raw_main(args: list[str] | None) -> None: def _bootstrap_config(options: optparse.Values) -> confuse.ConfigError | None: - """Apply CLI to config, initialize logging, return error as value if any.""" + """Apply CLI to config, return error as value if any.""" deferred_error: confuse.ConfigError | None = None try: @@ -932,7 +932,7 @@ def _bootstrap_config(options: optparse.Values) -> confuse.ConfigError | None: return deferred_error -def _bootstrap_logging(): +def _bootstrap_logging() -> None: if not log.handlers: handler = logging.StreamHandler() handler.setFormatter(