meerschaum.utils.warnings

Handle all things warnings and errors.

  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3# vim:fenc=utf-8
  4
  5"""
  6Handle all things warnings and errors.
  7"""
  8
  9from __future__ import annotations
 10from meerschaum.utils.typing import Any, Union
 11from meerschaum.utils.debug import dprint
 12
 13import sys
 14import warnings
 15
 16#: Set (process-wide, like `warnings.catch_warnings`) while warnings are being recorded.
 17_RECORDING: bool = False
 18
 19__all__ = (
 20    'warn',
 21    'info',
 22    'error',
 23    'dprint',
 24)
 25
 26warnings.filterwarnings(
 27    "always",
 28    category = UserWarning
 29)
 30warnings.filterwarnings(
 31    "always",
 32    category = ImportWarning
 33)
 34warnings.filterwarnings(
 35    "ignore",
 36    category = RuntimeWarning
 37)
 38
 39
 40def enable_depreciation_warnings(name) -> None:
 41    """Enable depreciation warnings in the warnings module."""
 42    import meerschaum.actions
 43    warnings.filterwarnings(
 44        "always",
 45        category = DeprecationWarning,
 46        module = name
 47    )
 48
 49def warn(*args, stacklevel=2, stack=True, color: bool = True, **kw) -> None:
 50    """
 51    Print a stylized warning message.
 52    May be captured by `warnings.filterwarnings()`.
 53    """
 54    if stacklevel is None:
 55        stacklevel = 1
 56        stack = False
 57    _old_sw = warnings.showwarning
 58
 59    get_config = None
 60    if color:
 61        try:
 62            from meerschaum.utils.formatting import (
 63                CHARSET, colored, fill_ansi, highlight_pipes, _init, stderr_ansi
 64            )
 65            ### Warnings go to stderr: styled by stderr's own terminal check.
 66            ANSI = stderr_ansi()
 67        except ImportError:
 68            CHARSET = 'ascii'
 69            ANSI = False
 70        try:
 71            from meerschaum.config import get_config as _get_config
 72            from meerschaum.config import _config
 73            cf = _config()
 74            get_config = _get_config
 75        except ImportError:
 76            get_config = None
 77
 78    if get_config is None and color:
 79        try:
 80            warn_config = cf['formatting']['warnings']
 81        except KeyError:
 82            warn_config = {
 83                'ansi' : {'color' : []},
 84                'unicode' : {'icon' : ''},
 85                'ascii' : {'icon' : ''},
 86            }
 87    elif color:
 88        warn_config = get_config('formatting', 'warnings', patch=True)
 89    a = list(args)
 90    a[0] = ' ' + (warn_config[CHARSET]['icon'] if color else '') + ' ' + str(a[0])
 91    if color:
 92        if ANSI:
 93            _init()
 94            a[0] = fill_ansi(highlight_pipes(a[0]), ansi=True, **warn_config['ansi']['rich'])
 95
 96    ### Optionally omit the warning location.
 97    def _no_stack_sw(message, category, filename, lineno, file=None, line=None):
 98        sys.stderr.write(str(message) + '\n')
 99
