Skip to content

Commit

Permalink
Make output filename more flexible
Browse files Browse the repository at this point in the history
  • Loading branch information
gaogaotiantian committed Apr 22, 2024
1 parent f2a7f31 commit cc1a910
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 29 deletions.
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,36 @@ coredumpy saves your crash site so you can better debug your python program.
* Easy to use
* Supports pdb interface
* Does not rely on pickle
* Dump file is independent of environment
* Open the dump file on any machine

## Usage

### dump

You can dump any frame (and its parent frames) manually by
You can dump the current frame stack by

```python
from coredumpy import dump

dump("coredumpy_dump", frame)

# without frame argument, it will dump the current frame stack
dump("coredumpy_dump")
import coredumpy

# Without frame argument, top frame will be the caller of coredumpy.dump()
coredumpy.dump()
# Specify a specific frame as the top frame to dump
coredumpy.dump(frame)
# Specify a filename to save the dump, without it a unique name will be generated
coredumpy.dump(path='coredumpy.dump')
# You can use a function for path
coredumpy.dump(path=lambda: f"coredumpy_{time.time()}.dump")
# Specify a directory to keep the dump
coredumpy.dump(directory='./dumps')
```

You can hook the exception so a dump will be automatically created if your program crashes due to an exception

```python
from coredumpy import patch_excepthook
patch_excepthook()
import coredumpy
coredumpy.patch_except()
# patch_except takes the same path/directory arguments as dump
# coredumpy.patch_except(directory='./dumps')
```

### load
Expand All @@ -53,8 +61,10 @@ but none of the user-created objects will have the actual functionality.

This library is still in development phase and is not recommended for production use.

The APIs could change during development phase.

## License

Copyright 2024 Tian Gao.

Distributed under the terms of the [Apache 2.0 license](https://github.com/gaogaotiantian/coredumpy/blob/master/LICENSE).
Distributed under the terms of the [Apache 2.0 license](https://github.com/gaogaotiantian/coredumpy/blob/master/LICENSE).
4 changes: 2 additions & 2 deletions src/coredumpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
__version__ = "0.0.1"

from .coredumpy import Coredumpy, dump, load
from .except_hook import patch_excepthook
from .except_hook import patch_except
from .main import main
from .type_support import add_supports

Expand All @@ -16,5 +16,5 @@
"dump",
"load",
"main",
"patch_excepthook",
"patch_except",
]
34 changes: 30 additions & 4 deletions src/coredumpy/coredumpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,41 @@
import json
import linecache
import os
import tokenize
import pdb
import tokenize
import types
import typing

from .patch import patch_all
from .py_object_proxy import PyObjectProxy
from .utils import get_dump_filename


class Coredumpy:
@classmethod
def dump(cls, path, frame=None):
def dump(cls,
frame: types.FrameType | None = None,
*,
path: str | typing.Callable[[], str] | None = None,
directory: str | None = None):
"""
dump the current frame stack to a file
@param frame:
The top frame to dump, if not specified, the frame of the caller will be used
@param path:
The path to save the dump file. It could be a string or a callable that returns a string.
if not specified, the default filename will be used
@param directory:
The directory to save the dump file, only works when path is not specified.
@return:
The path of the dump file
"""
files = set()
if frame is None:
frame = inspect.currentframe().f_back
inner_frame = inspect.currentframe()
assert inner_frame is not None
frame = inner_frame.f_back
curr_frame = frame
while frame:
filename = frame.f_code.co_filename
Expand All @@ -29,7 +51,9 @@ def dump(cls, path, frame=None):
PyObjectProxy.add_object(frame)
frame = frame.f_back

with open(path, "w") as f:
output_file = get_dump_filename(curr_frame, path, directory)

with open(output_file, "w") as f:
json.dump({
"objects": PyObjectProxy._objects,
"frame": str(id(curr_frame)),
Expand All @@ -40,6 +64,8 @@ def dump(cls, path, frame=None):

PyObjectProxy.clear()

return output_file

@classmethod
def load(cls, path):
with open(path, "r") as f:
Expand Down
28 changes: 18 additions & 10 deletions src/coredumpy/except_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,33 @@
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt


import os
import sys
import typing

from .coredumpy import dump


_original_excepthook = sys.excepthook


def _excepthook(type, value, traceback):
while traceback.tb_next:
traceback = traceback.tb_next
def patch_except(path: str | typing.Callable[[], str] | None = None,
directory: str | None = None):
""" Patch the excepthook to dump the frame stack when an unhandled exception occurs.
filename = os.path.abspath("coredumpy_dump")
dump(filename, traceback.tb_frame)
_original_excepthook(type, value, traceback)
print(f'Your frame stack has been dumped to "{filename}", '
f'open it with\ncoredumpy load {filename}')
@param path:
The path to save the dump file. It could be a string or a callable that returns a string.
if not specified, the default filename will be used
@param directory:
The directory to save the dump file, only works when path is not specified.
"""

def _excepthook(type, value, traceback):
while traceback.tb_next:
traceback = traceback.tb_next

filename = dump(traceback.tb_frame, path=path, directory=directory)
_original_excepthook(type, value, traceback)
print(f'Your frame stack has been dumped to "{filename}", '
f'open it with\ncoredumpy load {filename}')

def patch_excepthook():
sys.excepthook = _excepthook
24 changes: 24 additions & 0 deletions src/coredumpy/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt


import datetime
import os


def get_dump_filename(frame, path, directory):
if path is not None:
if directory is not None:
raise ValueError("Cannot specify both path and directory")
if callable(path):
return os.path.abspath(path())
return os.path.abspath(path)

funcname = os.path.basename(frame.f_code.co_name)
d = datetime.datetime.now()
filename = f"coredumpy_{funcname}_{d.strftime('%Y%m%d_%H%M%S_%f')}.dump"

if directory is None:
return os.path.abspath(filename)

return os.path.abspath(os.path.join(directory, filename))
4 changes: 2 additions & 2 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def test_simple(self):
script = """
import coredumpy
def g(arg):
coredumpy.dump("coredumpy_dump")
coredumpy.dump(path="coredumpy_dump")
return arg
def f():
x = 142857
Expand All @@ -36,7 +36,7 @@ def f():
def test_except(self):
script = """
import coredumpy
coredumpy.patch_excepthook()
coredumpy.patch_except(path='coredumpy_dump')
def g(arg):
return 1 / arg
g(0)
Expand Down
38 changes: 38 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt

import os

from coredumpy.utils import get_dump_filename

from .base import TestBase


class TestUtils(TestBase):
def test_get_dump_filename(self):
class FakeFrame:
def __init__(self, name):
self.f_code = FakeCode(name)

class FakeCode:
def __init__(self, name):
self.co_name = name

frame = FakeFrame("test_get_dump_filename")
filename = get_dump_filename(frame, None, None)
self.assertTrue(filename.startswith("/"))
self.assertIn("test_get_dump_filename", filename)

filename = get_dump_filename(frame, "test.dump", None)
self.assertEqual(filename, os.path.abspath("test.dump"))

filename = get_dump_filename(frame, lambda: "test.dump", None)
self.assertEqual(filename, os.path.abspath("test.dump"))

filename = get_dump_filename(frame, None, "dir")
self.assertTrue(filename.startswith("/"))
self.assertIn("test_get_dump_filename", filename)
self.assertIn("dir", filename)

with self.assertRaises(ValueError):
filename = get_dump_filename(frame, "test.dump", "dir")

0 comments on commit cc1a910

Please sign in to comment.