diff --git a/_drgn.pyi b/_drgn.pyi index 1981525b8..90459ee8d 100644 --- a/_drgn.pyi +++ b/_drgn.pyi @@ -1406,6 +1406,7 @@ class Object: value (i.e., for C, zero-initialized). Defaults to ``False``. """ ... + def _repr_pretty_(self, p: Any, cycle: bool) -> None: ... def __iter__(self) -> Iterator[Object]: ... def __bool__(self) -> bool: ... def __lt__(self, other: Any) -> bool: ... @@ -1621,6 +1622,7 @@ class StackTrace: """Program that this stack trace is from.""" def __getitem__(self, idx: IntegerLike) -> StackFrame: ... + def _repr_pretty_(self, p: Any, cycle: bool) -> None: ... class StackFrame: """ @@ -1741,6 +1743,7 @@ class StackFrame: dictionary with the register names as keys. """ ... + def _repr_pretty_(self, p: Any, cycle: bool) -> None: ... class Type: """ @@ -1913,6 +1916,7 @@ class Type: :raises TypeError: if this type is not a structure, union, or class type """ + def _repr_pretty_(self, p: Any, cycle: bool) -> None: ... class TypeMember: """ diff --git a/docs/user_guide.rst b/docs/user_guide.rst index ee2a8ed7e..c521af4fa 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -396,6 +396,62 @@ explicitly:: int counter; } atomic_t +Jupyter +^^^^^^^ + +drgn can also be use in `Jupyter `_ with the `IPython +kernel `_, where the former provides the follows features, +similar to `Interactive Mode`_: + +* History +* Tab completion +* Pretty printing of objects and types +* Helpers + + * Saving session ``%save`` + * Running shell command ``!`` + * `and more `_ + +It shares much of the same features as `Interactive Mode`_ plus `a few extra +tricks `_, +albeit much more heavy-weight compaired to the built-in counterpart. + +Similarly, ``str()`` is used instead of ``repr()`` to save one from having to +call ``print()`` for objects and types.:: + + $ pip install ipython + ... + + $ ipython + IPython 8.4.0 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: import drgn + In [2]: prog = drgn.Program() + In [3]: !ps aux | grep bash | head -n1 # Run shell command + drgn 2022 0.0 0.0 20000 10000 pts/1 Ss Aug16 0:00 /bin/bash + In [4]: prog.set_pid(2022) + In [5]: prog.load_default_debug_info() + In [6]: %save session.py # Save the session inside a .py file + The following commands were written to file `session.py`: + ... + In [7]: exit + + $ ipython + IPython 8.4.0 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: %run -i drgn_session.py # Re-execute all previous statements + In [2]: prog['main'] + Out[2]: (int (int argc, char **argv, char **env)) + +For a fully-fledges Jupyter experience, it is recommended to use `Jupyter +notebook +`_, +which is a web-based interactive programming interface that supports richer +formats such as `Markdown +`_ +and can be `easily shared +`_. + Next Steps ---------- diff --git a/libdrgn/python/drgnpy.h b/libdrgn/python/drgnpy.h index abe20b89d..bf3c92a25 100644 --- a/libdrgn/python/drgnpy.h +++ b/libdrgn/python/drgnpy.h @@ -267,6 +267,9 @@ DrgnType *Program_function_type(Program *self, PyObject *args, PyObject *kwds); int append_string(PyObject *parts, const char *s); int append_format(PyObject *parts, const char *format, ...); PyObject *join_strings(PyObject *parts); +PyObject *repr_pretty_from_str(PyObject *self, PyObject *args, PyObject *kwds); +PyDoc_STRVAR(repr_pretty_DOC, "Helper method used for pretty printing in " + "Jupyter notebook."); struct index_arg { bool allow_none; diff --git a/libdrgn/python/object.c b/libdrgn/python/object.c index 2f11fabbc..ed0479cc8 100644 --- a/libdrgn/python/object.c +++ b/libdrgn/python/object.c @@ -1653,6 +1653,8 @@ static PyMethodDef DrgnObject_methods[] = { drgn_Object_from_bytes__DOC}, {"format_", (PyCFunction)DrgnObject_format, METH_VARARGS | METH_KEYWORDS, drgn_Object_format__DOC}, + {"_repr_pretty_", (PyCFunction)repr_pretty_from_str, + METH_VARARGS | METH_KEYWORDS, repr_pretty_DOC}, {"__round__", (PyCFunction)DrgnObject_round, METH_VARARGS | METH_KEYWORDS}, {"__trunc__", (PyCFunction)DrgnObject_trunc, METH_NOARGS}, diff --git a/libdrgn/python/stack_trace.c b/libdrgn/python/stack_trace.c index e332d02c5..58f47cbb9 100644 --- a/libdrgn/python/stack_trace.c +++ b/libdrgn/python/stack_trace.c @@ -68,6 +68,12 @@ static StackFrame *StackTrace_item(StackTrace *self, Py_ssize_t i) return ret; } +static PyMethodDef StackTrace_methods[] = { + {"_repr_pretty_", (PyCFunction)repr_pretty_from_str, + METH_VARARGS | METH_KEYWORDS, repr_pretty_DOC}, + {}, +}; + static PySequenceMethods StackTrace_as_sequence = { .sq_length = (lenfunc)StackTrace_length, .sq_item = (ssizeargfunc)StackTrace_item, @@ -87,6 +93,7 @@ PyTypeObject StackTrace_type = { .tp_str = (reprfunc)StackTrace_str, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = drgn_StackTrace_DOC, + .tp_methods = StackTrace_methods, .tp_getset = StackTrace_getset, }; @@ -300,6 +307,8 @@ static PyMethodDef StackFrame_methods[] = { METH_O, drgn_StackFrame_register_DOC}, {"registers", (PyCFunction)StackFrame_registers, METH_NOARGS, drgn_StackFrame_registers_DOC}, + {"_repr_pretty_", (PyCFunction)repr_pretty_from_str, + METH_VARARGS | METH_KEYWORDS, repr_pretty_DOC}, {}, }; diff --git a/libdrgn/python/type.c b/libdrgn/python/type.c index 7b4ecee23..80a0d4d89 100644 --- a/libdrgn/python/type.c +++ b/libdrgn/python/type.c @@ -751,6 +751,8 @@ static PyMethodDef DrgnType_methods[] = { drgn_Type_member_DOC}, {"has_member", (PyCFunction)DrgnType_has_member, METH_VARARGS | METH_KEYWORDS, drgn_Type_has_member_DOC}, + {"_repr_pretty_", (PyCFunction)repr_pretty_from_str, + METH_VARARGS | METH_KEYWORDS, repr_pretty_DOC}, {}, }; diff --git a/libdrgn/python/util.c b/libdrgn/python/util.c index ad5c5578e..057a57869 100644 --- a/libdrgn/python/util.c +++ b/libdrgn/python/util.c @@ -54,6 +54,29 @@ PyObject *join_strings(PyObject *parts) return ret; } +PyObject *repr_pretty_from_str(PyObject *self, PyObject *args, + PyObject *kwds) +{ + static char *keywords[] = {"p", "cycle", NULL}; + PyObject *p, *str_obj, *ret; + int cycle; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Op:_repr_pretty_", keywords, + &p, &cycle)) + return NULL; + + if (cycle) + return PyObject_CallMethod(p, "text", "s", "..."); + + str_obj = PyObject_Str(self); + if (!str_obj) + return NULL; + + ret = PyObject_CallMethod(p, "text", "O", str_obj); + Py_DECREF(str_obj); + return ret; +} + int index_converter(PyObject *o, void *p) { struct index_arg *arg = p; diff --git a/tests/__init__.py b/tests/__init__.py index afbf1a5a2..8fa472bca 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,6 +4,7 @@ import functools from typing import Any, NamedTuple, Optional import unittest +from unittest.mock import Mock from drgn import ( Architecture, @@ -108,6 +109,16 @@ def mock_object_find(prog, name, flags, filename): return prog +def assertReprPrettyEqualsStr(obj): + pretty_printer_mock = Mock() + + obj._repr_pretty_(pretty_printer_mock, False) + pretty_printer_mock.text.assert_called_with(str(obj)) + + obj._repr_pretty_(p=pretty_printer_mock, cycle=True) + pretty_printer_mock.text.assert_called_with("...") + + def identical(a, b): """ Return whether two objects are "identical". diff --git a/tests/linux_kernel/test_stack_trace.py b/tests/linux_kernel/test_stack_trace.py index cb86cc065..7e6bb52f4 100644 --- a/tests/linux_kernel/test_stack_trace.py +++ b/tests/linux_kernel/test_stack_trace.py @@ -6,6 +6,7 @@ from drgn import Object, Program, cast from drgn.helpers.linux.pid import find_task +from tests import assertReprPrettyEqualsStr from tests.linux_kernel import ( LinuxKernelTestCase, fork_and_pause, @@ -97,3 +98,13 @@ def test_prog(self): self.prog.stack_trace(Object(self.prog, "struct pt_regs", value={})).prog, self.prog, ) + + def test_stack__repr_pretty_(self): + pid = fork_and_pause() + wait_until(proc_blocked, pid) + trace = self.prog.stack_trace(pid) + assertReprPrettyEqualsStr(trace) + for frame in trace: + assertReprPrettyEqualsStr(frame) + os.kill(pid, signal.SIGKILL) + os.waitpid(pid, 0) diff --git a/tests/test_object.py b/tests/test_object.py index 2697539fb..757fbeb60 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -16,7 +16,12 @@ reinterpret, sizeof, ) -from tests import MockMemorySegment, MockProgramTestCase, mock_program +from tests import ( + MockMemorySegment, + MockProgramTestCase, + assertReprPrettyEqualsStr, + mock_program, +) class TestInit(MockProgramTestCase): @@ -1718,3 +1723,7 @@ def test_iter(self): iter, Object(self.prog, "int []", address=0), ) + + def test__repr_pretty_(self): + obj = Object(self.prog, "int", value=0) + assertReprPrettyEqualsStr(obj) diff --git a/tests/test_type.py b/tests/test_type.py index add26ebbb..1d1c48066 100644 --- a/tests/test_type.py +++ b/tests/test_type.py @@ -14,7 +14,12 @@ offsetof, sizeof, ) -from tests import DEFAULT_LANGUAGE, MockProgramTestCase, mock_program +from tests import ( + DEFAULT_LANGUAGE, + MockProgramTestCase, + assertReprPrettyEqualsStr, + mock_program, +) class TestType(MockProgramTestCase): @@ -973,6 +978,10 @@ def test_language_repr(self): "prog.void_type(language=Language.CPP)", ) + def test__repr_pretty_(self): + t = self.prog.void_type() + assertReprPrettyEqualsStr(t) + def test_different_programs_compound(self): self.assertRaisesRegex( ValueError,