100    ### While a caller records warnings (`_RECORDING`, the worker's quiet connector builds), a
101    ### stackless warning goes through `warnings` like any other, so it can be recorded instead
102    ### of being written straight to stderr.
103    swap = not stack and not _RECORDING
104    if swap:
105        warnings.showwarning = _no_stack_sw
106    warnings.warn(*a, stacklevel=stacklevel, **kw)
107    if swap:
108        warnings.showwarning = _old_sw
109
110
111def exception_with_traceback(
112    message: str,
113    exception_class = Exception, 
114    stacklevel = 1,
115    tb_type = 'single'
116):
117    """Traceback construction help found here:
118    https://stackoverflow.com/questions/27138440/how-to-create-a-traceback-object
119    """
120    import types
121    tb, depth = None, 0
122    while True:
123        try:
124            frame = sys._getframe(depth)
125            depth += 1
126        except ValueError as e:
127            break
128
129        tb = types.TracebackType(tb, frame, frame.f_lasti, frame.f_lineno)
130
131    tbs, _tb = [], tb
132    while True:
133        if _tb is None:
134            break
135        tbs.append(_tb)
136        _tb = _tb.tb_next
137
138    found_main, main_i = False, 0
139    first_mrsm_after_main = None
140    last_mrsm_i = None
141    tbs[(-1 * stacklevel)].tb_next = None
142    for i, _tb in enumerate([_tb for _tb in tbs]):
143        if 'meerschaum' in str(_tb.tb_frame) and '__main__.py' in str(_tb.tb_frame):
144            found_main = True
145            main_i = i
146            continue
147        if i >= (len(tbs) - (stacklevel - 1)):
148            tbs[i] = None
149        elif (
150                found_main and 'meerschaum' in str(_tb.tb_frame)
151                and first_mrsm_after_main is None
152                and 'Shell' not in str(_tb.tb_frame)
153            ):
154            first_mrsm_after_main = i
155        elif 'meerschaum' in str(_tb.tb_frame):
156            last_mrsm_i = i
157
158    tbs = [_tb for _tb in tbs if tb is not None]
159
160    if tb_type == 'single':
161        return exception_class(message).with_traceback(tbs[-3])
162    return exception_class(message).with_traceback(tbs[first_mrsm_after_main])
163
164
165def error(
166    message: str,
167    exception_class = Exception,
168    nopretty: bool = False,
169    silent: bool = True,
170    stack: bool = True,
171    raise_: bool = True,
172):
173    """
174    Raise an exception with supressed traceback.
175    """
176    from meerschaum.utils.formatting import (
177        CHARSET,
178        ANSI,
179        get_console,
180        fill_ansi,
181        highlight_pipes,
182        _init,
183    )
184    from meerschaum.utils.packages import import_rich, attempt_import
185    from meerschaum.config import get_config
186    _ = import_rich()
187    rich_traceback = attempt_import('rich.traceback')
188    error_config = get_config('formatting', 'errors', patch=True)
189    message = ' ' + error_config[CHARSET]['icon'] + ' ' + str(message)
190    exception = exception_with_traceback(message, exception_class, stacklevel=3)
191    color_message = str(message)
192    color_exception = exception_with_traceback(color_message, exception_class, stacklevel=3)
193    if ANSI and not nopretty:
194        _init()
195        color_message = '\n' + fill_ansi(highlight_pipes(message), **error_config['ansi']['rich'])
196        color_exception = exception_with_traceback(color_message, exception_class, stacklevel=3)
197    try:
198        trace = rich_traceback.Traceback.extract(
199            exception_class, exception, exception.__traceback__
200        )
201        rtb = rich_traceback.Traceback(trace)
202    except Exception:
203        trace, rtb = None, None
204    if trace is None or rtb is None:
205        nopretty = True
206    if not nopretty and stack:
207        if get_console() is not None:
208            get_console().print(rtb)
209    if raise_:
210        raise color_exception
211
212
213def info(message: str, icon: bool = True, **kw):
214    """Print an informative message."""
215    from meerschaum.utils.formatting import (
216        CHARSET,
217        ANSI,
218        highlight_pipes,
219        fill_ansi,
220        _init,
221    )
222    from meerschaum.config import get_config
223    info_config = get_config('formatting', 'info', patch=True)
224    if icon:
225        message = ' ' + info_config[CHARSET]['icon'] + ' ' + message
226    if ANSI:
227        _init()
228        message = highlight_pipes(message)
229        lines = message.split('\n')
230        message = fill_ansi(lines[0], **info_config['ansi']['rich']) + (
231            '\n' + '\n'.join(lines[1:]) if len(lines) > 1 else ''
232        )
233    print(message, flush=True)
def warn(*args, stacklevel=2, stack=True, color: bool = True, **kw) -> None:
 50def warn(*args, stacklevel=2, stack=True, color: bool = True, **kw) -> None:
 51    """
 52    Print a stylized warning message.
 53    May be captured by `warnings.filterwarnings()`.
 54    """
 55    if stacklevel is None:
 56        stacklevel = 1
 57        stack = False
 58    _old_sw = warnings.showwarning
 59
 60    get_config = None
 61    if color:
 62        try:
 63            from meerschaum.utils.formatting import (
 64                CHARSET, colored, fill_ansi, highlight_pipes, _init, stderr_ansi
 65            )
 66            ### Warnings go to stderr: styled by stderr's own terminal check.
 67            ANSI = stderr_ansi()
 68        except ImportError:
 69            CHARSET = 'ascii'
 70            ANSI = False
 71        try:
 72            from meerschaum.config import get_config as _get_config
 73            from meerschaum.config import _config
 74            cf = _config()
 75            get_config = _get_config
 76        except ImportError:
 77            get_config = None
 78
 79    if get_config is None and color:
 80        try:
 81            warn_config = cf['formatting']['warnings']
 82        except KeyError:
 83            warn_config = {
 84                'ansi' : {'color' : []},
 85                'unicode' : {'icon' : ''},
 86                'ascii' : {'icon' : ''},
 87            }
 88    elif color:
 89        warn_config = get_config('formatting', 'warnings', patch=True)
 90    a = list(args)
 91    a[0] = ' ' + (warn_config[CHARSET]['icon'] if color else '') + ' ' + str(a[0])
 92    if color:
 93        if ANSI:
 94            _init()
 95            a[0] = fill_ansi(highlight_pipes(a[0]), ansi=True, **warn_config['ansi']['rich'])
 96
 97    ### Optionally omit the warning location.
 98    def _no_stack_sw(message, category, filename, lineno, file=None, line=None):
 99        sys.stderr.write(str(message) + '\n')
