PHPFixing
  • Privacy Policy
  • TOS
  • Ask Question
  • Contact Us
  • Home
  • PHP
  • Programming
  • SQL Injection
  • Web3.0

Thursday, August 4, 2022

[FIXED] How to tell a Python Exception where the function that raised it came from?

 August 04, 2022     exception, python, python-3.x, traceback     No comments   

Issue

Suppose I write a simple Python class, Delay, whose point is to encapsulate a delayed (lazy) computation:

class Delay:
    def __init__(self, fn, *args, **kwargs):
        self.partial = (fn, args, kwargs)
        self.result = None
    def __call__(self):
        if self.partial is not None:
            (fn, args, kwargs) = self.partial
            self.result = fn(*args, **kwargs)
            self.partial = None
        return self.result

This is straightforward enough, but now let's think about how it will be used:

# Somewhere in some initialization module of the codebase:
def load_historical_data(filename):
    with open(filename, 'r') as f:
        return json.load(f)
def init_runtime(param_dict):
    ...
    # (Prepare to) load some data that won't be needed right away.
    runtime['historical_data'] = Delay(load_historical_data,
                                       param_dict['hist_filename'])
    ...
    return runtime

# Some far remote corner of the codebase...
def _analyze_history_job_firstpass(runtime, kwargs*):
    ...
    histdata = runtime['historical_data']()
    ...

One problem with the above paradigm arises when there is a bug in the init_runtime function—for example, if the param_dict['hist_filename'] isn't a valid filename. In this case, when _analyze_history_job_firstpass eventually gets called, it will raise an exception due to the file not being found, but nothing about that exception will point back to init_runtime, which makes debugging difficult.

It would be ideal if the Delay class could detect the exception during its __call__ method and instead raise an alternate exception that documented both the call stack that created fn and the call stack that called fn. How can one achieve this? (If there are multiple ways, what are the advantages/disadvantages of each?)


Solution

An arguably friendlier approach would be to produce a forced exception and save it into the object during initialization, and then raise the saved exception when handling an exception that actually occurs during the execution of the delayed call:

class Delay:
    def __init__(self, fn, *args, **kwargs):
        self.partial = (fn, args, kwargs)
        self.result = None
        try:
            # artificially create an exception that can be re-raised later
            raise ValueError(f'{fn.__name__} failed with args {args} and kwargs {kwargs}')
        except ValueError as e:
            self.init_exception = e

    def __call__(self):
        if self.partial is not None:
            (fn, args, kwargs) = self.partial
            try:
                self.result = fn(*args, **kwargs)
            except Exception:
                # raise the saved exception
                raise self.init_exception
            self.partial = None
        return self.result

def load_historical_data(filename):
    with open(filename, 'r') as f:
        return f.read()

def init_runtime(filename):
    runtime = Delay(load_historical_data, filename)
    return runtime

def _analyze_history_job_firstpass(runtime):
    return runtime()

_analyze_history_job_firstpass(init_runtime('foobar'))

This produces an error output with tracebacks of both the call stack that created fn and the call stack that called fn:

Traceback (most recent call last):
  File "main.py", line 15, in __call__
    self.result = fn(*args, **kwargs)
  File "main.py", line 23, in load_historical_data
    with open(filename, 'r') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'foobar'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "main.py", line 33, in <module>
    _analyze_history_job_firstpass(init_runtime('foobar'))
  File "main.py", line 31, in _analyze_history_job_firstpass
    return runtime()
  File "main.py", line 18, in __call__
    raise self.init_exception
  File "main.py", line 7, in __init__
    raise ValueError(f'{fn.__name__} failed with args {args} and kwargs {kwargs}')
ValueError: load_historical_data failed with args ('foobar',) and kwargs {}

Demo: https://replit.com/@blhsing/PassionateKnowingAlgorithms#main.py



Answered By - blhsing
Answer Checked By - Timothy Miller (PHPFixing Admin)
  • Share This:  
  •  Facebook
  •  Twitter
  •  Stumble
  •  Digg
Newer Post Older Post Home
View mobile version

0 Comments:

Post a Comment

Note: Only a member of this blog may post a comment.

Total Pageviews

Featured Post

Why Learn PHP Programming

Why Learn PHP Programming A widely-used open source scripting language PHP is one of the most popular programming languages in the world. It...

Subscribe To

Posts
Atom
Posts
Comments
Atom
Comments

Copyright © PHPFixing