Skip to content
18 changes: 12 additions & 6 deletions Doc/library/tracemalloc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -650,8 +650,8 @@ Traceback

.. class:: Traceback

Sequence of :class:`Frame` instances sorted from the most recent frame to
the oldest frame.
Sequence of :class:`Frame` instances sorted from the oldest frame to the
most recent frame.

A traceback contains at least ``1`` frame. If the ``tracemalloc`` module
failed to get a frame, the filename ``"<unknown>"`` at line number ``0`` is
Expand All @@ -663,11 +663,17 @@ Traceback
The :attr:`Trace.traceback` attribute is an instance of :class:`Traceback`
instance.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reversing the order of frames is a major backward incompatible change for this API, please mention it:

.. versionchanged:: 3.7
   Frames are now sorted from the oldest to the most recent, instead of most recent to oldest.

Moreover, since it's a backward incompatible change, it must be documented in the "Porting" section of Doc/whatsnew/3.7.rst:

https://docs.python.org/dev/whatsnew/3.7.html#changes-in-the-python-api


.. method:: format(limit=None)
.. versionchanged:: 3.7
Frames are now sorted from the oldest to the most recent, instead of most recent to oldest.

Format the traceback as a list of lines with newlines. Use the
:mod:`linecache` module to retrieve lines from the source code. If
*limit* is set, only format the *limit* most recent frames.
.. method:: format(limit=None, most_recent_first=False)

Format the traceback as a list of lines with newlines. Use the
:mod:`linecache` module to retrieve lines from the source code.
If *limit* is set, format the *limit* most recent frames if *limit*
is positive. Otherwise, format the ``abs(limit)`` oldest frames.
If *most_recent_first* is ``True``, the order of the formatted frames
is reversed, returning the most recent frame first instead of last.

Similar to the :func:`traceback.format_tb` function, except that
:meth:`.format` does not include newlines.
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,10 @@ Changes in the Python API
avoid a warning escape them with a backslash.
(Contributed by Serhiy Storchaka in :issue:`30349`.)

* :class:`tracemalloc.Traceback` frames are now sorted from oldest to most
recent to be more consistent with :mod:`traceback`.
(Contributed by Jesse Bakker in :issue:`32121`.)

.. _Unicode Technical Standard #18: https://unicode.org/reports/tr18/


Expand Down
30 changes: 24 additions & 6 deletions Lib/test/test_tracemalloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ def allocate_bytes4(size):

traces = tracemalloc._get_traces()

obj1_traceback._frames = tuple(reversed(obj1_traceback._frames))
obj2_traceback._frames = tuple(reversed(obj2_traceback._frames))

trace1 = self.find_trace(traces, obj1_traceback)
trace2 = self.find_trace(traces, obj2_traceback)
domain1, size1, traceback1 = trace1
Expand Down Expand Up @@ -537,11 +540,11 @@ def test_snapshot_group_by_cumulative(self):
def test_trace_format(self):
snapshot, snapshot2 = create_snapshots()
trace = snapshot.traces[0]
self.assertEqual(str(trace), 'a.py:2: 10 B')
self.assertEqual(str(trace), 'b.py:4: 10 B')
traceback = trace.traceback
self.assertEqual(str(traceback), 'a.py:2')
self.assertEqual(str(traceback), 'b.py:4')
frame = traceback[0]
self.assertEqual(str(frame), 'a.py:2')
self.assertEqual(str(frame), 'b.py:4')

def test_statistic_format(self):
snapshot, snapshot2 = create_snapshots()
Expand Down Expand Up @@ -574,17 +577,32 @@ def getline(filename, lineno):
side_effect=getline):
tb = snapshot.traces[0].traceback
self.assertEqual(tb.format(),
[' File "b.py", line 4',
' <b.py, 4>',
' File "a.py", line 2',
' <a.py, 2>'])

self.assertEqual(tb.format(limit=1),
[' File "a.py", line 2',
' <a.py, 2>'])

self.assertEqual(tb.format(limit=-1),
[' File "b.py", line 4',
' <b.py, 4>'])

self.assertEqual(tb.format(most_recent_first=True),
[' File "a.py", line 2',
' <a.py, 2>',
' File "b.py", line 4',
' <b.py, 4>'])

self.assertEqual(tb.format(limit=1),
self.assertEqual(tb.format(limit=1, most_recent_first=True),
[' File "a.py", line 2',
' <a.py, 2>'])

self.assertEqual(tb.format(limit=-1),
[])
self.assertEqual(tb.format(limit=-1, most_recent_first=True),
[' File "b.py", line 4',
' <b.py, 4>'])


class TestFilters(unittest.TestCase):
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -941,11 +941,11 @@ def func():
expected = textwrap.dedent('''
{fname}:5: ResourceWarning: unclosed file <...>
f = None
Object allocated at (most recent call first):
File "{fname}", lineno 3
f = open(__file__)
Object allocated at (most recent call last):
File "{fname}", lineno 7
func()
File "{fname}", lineno 3
f = open(__file__)
''')
expected = expected.format(fname=support.TESTFN).strip()
self.assertEqual(stderr, expected)
Expand Down
26 changes: 18 additions & 8 deletions Lib/tracemalloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,18 @@ def __repr__(self):
@total_ordering
class Traceback(Sequence):
"""
Sequence of Frame instances sorted from the most recent frame
to the oldest frame.
Sequence of Frame instances sorted from the oldest frame
to the most recent frame.
"""
__slots__ = ("_frames",)

def __init__(self, frames):
Sequence.__init__(self)
# frames is a tuple of frame tuples: see Frame constructor for the
# format of a frame tuple
self._frames = frames
# format of a frame tuple; it is reversed, because _tracemalloc
# returns frames sorted from most recent to oldest, but the
# Python API expects oldest to most recent
self._frames = tuple(reversed(frames))

def __len__(self):
return len(self._frames)
Expand Down Expand Up @@ -209,11 +211,19 @@ def __str__(self):
def __repr__(self):
return "<Traceback %r>" % (tuple(self),)

def format(self, limit=None):
def format(self, limit=None, most_recent_first=False):
lines = []
if limit is not None and limit < 0:
return lines
for frame in self[:limit]:
if limit is not None:
if limit > 0:
frame_slice = self[-limit:]
else:
frame_slice = self[:limit]
else:
frame_slice = self

if most_recent_first:
frame_slice = reversed(frame_slice)
for frame in frame_slice:
lines.append(' File "%s", line %s'
% (frame.filename, frame.lineno))
line = linecache.getline(frame.filename, frame.lineno).strip()
Expand Down
2 changes: 1 addition & 1 deletion Lib/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _formatwarnmsg_impl(msg):
tb = None

if tb is not None:
s += 'Object allocated at (most recent call first):\n'
s += 'Object allocated at (most recent call last):\n'
for frame in tb:
s += (' File "%s", lineno %s\n'
% (frame.filename, frame.lineno))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Made ``tracemalloc.Traceback`` behave more like the traceback module,
sorting the frames from oldest to most recent. ``Traceback.format()``
now accepts negative *limit*, truncating the result to the ``abs(limit)``
oldest frames. To get the old behaviour, one can use the new
*most_recent_first* argument to ``Traceback.format()``.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would you mind to credit yourself? Add "Patch by Jesse Bakker."

(Patch by Jesse Bakker.)