100
101    ### While a caller records warnings (`_RECORDING`, the worker's quiet connector builds), a
102    ### stackless warning goes through `warnings` like any other, so it can be recorded instead
103    ### of being written straight to stderr.
104    swap = not stack and not _RECORDING
105    if swap:
106        warnings.showwarning = _no_stack_sw
107    warnings.warn(*a, stacklevel=stacklevel, **kw)
108    if swap:
109        warnings.showwarning = _old_sw

Print a stylized warning message. May be captured by warnings.filterwarnings().

def info(message: str, icon: bool = True, **kw):
214def info(message: str, icon: bool = True, **kw):
215    """Print an informative message."""
216    from meerschaum.utils.formatting import (
217        CHARSET,
218        ANSI,
219        highlight_pipes,
220        fill_ansi,
221        _init,
222    )
223    from meerschaum.config import get_config
224    info_config = get_config('formatting', 'info', patch=True)
225    if icon:
226        message = ' ' + info_config[CHARSET]['icon'] + ' ' + message
227    if ANSI:
228        _init()
229        message = highlight_pipes(message)
230        lines = message.split('\n')
231        message = fill_ansi(lines[0], **info_config['ansi']['rich']) + (
232            '\n' + '\n'.join(lines[1:]) if len(lines) > 1 else ''
233        )
234    print(message, flush=True)

Print an informative message.

def error( message: str, exception_class=<class 'Exception'>, nopretty: bool = False, silent: bool = True, stack: bool = True, raise_: bool = True):
166def error(
167    message: str,
168    exception_class = Exception,
169    nopretty: bool = False,
170    silent: bool = True,
171    stack: bool = True,
172    raise_: bool = True,
173):
174    """
175    Raise an exception with supressed traceback.
176    """
177    from meerschaum.utils.formatting import (
178        CHARSET,
179        ANSI,
180        get_console,
181        fill_ansi,
182        highlight_pipes,
183        _init,
184    )
185    from meerschaum.utils.packages import import_rich, attempt_import
186    from meerschaum.config import get_config
187    _ = import_rich()
188    rich_traceback = attempt_import('rich.traceback')
189    error_config = get_config('formatting', 'errors', patch=True)
190    message = ' ' + error_config[CHARSET]['icon'] + ' ' + str(message)
191    exception = exception_with_traceback(message, exception_class, stacklevel=3)
192    color_message = str(message)
193    color_exception = exception_with_traceback(color_message, exception_class, stacklevel=3)
194    if ANSI and not nopretty:
195        _init()
196        color_message = '\n' + fill_ansi(highlight_pipes(message), **error_config['ansi']['rich'])
197        color_exception = exception_with_traceback(color_message, exception_class, stacklevel=3)
198    try:
199        trace = rich_traceback.Traceback.extract(
200            exception_class, exception, exception.__traceback__
201        )
202        rtb = rich_traceback.Traceback(trace)
203    except Exception:
204        trace, rtb = None, None
205    if trace is None or rtb is None:
206        nopretty = True
207    if not nopretty and stack:
208        if get_console() is not None:
209            get_console().print(rtb)
210    if raise_:
211        raise color_exception

Raise an exception with supressed traceback.

def dprint( msg: str, leader: bool = True, timestamp: bool = True, package: bool = True, color: 'Optional[Union[str, List[str]], bool]' = None, attrs: List[str] | None = None, nopretty: bool = False, _progress: rich.progress.Progress | None = None, _task: int | None = None, _package: str | None = None, _lineno: int | None = None, **kw) -> None:
 32def dprint(
 33    msg: str,
 34    leader: bool = True,
 35    timestamp: bool = True,
 36    package: bool = True,
 37    color: Optional[Union[str, List[str]], bool] = None,
 38    attrs: Optional[List[str]] = None,
 39    nopretty: bool = False,
 40    _progress: Optional['rich.progress.Progress'] = None,
 41    _task: Optional[int] = None,
 42    _package: Optional[str] = None,
 43    _lineno: Optional[int] = None,
 44    **kw
 45) -> None:
 46    """
 47    Print a debug message to stderr.
 48
 49    The header names the calling module and line. The Rust core passes its own module path and
 50    line as `_package` and `_lineno` (its debug events have no Python caller).
 51    """
 52    import meerschaum.config.paths as paths
 53    if attrs is None:
 54        attrs = []
 55
 56    if not isinstance(color, bool) and not nopretty:
 57        try:
 58            from meerschaum.utils.formatting import CHARSET, ANSI, colored
 59        except Exception as e:
 60            CHARSET, ANSI, colored = 'ascii', False, None
 61        from meerschaum.config import _config
 62        cf = _config('formatting')
 63        _color = color
 64    else:
 65        CHARSET, ANSI, colored, _color, cf = 'ascii', False, None, None, None
 66
 67    if timestamp:
 68        from meerschaum.utils.dtypes import get_current_timestamp
 69        now = get_current_timestamp('ms').replace(tzinfo=None)
 70    else:
 71        now = None
 72
 73    import logging, sys, inspect
 74    logging.basicConfig(format='%(message)s')
 75    log = logging.getLogger(__name__)
 76
 77    if _package is not None:
 78        parent_package = _package
 79        parent_lineno = _lineno
 80    else:
 81        parent_frame = inspect.stack()[1][0]
 82        parent_info = inspect.getframeinfo(parent_frame)
 83        parent_lineno = parent_info.lineno
 84        parent_globals = parent_frame.f_globals
 85        parent_package = parent_globals['__name__']
 86    msg = str(msg)
 87    premsg = ""
 88
 89    if now:
 90        premsg = now.isoformat().split('T', maxsplit=1)[-1][:-3] + (' | ' if package else ':')
 91    if package:
 92        premsg = premsg + parent_package + (
 93            (':' + str(parent_lineno)) if parent_lineno is not None else ''
 94        )
 95    if premsg:
 96        premsg += "\n"
 97    if leader and cf is not None:
 98        try:
 99            debug_leader = cf['formatting']['debug'][CHARSET]['icon'] if cf is not None else ''
100        except KeyError:
101            print(
102                "Failed to load config. " +
103                "Please delete the following directories and restart Meerschaum:"
104            )
105            for p in [paths.CONFIG_DIR_PATH, paths.PERMANENT_PATCH_DIR_PATH]:
106                print('  - ' + str(p))
107            debug_leader = ''
108        premsg = ' ' + debug_leader + ' ' + premsg
109
110    if ANSI:
111        if _color is not None:
112            if isinstance(_color, str):
113                _color = [_color]
114        else:
115            if cf is not None and not nopretty:
116                try:
117                    _color = cf['formatting']['debug']['ansi']['rich'] if cf is not None else {}
118                except KeyError:
119                    _color = {}
120            else:
121                _color = {}
122        if colored is not None:
123            premsg = colored(premsg, **_color)
124
125    if _progress is not None:
126        rich_text = _import_rich_text_for_dprint()
127        text = rich_text.Text.from_ansi(premsg + msg)
128        _progress.console.log(text)
129    else:
130        ### v5: debug output goes to stderr (v4 printed to stdout), like warnings and the
131        ### Rust core's debug output. Job logs capture both streams.
132        print(premsg + msg, file=sys.stderr)

Print a debug message to stderr.

The header names the calling module and line. The Rust core passes its own module path and line as _package and _lineno (its debug events have no Python caller).