diff --git a/.agents/codex-setup b/.agents/codex-setup index 6c11900..81b23ae 100755 --- a/.agents/codex-setup +++ b/.agents/codex-setup @@ -1,7 +1,15 @@ #!/usr/bin/env bash +set -euo pipefail + +# Install the tooling required to build and test the CodeTracer project. +# The list of packages is derived from `Justfile` and `flake.nix`. AGENTS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -cd $AGENTS_DIR +cd "$AGENTS_DIR" apt-get update -apt-get install -y --no-install-recommends just +DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv python3-dev \ + cargo rustc just pkg-config capnproto + +pip3 install --no-cache-dir --break-system-packages maturin uv diff --git a/.agents/tasks/2025/08/14-1027-initial-python-api b/.agents/tasks/2025/08/14-1027-initial-python-api new file mode 100644 index 0000000..64c74b9 --- /dev/null +++ b/.agents/tasks/2025/08/14-1027-initial-python-api @@ -0,0 +1,3 @@ +Implement the Python API described in the design document for the Rust-based module. Write tests. Don't actually implement tracing using `runtime_tracing` yet, just add placeholders. +--- FOLLOW UP TASK --- +Implement the Python API described in the design document for the Rust-based module. Write tests. Don't actually implement tracing using runtime_tracing yet, just add placeholders. \ No newline at end of file diff --git a/.agents/tasks/2025/08/15-1323-initial-test-suite b/.agents/tasks/2025/08/15-1323-initial-test-suite new file mode 100644 index 0000000..18d1273 --- /dev/null +++ b/.agents/tasks/2025/08/15-1323-initial-test-suite @@ -0,0 +1,25 @@ +Create an initial test suite for codetracer-python-recorder. + +Following TDD we want to start by writing tests. + +The test suite should follow the specification at design-docs/test-design-001.md +The tests should be written in the `/tests` subdirectory and it should be possible to run them using our current approach of `just venv dev test`. + +If the module turns out to be missing functions or types required for the test, add them but with empty bodies. (for Python functions raise `NotImplemented` exception in the body of the function) +If a test can be written both in Python and in Rust prefer writing it in Python. +If a code modification needs to be done in the module and it is possible to do it both in Python and in Rust, prefer writing it in Python. + +If there is any item in the test suite which doesn't contain enough +--- FOLLOW UP TASK --- +Create a script Hit:2 https://mise.jdx.dev/deb stable InRelease +Hit:1 https://apt.llvm.org/noble llvm-toolchain-noble-20 InRelease +Hit:3 http://archive.ubuntu.com/ubuntu noble InRelease +Hit:4 http://security.ubuntu.com/ubuntu noble-security InRelease +Hit:5 http://archive.ubuntu.com/ubuntu noble-updates InRelease +Hit:6 http://archive.ubuntu.com/ubuntu noble-backports InRelease +Reading package lists... +Reading package lists... +Building dependency tree... +Reading state information... +just is already the newest version (1.21.0-1). +0 upgraded, 0 newly installed, 0 to remove and 25 not upgraded. which will be used in future to set-up the Codex environment for development and testing of this project. Base this on what you see in Justfile and flake.nix. Note that not all packages described in flake.nix will be needed in the Codex environment, but we do care about building the project and running the tests. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 170dbbd..8ca839c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,11 @@ on: jobs: nix-tests: + name: Testing on Python ${{matrix.python-version}} runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10","3.11","3.12","3.13"] steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v27 @@ -17,30 +21,31 @@ jobs: nix_path: nixpkgs=channel:nixos-25.05 extra_nix_config: | experimental-features = nix-command flakes - - name: Run tests via Nix - run: nix develop --command just test + - name: Build Rust module and run tests via Nix + run: nix develop --command bash -lc 'just venv ${{matrix.python-version}} dev test' - rust-tests: - name: Rust module test on ${{ matrix.os }} (Python ${{ matrix.python-version }}) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["10", "11", "12", "13"] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.${{ matrix.python-version }} - - uses: astral-sh/setup-uv@v4 - - uses: messense/maturin-action@v1 - with: - command: build - args: --interpreter python3.${{ matrix.python-version }} -m crates/codetracer-python-recorder/Cargo.toml --release - - name: Install and test built wheel with uv (pytest) - shell: bash - run: | - v=${{matrix.python-version}} - file=(crates/codetracer-python-recorder/target/wheels/*.whl) - file="${file[0]}" - uv run -p python3.$v --with "${file}" --with pytest -- python -m pytest crates/codetracer-python-recorder/test -q + # rust-tests: + # name: Rust module test on ${{ matrix.os }} (Python ${{ matrix.python-version }}) + # runs-on: ${{ matrix.os }} + # strategy: + # matrix: + # os: [ubuntu-latest, macos-latest, windows-latest] + # python-version: ["10", "11", "12", "13"] + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v5 + # with: + # python-version: 3.${{ matrix.python-version }} + # - uses: astral-sh/setup-uv@v4 + # - uses: messense/maturin-action@v1 + # with: + # command: build + # args: --interpreter python3.${{ matrix.python-version }} -m crates/codetracer-python-recorder/Cargo.toml --release + # - name: Install and test built wheel with uv (pytest) + # shell: bash + # run: | + # v=${{matrix.python-version}} + # file=(crates/codetracer-python-recorder/target/wheels/*.whl) + # file="${file[0]}" + # uv run -p python3.$v --with "${file}" --with pytest -- \ + # python -m pytest crates/codetracer-python-recorder/test tests/test_codetracer_api.py -q diff --git a/.gitignore b/.gitignore index 48ffc7e..de5bba0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .aider* .venv/ **/target/ -build \ No newline at end of file +build +*~ diff --git a/AGENTS.md b/AGENTS.md index feee796..c56a4c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,20 +5,16 @@ This repository contains two related projects: - codetracer-pure-python-recorder — the original pure-Python tracer. - codetracer-python-recorder — a Rust-backed Python module built with PyO3 and maturin. -To run the Python test suite for the pure-Python tracer, execute: +To build the modules in development mode run: -``` -just test -``` +```sh +just venv 3.13 dev #You can use any other Python version >=3.12 +`` -The tester executes a number of sample programs in `tests/programs` and compares their outputs to the fixtures in `tests/fixtures`. +Then to run the tests do -To build and locally develop-install the Rust-backed module: - -``` -just build-rust -# or: -maturin develop -m crates/codetracer-python-recorder/Cargo.toml +```sh +just test ``` # Code quality guidelines diff --git a/Justfile b/Justfile index 464ae9a..ce4a73b 100644 --- a/Justfile +++ b/Justfile @@ -1,48 +1,58 @@ +default: + @just --list + # Development helpers for the monorepo +# Python version used for development +PYTHON_DEFAULT_VERSION := "3.13" + # Python versions used for multi-version testing/building with uv PY_VERSIONS := "3.10 3.11 3.12 3.13" PY_SHORT_VERSIONS := "10 11 12 13" + # Print toolchain versions to verify the dev environment env: + uv --version python3 --version cargo --version rustc --version maturin --version -# Create a local virtualenv for Python tooling -venv: - test -d .venv || python3 -m venv .venv +clean: + rm -rf .venv **/__pycache__ **/*.pyc **/*.pyo **/.pytest_cache + rm -rf codetracer-python-recorder/target codetracer-python-recorder/**/*.so + -# Build and develop-install the Rust-backed Python module -build-rust: - test -d .venv || python3 -m venv .venv - VIRTUAL_ENV=.venv maturin develop -m crates/codetracer-python-recorder/Cargo.toml +# Create a clean local virtualenv for Python tooling (without editable packages installed) +venv version=PYTHON_DEFAULT_VERSION: + uv sync -p {{version}} -# Smoke test the Rust module after build -smoke-rust: - .venv/bin/python -m pip install -U pip pytest - .venv/bin/python -m pytest crates/codetracer-python-recorder/test -q +# Build the module in dev mode +dev: + uv run --directory codetracer-python-recorder maturin develop --uv -# Run the Python test suite for the pure-Python recorder +# Run unit tests of dev build test: - python3 -m unittest discover -v + uv run --group dev --group test pytest -# Run the test suite across multiple Python versions using uv -test-uv-all: - uv python install {{PY_VERSIONS}} - for v in {{PY_VERSIONS}}; do uv run -p "$v" -m unittest discover -v; done +# Run tests only on the pure recorder +test-pure: + uv run --group dev --group test pytest codetracer-pure-python-recorder + +# Build the module in release mode +build: + just venv \ + uv run --directory codetracer-python-recorder maturin build --release # Build wheels for all target Python versions with maturin -build-rust-uv-all: - for v in {{PY_VERSIONS}}; do \ - maturin build --interpreter "python$v" -m crates/codetracer-python-recorder/Cargo.toml --release; \ - done +build-all: + just venv + uv run --directory codetracer-python-recorder maturin build --release --interpreter {{PY_VERSIONS}} # Smoke the built Rust wheels across versions using uv -smoke-rust-uv-all: +test-all: for v in {{PY_SHORT_VERSIONS}}; do \ - file=(crates/codetracer-python-recorder/target/wheels/codetracer_python_recorder-*-cp3$v-cp3$v-*.whl); \ + file=(codetracer-python-recorder/target/wheels/codetracer_python_recorder-*-cp3$v-cp3$v-*.whl); \ file="${file[0]}"; \ - uv run -p "python3.$v" --with "${file}" --with pytest -- python -m pytest crates/codetracer-python-recorder/test -q; \ + uv run -p "python3.$v" --with "${file}" --with pytest -- pytest -q; \ done diff --git a/codetracer-pure-python-recorder/pyproject.toml b/codetracer-pure-python-recorder/pyproject.toml new file mode 100644 index 0000000..558ea65 --- /dev/null +++ b/codetracer-pure-python-recorder/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "codetracer-pure-python-recorder" +version = "0.1.0" +description = "Pure-Python prototype recorder producing CodeTracer traces" +authors = [{name = "Metacraft Labs Ltd"}] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", +] + +[tool.setuptools] +py-modules = ["trace"] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[project.scripts] +codetracer-record = "codetracer_pure_python_recorder.cli:main" +codetracer-record-pure = "codetracer_pure_python_recorder.cli:main" diff --git a/src/codetracer_pure_python_recorder/__init__.py b/codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/__init__.py similarity index 100% rename from src/codetracer_pure_python_recorder/__init__.py rename to codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/__init__.py diff --git a/src/codetracer_pure_python_recorder/cli.py b/codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/cli.py similarity index 100% rename from src/codetracer_pure_python_recorder/cli.py rename to codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/cli.py diff --git a/src/trace.py b/codetracer-pure-python-recorder/src/trace.py similarity index 100% rename from src/trace.py rename to codetracer-pure-python-recorder/src/trace.py diff --git a/tests/__init__.py b/codetracer-pure-python-recorder/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to codetracer-pure-python-recorder/tests/__init__.py diff --git a/tests/fixtures/array_sum.json b/codetracer-pure-python-recorder/tests/fixtures/array_sum.json similarity index 100% rename from tests/fixtures/array_sum.json rename to codetracer-pure-python-recorder/tests/fixtures/array_sum.json diff --git a/tests/fixtures/calc.json b/codetracer-pure-python-recorder/tests/fixtures/calc.json similarity index 100% rename from tests/fixtures/calc.json rename to codetracer-pure-python-recorder/tests/fixtures/calc.json diff --git a/tests/programs/array_sum.py b/codetracer-pure-python-recorder/tests/programs/array_sum.py similarity index 100% rename from tests/programs/array_sum.py rename to codetracer-pure-python-recorder/tests/programs/array_sum.py diff --git a/tests/programs/calc.py b/codetracer-pure-python-recorder/tests/programs/calc.py similarity index 100% rename from tests/programs/calc.py rename to codetracer-pure-python-recorder/tests/programs/calc.py diff --git a/tests/test_trace.py b/codetracer-pure-python-recorder/tests/test_trace.py similarity index 100% rename from tests/test_trace.py rename to codetracer-pure-python-recorder/tests/test_trace.py diff --git a/codetracer-python-recorder/Cargo.lock b/codetracer-python-recorder/Cargo.lock new file mode 100644 index 0000000..da12a63 --- /dev/null +++ b/codetracer-python-recorder/Cargo.lock @@ -0,0 +1,428 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "capnp" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def25bdbbc2758b363d79129c7f277520e3347e8b647c404d4823591f837c4ad" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "capnpc" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93a18ec8176d4a87f1852b6a560b4196729365c01ba3cad03b73a376a23c56e" +dependencies = [ + "capnp", +] + +[[package]] +name = "cbor4ii" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189a2a2e5eec2f203b2bb8bc4c2db55c7253770d2c6bf3ae5f79ace5a15c305f" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "codetracer-python-recorder" +version = "0.1.0" +dependencies = [ + "pyo3", + "runtime_tracing", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "fscommon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315ce685aca5ddcc5a3e7e436ef47d4a5d0064462849b6f0f628c28140103531" +dependencies = [ + "log", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "proc-macro2" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "runtime_tracing" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb39bbb7e2fe3f83c9020a2f871e7affd293e1ef5cc2f1c137012d9931611db6" +dependencies = [ + "base64", + "capnp", + "capnpc", + "cbor4ii", + "fscommon", + "num-derive", + "num-traits", + "serde", + "serde_json", + "serde_repr", + "zeekstd", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zeekstd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78be0afb4741f4d364cbc6a3151b93d4564e48c2fea7ec244e938f13465f847e" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/crates/codetracer-python-recorder/Cargo.toml b/codetracer-python-recorder/Cargo.toml similarity index 93% rename from crates/codetracer-python-recorder/Cargo.toml rename to codetracer-python-recorder/Cargo.toml index 7150965..e3c31c3 100644 --- a/crates/codetracer-python-recorder/Cargo.toml +++ b/codetracer-python-recorder/Cargo.toml @@ -12,3 +12,4 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.25.1", features = ["extension-module"] } +runtime_tracing = "0.14.0" \ No newline at end of file diff --git a/codetracer-python-recorder/codetracer_python_recorder/.gitignore b/codetracer-python-recorder/codetracer_python_recorder/.gitignore new file mode 100644 index 0000000..f1fe8d1 --- /dev/null +++ b/codetracer-python-recorder/codetracer_python_recorder/.gitignore @@ -0,0 +1 @@ +*.so \ No newline at end of file diff --git a/codetracer-python-recorder/codetracer_python_recorder/__init__.py b/codetracer-python-recorder/codetracer_python_recorder/__init__.py new file mode 100644 index 0000000..c4ea7f1 --- /dev/null +++ b/codetracer-python-recorder/codetracer_python_recorder/__init__.py @@ -0,0 +1,13 @@ +"""High-level tracing API built on a Rust backend. + +This module exposes a minimal interface for starting and stopping +runtime traces. The heavy lifting is delegated to the +`codetracer_python_recorder` Rust extension which will eventually hook +into `runtime_tracing` and `sys.monitoring`. For now the Rust side only +maintains placeholder state and performs no actual tracing. +""" + +from .api import * + +__all__ = api.__all__ + diff --git a/codetracer-python-recorder/codetracer_python_recorder/api.py b/codetracer-python-recorder/codetracer_python_recorder/api.py new file mode 100644 index 0000000..1b598e4 --- /dev/null +++ b/codetracer-python-recorder/codetracer_python_recorder/api.py @@ -0,0 +1,155 @@ +"""High-level tracing API built on a Rust backend. + +This module exposes a minimal interface for starting and stopping +runtime traces. The heavy lifting is delegated to the +`codetracer_python_recorder` Rust extension which will eventually hook +into `runtime_tracing` and `sys.monitoring`. For now the Rust side only +maintains placeholder state and performs no actual tracing. +""" +from __future__ import annotations + +import contextlib +import os +from pathlib import Path +from typing import Iterable, Iterator, Optional + +from .codetracer_python_recorder import ( + flush_tracing as _flush_backend, + is_tracing as _is_tracing_backend, + start_tracing as _start_backend, + stop_tracing as _stop_backend, +) + +TRACE_BINARY: str = "binary" +TRACE_JSON: str = "json" +DEFAULT_FORMAT: str = TRACE_BINARY + +_active_session: Optional["TraceSession"] = None + + +def _normalize_source_roots(source_roots: Iterable[os.PathLike | str] | None) -> Optional[list[str]]: + if source_roots is None: + return None + return [str(Path(p)) for p in source_roots] + + +def start( + path: os.PathLike | str, + *, + format: str = DEFAULT_FORMAT, + capture_values: bool = True, + source_roots: Iterable[os.PathLike | str] | None = None, +) -> "TraceSession": + """Start a global trace session. + + Parameters mirror the design document. The current implementation + merely records the active state on the Rust side and performs no + tracing. + """ + global _active_session + if _is_tracing_backend(): + raise RuntimeError("tracing already active") + + trace_path = Path(path) + _start_backend(str(trace_path), format, capture_values, _normalize_source_roots(source_roots)) + session = TraceSession(path=trace_path, format=format) + _active_session = session + return session + + +def stop() -> None: + """Stop the active trace session if one is running.""" + global _active_session + if not _is_tracing_backend(): + return + _stop_backend() + _active_session = None + + +def is_tracing() -> bool: + """Return ``True`` when a trace session is active.""" + return _is_tracing_backend() + + +def flush() -> None: + """Flush buffered trace data. + + With the current placeholder implementation this is a no-op but the + function is provided to match the planned public API. + """ + if _is_tracing_backend(): + _flush_backend() + + +@contextlib.contextmanager +def trace( + path: os.PathLike | str, + *, + format: str = DEFAULT_FORMAT, + capture_values: bool = True, + source_roots: Iterable[os.PathLike | str] | None = None, +) -> Iterator["TraceSession"]: + """Context manager helper for scoped tracing.""" + session = start( + path, + format=format, + capture_values=capture_values, + source_roots=source_roots, + ) + try: + yield session + finally: + session.stop() + + +class TraceSession: + """Handle representing a live tracing session.""" + + path: Path + format: str + + def __init__(self, path: Path, format: str) -> None: + self.path = path + self.format = format + + def stop(self) -> None: + """Stop this trace session.""" + if _active_session is self: + stop() + + def flush(self) -> None: + """Flush buffered trace data for this session.""" + flush() + + def __enter__(self) -> "TraceSession": + return self + + def __exit__(self, exc_type, exc, tb) -> None: # pragma: no cover - thin wrapper + self.stop() + + +def _auto_start_from_env() -> None: + path = os.getenv("CODETRACER_TRACE") + if not path: + return + fmt = os.getenv("CODETRACER_FORMAT", DEFAULT_FORMAT) + capture_env = os.getenv("CODETRACER_CAPTURE_VALUES") + capture = True + if capture_env is not None: + capture = capture_env.lower() not in {"0", "false", "no"} + start(path, format=fmt, capture_values=capture) + + +_auto_start_from_env() + +__all__ = [ + "TraceSession", + "DEFAULT_FORMAT", + "TRACE_BINARY", + "TRACE_JSON", + "start", + "stop", + "is_tracing", + "trace", + "flush", +] diff --git a/crates/codetracer-python-recorder/pyproject.toml b/codetracer-python-recorder/pyproject.toml similarity index 87% rename from crates/codetracer-python-recorder/pyproject.toml rename to codetracer-python-recorder/pyproject.toml index b5a79fd..5bfc8ee 100644 --- a/crates/codetracer-python-recorder/pyproject.toml +++ b/codetracer-python-recorder/pyproject.toml @@ -1,11 +1,7 @@ -[build-system] -requires = ["maturin>=1.5,<2"] -build-backend = "maturin" - [project] name = "codetracer-python-recorder" version = "0.1.0" -description = "Rust-backed Python module for CodeTracer recording (PyO3)" +description = "Low-level Rust-backed Python module for CodeTracer recording (PyO3)" authors = [{name = "Metacraft Labs Ltd"}] license = {text = "MIT"} requires-python = ">=3.8" @@ -20,3 +16,7 @@ classifiers = [ # Build the PyO3 extension module bindings = "pyo3" # Use the library name as the Python import name: codetracer_python_recorder + +[build-system] +requires = ["maturin>=1.5,<2"] +build-backend = "maturin" diff --git a/codetracer-python-recorder/src/lib.rs b/codetracer-python-recorder/src/lib.rs new file mode 100644 index 0000000..92782b5 --- /dev/null +++ b/codetracer-python-recorder/src/lib.rs @@ -0,0 +1,51 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; + +/// Global flag tracking whether tracing is active. +static ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Start tracing. Placeholder implementation that simply flips the +/// global active flag and ignores all parameters. +#[pyfunction] +fn start_tracing( + _path: &str, + _format: &str, + _capture_values: bool, + _source_roots: Option>, +) -> PyResult<()> { + if ACTIVE.swap(true, Ordering::SeqCst) { + return Err(PyRuntimeError::new_err("tracing already active")); + } + Ok(()) +} + +/// Stop tracing by resetting the global flag. +#[pyfunction] +fn stop_tracing() -> PyResult<()> { + ACTIVE.store(false, Ordering::SeqCst); + Ok(()) +} + +/// Query whether tracing is currently active. +#[pyfunction] +fn is_tracing() -> PyResult { + Ok(ACTIVE.load(Ordering::SeqCst)) +} + +/// Flush buffered trace data. No-op placeholder for now. +#[pyfunction] +fn flush_tracing() -> PyResult<()> { + Ok(()) +} + +/// Python module definition. +#[pymodule] +fn codetracer_python_recorder(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(start_tracing, m)?)?; + m.add_function(wrap_pyfunction!(stop_tracing, m)?)?; + m.add_function(wrap_pyfunction!(is_tracing, m)?)?; + m.add_function(wrap_pyfunction!(flush_tracing, m)?)?; + Ok(()) +} diff --git a/crates/codetracer-python-recorder/test/smoke.py b/codetracer-python-recorder/test/smoke.py similarity index 100% rename from crates/codetracer-python-recorder/test/smoke.py rename to codetracer-python-recorder/test/smoke.py diff --git a/codetracer-python-recorder/test/test_codetracer_api.py b/codetracer-python-recorder/test/test_codetracer_api.py new file mode 100644 index 0000000..353caee --- /dev/null +++ b/codetracer-python-recorder/test/test_codetracer_api.py @@ -0,0 +1,46 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +import codetracer_python_recorder as codetracer + + +class TracingApiTests(unittest.TestCase): + def setUp(self) -> None: # ensure clean state before each test + codetracer.stop() + + def test_start_stop_and_status(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + trace_path = Path(tmpdir) / "trace.bin" + session = codetracer.start(trace_path) + self.assertTrue(codetracer.is_tracing()) + self.assertIsInstance(session, codetracer.TraceSession) + self.assertEqual(session.path, trace_path) + self.assertEqual(session.format, codetracer.DEFAULT_FORMAT) + codetracer.flush() # should not raise + session.flush() # same + session.stop() + self.assertFalse(codetracer.is_tracing()) + + def test_context_manager(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + trace_path = Path(tmpdir) / "trace.bin" + with codetracer.trace(trace_path) as session: + self.assertTrue(codetracer.is_tracing()) + self.assertIsInstance(session, codetracer.TraceSession) + self.assertFalse(codetracer.is_tracing()) + + def test_environment_auto_start(self) -> None: + script = "import codetracer_python_recorder as codetracer, sys; sys.stdout.write(str(codetracer.is_tracing()))" + with tempfile.TemporaryDirectory() as tmpdir: + env = os.environ.copy() + env["CODETRACER_TRACE"] = str(Path(tmpdir) / "trace.bin") + out = subprocess.check_output([sys.executable, "-c", script], env=env) + self.assertEqual(out.decode(), "True") + + +if __name__ == "__main__": + unittest.main() diff --git a/codetracer-python-recorder/test/test_smoke.py b/codetracer-python-recorder/test/test_smoke.py new file mode 100644 index 0000000..4de501e --- /dev/null +++ b/codetracer-python-recorder/test/test_smoke.py @@ -0,0 +1,5 @@ +import codetracer_python_recorder as m + +def test_is_tracing_returns_false() -> None: + assert m.is_tracing() == False + diff --git a/crates/codetracer-python-recorder/Cargo.lock b/crates/codetracer-python-recorder/Cargo.lock deleted file mode 100644 index ff1e5ba..0000000 --- a/crates/codetracer-python-recorder/Cargo.lock +++ /dev/null @@ -1,164 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "codetracer-python-recorder" -version = "0.1.0" -dependencies = [ - "pyo3", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - -[[package]] -name = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "proc-macro2" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyo3" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" -dependencies = [ - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "syn" -version = "2.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "target-lexicon" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/crates/codetracer-python-recorder/src/lib.rs b/crates/codetracer-python-recorder/src/lib.rs deleted file mode 100644 index 830e44f..0000000 --- a/crates/codetracer-python-recorder/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -use pyo3::prelude::*; - -/// codetracer_python_recorder -/// -/// Minimal placeholder for the Rust-backed recorder. This exposes a trivial -/// function to verify the module builds and imports successfully. -#[pyfunction] -fn hello() -> PyResult { - Ok("Hello from codetracer-python-recorder (Rust)".to_string()) -} - -#[pymodule] -fn codetracer_python_recorder(_py: Python<'_>, m: Bound<'_, PyModule>) -> PyResult<()> { - let hello_fn = wrap_pyfunction!(hello, &m)?; - m.add_function(hello_fn)?; - Ok(()) -} diff --git a/crates/codetracer-python-recorder/test/test_smoke.py b/crates/codetracer-python-recorder/test/test_smoke.py deleted file mode 100644 index aab35b5..0000000 --- a/crates/codetracer-python-recorder/test/test_smoke.py +++ /dev/null @@ -1,5 +0,0 @@ -import codetracer_python_recorder as m - - -def test_hello_returns_expected_string() -> None: - assert m.hello() == "Hello from codetracer-python-recorder (Rust)" diff --git a/crates/codetracer-python-recorder/uv.lock b/crates/codetracer-python-recorder/uv.lock deleted file mode 100644 index 52f9c5d..0000000 --- a/crates/codetracer-python-recorder/uv.lock +++ /dev/null @@ -1,8 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.8" - -[[package]] -name = "codetracer-python-recorder" -version = "0.1.0" -source = { editable = "." } diff --git a/design-docs/design-001.md b/design-docs/design-001.md new file mode 100644 index 0000000..329f577 --- /dev/null +++ b/design-docs/design-001.md @@ -0,0 +1,236 @@ +# Python sys.monitoring Tracer Design + +## Overview + +This document outlines the design for integrating Python's `sys.monitoring` API with the `runtime_tracing` format. The goal is to produce CodeTracer-compatible traces for Python programs without modifying the interpreter. + +The tracer collects `sys.monitoring` events, converts them to `runtime_tracing` events, and streams them to `trace.json`/`trace.bin` along with metadata and source snapshots. + +## Architecture + +### Tool Initialization +- Acquire a tool identifier via `sys.monitoring.use_tool_id`; store it for the lifetime of the tracer. + ```rs + pub const MONITORING_TOOL_NAME: &str = "codetracer"; + pub struct ToolId { pub id: u8 } + pub fn acquire_tool_id() -> PyResult; + ``` +- Register one callback per event using `sys.monitoring.register_callback`. + ```rs + pub enum MonitoringEvent { PyStart, PyResume, PyReturn, PyYield, StopIteration, PyUnwind, PyThrow, Reraise, Call, Line, Instruction, Jump, Branch, Raise, ExceptionHandled, CReturn, CRaise } + pub type CallbackFn = unsafe extern "C" fn(event: MonitoringEvent, frame: *mut PyFrameObject); + pub fn register_callback(tool: &ToolId, event: MonitoringEvent, cb: CallbackFn); + ``` +- Enable all desired events by bitmask with `sys.monitoring.set_events`. + ```rs + pub const ALL_EVENTS_MASK: u64 = 0xffff; + pub fn enable_events(tool: &ToolId, mask: u64); + ``` + +### Writer Management +- Open a `runtime_tracing` writer (`trace.json` or `trace.bin`) during `start_tracing`. + ```rs + pub enum OutputFormat { Json, Binary } + pub struct TraceWriter { pub format: OutputFormat } + pub fn start_tracing(path: &Path, format: OutputFormat) -> io::Result; + ``` +- Expose methods to append metadata and file copies using existing `runtime_tracing` helpers. + ```rs + pub fn append_metadata(writer: &mut TraceWriter, meta: &TraceMetadata); + pub fn copy_source_file(writer: &mut TraceWriter, path: &Path) -> io::Result<()>; + ``` +- Flush and close the writer when tracing stops. + ```rs + pub fn stop_tracing(writer: TraceWriter) -> io::Result<()>; + ``` + +### Frame and Thread Tracking +- Maintain a per-thread stack of frame identifiers to correlate `CALL`, `PY_START`, and returns. + ```rs + pub type FrameId = u64; + pub struct ThreadState { pub stack: Vec } + pub fn current_thread_state() -> &'static mut ThreadState; + ``` +- Map `frame` objects to internal IDs for cross-referencing events. + ```rs + pub struct FrameRegistry { next: FrameId, map: HashMap<*mut PyFrameObject, FrameId> } + pub fn intern_frame(reg: &mut FrameRegistry, frame: *mut PyFrameObject) -> FrameId; + ``` +- Record thread start/end events when a new thread registers callbacks. + ```rs + pub fn on_thread_start(thread_id: u64); + pub fn on_thread_stop(thread_id: u64); + ``` + +## Event Handling + +Each bullet below represents a low-level operation translating a single `sys.monitoring` event into the `runtime_tracing` stream. + +### Control Flow +- **PY_START** – Create a `Function` event for the code object and push a new frame ID onto the thread's stack. + ```rs + pub fn on_py_start(frame: *mut PyFrameObject); + ``` +- **PY_RESUME** – Emit an `Event` log noting resumption and update the current frame's state. + ```rs + pub fn on_py_resume(frame: *mut PyFrameObject); + ``` +- **PY_RETURN** – Pop the frame ID, write a `Return` event with the value (if retrievable), and link to the caller. + ```rs + pub struct ReturnRecord { pub frame: FrameId, pub value: Option } + pub fn on_py_return(frame: *mut PyFrameObject, value: *mut PyObject); + ``` +- **PY_YIELD** – Record a `Return` event flagged as a yield and keep the frame on the stack for later resumes. + ```rs + pub fn on_py_yield(frame: *mut PyFrameObject, value: *mut PyObject); + ``` +- **STOP_ITERATION** – Emit an `Event` indicating iteration exhaustion for the current frame. + ```rs + pub fn on_stop_iteration(frame: *mut PyFrameObject); + ``` +- **PY_UNWIND** – Mark the beginning of stack unwinding and note the target handler in an `Event`. + ```rs + pub fn on_py_unwind(frame: *mut PyFrameObject); + ``` +- **PY_THROW** – Emit an `Event` describing the thrown value and the target generator/coroutine. + ```rs + pub fn on_py_throw(frame: *mut PyFrameObject, value: *mut PyObject); + ``` +- **RERAISE** – Log a re-raise event referencing the original exception. + ```rs + pub fn on_reraise(frame: *mut PyFrameObject, exc: *mut PyObject); + ``` + +### Call and Line Tracking +- **CALL** – Record a `Call` event, capturing argument values and the callee's `Function` ID. + ```rs + pub fn on_call(callee: *mut PyObject, args: &PyTupleObject) -> FrameId; + ``` +- **LINE** – Write a `Step` event with current path and line number; ensure the path is registered. + ```rs + pub fn on_line(frame: *mut PyFrameObject, lineno: u32); + ``` +- **INSTRUCTION** – Optionally emit a fine-grained `Event` containing the opcode name for detailed traces. + ```rs + pub fn on_instruction(frame: *mut PyFrameObject, opcode: u8); + ``` +- **JUMP** – Append an `Event` describing the jump target offset for control-flow visualization. + ```rs + pub fn on_jump(frame: *mut PyFrameObject, target: u32); + ``` +- **BRANCH** – Record an `Event` with branch outcome (taken or not) to aid coverage analysis. + ```rs + pub fn on_branch(frame: *mut PyFrameObject, taken: bool); + ``` + +### Exception Lifecycle +- **RAISE** – Emit an `Event` containing exception type and message when raised. + ```rs + pub fn on_raise(frame: *mut PyFrameObject, exc: *mut PyObject); + ``` +- **EXCEPTION_HANDLED** – Log an `Event` marking when an exception is caught. + ```rs + pub fn on_exception_handled(frame: *mut PyFrameObject); + ``` + +### C API Boundary +- **C_RETURN** – On returning from a C function, emit a `Return` event tagged as foreign and include result summary. + ```rs + pub fn on_c_return(func: *mut PyObject, result: *mut PyObject); + ``` +- **C_RAISE** – When a C function raises, record an `Event` with the exception info and current frame ID. + ```rs + pub fn on_c_raise(func: *mut PyObject, exc: *mut PyObject); + ``` + +### No Events +- **NO_EVENTS** – Special constant; used only to disable monitoring. No runtime event is produced. + ```rs + pub const NO_EVENTS: u64 = 0; + ``` + +## Metadata and File Capture +- Collect the working directory, program name, and arguments and store them in `trace_metadata.json`. + ```rs + pub struct TraceMetadata { pub cwd: PathBuf, pub program: String, pub args: Vec } + pub fn write_metadata(writer: &mut TraceWriter, meta: &TraceMetadata); + ``` +- Track every file path referenced; copy each into the trace directory under `files/`. + ```rs + pub fn track_file(writer: &mut TraceWriter, path: &Path) -> io::Result<()>; + ``` +- Record `VariableName`, `Type`, and `Value` entries when variables are inspected or logged. + ```rs + pub struct VariableRecord { pub name: String, pub ty: TypeId, pub value: ValueRecord } + pub fn record_variable(writer: &mut TraceWriter, rec: VariableRecord); + ``` + +## Value Translation and Recording +- Maintain a type registry that maps Python `type` objects to `runtime_tracing` `Type` entries and assigns new `type_id` values on first encounter. + ```rs + pub type TypeId = u32; + pub type ValueId = u64; + pub enum ValueRecord { Int(i64), Float(f64), Bool(bool), None, Str(String), Raw(Vec), Sequence(Vec), Tuple(Vec), Struct(Vec<(String, ValueRecord)>), Reference(ValueId) } + pub struct TypeRegistry { next: TypeId, map: HashMap<*mut PyTypeObject, TypeId> } + pub fn intern_type(reg: &mut TypeRegistry, ty: *mut PyTypeObject) -> TypeId; + ``` +- Convert primitives (`int`, `float`, `bool`, `None`, `str`) directly to their corresponding `ValueRecord` variants. + ```rs + pub fn encode_primitive(obj: *mut PyObject) -> Option; + ``` +- Encode `bytes` and `bytearray` as `Raw` records containing base64 text to preserve binary data. + ```rs + pub fn encode_bytes(obj: *mut PyObject) -> ValueRecord; + ``` +- Represent lists and sets as `Sequence` records and tuples as `Tuple` records, converting each element recursively. + ```rs + pub fn encode_sequence(iter: &PySequence) -> ValueRecord; + pub fn encode_tuple(tuple: &PyTupleObject) -> ValueRecord; + ``` +- Serialize dictionaries as a `Sequence` of two-element `Tuple` records for key/value pairs to avoid fixed field layouts. + ```rs + pub fn encode_dict(dict: &PyDictObject) -> ValueRecord; + ``` +- For objects with accessible attributes, emit a `Struct` record with sorted field names; fall back to `Raw` with `repr(obj)` when inspection is unsafe. + ```rs + pub fn encode_object(obj: *mut PyObject) -> ValueRecord; + ``` +- Track object identities to detect cycles and reuse `Reference` records with `id(obj)` for repeated structures. + ```rs + pub struct SeenSet { map: HashMap } + pub fn record_reference(seen: &mut SeenSet, obj: *mut PyObject) -> Option; + ``` + +## Shutdown +- On `stop_tracing`, call `sys.monitoring.set_events` with `NO_EVENTS` for the tool ID. + ```rs + pub fn disable_events(tool: &ToolId); + ``` +- Unregister callbacks and free the tool ID with `sys.monitoring.free_tool_id`. + ```rs + pub fn unregister_callbacks(tool: ToolId); + pub fn free_tool_id(tool: ToolId); + ``` +- Close the writer and ensure all buffered events are flushed to disk. + ```rs + pub fn finalize(writer: TraceWriter) -> io::Result<()>; + ``` + +## Current Limitations +- **No structured support for threads or async tasks** – the trace format lacks explicit identifiers for concurrent execution. + Distinguishing events emitted by different Python threads or `asyncio` tasks requires ad hoc `Event` entries, complicating + analysis and preventing downstream tools from reasoning about scheduling. +- **Generic `Event` log** – several `sys.monitoring` notifications like resume, unwind, and branch outcomes have no dedicated + `runtime_tracing` variant. They must be encoded as free‑form `Event` logs, which reduces machine readability and hinders + automation. +- **Heavy value snapshots** – arguments and returns expect full `ValueRecord` structures. Serializing arbitrary Python objects is + expensive and often degrades to lossy string dumps, limiting the visibility of rich runtime state. +- **Append‑only path and function tables** – `runtime_tracing` assumes files and functions are discovered once and never change. + Dynamically generated code (`eval`, REPL snippets) forces extra bookkeeping and cannot update earlier entries, making + dynamic features awkward to trace. +- **No built‑in compression or streaming** – traces are written as monolithic JSON or binary files. Long sessions quickly grow in + size and cannot be streamed to remote consumers without additional tooling. + +## Future Extensions +- Add filtering to enable subsets of events for performance-sensitive scenarios. +- Support streaming traces over a socket for live debugging. diff --git a/design-docs/py-api-001.md b/design-docs/py-api-001.md new file mode 100644 index 0000000..5043824 --- /dev/null +++ b/design-docs/py-api-001.md @@ -0,0 +1,64 @@ +# Python sys.monitoring Tracer API + +## Overview +This document describes the user-facing Python API for the `codetracer` module built on top of `runtime_tracing` and `sys.monitoring`. The API exposes a minimal surface for starting and stopping traces, managing trace sessions, and integrating tracing into scripts or test suites. + +## Module `codetracer` + +### Constants +- `DEFAULT_FORMAT: str = "binary"` +- `TRACE_BINARY: str = "binary"` +- `TRACE_JSON: str = "json"` + +### Session Management +- Start a global trace; returns a `TraceSession`. + ```py + def start(path: str | os.PathLike, *, format: str = DEFAULT_FORMAT, + capture_values: bool = True, source_roots: Iterable[str | os.PathLike] | None = None) -> TraceSession + ``` +- Stop the active trace if any. + ```py + def stop() -> None + ``` +- Query whether tracing is active. + ```py + def is_tracing() -> bool + ``` +- Context manager helper for scoped tracing. + ```py + @contextlib.contextmanager + def trace(path: str | os.PathLike, *, format: str = DEFAULT_FORMAT, + capture_values: bool = True, source_roots: Iterable[str | os.PathLike] | None = None): + ... + ``` +- Flush buffered data to disk without ending the session. + ```py + def flush() -> None + ``` + +## Class `TraceSession` +Represents a live tracing session returned by `start()` and used by the context manager. + +```py +class TraceSession: + path: pathlib.Path + format: str + + def stop(self) -> None: ... + def flush(self) -> None: ... + def __enter__(self) -> TraceSession: ... + def __exit__(self, exc_type, exc, tb) -> None: ... +``` + +## Environment Integration +- Auto-start tracing when `CODETRACER_TRACE` is set; the value is interpreted as the output path. +- When `CODETRACER_FORMAT` is provided, it overrides the default output format. +- `CODETRACER_CAPTURE_VALUES` toggles value recording. + +## Usage Example +```py +import codetracer + +with codetracer.trace("trace.bin"): + run_application() +``` diff --git a/design-docs/test-design-001.md b/design-docs/test-design-001.md new file mode 100644 index 0000000..ed4133a --- /dev/null +++ b/design-docs/test-design-001.md @@ -0,0 +1,60 @@ +# Python sys.monitoring Tracer Test Design + +## Overview +This document outlines a test suite for validating the Python tracer built on `sys.monitoring` and `runtime_tracing`. Each test item corresponds to roughly 1–10 lines of implementation and exercises tracer behavior under typical and edge conditions. + +## Setup +- Establish a temporary directory for trace output and source snapshots. +- Install the tracer module and import helper utilities for running traced Python snippets. +- Provide fixtures that clear the trace buffer and reset global state between tests. + +## Tool Initialization +- Acquire a monitoring tool ID and ensure subsequent calls reuse the same identifier. +- Register callbacks for all enabled events and verify the resulting mask matches the design. +- Unregister callbacks on shutdown and confirm no events fire afterward. + +## Event Recording +### Control Flow Events +- Capture `PY_START` and `PY_RETURN` for a simple script and assert a start/stop pair is recorded. +- Resume and yield events within a generator function produce matching `PY_RESUME`/`PY_YIELD` entries. +- A `PY_THROW` followed by `RERAISE` generates the expected unwind and rethrow sequence. + +### Call Tracking +- Direct function calls record `CALL` and `PY_RETURN` with correct frame identifiers. +- Recursive calls nest frames correctly and unwind in LIFO order. +- Decorated functions ensure wrapper frames are recorded separately from wrapped frames. + +### Line and Branch Coverage +- A loop with conditional branches emits `LINE` events for each executed line and `BRANCH` for each branch taken or skipped. +- Jump statements such as `continue` and `break` produce `JUMP` events with source and destination line numbers. + +### Exception Handling +- Raising and catching an exception emits `RAISE` and `EXCEPTION_HANDLED` events with matching exception IDs. +- An uncaught exception records `RAISE` followed by `PY_UNWIND` and terminates the trace with a `PY_THROW`. + +### C API Boundary +- Calling a built-in like `len` results in `C_CALL` and `C_RETURN` events linked to the Python frame. +- A built-in that raises, such as `int("a")`, generates `C_RAISE` with the translated exception value. + +## Value Translation +- Primitive values (ints, floats, strings, bytes) round-trip through the value registry and appear in the trace as expected. +- Complex collections like lists of dicts are serialized recursively with cycle detection preventing infinite loops. +- Object references without safe representations fall back to `repr` with a stable identifier. + +## Metadata and Source Capture +- The trace writer copies the executing script into the output directory and records its SHA-256 hash. +- Traces include `ProcessMetadata` fields for Python version and platform. + +## Shutdown Behavior +- Normal interpreter exit flushes the trace and closes files without losing events. +- An abrupt shutdown via `os._exit` truncates the trace file but leaves previous events intact. + +## Error and Edge Cases +- Invalid event names in manual callback registration raise a clear `ValueError`. +- Attempting to trace after the writer is closed results in a no-op without raising. +- Large string values exceeding the configured limit are truncated with an explicit marker. + +## Performance and Stress +- Tracing a tight loop of 10⁶ iterations completes within an acceptable time budget. +- Concurrent threads each produce isolated traces with no frame ID collisions. + diff --git a/flake.nix b/flake.nix index 20d149d..3c462e8 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,6 @@ ruff black mypy - python3Packages.pytest # Rust toolchain for the Rust-backed Python module cargo @@ -37,7 +36,16 @@ maturin uv pkg-config + + # CapNProto + capnproto ]; + + shellHook = '' + # When having more than one python version in the shell this variable breaks `maturin build` + # because it always leads to having SOABI be the one from the highest version + unset PYTHONPATH + ''; }; }); }; diff --git a/pyproject.toml b/pyproject.toml index 558ea65..9798331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,34 @@ -[build-system] -requires = ["setuptools>=61"] -build-backend = "setuptools.build_meta" - [project] -name = "codetracer-pure-python-recorder" +name = "codetracer-python-recorders" version = "0.1.0" -description = "Pure-Python prototype recorder producing CodeTracer traces" -authors = [{name = "Metacraft Labs Ltd"}] -license = {text = "MIT"} -readme = "README.md" requires-python = ">=3.8" -classifiers = [ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", +dependencies = [] + +[tool.uv] +managed = true + +[tool.uv.workspace] +members = [ + "codetracer-python-recorder", + "codetracer-pure-python-recorder", ] -[tool.setuptools] -py-modules = ["trace"] -package-dir = {"" = "src"} +[tool.uv.sources] +codetracer-python-recorder = {workspace= true} +codetracer-pure-python-recorder = {workspace= true} + +[tool.ruff] +line-length = 100 -[tool.setuptools.packages.find] -where = ["src"] +[tool.pyright] +typeCheckingMode = "basic" -[project.scripts] -codetracer-record = "codetracer_pure_python_recorder.cli:main" -codetracer-record-pure = "codetracer_pure_python_recorder.cli:main" +[dependency-groups] +dev = [ + "pytest>=8.3.5", +] + +test = [ + "codetracer-python-recorder", + "codetracer-pure-python-recorder" +] diff --git a/uv.lock b/uv.lock index e4956c6..dfbc830 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,225 @@ version = 1 revision = 2 requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.9'", + "python_full_version < '3.9'", +] + +[manifest] +members = [ + "codetracer-pure-python-recorder", + "codetracer-python-recorder", + "codetracer-python-recorders", +] [[package]] name = "codetracer-pure-python-recorder" version = "0.1.0" -source = { editable = "." } +source = { editable = "codetracer-pure-python-recorder" } + +[[package]] +name = "codetracer-python-recorder" +version = "0.1.0" +source = { editable = "codetracer-python-recorder" } + +[[package]] +name = "codetracer-python-recorders" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +test = [ + { name = "codetracer-pure-python-recorder" }, + { name = "codetracer-python-recorder" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.5" }] +test = [ + { name = "codetracer-pure-python-recorder", editable = "codetracer-pure-python-recorder" }, + { name = "codetracer-python-recorder", editable = "codetracer-python-recorder" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +]