Skip to content
Merged
22 changes: 22 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ Object Protocol
instead of the :func:`repr`.


.. c:function:: void PyObject_Dump(PyObject* op)

Dump an object *op* to ``stderr``. Function used for debugging.

It can be called without an :term:`attached thread state`, even if it's not
recommended.

Implement an heuristic to detect if the object memory has been freed.

Example of output:

.. code-block:: output

object address : 0x7f80124702c0
object refcount : 2
object type : 0x9902e0
object type name: str
object repr : 'abcdef'

.. versionadded:: next


.. c:function:: int PyObject_HasAttrWithError(PyObject *o, PyObject *attr_name)

Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise.
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@ New features

(Contributed by Victor Stinner in :gh:`129813`.)

* Add :c:func:`PyObject_Dump` to dump an object to ``stderr``. Function used
for debugging.
(Contributed by Victor Stinner in :gh:`141070`.)

* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
(Contributed by Victor Stinner in :gh:`111489`.)

Expand Down
7 changes: 5 additions & 2 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,10 @@ PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *);

PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int);
PyAPI_FUNC(void) _Py_BreakPoint(void);
PyAPI_FUNC(void) _PyObject_Dump(PyObject *);
PyAPI_FUNC(void) PyObject_Dump(PyObject *);

// Alias for backward compatibility
#define _PyObject_Dump PyObject_Dump

PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *);

Expand Down Expand Up @@ -387,7 +390,7 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
process with a message on stderr if the given condition fails to hold,
but compile away to nothing if NDEBUG is defined.

However, before aborting, Python will also try to call _PyObject_Dump() on
However, before aborting, Python will also try to call PyObject_Dump() on
the given object. This may be of use when investigating bugs in which a
particular object is corrupt (e.g. buggy a tp_visit method in an extension
module breaking the garbage collector), to help locate the broken objects.
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import enum
import os
import sys
import textwrap
import unittest
Expand All @@ -13,6 +14,8 @@
_testcapi = import_helper.import_module('_testcapi')
_testinternalcapi = import_helper.import_module('_testinternalcapi')

STDERR_FD = 2


class Constant(enum.IntEnum):
Py_CONSTANT_NONE = 0
Expand Down Expand Up @@ -247,5 +250,40 @@ def func(x):

func(object())

def test_pyobject_dump(self):
pyobject_dump = _testcapi.pyobject_dump
obj = 'test string'

filename = os_helper.TESTFN
self.addCleanup(os_helper.unlink, filename)

try:
old_stderr = os.dup(STDERR_FD)
except OSError as exc:
# os.dup(STDERR_FD) is not supported on WASI
self.skipTest(f"os.dup() failed with {exc!r}")

try:
with open(filename, "wb") as fp:
fd = fp.fileno()
os.dup2(fd, STDERR_FD)
pyobject_dump(obj)
finally:
os.dup2(old_stderr, STDERR_FD)
os.close(old_stderr)

with open(filename) as fp:
output = fp.read()

hex_regex = r'(0x)?[0-9a-fA-F]+'
self.assertRegex(output.rstrip(),
fr"object address : {hex_regex}\n"
r"object refcount : [0-9]+\n"
fr"object type : {hex_regex}\n"
r"object type name: str\n"
r"object repr : 'test string'"
)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`PyObject_Dump` to dump an object to ``stderr``. Function used
for debugging. Patch by Victor Stinner.
9 changes: 9 additions & 0 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,14 @@ is_uniquely_referenced(PyObject *self, PyObject *op)
}


static PyObject *
pyobject_dump(PyObject *self, PyObject *op)
{
PyObject_Dump(op);
Py_RETURN_NONE;
}


static PyMethodDef test_methods[] = {
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
{"pyobject_print_null", pyobject_print_null, METH_VARARGS},
Expand All @@ -511,6 +519,7 @@ static PyMethodDef test_methods[] = {
{"test_py_is_funcs", test_py_is_funcs, METH_NOARGS},
{"clear_managed_dict", clear_managed_dict, METH_O, NULL},
{"is_uniquely_referenced", is_uniquely_referenced, METH_O},
{"pyobject_dump", pyobject_dump, METH_O},
{NULL},
};

Expand Down
4 changes: 2 additions & 2 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ _PyObject_IsFreed(PyObject *op)

/* For debugging convenience. See Misc/gdbinit for some useful gdb hooks */
void
_PyObject_Dump(PyObject* op)
PyObject_Dump(PyObject* op)
{
if (_PyObject_IsFreed(op)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a special case for op == NULL?

And for type == NULL?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It displays freed in both cases. There is now an unit test for that.

How do you suggest to format op==NULL and/or type==NULL?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is already tested, then good. I was not sure that _PyObject_IsFreed() works with NULL (it could crash).

If type==NULL (very unlikely), then type->tp_name and repr() will crash. They will also crash if type was freed or if it is just a garbage. We could skip prints in that case avoid crash, although it may be too late.

Looking at the code, it seems the ideas I had (testing against the patterns used to fill the freed memory) was already implemented in _PyMem_IsPtrFreed().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If type==NULL (very unlikely), then type->tp_name and repr() will crash

_PyObject_IsFreed() checks _PyMem_IsPtrFreed(Py_TYPE(op)). We just display freed in this case, without displaying tp_name or trying to call repr().

/* It seems like the object memory has been freed:
Expand Down Expand Up @@ -3150,7 +3150,7 @@ _PyObject_AssertFailed(PyObject *obj, const char *expr, const char *msg,

/* This might succeed or fail, but we're about to abort, so at least
try to provide any extra info we can: */
_PyObject_Dump(obj);
PyObject_Dump(obj);

fprintf(stderr, "\n");
fflush(stderr);
Expand Down
2 changes: 1 addition & 1 deletion Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ unicode_check_encoding_errors(const char *encoding, const char *errors)
}

/* Disable checks during Python finalization. For example, it allows to
call _PyObject_Dump() during finalization for debugging purpose. */
call PyObject_Dump() during finalization for debugging purpose. */
if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
return 0;
}
Expand Down
2 changes: 1 addition & 1 deletion Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2235,7 +2235,7 @@ _PyGC_Fini(PyInterpreterState *interp)
void
_PyGC_Dump(PyGC_Head *g)
{
_PyObject_Dump(FROM_GC(g));
PyObject_Dump(FROM_GC(g));
}


Expand Down
8 changes: 4 additions & 4 deletions Python/pythonrun.c
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,7 @@ _PyErr_Display(PyObject *file, PyObject *unused, PyObject *value, PyObject *tb)
}
if (print_exception_recursive(&ctx, value) < 0) {
PyErr_Clear();
_PyObject_Dump(value);
PyObject_Dump(value);
fprintf(stderr, "lost sys.stderr\n");
}
Py_XDECREF(ctx.seen);
Expand All @@ -1199,14 +1199,14 @@ PyErr_Display(PyObject *unused, PyObject *value, PyObject *tb)
PyObject *file;
if (PySys_GetOptionalAttr(&_Py_ID(stderr), &file) < 0) {
PyObject *exc = PyErr_GetRaisedException();
_PyObject_Dump(value);
PyObject_Dump(value);
fprintf(stderr, "lost sys.stderr\n");
_PyObject_Dump(exc);
PyObject_Dump(exc);
Py_DECREF(exc);
return;
}
if (file == NULL) {
_PyObject_Dump(value);
PyObject_Dump(value);
fprintf(stderr, "lost sys.stderr\n");
return;
}
Expand Down
Loading