Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: 'Build documentation'
run: xvfb-run make -C Doc/ PYTHON=../python SPHINXOPTS="-q -W --keep-going -j4" doctest html
- name: 'Upload'
uses: actions/upload-artifact@v2.2.3
uses: actions/upload-artifact@v2.2.4
with:
name: doc-html
path: Doc/build/html
8 changes: 8 additions & 0 deletions Doc/c-api/code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,11 @@ bound into a function.

For efficiently iterating over the line numbers in a code object, use `the API described in PEP 626
<https://www.python.org/dev/peps/pep-0626/#out-of-process-debuggers-and-profilers>`_.

.. c:function:: int PyCode_Addr2Location(PyObject *co, int byte_offset, int *start_line, int *start_column, int *end_line, int *end_column)

Sets the passed ``int`` pointers to the source code line and column numbers
for the instruction at ``byte_offset``. Sets the value to ``0`` when
information is not available for any particular element.

Returns ``1`` if the function succeeds and 0 otherwise.
2 changes: 2 additions & 0 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,8 @@ is available in ``argparse`` and adds support for boolean actions such as
>>> parser.parse_args(['--no-foo'])
Namespace(foo=False)

.. versionadded:: 3.9

The recommended way to create a custom action is to extend :class:`Action`,
overriding the ``__call__`` method and optionally the ``__init__`` and
``format_usage`` methods.
Expand Down
6 changes: 2 additions & 4 deletions Doc/library/importlib.metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
:synopsis: The implementation of the importlib metadata.

.. versionadded:: 3.8
.. versionchanged:: 3.10
``importlib.metadata`` is no longer provisional.

**Source code:** :source:`Lib/importlib/metadata.py`

.. note::
This functionality is provisional and may deviate from the usual
version semantics of the standard library.

