Qix-/better-exceptions

Don't print stack frames from certain libraries

Closed this issue · 5 comments

Is it possible to not print stack frames from third party library? Ideally with a confiuration ignore=['pandas', 'numpy'] or a function ignore(['pandas', 'numpy'])?

Qix- commented

Unfortunately not, no. I'm not sure how much I want that either, to be honest - I'm not a fan of hiding information from error messages. They should be helpful, not terse. Can you show me an example of an undesirable stacktrace?

Below is a code snippet that perfectly illustrates the issue:

from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ProcessPoolExecutor
from datetime import datetime, timedelta, timezone
from loguru import logger
from pytz import utc

def create_scheduler():
    jobstores = {
        'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
    }
    executors = {
        'default': ProcessPoolExecutor(1),
    }
    job_defaults = {
        'coalesce': False,
        'max_instances': 4
    }
    scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
    return scheduler

def add_adhoc_job(scheduler, task_func, task_name, task_args=None):
    scheduler.add_job(
        id=task_name,
        trigger='date',
        func=task_func,
        args=task_args,
        next_run_time=datetime.now(timezone.utc) + timedelta(minutes=1),
        name=f"scheduled_{task_name}",
    )
    logger.debug(f"Added adhoc task {task_name}")

def print_greeting(greeting, name):
    print(f"{greeting} {name}")
    try:
        raise Exception("NOOOOOOOOOOOOOO")
    except:
        logger.exception('')

scheduler = create_scheduler()
add_adhoc_job(scheduler, print_greeting, "print_task1", task_args=("Hello", "World",))
scheduler.start()

Here's the stacktrace from the better-exceptions lib:

2021-10-18 20:10:47.239 | ERROR    | __main__:print_greeting:120 - 
Traceback (most recent call last):

  File "python-test/apscheduler-test.py", line 127, in <module>
    scheduler.start()
    │         └ <function BlockingScheduler.start at 0x7f1078a7e7b8>
    └ <apscheduler.schedulers.blocking.BlockingScheduler object at 0x7f1075f86cf8>

  File "/usr/local/lib/python3.6/dist-packages/apscheduler/schedulers/blocking.py", line 19, in start
    self._main_loop()
    │    └ <function BlockingScheduler._main_loop at 0x7f1077883d90>
    └ <apscheduler.schedulers.blocking.BlockingScheduler object at 0x7f1075f86cf8>

  File "/usr/local/lib/python3.6/dist-packages/apscheduler/schedulers/blocking.py", line 30, in _main_loop
    wait_seconds = self._process_jobs()
                   │    └ <function BaseScheduler._process_jobs at 0x7f1077883b70>
                   └ <apscheduler.schedulers.blocking.BlockingScheduler object at 0x7f1075f86cf8>

  File "/usr/local/lib/python3.6/dist-packages/apscheduler/schedulers/base.py", line 974, in _process_jobs
    executor.submit_job(job, run_times)
    │        │          │    └ [datetime.datetime(2021, 10, 19, 0, 10, 47, 222096, tzinfo=datetime.timezone.utc)]
    │        │          └ <Job (id=print_task1 name=scheduled_print_task1)>
    │        └ <function BaseExecutor.submit_job at 0x7f107898f488>
    └ <apscheduler.executors.pool.ProcessPoolExecutor object at 0x7f107652fd68>

  File "/usr/local/lib/python3.6/dist-packages/apscheduler/executors/base.py", line 71, in submit_job
    self._do_submit_job(job, run_times)
    │    │              │    └ [datetime.datetime(2021, 10, 19, 0, 10, 47, 222096, tzinfo=datetime.timezone.utc)]
    │    │              └ <Job (id=print_task1 name=scheduled_print_task1)>
    │    └ <function BasePoolExecutor._do_submit_job at 0x7f10788e0d08>
    └ <apscheduler.executors.pool.ProcessPoolExecutor object at 0x7f107652fd68>

  File "/usr/local/lib/python3.6/dist-packages/apscheduler/executors/pool.py", line 22, in _do_submit_job
    f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name)
        │    │     │      │        │    │   │                │          │    │       └ 'apscheduler.executors.default'
        │    │     │      │        │    │   │                │          │    └ <Logger apscheduler.executors.default (WARNING)>
        │    │     │      │        │    │   │                │          └ <apscheduler.executors.pool.ProcessPoolExecutor object at 0x7f107652fd68>
        │    │     │      │        │    │   │                └ [datetime.datetime(2021, 10, 19, 0, 10, 47, 222096, tzinfo=datetime.timezone.utc)]
        │    │     │      │        │    │   └ <member '_jobstore_alias' of 'Job' objects>
        │    │     │      │        │    └ <Job (id=print_task1 name=scheduled_print_task1)>
        │    │     │      │        └ <Job (id=print_task1 name=scheduled_print_task1)>
        │    │     │      └ <function run_job at 0x7f10789aad08>
        │    │     └ <function ProcessPoolExecutor.submit at 0x7f10788e0840>
        │    └ <concurrent.futures.process.ProcessPoolExecutor object at 0x7f107652fe10>
        └ <apscheduler.executors.pool.ProcessPoolExecutor object at 0x7f107652fd68>

  File "/usr/lib/python3.6/concurrent/futures/process.py", line 466, in submit
    self._start_queue_management_thread()
    │    └ <function ProcessPoolExecutor._start_queue_management_thread at 0x7f10788e0730>
    └ <concurrent.futures.process.ProcessPoolExecutor object at 0x7f107652fe10>
  File "/usr/lib/python3.6/concurrent/futures/process.py", line 427, in _start_queue_management_thread
    self._adjust_process_count()
    │    └ <function ProcessPoolExecutor._adjust_process_count at 0x7f10788e07b8>
    └ <concurrent.futures.process.ProcessPoolExecutor object at 0x7f107652fe10>
  File "/usr/lib/python3.6/concurrent/futures/process.py", line 446, in _adjust_process_count
    p.start()
    │ └ <function BaseProcess.start at 0x7f10789699d8>
    └ <Process(Process-1, started)>
  File "/usr/lib/python3.6/multiprocessing/process.py", line 105, in start
    self._popen = self._Popen(self)
    │    │        │    │      └ <Process(Process-1, started)>
    │    │        │    └ <staticmethod object at 0x7f1078983ac8>
    │    │        └ <Process(Process-1, started)>
    │    └ None
    └ <Process(Process-1, started)>
  File "/usr/lib/python3.6/multiprocessing/context.py", line 223, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
           │                │                            └ <Process(Process-1, started)>
           │                └ <function DefaultContext.get_context at 0x7f10788d0510>
           └ <multiprocessing.context.DefaultContext object at 0x7f10789518d0>
  File "/usr/lib/python3.6/multiprocessing/context.py", line 277, in _Popen
    return Popen(process_obj)
           │     └ <Process(Process-1, started)>
           └ <class 'multiprocessing.popen_fork.Popen'>
  File "/usr/lib/python3.6/multiprocessing/popen_fork.py", line 19, in __init__
    self._launch(process_obj)
    │    │       └ <Process(Process-1, started)>
    │    └ <function Popen._launch at 0x7f1075f04a60>
    └ <multiprocessing.popen_fork.Popen object at 0x7f1075f65b00>
  File "/usr/lib/python3.6/multiprocessing/popen_fork.py", line 73, in _launch
    code = process_obj._bootstrap()
           │           └ <function BaseProcess._bootstrap at 0x7f10789611e0>
           └ <Process(Process-1, started)>
  File "/usr/lib/python3.6/multiprocessing/process.py", line 258, in _bootstrap
    self.run()
    │    └ <function BaseProcess.run at 0x7f1078969950>
    └ <Process(Process-1, started)>
  File "/usr/lib/python3.6/multiprocessing/process.py", line 93, in run
    self._target(*self._args, **self._kwargs)
    │    │        │    │        │    └ {}
    │    │        │    │        └ <Process(Process-1, started)>
    │    │        │    └ (<multiprocessing.queues.Queue object at 0x7f107652feb8>, <multiprocessing.queues.SimpleQueue object at 0x7f1075f86a90>)
    │    │        └ <Process(Process-1, started)>
    │    └ <function _process_worker at 0x7f10788e0378>
    └ <Process(Process-1, started)>
  File "/usr/lib/python3.6/concurrent/futures/process.py", line 175, in _process_worker
    r = call_item.fn(*call_item.args, **call_item.kwargs)
        │         │   │         │       │         └ {}
        │         │   │         │       └ <concurrent.futures.process._CallItem object at 0x7f1075f86a58>
        │         │   │         └ (<Job (id=print_task1 name=scheduled_print_task1)>, 'default', [datetime.datetime(2021, 10, 19, 0, 10, 47, 222096, tzinfo=dat...
        │         │   └ <concurrent.futures.process._CallItem object at 0x7f1075f86a58>
        │         └ <function run_job at 0x7f10789aad08>
        └ <concurrent.futures.process._CallItem object at 0x7f1075f86a58>

  File "/usr/local/lib/python3.6/dist-packages/apscheduler/executors/base.py", line 125, in run_job
    retval = job.func(*job.args, **job.kwargs)
             │   │     │   │       │   └ <member 'kwargs' of 'Job' objects>
             │   │     │   │       └ <Job (id=print_task1 name=scheduled_print_task1)>
             │   │     │   └ <member 'args' of 'Job' objects>
             │   │     └ <Job (id=print_task1 name=scheduled_print_task1)>
             │   └ <member 'func' of 'Job' objects>
             └ <Job (id=print_task1 name=scheduled_print_task1)>

> File "python-test/apscheduler-test.py", line 118, in print_greeting
    raise Exception("NOOOOOOOOOOOOOO")

Exception: NOOOOOOOOOOOOOO

As you can see most of the stacktrace from apscheduler and multiprocessing libs don't give any valuable info. Rather they create so much noice that it's hard to find the frames of my own code. That's why if there's an option to hide stackframes from blacklisted libs, it'd be a great help in debugging.

Qix- commented

Yep, agreed.

Perhaps it would be useful to ignore paths that exist outside of the root of the module entirely, with the ability to enable them in scenarios where it's necessary via an environment variable.

@Delgan (hi! long time 👋🏻 hope you're doing well) what do you think about that idea?

Hey @Qix-! Sorry for this answer a month later, I had taken a little break. :)

Regarding the issue raised here, it looks to me that it has little to do with better_exceptions and is rather a problem on loguru side. The traceback is displayed entirely which leads to printing undesired frames from third libraries. Using solely better_exceptions it would be displayed as follow:

Traceback (most recent call last):
  File "python-test/apscheduler-test.py", line 118, in print_greeting
    raise Exception("NOOOOOOOOOOOOOO")
Exception: NOOOOOOOOOOOOOO

However, if there exist cases for which better_exceptions formatting is considered too verbose, we could indeed imagine a solution to make it more readable. Adding an option to hide external paths seems like a good idea. Another possibility that I think we discussed a few years ago is to highlight the most interesting ones as done by tbvaccine:

tbvaccine

Two others libraries that could be inspiring are PrettyErrors and rich.

Anyway, thanks for asking my opinion, I hope you're doing well too!

@Delgan I've created a new issue since it's specific to loguru Delgan/loguru#622. I am closing this one.