stratified_packager.toolbelt.logging
A thread-safe, structured logging suite for QGIS 4.0+ plugins.
This module provides a QgisHandler — a logging.Handler
subclass — that routes standard-library log records to any combination of:
qgis.core.QgsMessageLog(the QGIS application log panel)qgis.gui.QgsMessageBar(the in-canvas notification bar)QMessageBox(modal dialogs)
Plugin developers call QgisLoggerWrapper.setup() once per plugin to
create the root logger wired to a QgisHandler, then call
QgisLoggerWrapper.get_logger() in each sub-module to obtain a named
child wrapper that propagates records up to the root handler automatically:
# In the plugin's __init__.py — once per plugin:
root = QgisLoggerWrapper.setup(
name="MyPlugin",
targets=Target.LOG | Target.BAR,
)
# In any sub-module — no handler needed:
log = QgisLoggerWrapper.get_logger("MyPlugin.utils")
log = QgisLoggerWrapper.get_logger("MyPlugin.network.http")
# Per-call overrides work on any wrapper regardless of hierarchy level.
log.warning(
"No features selected.",
targets=Target.DIALOG,
box_config=MessageBoxConfig(title="Selection warning"),
)
On plugin unload, call QgisLoggerWrapper.teardown() to detach and dispose everything
setup() attached: the logging registry is process-global and
survives QGIS plugin reloads, so a handler left behind would render every record one extra
time per reload.
Thread safety
QgisHandler owns a QgisHandlerSignals instance that lives
on the GUI thread. Cross-thread dispatch is handled by connecting the private
_render_requested signal to QgisHandler._render()
with the default AutoConnection, which Qt automatically promotes to a
QueuedConnection when the signal is emitted from a worker thread.
Classes
|
A |
|
Public Qt signals emitted by |
|
A |
- class stratified_packager.toolbelt.logging.QgisHandler(plugin_name, targets=<Target.LOG: 1>, tag=None, bar=None, box_parent=None, bar_config=None, box_config=None, level=0)[source]
A
logging.Handlerthat routes records to QGIS UI targets.Handler-level defaults apply to every record. When the record was emitted by a
QgisLoggerWrapper, per-call overrides stored under the private sentinel keys take precedence for that record only.Each handler owns a
QgisHandlerSignalsinstance accessible viasignals. Connect toQgisHandlerSignals.message_emittedorQgisHandlerSignals.error_emittedto react to log records from other parts of a plugin without subclassing or patching the handler.Note
Cross-thread dispatch is handled by the private
_render_requestedsignal onsignals, which is connected to_render()with anAutoConnection. Qt automatically promotes this to aQueuedConnectionwhen the signal is emitted from a worker thread, ensuring all Qt widget operations execute on the GUI thread.- Parameters:
plugin_name (str)
targets (Target)
tag (str | None)
bar (QgsMessageBar | None)
box_parent (QWidget | None)
bar_config (MessageBarConfig | None)
box_config (MessageBoxConfig | None)
level (int)
- static _render_log(payload)[source]
Write to
qgis.core.QgsMessageLog.- Parameters:
payload (
EmitPayload) – TheEmitPayloaddescribing the message.- Return type:
- __init__(plugin_name, targets=<Target.LOG: 1>, tag=None, bar=None, box_parent=None, bar_config=None, box_config=None, level=0)[source]
Initialize the handler.
- Parameters:
plugin_name (str) – Human-readable name shown in log tags and dialog titles.
targets (Target) – Default combination of
Targetflags. Can be overridden per call viaQgisLoggerWrapper.tag (str | None) – Tag shown in
qgis.core.QgsMessageLog; falls back to plugin_name whenNone.bar (QgsMessageBar | None) – The
qgis.gui.QgsMessageBarfrom the QGIS interface. Required whenTarget.BARmay ever be active.box_parent (QWidget | None) – Parent widget for
QMessageBoxmodal dialogs (typicallyiface.mainWindow()); whenNone, dialogs are created without a parent (not recommended).bar_config (MessageBarConfig | None) – Default message-bar appearance.
box_config (MessageBoxConfig | None) – Default modal-dialog appearance.
level (int) – Minimum logging level forwarded by this handler.
- Raises:
ValueError – If
Target.BARis enabled but no bar was provided.
- _render(payload)[source]
Dispatch payload to each active target and emit public signals.
Always runs on the GUI thread — either called directly from
emit()when already on the GUI thread, or delivered via theQueuedConnectionon_render_requestedwhen called from a worker thread.After all render targets have been called,
QgisHandlerSignals.message_emittedis emitted with the formatted message and logging level. If the level islogging.WARNINGor above,QgisHandlerSignals.error_emittedis also emitted.A logging sink must never raise into the caller of
log.xxx()(sync path) or into Qt’s event loop (queued path), so any exception from a render target or from a user-connected signal slot is routed tologging.Handler.handleError().- Parameters:
payload (
EmitPayload) – TheEmitPayloaddescribing the message.- Return type:
- _render_bar(payload)[source]
Push a notification onto
qgis.gui.QgsMessageBar.If
show_progressisTrue, aQProgressBaris created and embedded. Any caller-suppliedbuttonsare also added.- Parameters:
payload (
EmitPayload) – TheEmitPayloaddescribing the message.- Raises:
RuntimeError – If the handler was not configured with a
qgis.gui.QgsMessageBarbut the BAR target is active, or if the message bar item layout cannot be obtained when buttons or a progress bar need to be added.- Return type:
- _render_dialog(payload)[source]
Show a
QMessageBoxmodal dialog.The dialog is parented to the
box_parentwidget supplied at construction (typically the QGIS main window) so Qt manages its lifetime and stacking; when no parent was supplied it is created parentless.- Parameters:
payload (
EmitPayload) – TheEmitPayloaddescribing the message.- Return type:
- emit(record)[source]
Format record and dispatch it to all active targets.
Per-call overrides injected by
QgisLoggerWrapper— stored on the record under the_K_TARGETS,_K_BAR_CFG, and_K_BOX_CFGkeys — take precedence over the handler’s own defaults. WhenTarget.BARis requested via a per-call override but noqgis.gui.QgsMessageBarwas supplied to the handler, the BAR target is silently dropped from that record’s targets to avoid a runtime error.When called from a worker thread, the private
_render_requestedsignal is emitted; Qt delivers it to_render()on the GUI thread via aQueuedConnection. When called from the GUI thread,_render()is called directly with no event-loop round-trip.- Parameters:
record (
LogRecord) – Thelogging.LogRecordto handle.- Return type:
- _bar: QgsMessageBar | None
- _bar_config: MessageBarConfig
- _box_config: MessageBoxConfig
- _signals: QgisHandlerSignals
- property bar_config: MessageBarConfig
Default message-bar configuration (may be overridden per call).
- Returns:
The current
MessageBarConfig.
- property box_config: MessageBoxConfig
Default message-box configuration (may be overridden per call).
- Returns:
The current
MessageBoxConfig.
- property signals: QgisHandlerSignals
Public Qt signals for this handler.
Connect to
QgisHandlerSignals.message_emittedorQgisHandlerSignals.error_emittedto react to log records without subclassing the handler:handler.signals.message_emitted.connect(my_slot)
- Returns:
The
QgisHandlerSignalsinstance owned by this handler.
- class stratified_packager.toolbelt.logging.QgisHandlerSignals(*args, **kwargs)[source]
Public Qt signals emitted by
QgisHandleron every handled record.This class is a
QObjectsignals carrier: it exists solely to hostpyqtSignal()definitions, sidestepping theQObjectmultiple-inheritance limitations of PyQt6 while still providing fully typed, connectable Qt signals.An instance is created automatically by
QgisHandlerand exposed viaQgisHandler.signals. Plugin developers connect to its signals to drive custom UI elements — for example a status-bar label or a secondary log widget — without needing to subclass or patch the handler.Signals are emitted on the GUI thread regardless of which thread originally called
QgisHandler.emit(), because the internal_render_requestedsignal uses aQueuedConnectionfor cross-thread delivery and these public signals are emitted fromQgisHandler._render(), which always runs on the GUI thread.handler = QgisHandler(plugin_name="MyPlugin") # Show every log message in a custom label. handler.signals.message_emitted.connect(lambda msg, level: status_label.setText(msg)) # Flash the toolbar red on errors. handler.signals.error_emitted.connect(lambda msg: toolbar.setStyleSheet("background: red"))
- _render_requested
Private signal used for cross-thread dispatch to
QgisHandler._render().Plugin code must not connect to this signal directly.
alias of
object
- error_emitted
Emitted only when the record’s level is
logging.WARNINGor above.Argument is the formatted message string. Useful for connecting a single error-notification slot without a level-filtering wrapper.
alias of
str
- message_emitted
Emitted for every successfully handled record, after all render targets have been called.
Arguments are the formatted message string and the integer logging level (e.g.
logging.INFO).alias of
str
- class stratified_packager.toolbelt.logging.QgisLoggerWrapper(logger)[source]
A
logging.Loggerwrapper that takes QGIS-specific keyword arguments on logging calls.Rather than subclassing
logging.Logger— which would require overriding the privatelogging.Logger._logmethod with a signature incompatible with the base class, violating the Liskov Substitution Principle — this class holds a plainlogging.Loggerinstance via composition and delegates all logging calls to it after packing any QGIS-specific kwargs into theextradict.The wrapped logger is accessible via the
loggerproperty for the rare cases where a plainlogging.Loggerreference may be needed.All standard logging methods (
debug(),info(),warning(),error(),critical(),exception(),log()) — plus the custom-levelsuccess()— accept three optional keyword-only arguments in addition to the standard ones:targets— override the handler’sQgisHandler.targetsfor this record only.bar_config— override the handler’sQgisHandler.bar_configfor this record.box_config— override the handler’sQgisHandler.box_configfor this record.
Typical usage — one
setup()call per plugin, thenget_logger()in every sub-module:# plugin/__init__.py root = QgisLoggerWrapper.setup("MyPlugin", targets=Target.LOG) # plugin/plugin_main.py log = QgisLoggerWrapper.get_logger("MyPlugin.plugin_main") # plugin/processing/provider.py log = QgisLoggerWrapper.get_logger("MyPlugin.processing.provider")
Do not call the constructor directly; use
setup()orget_logger()so that theloggingregistry is managed correctly and the wrapped logger participates in the hierarchy.- Parameters:
logger (
Logger)
- classmethod get_logger(name)[source]
Retrieve or create a named child logger with no handler attached.
Use this in every sub-module that needs a logger. Records propagate to the nearest ancestor that has a handler — which should be the root plugin logger created by
setup():log = QgisLoggerWrapper.get_logger("MyPlugin.utils") log = QgisLoggerWrapper.get_logger("MyPlugin.network.http")
- Parameters:
name (
str) – Dotted logger name. Should start with the root plugin name to participate in the plugin’s logging hierarchy (e.g."MyPlugin.utils").- Return type:
- Returns:
A
QgisLoggerWrapperaround the named logger, which has no handlers andpropagateset toTrue(theloggingdefault for new loggers).
- classmethod setup(name, targets=<Target.LOG: 1>, iface=None, tag=None, bar_config=None, box_config=None, level=10, formatter=None, *, propagate=False, filters=None)[source]
Create or retrieve the root plugin logger, wired to a
QgisHandler.Call this once per plugin (typically in the plugin’s
__init__.pyorclassFactory). It obtains alogging.Loggernamed name from theloggingregistry, attaches aQgisHandlerto it, wraps it in aQgisLoggerWrapper, and returns the wrapper.Repeated calls are safe and idempotent: handlers and filters attached by a previous call are detached and disposed first, then rebuilt from the current arguments. This also holds across QGIS plugin reloads — the reloader purges the plugin’s modules and re-imports them, so the handler left attached to the process-global
loggingregistry by the previous load is an instance of a stale class object; it is detected by type name and replaced. Leaving it attached would render every record one extra time per reload. Callteardown()from the plugin’sunload()to detach without re-attaching.Sub-module loggers should be created with
get_logger()using dotted child names; they propagate records to this root logger automatically and require no handler of their own.- Parameters:
name (str) – Logger name and human-readable identifier used in QGIS UI targets (e.g. the
QgsMessageLogtag). Typically the plugin’s display name (e.g."MyPlugin").targets (Target) – Default output targets. Can be overridden per call. Defaults to
Target.LOG.iface (QgisInterface | None) – The QGIS interface instance, used to obtain the message bar for
Target.BARoutput and the main window as the parent forTarget.DIALOGmodal dialogs.tag (str | None) – Topic label for
QgsMessageLog; falls back to name whenNone.bar_config (MessageBarConfig | None) – Default message-bar display options.
box_config (MessageBoxConfig | None) – Default modal-dialog display options.
level (int) – Minimum level this logger forwards to its handler.
formatter (logging.Formatter | None) – A custom
logging.Formatter. WhenNone,_default_formatter()is used.propagate (bool) – Whether to propagate records to ancestor loggers. Defaults to
Falseto prevent duplicate output via the root logging handler.filters (Iterable[_FilterType] | None) – Iterable of filters to add to the logger. Filters of the same types attached by a previous call are replaced.
- Return type:
QgisLoggerWrapper
- Returns:
A
QgisLoggerWrapperwrapping the configured root logger.
- classmethod teardown(name)[source]
Detach and dispose everything
setup()attached to the logger named name.Call this from the plugin’s
unload()so the handler does not outlive the plugin: theloggingregistry is process-global, so a handler left attached would both leak (itsQgisHandlerSignalscarrier is aQObject) and render every record once more alongside the handler installed by the next load. The handler is closed and its signals carrier disconnected and scheduled for deletion; cross-thread render payloads still queued on it are dropped. Filters attached bysetup()are removed; handlers and filters attached by other code are left untouched.Safe to call when
setup()never ran for name. Records logged after teardown fall back tologging.lastResort(stderr) untilsetup()runs again.
- static _default_formatter()[source]
Build the default structured log formatter.
The format string produces records like:
2024-11-01 14:23:05,123 [WARNING ] MyPlugin.plugin_main:42 — File not found.
- Return type:
- Returns:
A
logging.Formatterinstance.
- static _pack_extra(extra, *, targets=None, bar_config=None, box_config=None)[source]
- Overloads:
extra (None), targets (None), bar_config (None), box_config (None) → None
extra (Mapping[str, object]), targets (Target | None), bar_config (MessageBarConfig | None), box_config (MessageBoxConfig | None) → dict[str, object]
extra (Mapping[str, object] | None), targets (Target), bar_config (MessageBarConfig | None), box_config (MessageBoxConfig | None) → dict[str, object]
extra (Mapping[str, object] | None), targets (Target | None), bar_config (MessageBarConfig), box_config (MessageBoxConfig | None) → dict[str, object]
extra (Mapping[str, object] | None), targets (Target | None), bar_config (MessageBarConfig | None), box_config (MessageBoxConfig) → dict[str, object]
Merge QGIS-specific overrides into a fresh copy of
extra.When all arguments are
None,Noneis returned immediately so the common no-override path has no allocation cost.- Parameters:
extra (
Mapping[str,object] |None) – A dictionary used to populate the __dict__ of the LogRecord created for the logging event with user-defined attributes.targets (
Target|None) – Per-callTargetoverride, orNoneto use the handler default.bar_config (
MessageBarConfig|None) – Per-callMessageBarConfigoverride, orNoneto use the handler default.box_config (
MessageBoxConfig|None) – Per-callMessageBoxConfigoverride, orNoneto use the handler default.
- Returns:
A new dict merging the provided extra with the QGIS-specific overrides under the private keys defined in this module, or
Noneif all arguments areNone.
- __init__(logger)[source]
Private constructor; use
setup()orget_logger().- Parameters:
logger (
Logger) – Thelogging.Loggerto wrap.
- critical(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith severityCRITICAL.See
QgisLoggerWrapper.log()for details on parameters.- Parameters:
msg (
object)args (
object)exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None)stack_info (
bool)stacklevel (
int)bar_config (
MessageBarConfig|None)box_config (
MessageBoxConfig|None)
- Return type:
- debug(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith severityDEBUG.See
QgisLoggerWrapper.log()for details on parameters.- Parameters:
msg (
object)args (
object)exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None)stack_info (
bool)stacklevel (
int)bar_config (
MessageBarConfig|None)box_config (
MessageBoxConfig|None)
- Return type:
- error(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith severityERROR.See
QgisLoggerWrapper.log()for details on parameters.- Parameters:
msg (
object)args (
object)exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None)stack_info (
bool)stacklevel (
int)bar_config (
MessageBarConfig|None)box_config (
MessageBoxConfig|None)
- Return type:
- exception(msg, *args, exc_info=True, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith severityERROR, including exception information.Equivalent to calling
error()withexc_info=True. Meant to be called from anexceptblock.See
QgisLoggerWrapper.log()for details on parameters.- Parameters:
msg (
object)args (
object)exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None)stack_info (
bool)stacklevel (
int)bar_config (
MessageBarConfig|None)box_config (
MessageBoxConfig|None)
- Return type:
- info(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith severityINFO.See
QgisLoggerWrapper.log()for details on parameters.- Parameters:
msg (
object)args (
object)exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None)stack_info (
bool)stacklevel (
int)bar_config (
MessageBarConfig|None)box_config (
MessageBoxConfig|None)
- Return type:
- log(level, msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith the integer severitylevel.To pass exception information, use the keyword argument exc_info with a true value, e.g.
`python logger.log(level, "We have a %s", "mysterious problem", exc_info=True) `The QGIS-specific parameters are removed from kwargs so that the standard
logging.Logger.log()never sees them. They are instead stored in theextradict under the private sentinel keys_K_TARGETS,_K_BAR_CFG, and_K_BOX_CFG, whereQgisHandler.emit()will retrieve them.- Parameters:
level (
int) – Integer logging level.msg (
object) – The log message (may contain %-style format placeholders).*args (
object) – Positional arguments for message formatting.exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None) – If it doesn’t evaluate toFalse, it causes exception information to be added to the logging message. If an exception tuple (in the format returned by sys.exc_info()) or an exception instance is provided, it’s used; otherwise, sys.exc_info() is called to get the exception information.stack_info (
bool) – If true, stack information is added to the logging message, including the actual logging call.stacklevel (
int) – If greater than 1, the corresponding number of stack frames are skipped when computing the line number and function name set in the LogRecord created for the logging event.extra (
Mapping[str,object] |None) – A dictionary used to populate the __dict__ of the LogRecord created for the logging event with user-defined attributes.targets (
Target|None) – Per-callTargetoverride, orNoneto use the handler default.bar_config (
MessageBarConfig|None) – Per-callMessageBarConfigoverride, orNoneto use the handler default.box_config (
MessageBoxConfig|None) – Per-callMessageBoxConfigoverride, orNoneto use the handler default.
- Return type:
- success(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith severitySUCCESS.See
QgisLoggerWrapper.log()for details on parameters.- Parameters:
msg (
object)args (
object)exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None)stack_info (
bool)stacklevel (
int)bar_config (
MessageBarConfig|None)box_config (
MessageBoxConfig|None)
- Return type:
- warning(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]
Log
msg % argswith severityWARNING.See
QgisLoggerWrapper.log()for details on parameters.- Parameters:
msg (
object)args (
object)exc_info (
bool|tuple[type[BaseException],BaseException,TracebackType|None] |tuple[None,None,None] |BaseException|None)stack_info (
bool)stacklevel (
int)bar_config (
MessageBarConfig|None)box_config (
MessageBoxConfig|None)
- Return type:
- property logger: Logger
The underlying
logging.Loggerinstance.- Returns:
The wrapped
logging.Logger.
- class stratified_packager.toolbelt.logging._SupportsFilter(*args, **kwargs)[source]
- __init__(*args, **kwargs)
- _abc_impl = <_abc._abc_data object>
- _is_protocol = True
- stratified_packager.toolbelt.logging._detach_setup_artifacts(logger, filter_types=())[source]
Detach and dispose everything
QgisLoggerWrapper.setup()attached to logger.Handlers are detached when they carry the
_K_SETUP_OWNEDmarker or are (possibly stale)QgisHandlerinstances; each one is closed, itsQgisHandlerSignalscarrier is disconnected and scheduled for deletion, and cross-thread render payloads still queued on it are dropped. Filters are detached when they carry the marker or when their type’s(__module__, __qualname__)pair is in filter_types. Handlers and filters attached by other code are left untouched.Disposal of each handler is contained: a failure is logged at WARNING level and the remaining cleanup proceeds.
- stratified_packager.toolbelt.logging._is_main_thread()[source]
Return
Truewhen called from Qt’s GUI thread.Uses
qgis.PyQt.QtCore.QThread.currentThread()compared against the thread that owns theQApplicationinstance, which is always Qt’s GUI thread. This is reliable across all threading models — Python threads, Qt threads created viaQThread, and threads from Qt’s C++ thread pool (QThreadPool/QtConcurrent) — unlikethreading.current_thread() is threading.main_thread(), which only tracks threads known to the CPython runtime.
- stratified_packager.toolbelt.logging._is_qgis_handler_like(handler)[source]
Return whether handler is a
QgisHandler, including stale-class instances.An
isinstance()check alone is not reload-safe: QGIS purges the plugin’s modules fromsys.moduleson reload and re-imports them, so a handler attached by a previous load is an instance of that load’s — now stale — class object, which failsisinstance()against the freshly importedQgisHandler. Such instances are recognized by their type’s__module__/__qualname__pair instead.- Parameters:
handler (
Handler) – The handler to inspect.- Return type:
- Returns:
Truefor current and staleQgisHandlerinstances.
- stratified_packager.toolbelt.logging._is_setup_owned(obj)[source]
Return whether obj carries the
_K_SETUP_OWNEDownership marker.- Parameters:
obj (
object) – A handler or filter currently attached to a logger.- Return type:
- Returns:
Truewhen obj was attached byQgisLoggerWrapper.setup().
- stratified_packager.toolbelt.logging._mark_setup_owned(obj)[source]
Stamp the
_K_SETUP_OWNEDownership marker onto obj, best-effort.Objects that reject new attributes (e.g. slotted classes) are left unmarked; stale instances of such filters are still replaced by the type matching in
QgisLoggerWrapper.setup().- Parameters:
obj (
object) – A handler or filter being attached byQgisLoggerWrapper.setup().- Return type:
- stratified_packager.toolbelt.logging._K_SETUP_OWNED: Final = '_qgis_setup_owned'
Marker attribute stamped on handlers and filters attached by
QgisLoggerWrapper.setup().Stored on the attached objects themselves (not on records) so that a later call — possibly made by a fresh import of this module after a QGIS plugin reload, when all class objects have been re-created — can still recognize and replace them.
- type stratified_packager.toolbelt.logging._FilterType = Filter | Callable[[LogRecord], bool | LogRecord] | _SupportsFilter
- type stratified_packager.toolbelt.logging._SysExcInfoType = tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None]