# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2014-2021 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # # qutebrowser is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # qutebrowser is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . """Utilities used for debugging.""" import re import inspect import logging import functools import datetime import types from typing import ( Any, Callable, List, Mapping, MutableSequence, Optional, Sequence, Type, Union) from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSignal from qutebrowser.utils import log, utils, qtutils, objreg from qutebrowser.misc import objects from qutebrowser.qt import sip def log_events(klass: Type[QObject]) -> Type[QObject]: """Class decorator to log Qt events.""" old_event = klass.event @functools.wraps(old_event) def new_event(self: Any, e: QEvent) -> bool: """Wrapper for event() which logs events.""" log.misc.debug("Event in {}: {}".format(utils.qualname(klass), qenum_key(QEvent, e.type()))) return old_event(self, e) klass.event = new_event # type: ignore[assignment] return klass def log_signals(obj: QObject) -> QObject: """Log all signals of an object or class. Can be used as class decorator. """ def log_slot(obj: QObject, signal: pyqtBoundSignal, *args: Any) -> None: """Slot connected to a signal to log it.""" dbg = dbg_signal(signal, args) try: r = repr(obj) except RuntimeError: # pragma: no cover r = '' log.signals.debug("Signal in {}: {}".format(r, dbg)) def connect_log_slot(obj: QObject) -> None: """Helper function to connect all signals to a logging slot.""" metaobj = obj.metaObject() for i in range(metaobj.methodCount()): meta_method = metaobj.method(i) qtutils.ensure_valid(meta_method) if meta_method.methodType() == QMetaMethod.Signal: name = meta_method.name().data().decode('ascii') if name != 'destroyed': signal = getattr(obj, name) try: signal.connect(functools.partial( log_slot, obj, signal)) except TypeError: # pragma: no cover pass if inspect.isclass(obj): old_init = obj.__init__ @functools.wraps(old_init) def new_init(self: Any, *args: Any, **kwargs: Any) -> None: """Wrapper for __init__() which logs signals.""" old_init(self, *args, **kwargs) connect_log_slot(self) obj.__init__ = new_init else: connect_log_slot(obj) return obj _EnumValueType = Union[sip.simplewrapper, int] def qenum_key(base: Type[_EnumValueType], value: _EnumValueType, add_base: bool = False, klass: Type[_EnumValueType] = None) -> str: """Convert a Qt Enum value to its key as a string. Args: base: The object the enum is in, e.g. QFrame. value: The value to get. add_base: Whether the base should be added to the printed name. klass: The enum class the value belongs to. If None, the class will be auto-guessed. Return: The key associated with the value as a string if it could be found. The original value as a string if not. """ if klass is None: klass = value.__class__ if klass == int: raise TypeError("Can't guess enum class of an int!") try: meta_obj = base.staticMetaObject # type: ignore[union-attr] idx = meta_obj.indexOfEnumerator(klass.__name__) meta_enum = meta_obj.enumerator(idx) ret = meta_enum.valueToKey(int(value)) # type: ignore[arg-type] except AttributeError: ret = None if ret is None: for name, obj in vars(base).items(): if isinstance(obj, klass) and obj == value: ret = name break else: ret = '0x{:04x}'.format(int(value)) # type: ignore[arg-type] if add_base and hasattr(base, '__name__'): return '.'.join([base.__name__, ret]) else: return ret def qflags_key(base: Type[_EnumValueType], value: _EnumValueType, add_base: bool = False, klass: Type[_EnumValueType] = None) -> str: """Convert a Qt QFlags value to its keys as string. Note: Passing a combined value (such as Qt.AlignCenter) will get the names for the individual bits (e.g. Qt.AlignVCenter | Qt.AlignHCenter). FIXME https://github.com/qutebrowser/qutebrowser/issues/42 Args: base: The object the flags are in, e.g. QtCore.Qt value: The value to get. add_base: Whether the base should be added to the printed names. klass: The flags class the value belongs to. If None, the class will be auto-guessed. Return: The keys associated with the flags as a '|' separated string if they could be found. Hex values as a string if not. """ if klass is None: # We have to store klass here because it will be lost when iterating # over the bits. klass = value.__class__ if klass == int: raise TypeError("Can't guess enum class of an int!") if not value: return qenum_key(base, value, add_base, klass) bits = [] names = [] mask = 0x01 value = int(value) # type: ignore[arg-type] while mask <= value: if value & mask: bits.append(mask) mask <<= 1 for bit in bits: # We have to re-convert to an enum type here or we'll sometimes get an # empty string back. enum_value = klass(bit) # type: ignore[call-arg] names.append(qenum_key(base, enum_value, add_base)) return '|'.join(names) def signal_name(sig: pyqtBoundSignal) -> str: """Get a cleaned up name of a signal. Unfortunately, the way to get the name of a signal differs based on bound vs. unbound signals. Here, we try to get the name from .signal or .signatures, or if all else fails, extract it from the repr(). Args: sig: A bound signal. Return: The cleaned up signal name. """ if hasattr(sig, 'signal'): # Bound signal # Examples: # sig.signal == '2signal1' # sig.signal == '2signal2(QString,QString)' m = re.fullmatch(r'[0-9]+(?P.*)\(.*\)', sig.signal) else: # Unbound signal, PyQt >= 5.11 # Examples: # sig.signatures == ('signal1()',) # sig.signatures == ('signal2(QString,QString)',) m = re.fullmatch(r'(?P.*)\(.*\)', sig.signatures[0]) # type: ignore[attr-defined] assert m is not None, sig return m.group('name') def format_args(args: Sequence[Any] = None, kwargs: Mapping[str, Any] = None) -> str: """Format a list of arguments/kwargs to a function-call like string.""" if args is not None: arglist = [utils.compact_text(repr(arg), 200) for arg in args] else: arglist = [] if kwargs is not None: for k, v in kwargs.items(): arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 200))) return ', '.join(arglist) def dbg_signal(sig: pyqtBoundSignal, args: Any) -> str: """Get a string representation of a signal for debugging. Args: sig: A bound signal. args: The arguments as list of strings. Return: A human-readable string representation of signal/args. """ return '{}({})'.format(signal_name(sig), format_args(args)) def format_call(func: Callable[..., Any], args: Sequence[Any] = None, kwargs: Mapping[str, Any] = None, full: bool = True) -> str: """Get a string representation of a function calls with the given args. Args: func: The callable to print. args: A list of positional arguments. kwargs: A dict of named arguments. full: Whether to print the full name Return: A string with the function call. """ if full: name = utils.qualname(func) else: name = func.__name__ return '{}({})'.format(name, format_args(args, kwargs)) class log_time: # noqa: N801,N806 pylint: disable=invalid-name """Log the time an operation takes. Usable as context manager or as decorator. """ def __init__(self, logger: Union[logging.Logger, str], action: str = 'operation') -> None: """Constructor. Args: logger: The logging.Logger to use for logging, or a logger name. action: A description of what's being done. """ if isinstance(logger, str): self._logger = logging.getLogger(logger) else: self._logger = logger self._started: Optional[datetime.datetime] = None self._action = action def __enter__(self) -> None: self._started = datetime.datetime.now() def __exit__(self, _exc_type: Optional[Type[BaseException]], _exc_val: Optional[BaseException], _exc_tb: Optional[types.TracebackType]) -> None: assert self._started is not None finished = datetime.datetime.now() delta = (finished - self._started).total_seconds() self._logger.debug("{} took {} seconds.".format( self._action.capitalize(), delta)) def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: @functools.wraps(func) def wrapped(*args: Any, **kwargs: Any) -> Any: """Call the original function.""" with self: return func(*args, **kwargs) return wrapped def _get_widgets() -> Sequence[str]: """Get a string list of all widgets.""" widgets = objects.qapp.allWidgets() widgets.sort(key=repr) return [repr(w) for w in widgets] def _get_pyqt_objects(lines: MutableSequence[str], obj: QObject, depth: int = 0) -> None: """Recursive method for get_all_objects to get Qt objects.""" for kid in obj.findChildren(QObject, '', Qt.FindDirectChildrenOnly): lines.append(' ' * depth + repr(kid)) _get_pyqt_objects(lines, kid, depth + 1) def get_all_objects(start_obj: QObject = None) -> str: """Get all children of an object recursively as a string.""" output = [''] widget_lines = _get_widgets() widget_lines = [' ' + e for e in widget_lines] widget_lines.insert(0, "Qt widgets - {} objects:".format( len(widget_lines))) output += widget_lines if start_obj is None: start_obj = objects.qapp pyqt_lines: List[str] = [] _get_pyqt_objects(pyqt_lines, start_obj) pyqt_lines = [' ' + e for e in pyqt_lines] pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(len(pyqt_lines))) output += [''] output += pyqt_lines output += objreg.dump_objects() return '\n'.join(output)