``importlib.metadata`` is a library that provides for access to installed
package metadata. Built in part on Python's import system, this library
intends to replace similar functionality in the `entry point
Expand Down
8 changes: 8 additions & 0 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ the :mod:`glob` module.)
that contains symbolic links. On Windows, it converts forward slashes to
backward slashes. To normalize case, use :func:`normcase`.

.. note::
On POSIX systems, in accordance with `IEEE Std 1003.1 2013 Edition; 4.13
Pathname Resolution <http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13>`_,
if a pathname begins with exactly two slashes, the first component
following the leading characters may be interpreted in an implementation-defined
manner, although more than two leading characters shall be treated as a
single character.

.. versionchanged:: 3.6
Accepts a :term:`path-like object`.

Expand Down
4 changes: 2 additions & 2 deletions Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ The output for the example would look similar to this:
['Traceback (most recent call last):\n',
' File "<doctest default[0]>", line 10, in <module>\n lumberjack()\n ^^^^^^^^^^^^\n',
' File "<doctest default[0]>", line 4, in lumberjack\n bright_side_of_death()\n ^^^^^^^^^^^^^^^^^^^^^^\n',
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ^^^^^^^^^^\n',
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ~~~~~~~^^^\n',
'IndexError: tuple index out of range\n']
*** extract_tb:
[<FrameSummary file <doctest...>, line 10 in <module>>,
Expand All @@ -482,7 +482,7 @@ The output for the example would look similar to this:
*** format_tb:
[' File "<doctest default[0]>", line 10, in <module>\n lumberjack()\n ^^^^^^^^^^^^\n',
' File "<doctest default[0]>", line 4, in lumberjack\n bright_side_of_death()\n ^^^^^^^^^^^^^^^^^^^^^^\n',
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ^^^^^^^^^^\n']
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ~~~~~~~^^^\n']
*** tb_lineno: 10


Expand Down
33 changes: 33 additions & 0 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,39 @@ Internal types
If a code object represents a function, the first item in :attr:`co_consts` is
the documentation string of the function, or ``None`` if undefined.

.. method:: codeobject.co_positions()

Returns an iterable over the source code positions of each bytecode
instruction in the code object.

The iterator returns tuples containing the ``(start_line, end_line,
start_column, end_column)``. The *i-th* tuple corresponds to the
position of the source code that compiled to the *i-th* instruction.
Column information is 0-indexed utf-8 byte offsets on the given source
line.

This positional information can be missing. A non-exhaustive lists of
cases where this may happen:

- Running the interpreter with :option:`-X` ``no_debug_ranges``.
- Loading a pyc file compiled while using :option:`-X` ``no_debug_ranges``.
- Position tuples corresponding to artificial instructions.
- Line and column numbers that can't be represented due to
implementation specific limitations.

When this occurs, some or all of the tuple elements can be
:const:`None`.

.. versionadded:: 3.11

.. note::
This feature requires storing column positions in code objects which may
result in a small increase of disk usage of compiled Python files or
interpreter memory usage. To avoid storing the extra information and/or
deactivate printing the extra traceback information, the
:option:`-X` ``no_debug_ranges`` command line flag or the :envvar:`PYTHONNODEBUGRANGES`
environment variable can be used.

.. _frame-objects:

Frame objects
Expand Down
77 changes: 77 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,83 @@ Summary -- Release highlights
New Features
============

.. _whatsnew311-pep657:

Enhanced error locations in tracebacks
--------------------------------------

When printing tracebacks, the interpreter will now point to the exact expression
that caused the error instead of just the line. For example:

.. code-block:: python

Traceback (most recent call last):
File "distance.py", line 11, in <module>
print(manhattan_distance(p1, p2))
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "distance.py", line 6, in manhattan_distance
return abs(point_1.x - point_2.x) + abs(point_1.y - point_2.y)
^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'x'

Previous versions of the interpreter would point to just the line making it
ambiguous which object was ``None``. These enhanced errors can also be helpful
when dealing with deeply nested dictionary objects and multiple function calls,

.. code-block:: python

Traceback (most recent call last):
File "query.py", line 37, in <module>
magic_arithmetic('foo')
^^^^^^^^^^^^^^^^^^^^^^^
File "query.py", line 18, in magic_arithmetic
return add_counts(x) / 25
^^^^^^^^^^^^^
File "query.py", line 24, in add_counts
return 25 + query_user(user1) + query_user(user2)
^^^^^^^^^^^^^^^^^
File "query.py", line 32, in query_user
return 1 + query_count(db, response['a']['b']['c']['user'], retry=True)
~~~~~~~~~~~~~~~~~~^^^^^
TypeError: 'NoneType' object is not subscriptable

as well as complex arithmetic expressions:

.. code-block:: python

Traceback (most recent call last):
File "calculation.py", line 54, in <module>
result = (x / y / z) * (a / b / c)
~~~~~~^~~
ZeroDivisionError: division by zero

See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya
and Ammar Askar in :issue:`43950`.)

.. note::
This feature requires storing column positions in code objects which may
result in a small increase of disk usage of compiled Python files or
interpreter memory usage. To avoid storing the extra information and/or
deactivate printing the extra traceback information, the
:option:`-X` ``no_debug_ranges`` command line flag or the :envvar:`PYTHONNODEBUGRANGES`
environment variable can be used.

Column information for code objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The information used by the enhanced traceback feature is made available as a
general API that can be used to correlate bytecode instructions with source
code. This information can be retrieved using:

- The :meth:`codeobject.co_positions` method in Python.
- The :c:func:`PyCode_Addr2Location` function in the C-API.

The :option:`-X` ``no_debug_ranges`` option and the environment variable
:envvar:`PYTHONNODEBUGRANGES` can be used to disable this feature.

See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya
and Ammar Askar in :issue:`43950`.)



Other Language Changes
Expand Down
1 change: 1 addition & 0 deletions Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ def normpath(path):
initial_slashes = path.startswith(sep)
# POSIX allows one or two initial slashes, but treats three or more
# as single slash.
# (see http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13)
if (initial_slashes and
path.startswith(sep*2) and not path.startswith(sep*3)):
initial_slashes = 2
Expand Down
82 changes: 80 additions & 2 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
requires_debug_ranges, has_no_debug_ranges)
from test.support.os_helper import TESTFN, unlink
from test.support.script_helper import assert_python_ok, assert_python_failure
import textwrap

import os
import textwrap
import traceback
from functools import partial


test_code = namedtuple('code', ['co_filename', 'co_name'])
Expand Down Expand Up @@ -406,6 +408,82 @@ def f_with_multiline():
result_lines = self.get_exception(f_with_multiline)
self.assertEqual(result_lines, expected_f.splitlines())

def test_caret_for_binary_operators(self):
def f_with_binary_operator():
divisor = 20
return 10 + divisor / 0 + 30

lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = (
'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n'
' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
' return 10 + divisor / 0 + 30\n'
' ~~~~~~~~^~~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())

def test_caret_for_binary_operators_two_char(self):
def f_with_binary_operator():
divisor = 20
return 10 + divisor // 0 + 30

lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = (
'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n'
' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
' return 10 + divisor // 0 + 30\n'
' ~~~~~~~~^^~~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())

def test_caret_for_subscript(self):
def f_with_subscript():
some_dict = {'x': {'y': None}}
return some_dict['x']['y']['z']

lineno_f = f_with_subscript.__code__.co_firstlineno
expected_error = (
'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n'
' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n'
" return some_dict['x']['y']['z']\n"
' ~~~~~~~~~~~~~~~~~~~^^^^^\n'
)
result_lines = self.get_exception(f_with_subscript)
self.assertEqual(result_lines, expected_error.splitlines())

def test_traceback_specialization_with_syntax_error(self):
bytecode = compile("1 / 0 / 1 / 2\n", TESTFN, "exec")

with open(TESTFN, "w") as file:
# make the file's contents invalid
file.write("1 $ 0 / 1 / 2\n")
self.addCleanup(unlink, TESTFN)

func = partial(exec, bytecode)
result_lines = self.get_exception(func)

lineno_f = bytecode.co_firstlineno
expected_error = (
'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n'
' ^^^^^^^^^^\n'
f' File "{TESTFN}", line {lineno_f}, in <module>\n'
" 1 $ 0 / 1 / 2\n"
' ^^^^^\n'
)
self.assertEqual(result_lines, expected_error.splitlines())

@cpython_only
@requires_debug_ranges()
Expand Down Expand Up @@ -1615,7 +1693,7 @@ def f():
self.assertEqual(
output.getvalue().split('\n')[-5:],
[' x/0',
' ^^^',
' ~^~',
' x = 12',
'ZeroDivisionError: division by zero',
''])
Expand Down
60 changes: 59 additions & 1 deletion Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,9 +494,23 @@ def format(self):
colno = _byte_offset_to_character_offset(frame._original_line, frame.colno)
end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno)

try:
anchors = _extract_caret_anchors_from_line_segment(
frame._original_line[colno - 1:end_colno]
)
except Exception:
anchors = None

row.append(' ')
row.append(' ' * (colno - stripped_characters))
row.append('^' * (end_colno - colno))

if anchors:
row.append(anchors.primary_char * (anchors.left_end_offset))
row.append(anchors.secondary_char * (anchors.right_start_offset - anchors.left_end_offset))
row.append(anchors.primary_char * (end_colno - colno - anchors.right_start_offset))
else:
row.append('^' * (end_colno - colno))

row.append('\n')

if frame.locals:
Expand All @@ -520,6 +534,50 @@ def _byte_offset_to_character_offset(str, offset):
return len(as_utf8[:offset + 1].decode("utf-8"))


_Anchors = collections.namedtuple(
"_Anchors",
[
"left_end_offset",
"right_start_offset",
"primary_char",
"secondary_char",
],
defaults=["~", "^"]
)

def _extract_caret_anchors_from_line_segment(segment):
import ast

try:
tree = ast.parse(segment)
except SyntaxError:
return None

if len(tree.body) != 1:
return None

statement = tree.body[0]
match statement:
case ast.Expr(expr):
match expr:
case ast.BinOp():
operator_str = segment[expr.left.end_col_offset:expr.right.col_offset]
operator_offset = len(operator_str) - len(operator_str.lstrip())

left_anchor = expr.left.end_col_offset + operator_offset
right_anchor = left_anchor + 1
if (
operator_offset + 1 < len(operator_str)
and not operator_str[operator_offset + 1].isspace()
):
right_anchor += 1
return _Anchors(left_anchor, right_anchor)
case ast.Subscript():
return _Anchors(expr.value.end_col_offset, expr.slice.end_col_offset + 1)

return None


class TracebackException:
"""An exception ready for rendering.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Code objects can now provide the column information for instructions when
available. This is levaraged during traceback printing to show the
expressions responsible for errors.

Contributed by Pablo Galindo, Batuhan Taskaya and Ammar Askar as part of
:pep:`657`.
Loading