GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing peps.python.org — PEP 657 – Include Fine Grained Error Locations in Tracebacks | peps.python.org
PEP 657 – Include Fine Grained Error Locations in Tracebacks
PEP 657 – Include Fine Grained Error Locations in Tracebacks
AbstractThis PEP proposes adding a mapping from each bytecode instruction to the start and end column offsets of the line that generated them as well as the end line number. This data will be used to improve tracebacks displayed by the CPython interpreter in order to improve the debugging experience. The PEP also proposes adding APIs that allow other tools (such as coverage analysis tools, profilers, tracers, debuggers) to consume this information from code objects. MotivationThe primary motivation for this PEP is to improve the feedback presented about the location of errors to aid with debugging. Python currently keeps a mapping of bytecode to line numbers from compilation. The interpreter uses this mapping to point to the source line associated with an error. While this line-level granularity for instructions is useful, a single line of Python code can compile into dozens of bytecode operations making it hard to track which part of the line caused the error. Consider the following line of Python code: x['a']['b']['c']['d'] = 1 If any of the values in the dictionaries are Traceback (most recent call last): File "test.py", line 2, in <module> x['a']['b']['c']['d'] = 1 TypeError: 'NoneType' object is not subscriptable From the traceback, it is impossible to determine which one of the dictionaries
had the However, if the interpreter had a mapping of bytecode to column offsets as well as line numbers, it could helpfully display: Traceback (most recent call last): File "test.py", line 2, in <module> x['a']['b']['c']['d'] = 1 ~~~~~~~~~~~^^^^^ TypeError: 'NoneType' object is not subscriptable indicating to the user that the object Traceback (most recent call last): File "test.py", line 14, in <module> lel3(x) ^^^^^^^ File "test.py", line 12, in lel3 return lel2(x) / 23 ^^^^^^^ File "test.py", line 9, in lel2 return 25 + lel(x) + lel(x) ^^^^^^ File "test.py", line 6, in lel return 1 + foo(a,b,c=x['z']['x']['y']['z']['y'], d=e) ~~~~~~~~~~~~~~~~^^^^^ TypeError: 'NoneType' object is not subscriptable This problem presents itself in the following situations.
Debugging aside, this extra information would also be useful for code coverage tools, enabling them to measure expression-level coverage instead of just line-level coverage. For instance, given the following line: x = foo() if bar() else baz() coverage, profile or state analysis tools will highlight the full line in both branches, making it impossible to differentiate what branch was taken. This is a known problem in pycoverage. Similar efforts to this PEP have taken place in other languages such as Java in
the form of JEP358. RationaleIn order to identify the range of source code being executed when exceptions
are raised, this proposal requires adding new data for every bytecode
instruction. This will have an impact on the size of As an illustrative example to gauge the impact of this change, we have calculated that including the start and end offsets will increase the size of the standard library’s pyc files by 22% (6MB) from 28.4MB to 34.7MB. The overhead in memory usage will be the same (assuming the full standard library is loaded into the same program). We believe that this is a very acceptable number since the order of magnitude of the overhead is very small, especially considering the storage size and memory capabilities of modern computers. Additionally, in general the memory size of a Python program is not dominated by code objects. To check this assumption we have executed the test suite of several popular PyPI projects (including NumPy, pytest, Django and Cython) as well as several applications (Black, pylint, mypy executed over either mypy or the standard library) and we found that code objects represent normally 3-6% of the average memory size of the program. We understand that the extra cost of this information may not be acceptable for some users, so we propose an opt-out mechanism which will cause generated code objects to not have the extra information while also allowing pyc files to not include the extra information. SpecificationIn order to have enough information to correctly resolve the location within a given line where an error was raised, a map linking bytecode instructions to column offsets (start and end offset) and end line numbers is needed. This is similar in fashion to how line numbers are currently linked to bytecode instructions. The following changes will be performed as part of the implementation of this PEP:
The internal storage, compression and encoding of the information is left as an implementation detail and can be changed at any point as long as the public API remains unchanged. Offset semanticsThese offsets are propagated by the compiler from the ones stored currently in
all AST nodes. The output of the public APIs ( The AST nodes use As specified previously, the underlying storage of the offsets should be
considered an implementation detail, as the public APIs to obtain this values
will return either C Displaying tracebacksWhen displaying tracebacks, the default exception hook will be modified to query this information from the code objects and use it to display a sequence of carets for every displayed line in the traceback if the information is available. For instance: File "test.py", line 6, in lel return 1 + foo(a,b,c=x['z']['x']['y']['z']['y'], d=e) ~~~~~~~~~~~~~~~~^^^^^ TypeError: 'NoneType' object is not subscriptable When displaying tracebacks, instruction offsets will be taken from the traceback objects. This makes highlighting exceptions that are re-raised work naturally without the need to store the new information in the stack. For example, for this code: def foo(x): 1 + 1/0 + 2 def bar(x): try: 1 + foo(x) + foo(x) except Exception as e: raise ValueError("oh no!") from e bar(bar(bar(2))) The printed traceback would look like this: Traceback (most recent call last): File "test.py", line 6, in bar 1 + foo(x) + foo(x) ^^^^^^ File "test.py", line 2, in foo 1 + 1/0 + 2 ~^~ ZeroDivisionError: division by zero The above exception was the direct cause of the following exception: Traceback (most recent call last): File "test.py", line 10, in <module> bar(bar(bar(2))) ^^^^^^ File "test.py", line 8, in bar raise ValueError("oh no!") from e ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ValueError: oh no While this code: def foo(x): 1 + 1/0 + 2 def bar(x): try: 1 + foo(x) + foo(x) except Exception: raise bar(bar(bar(2))) Will be displayed as: Traceback (most recent call last): File "test.py", line 10, in <module> bar(bar(bar(2))) ^^^^^^ File "test.py", line 6, in bar 1 + foo(x) + foo(x) ^^^^^^ File "test.py", line 2, in foo 1 + 1/0 + 2 ~^~ ZeroDivisionError: division by zero Maintaining the current behavior, only a single line will be displayed in tracebacks. For instructions that span multiple lines (the end offset and the start offset belong to different lines), the end line number must be inspected to know if the end offset applies to the same line as the starting offset. Opt-out mechanismTo offer an opt-out mechanism for those users that care about the storage and memory overhead and to allow third party tools and other programs that are currently parsing tracebacks to catch up the following methods will be provided to deactivate this feature:
If any of these methods are used, the Python compiler will not populate
code objects with the new information (
Doing this has a very small performance hit as the interpreter state needs to be fetched when code objects are created to look up the configuration. Creating code objects is not a performance sensitive operation so this should not be a concern. Backwards CompatibilityThe change is fully backwards compatible. Reference ImplementationA reference implementation can be found in the implementation fork. Rejected IdeasUse a single caret instead of a rangeIt has been proposed to use a single caret instead of highlighting the full range when reporting errors as a way to simplify the feature. We have decided to not go this route for the following reasons:
Have a configure flag to opt outHaving a configure flag to opt out of the overhead even when executing Python in non-optimized mode may sound desirable, but it may cause problems when reading pyc files that were created with a version of the interpreter that was not compiled with the flag activated. This can lead to crashes that would be very difficult to debug for regular users and will make different pyc files incompatible between each other. As this pyc could be shipped as part of libraries or applications without the original source, it is also not always possible to force recompilation of said pyc files. For these reasons we have decided to use the -O flag to opt-out of this behaviour. Lazy loading of column informationOne potential solution to reduce the memory usage of this feature is to not load the column information from the pyc file when code is imported. Only if an uncaught exception bubbles up or if a call to the C-API functions is made will the column information be loaded from the pyc file. This is similar to how we only read source lines to display them in the traceback when an exception bubbles up. While this would indeed lower memory usage, it also results in a far more complex implementation requiring changes to the importing machinery to selectively ignore a part of the code object. We consider this an interesting avenue to explore but ultimately we think is out of the scope for this particular PEP. It also means that column information will not be available if the user is not using pyc files or for code objects created dynamically at runtime. Implement compressionAlthough it would be possible to implement some form of compression over the pyc files and the new data in code objects, we believe that this is out of the scope of this proposal due to its larger impact (in the case of pyc files) and the fact that we expect column offsets to not compress well due to the lack of patterns in them (in case of the new data in code objects). AcknowledgmentsThanks to Carl Friedrich Bolz-Tereick for showing an initial prototype of this idea for the Pypy interpreter and for the helpful discussion. CopyrightThis document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive. |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |