Metadata-Version: 2.4
Name: dodona-json-tracer
Version: 1.1.0
Summary: Generate json traces from a python script
Author-email: Dodona <dodona@dodona.be>
License: MIT License
        
        Copyright (c) 2023 Team Dodona
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
Project-URL: Homepage, https://github.com/dodona-edu/json-tracer
Keywords: debug,trace,json
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Dynamic: license-file

# json-tracer

A Python library that traces the execution of a Python program and writes
that trace as JSON. Each step of the trace holds the state of every variable
in scope at that point, which is what a program visualiser needs to show the
program running.

Published on PyPI as [`dodona-json-tracer`](https://pypi.org/project/dodona-json-tracer/)
(the import name stays `tracer`). Versions up to 0.7.0 were published as
`json-tracer`; that name is no longer maintained.

## Install

```bash
pip install dodona-json-tracer
```

## Usage

```python
from tracer import JSONTracer
tracer = JSONTracer()
result = tracer.runscript('''
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)
print(factorial(5))
''')

print(result)  # JSON string representing the execution trace
```

From the command line:

```bash
python -m tracer examples/hello_world.py
```

From a source checkout, without installing:

```bash
PYTHONPATH=src python3 -m tracer examples/hello_world.py
```

### Options

- `heap_primitives`: If True, primitive values (like integers and strings) are stored in the heap. Default is False.
- `frame_callback`: A callback function that is called whenever a new frame is created, it receives the JSON-encoded frame
   as a string. Default is None. This is useful to already handle partial trace output while the program is running.
- `module_name`: The name of the module being traced. Default is `'__main__'`. Code outside this module will not be traced.
- `frame_format`: `'full'` (default) or `'delta'`. In `'delta'` mode, every frame after the first omits its `heap`/`globals`
   and instead carries `heap_set`/`heap_del`/`globals_set`/`globals_del` describing the change since the previous frame, plus
   `"delta": true`. `uncaught_exception` frames are the exception: they carry no `heap`/`globals` to begin with and pass
   through unchanged, without delta fields. Consumers reconstruct the full frame by applying that diff to the previous
   reconstructed frame. `runscript`'s return value and `self.trace` are then in this same delta format.
- `max_steps`: Maximum number of frames to emit. Default is `None` (unlimited). Must be a positive int otherwise. The frame that
   reaches the budget is itself emitted, so `max_steps=N` yields exactly `N` frames. Reaching the budget *aborts the traced program*
   at that point (the same unwind `force_terminate()` uses); `runscript` then returns the frames collected so far, normally.

### Pre-importing modules

If you *don't* pre-import a module, then when it's imported in the user's code, it may take *forever*
to execute, because the tracer will try to trace all the code in that module.

```python
from tracer import preload_imports
preload_imports('''
import math
import random

from collections import defaultdict

b = defaultdict(int)

def foo(x):
    return math.sqrt(x) + random.random() + b[x]
''')
```

## How it works

The tracer runs the program under [`bdb`](https://docs.python.org/3/library/bdb.html),
the debugger framework of the standard library. `bdb` reports a call, line,
return or exception event for every step the interpreter takes. Events that
belong to imported libraries are dropped, and each event that remains becomes
one step of the trace.

Four modules share the work:

- `json_tracer.py` holds the `bdb` subclass. It decides which events become a
  step, and builds the step itself.
- `frames.py` reads the variables of a frame, and leaves out the names the
  interpreter adds itself.
- `closures.py` records which frame created which function, so that a closure
  can point at the frame around it. A frame that has returned keeps rendering
  as a *zombie frame* for as long as one of the functions it created is still
  alive.
- `json_encoder.py` turns each value into a form that JSON can hold.

### What a step looks like

Every step is a JSON object. This one comes from `examples/list_of_lists.py`,
at the first line of its loop:

```json
{
  "line": 5,
  "event": "step_line",
  "func_name": "<module>",
  "globals": {"x": 10, "y": ["REF", 1], "_": 0},
  "ordered_globals": ["x", "y", "_"],
  "stack_to_render": [],
  "heap": {"1": ["LIST", 1, 10, 2]}
}
```

- `line`, `event` and `func_name` say where the program is.
- `globals` holds the global variables, and `ordered_globals` their display
  order, which follows first appearance. A name that is deleted and then set
  again keeps its own place.
- `stack_to_render` holds one entry per frame on the stack, each with its own
  locals, its frame id and the id of its parent frame. Zombie frames appear
  here as well, marked with `is_zombie`.
- `heap` holds every value that is not a primitive.

### The heap

A primitive value (`None`, `int`, `float`, `str`, `bool`) is written where it
appears. Every other value goes on the heap under a small id, and the place
where it appears holds a reference to it, `["REF", 1]`. Two names for one
object therefore hold the same reference, which is what lets a visualiser
draw aliasing.

A small id stays with its object for as long as the object lives, so a
consumer can follow one value across steps. The heap of a step holds only the
values that step can reach; a value that the program drops is simply absent
from the next step.

Values that come from an imported module are never walked. They render as
`["IMPORTED_FAUX_PRIMITIVE", type, label]`, which keeps library internals out
of the trace. The header of `src/tracer/json_encoder.py` lists every form the
encoder produces.

## Development

Run the tests with:

```bash
python -m unittest discover -s tests
```

### Release

```bash
./release.sh
```

The script asks for the new version, updates `pyproject.toml` and
`src/tracer/__init__.py`, commits, and pushes a `v<version>` tag. CI
([`publish.yml`](.github/workflows/publish.yml)) then runs the tests, builds
the package, publishes it to PyPI, and creates a GitHub release with generated
notes. Use a PEP 440 pre-release version (e.g. `0.8.0rc1`) for a pre-release.

Publishing uses [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/),
so no PyPI tokens are needed. One-time setup, done on the Dodona PyPI account
under *Account settings → Publishing → Add a new pending publisher*: project
name `dodona-json-tracer`, owner `dodona-edu`, repository `json-tracer`,
workflow `publish.yml`, environment `pypi`. The first tagged publish then
creates the PyPI project automatically.

## Origin and license

This project started from the backend of [pythontutor](https://pythontutor.com/),
written by Philip Guo; all credit for that original code goes to him. Thanks
also to John DeNero, who made the encoder work on both Python 2 and 3. The
code here has since been refactored and rewritten extensively, but it is
still distributed under the same MIT terms, and the original notice applies
to it:

> Online Python Tutor
> https://github.com/pgbovine/OnlinePythonTutor/
>
> Copyright (C) Philip J. Guo (philip@pgbovine.net)
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of
> this software and associated documentation files (the "Software"), to deal in
> the Software without restriction, including without limitation the rights to
> use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
> the Software, and to permit persons to whom the Software is furnished to do so,
> subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
> FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
> COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
> IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
> CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Subsequent work is Copyright (c) 2023-2026 Team Dodona, see [LICENSE](LICENSE).
