https://github.com/konnov/itf-py
Python library to parse and emit Apalache/Quint traces as JSON ITF
https://github.com/konnov/itf-py
apalache json python quint tlaplus trace
Last synced: 2 months ago
JSON representation
Python library to parse and emit Apalache/Quint traces as JSON ITF
- Host: GitHub
- URL: https://github.com/konnov/itf-py
- Owner: konnov
- License: mit
- Created: 2025-07-24T15:50:15.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2025-11-07T16:30:18.000Z (2 months ago)
- Last Synced: 2025-11-07T17:12:29.004Z (2 months ago)
- Topics: apalache, json, python, quint, tlaplus, trace
- Language: Python
- Homepage:
- Size: 65.4 KB
- Stars: 3
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# ITF-py: Parser and Encoder for the ITF Trace Format
[](https://github.com/konnov/itf-py/actions/workflows/ci.yml)
[](https://codecov.io/gh/konnov/itf-py)
[](https://www.python.org/downloads/)
[](https://github.com/psf/black)
Python library to parse and emit Apalache ITF traces. Refer to [ADR015][] for
the format. ITF traces are emitted by [Apalache][] and [Quint][].
**Intentionally minimalistic.** We keep this library intentionally minimalistic.
You can use it in your projects without worrying about pulling dozens of
dependencies. The package depends on `frozendict`.
**Why?** It's much more convenient to manipulate with trace data in an
interactive prompt, similar to SQL.
**Alternatives.** If you need to (de-)serialize ITF traces in Rust, check
[itf-rs][].
## Installation
Simply use: `pip install itf-py`.
## Usage
### Deserializing and serializing traces
Assume that you have the following JSON trace stored in the variable
`trace_json`, e.g., as produced by [Apalache][] or [Quint][]:
```python
trace_json = {
"#meta": {"id": 23},
"params": ["N"],
"vars": ["pc", "x"],
"loop": 0,
"states": [
{
"#meta": {"no": 0},
"N": {"#bigint": "3"},
"pc": "idle",
"x": {"#bigint": "42"},
},
{
"#meta": {"no": 1},
"pc": "lock",
"x": {"#bigint": "43"},
},
],
}
```
We simply import the required function and parse the input:
```python
from itf_py import State, Trace, trace_from_json
output = trace_from_json(trace_json)
trace = Trace(
meta={"id": 23},
params=["N"],
vars=["pc", "x"],
loop=0,
states=[
State(meta={"no": 0}, values={"N": 3, "pc": "idle", "x": 42}),
State(meta={"no": 1}, values={"pc": "lock", "x": 43}),
],
)
assert output == trace, f"{output} != {trace}"
```
We serialize `trace` back to its JSON form:
```python
from itf_py import trace_to_json
output = trace_to_json(trace)
assert output == trace_json, f"{output} != {trace_json}"
```
### De-(serializing) states
Sometimes, you do not want to deal with whole traces, but only with a single
state. In this case, you can (de-)serialize states via
`state_from_json` and `state_to_json`:
```python
from itf_py import State, state_from_json, state_to_json
state_json = {
"#meta": {"no": 1},
"pc": "lock",
"x": {"#bigint": "43"},
}
output = state_from_json(state_json)
state = State(meta={"no": 1}, values={"pc": "lock", "x": 43})
assert output == state, f"{output} != {state}"
output = state_to_json(state)
assert output == state_json, f"{output} != {state_json}"
```
### (De-)serializing values
Finally, you can work at the level of individual values. The following examples
demonstrate how values of different types are (de-)serialized:
```python
from itf_py import value_from_json, value_to_json
# primitive values are easy, except integers are wrapped
assert value_to_json("hello") == "hello"
assert value_from_json("hello") == "hello"
assert value_from_json(True) == True
assert value_to_json(True) == True
assert value_to_json(3) == {"#bigint": "3"}
assert value_from_json({"#bigint": "3"}) == 3
# lists are serialized as JSON arrays
assert value_to_json(["a", "b", "c"]) == ["a", "b", "c"]
# ...and deserialized as immutable lists
assert value_from_json(["a", "b", "c"]) == ["a", "b", "c"]
# tuples are wrapped JSON arrays
assert value_to_json(("a", "b", "c")) == {"#tup": ["a", "b", "c"]}
assert value_from_json({"#tup": ["a", "b", "c"]}) == ("a", "b", "c")
# Sets are serialized as wrapped JSON arrays.
# Be careful, the order in the JSON array may differ!
j = value_to_json(frozenset(["a", "b", "c"]))
assert "#set" in j and len(j["#set"]) == 3
assert "a" in j["#set"] and "b" in j["#set"] and "c" in j["#set"]
# ...and deserialized as frozen sets
assert value_from_json({"#set": ["a", "b", "c"]}) == frozenset(["a", "b", "c"])
# data classes are serialized as JSON objects
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
name: str
age: int
active: bool
output = value_to_json(User(name="Alice", age=30, active=True))
assert output["name"] == "Alice"
assert output["age"] == {"#bigint": "30"}
assert output["active"] == True
# named tuples are serialized as JSON objects too
from collections import namedtuple
User = namedtuple("User", ["name", "age", "active"])
user = User(name="Bob", age=33, active=False)
output = value_to_json(user)
assert output["name"] == "Bob"
assert output["age"] == {"#bigint": "33"}
assert output["active"] == False
# ...and deserialized as immutable named tuples
output = value_from_json(output)
assert output.name == user.name
assert output.age == user.age
assert output.active == user.active
# Dictionaries are serialized as wrapped JSON arrays of key-value pairs.
# Be careful, the order in the JSON array may differ!
j = value_to_json({"key1": "val1", "key2": "val2"})
assert "#map" in j and len(j["#map"]) == 2
assert ["key1", "val1"] in j["#map"]
assert ["key2", "val2"] in j["#map"]
# ...and deserialized back as frozen dictionaries
from frozendict import frozendict
output = value_from_json({"#map": [["key1", "val1"], ["key2", "val2"]]})
assert output == frozendict({"key1": "val1", "key2": "val2"})
# Apalache tagged unions are deserialized as special named tuples.
# They have a special attribute '_itf_variant' to distinguish them from
# regular named tuples.
output = value_from_json({"tag": "Banana", "value": {"length": 5, "color": "yellow"}})
assert output.__class__.__name__ == "Banana"
assert hasattr(output.__class__, '_itf_variant') is True
assert output.length == 5
assert output.color == "yellow"
# ...but take care when the value is not a record!
# In this case, it simply has the single field 'value'.
output = value_from_json({"tag": "Init", "value": "u_OF_UNIT"})
assert output.__class__.__name__ == "Init"
assert hasattr(output.__class__, '_itf_variant') is True
assert output.value == "u_OF_UNIT"
# To construct such variants, use itf_variant decorator.
from itf_py import itf_variant
@itf_variant
@dataclass(frozen=True)
class Apple:
length: int
color: str
output = value_to_json(Apple(length=10, color="green"))
assert output["tag"] == "Apple"
#assert output["value"] == {"length": 10, "color": "green"}
# finally, unserializable values have special representation
from itf_py.itf import ITFUnserializable
output = value_from_json({"#unserializable": "custom-repr"})
assert output == ITFUnserializable("custom-repr")
```
### Pretty-printing
The deserialized values support nice pretty-printing:
```python
from pprint import pp, pformat
pp(value_from_json({"#set": ["a", "b", "c"]}))
# prints frozenset({'a', 'b', 'c'})
# ...or frozenset in another order
s = pformat(value_from_json({"#map": [["a", "b"], ["c", "d"]]}))
assert s == "{'a': 'b', 'c': 'd'}"
s = pformat(value_from_json(["a", "b", "c", "d"]))
assert s == "['a', 'b', 'c', 'd']"
s = pformat(value_from_json({"name": "Alice", "age": 30, "active": True}))
assert s == "Rec(name='Alice', age=30, active=True)", f"unexpected: {s}"
```
Tagged unions have beatified output:
```python
j = {"tag": "Banana", "value": {"length": 5, "color": "yellow"}}
s = pformat(value_from_json(j))
assert s == "Banana(length=5, color='yellow')", f"unexpected: {s}"
```
### Colorized pretty-printing
This module offers no special support for colorized output. However,
it works out-of-the-box with [rich][]. If you are already using
[IPython][], it's really easy:
```python
from rich import pretty
from itf_py import value_from_json
pretty.install()
value_from_json({"name": "Alice", "age": 30, "active": True})
# Rec(name='Alice', age=30, active=True)
# in nice colors!
```
[ADR015]: https://apalache-mc.org/docs/adr/015adr-trace.html
[Apalache]: https://github.com/apalache-mc/apalache
[Quint]: https://github.com/informalsystems/quint
[itf-rs]: https://github.com/informalsystems/itf-rs
[rich]: https://pypi.org/project/rich/
[IPython]: https://ipython.org/