oscilo 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscilo-1.0.0/.github/ISSUE_TEMPLATE/bug_report.yml +61 -0
- oscilo-1.0.0/.github/ISSUE_TEMPLATE/feature_request.yml +49 -0
- oscilo-1.0.0/.github/pull_request_template.md +23 -0
- oscilo-1.0.0/.github/workflows/publish.yml +94 -0
- oscilo-1.0.0/.github/workflows/test.yml +44 -0
- oscilo-1.0.0/.gitignore +14 -0
- oscilo-1.0.0/LICENSE +21 -0
- oscilo-1.0.0/PKG-INFO +217 -0
- oscilo-1.0.0/README.ko.md +199 -0
- oscilo-1.0.0/README.md +199 -0
- oscilo-1.0.0/pyproject.toml +33 -0
- oscilo-1.0.0/setup.cfg +4 -0
- oscilo-1.0.0/setup.py +11 -0
- oscilo-1.0.0/src/oscilo/CallContext.py +143 -0
- oscilo-1.0.0/src/oscilo/FileWriter.py +10 -0
- oscilo-1.0.0/src/oscilo/HistoryBuffer.py +39 -0
- oscilo-1.0.0/src/oscilo/ScopeResolver.py +32 -0
- oscilo-1.0.0/src/oscilo/TraceDispatcher.py +504 -0
- oscilo-1.0.0/src/oscilo/VariableTracker.py +121 -0
- oscilo-1.0.0/src/oscilo/__init__.py +32 -0
- oscilo-1.0.0/src/oscilo/_core.py +47 -0
- oscilo-1.0.0/src/oscilo/_version.py +24 -0
- oscilo-1.0.0/src/oscilo/oscilo_engine.c +151 -0
- oscilo-1.0.0/src/oscilo.egg-info/PKG-INFO +217 -0
- oscilo-1.0.0/src/oscilo.egg-info/SOURCES.txt +41 -0
- oscilo-1.0.0/src/oscilo.egg-info/dependency_links.txt +1 -0
- oscilo-1.0.0/src/oscilo.egg-info/not-zip-safe +1 -0
- oscilo-1.0.0/src/oscilo.egg-info/scm_file_list.json +37 -0
- oscilo-1.0.0/src/oscilo.egg-info/scm_version.json +8 -0
- oscilo-1.0.0/src/oscilo.egg-info/top_level.txt +1 -0
- oscilo-1.0.0/tests/test.py +36 -0
- oscilo-1.0.0/tests/test_call_context.py +242 -0
- oscilo-1.0.0/tests/test_log_register.py +125 -0
- oscilo-1.0.0/tests/test_oscilo_behavior.py +305 -0
- oscilo-1.0.0/tests/test_oscilo_components.py +173 -0
- oscilo-1.0.0/tests/test_oscilo_edge_cases.py +193 -0
- oscilo-1.0.0/tests/test_oscilo_engine.py +77 -0
- oscilo-1.0.0/tests/test_oscilo_process_lifecycle.py +163 -0
- oscilo-1.0.0/tests/test_scope_resolver_legb.py +125 -0
- oscilo-1.0.0/tests/test_trace_dispatcher.py +358 -0
- oscilo-1.0.0/tests/test_trace_dispatcher_enclosing.py +207 -0
- oscilo-1.0.0/tests/test_tracker_selection.py +161 -0
- oscilo-1.0.0/tests/test_variable_tracker.py +441 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: File a report to help us identify and fix a bug.
|
|
3
|
+
title: "[BUG]: "
|
|
4
|
+
labels: ["bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for taking the time to fill out this bug report!
|
|
10
|
+
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: description
|
|
13
|
+
attributes:
|
|
14
|
+
label: "Description"
|
|
15
|
+
placeholder: "A clear and concise description of what the bug is."
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: steps
|
|
21
|
+
attributes:
|
|
22
|
+
label: "Steps to Reproduce"
|
|
23
|
+
placeholder: |
|
|
24
|
+
1. Go to '...'
|
|
25
|
+
2. Click on '...'
|
|
26
|
+
3. Scroll down to '...'
|
|
27
|
+
4. See error
|
|
28
|
+
validations:
|
|
29
|
+
required: true
|
|
30
|
+
|
|
31
|
+
- type: textarea
|
|
32
|
+
id: expected
|
|
33
|
+
attributes:
|
|
34
|
+
label: "Expected Behavior"
|
|
35
|
+
placeholder: "A clear and concise description of what you expected to happen."
|
|
36
|
+
validations:
|
|
37
|
+
required: true
|
|
38
|
+
|
|
39
|
+
- type: textarea
|
|
40
|
+
id: screenshots
|
|
41
|
+
attributes:
|
|
42
|
+
label: "Screenshots / Visuals"
|
|
43
|
+
placeholder: "Drag and drop screenshots or videos here to help explain your problem."
|
|
44
|
+
validations:
|
|
45
|
+
required: false
|
|
46
|
+
|
|
47
|
+
- type: input
|
|
48
|
+
id: environment
|
|
49
|
+
attributes:
|
|
50
|
+
label: "Environment"
|
|
51
|
+
placeholder: "e.g. OS: macOS, Browser: Chrome 120, Version: v1.2.3"
|
|
52
|
+
validations:
|
|
53
|
+
required: false
|
|
54
|
+
|
|
55
|
+
- type: textarea
|
|
56
|
+
id: context
|
|
57
|
+
attributes:
|
|
58
|
+
label: "Additional Context"
|
|
59
|
+
placeholder: "Add any other context, error logs, or thoughts here."
|
|
60
|
+
validations:
|
|
61
|
+
required: false
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Feature Request
|
|
2
|
+
description: Suggest an idea or a new feature for this project.
|
|
3
|
+
title: "[FEATURE]: "
|
|
4
|
+
labels: ["enhancement"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Have a great idea for this project? Tell us all about it!
|
|
10
|
+
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: problem
|
|
13
|
+
attributes:
|
|
14
|
+
label: "Problem Statement"
|
|
15
|
+
placeholder: "Is your feature request related to a problem? Please describe. (e.g., I'm always frustrated when...)"
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: solution
|
|
21
|
+
attributes:
|
|
22
|
+
label: "Proposed Solution"
|
|
23
|
+
placeholder: "A clear and concise description of what you want to happen and how it should work."
|
|
24
|
+
validations:
|
|
25
|
+
required: true
|
|
26
|
+
|
|
27
|
+
- type: textarea
|
|
28
|
+
id: alternatives
|
|
29
|
+
attributes:
|
|
30
|
+
label: "Alternative Solutions"
|
|
31
|
+
placeholder: "A clear and concise description of any alternative solutions or features you've considered."
|
|
32
|
+
validations:
|
|
33
|
+
required: false
|
|
34
|
+
|
|
35
|
+
- type: textarea
|
|
36
|
+
id: mockups
|
|
37
|
+
attributes:
|
|
38
|
+
label: "Screenshots / Mockups"
|
|
39
|
+
placeholder: "Drag and drop visual examples, mockups, or diagrams to explain the request."
|
|
40
|
+
validations:
|
|
41
|
+
required: false
|
|
42
|
+
|
|
43
|
+
- type: textarea
|
|
44
|
+
id: context
|
|
45
|
+
attributes:
|
|
46
|
+
label: "Additional Context"
|
|
47
|
+
placeholder: "Add any other context or thoughts about the feature request here."
|
|
48
|
+
validations:
|
|
49
|
+
required: false
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
<!-- Provide a brief description of the overall changes in this PR. -->
|
|
3
|
+
-
|
|
4
|
+
|
|
5
|
+
## Key Changes
|
|
6
|
+
<!-- List the core updates and code modifications made in this PR. -->
|
|
7
|
+
- [ ]
|
|
8
|
+
- [ ]
|
|
9
|
+
|
|
10
|
+
## Issue Link
|
|
11
|
+
<!-- Link any related issues below. e.g., Closes #12 -->
|
|
12
|
+
- Closes #
|
|
13
|
+
|
|
14
|
+
## Screenshots / Videos (Optional)
|
|
15
|
+
<!-- Attach any visual evidence, mockups, or demos if there are UI/UX changes. -->
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Checklist
|
|
20
|
+
- [ ] My code follows the coding style and guidelines of this project.
|
|
21
|
+
- [ ] I have performed a self-review of my own code.
|
|
22
|
+
- [ ] I have verified and tested the changes successfully.
|
|
23
|
+
- [ ] I have removed unnecessary debugging logs (e.g., console.log, print) and comments.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: Publish oscilo to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
# 1. Build the sdist first so it carries the setuptools-scm resolved version.
|
|
8
|
+
# Wheels are then built from this sdist, so the C-extension build never needs
|
|
9
|
+
# git access inside the cibuildwheel containers.
|
|
10
|
+
build_sdist:
|
|
11
|
+
name: Build source distribution
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout code
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0 # setuptools-scm needs full history + tags to resolve the version
|
|
18
|
+
|
|
19
|
+
- name: Build sdist
|
|
20
|
+
run: pipx run build --sdist
|
|
21
|
+
|
|
22
|
+
- name: Upload sdist artifact
|
|
23
|
+
uses: actions/upload-artifact@v4
|
|
24
|
+
with:
|
|
25
|
+
name: cibw-sdist
|
|
26
|
+
path: dist/*.tar.gz
|
|
27
|
+
|
|
28
|
+
# 2. Build the C-extension wheels from the sdist produced above. The sdist
|
|
29
|
+
# already embeds the resolved version (PKG-INFO + _version.py), so no git
|
|
30
|
+
# metadata is required inside the build containers.
|
|
31
|
+
build_wheels:
|
|
32
|
+
name: Build wheels on ${{ matrix.os }}
|
|
33
|
+
needs: [build_sdist]
|
|
34
|
+
runs-on: ${{ matrix.os }}
|
|
35
|
+
strategy:
|
|
36
|
+
matrix:
|
|
37
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
38
|
+
|
|
39
|
+
steps:
|
|
40
|
+
- name: Set up QEMU
|
|
41
|
+
if: matrix.os == 'ubuntu-latest'
|
|
42
|
+
uses: docker/setup-qemu-action@v3
|
|
43
|
+
with:
|
|
44
|
+
platforms: all
|
|
45
|
+
|
|
46
|
+
- name: Download sdist
|
|
47
|
+
uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: cibw-sdist
|
|
50
|
+
path: dist
|
|
51
|
+
|
|
52
|
+
- name: Locate sdist
|
|
53
|
+
id: sdist
|
|
54
|
+
shell: bash
|
|
55
|
+
run: echo "path=$(ls dist/*.tar.gz)" >> "$GITHUB_OUTPUT"
|
|
56
|
+
|
|
57
|
+
- name: Build wheels via cibuildwheel
|
|
58
|
+
uses: pypa/cibuildwheel@v2.22.0
|
|
59
|
+
with:
|
|
60
|
+
package-dir: ${{ steps.sdist.outputs.path }}
|
|
61
|
+
env:
|
|
62
|
+
CIBW_BUILD: "cp311-* cp312-* cp313-*"
|
|
63
|
+
CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
|
|
64
|
+
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014
|
|
65
|
+
|
|
66
|
+
- name: Upload wheels artifact
|
|
67
|
+
uses: actions/upload-artifact@v4
|
|
68
|
+
with:
|
|
69
|
+
name: cibw-wheels-${{ matrix.os }}
|
|
70
|
+
path: ./wheelhouse/*.whl
|
|
71
|
+
|
|
72
|
+
upload_pypi:
|
|
73
|
+
name: Upload to PyPI
|
|
74
|
+
needs: [build_wheels, build_sdist]
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
permissions:
|
|
77
|
+
id-token: write
|
|
78
|
+
environment:
|
|
79
|
+
name: release
|
|
80
|
+
url: https://pypi.org/p/oscilo
|
|
81
|
+
|
|
82
|
+
steps:
|
|
83
|
+
- name: Download all artifacts
|
|
84
|
+
uses: actions/download-artifact@v4
|
|
85
|
+
with:
|
|
86
|
+
pattern: cibw-*
|
|
87
|
+
merge-multiple: true
|
|
88
|
+
path: dist
|
|
89
|
+
|
|
90
|
+
- name: Check dist
|
|
91
|
+
run: pipx run twine check dist/*
|
|
92
|
+
|
|
93
|
+
- name: Publish package to PyPI
|
|
94
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Run Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened, edited]
|
|
6
|
+
branches: [ develop, master ]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check-pr-checklist:
|
|
10
|
+
name: Verify PR Checklist
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Check all checklist items are completed
|
|
14
|
+
uses: actions/github-script@v7
|
|
15
|
+
with:
|
|
16
|
+
script: |
|
|
17
|
+
const body = context.payload.pull_request.body || '';
|
|
18
|
+
const unchecked = (body.match(/^- \[ \]/gm) || []).length;
|
|
19
|
+
if (unchecked > 0) {
|
|
20
|
+
core.setFailed(`${unchecked} checklist item(s) unchecked. Complete all items before merging.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
strategy:
|
|
26
|
+
matrix:
|
|
27
|
+
python-version: ['3.11', '3.12', '3.13']
|
|
28
|
+
|
|
29
|
+
steps:
|
|
30
|
+
- name: Checkout code
|
|
31
|
+
uses: actions/checkout@v4
|
|
32
|
+
with:
|
|
33
|
+
fetch-depth: 0 # setuptools-scm needs full history + tags to resolve the version
|
|
34
|
+
|
|
35
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
36
|
+
uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: ${{ matrix.python-version }}
|
|
39
|
+
|
|
40
|
+
- name: Install package with C extension
|
|
41
|
+
run: pip install -e .
|
|
42
|
+
|
|
43
|
+
- name: Run tests
|
|
44
|
+
run: python tests/test.py
|
oscilo-1.0.0/.gitignore
ADDED
oscilo-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sdkurjnk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
oscilo-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oscilo
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Zero-invasive Python variable-change tracker powered by a C-extension trace engine with automatic scope resolution and JSONL history output
|
|
5
|
+
Author-email: sdkurjnk <sdkurjnk@naver.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sdkurjnk/oscilo/tree/master
|
|
8
|
+
Keywords: logging,monitoring,c-extension
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# oscilo
|
|
20
|
+
|
|
21
|
+
Zero-invasive variable-change tracker for Python. Register a variable once and `oscilo` records every change it undergoes — without touching your code.
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/oscilo/)
|
|
24
|
+
[](https://pypi.org/project/oscilo/)
|
|
25
|
+
[](./LICENSE)
|
|
26
|
+
|
|
27
|
+
**English** | [한국어](./README.ko.md)
|
|
28
|
+
|
|
29
|
+
`oscilo` observes program execution in the background and captures how a variable evolves over time — reassignments, in-place mutations, and deletions — then writes the full history to a JSON Lines file when the program exits. The value comparison that runs on every line is delegated to a small C extension to keep the runtime overhead low.
|
|
30
|
+
|
|
31
|
+
## Contents
|
|
32
|
+
|
|
33
|
+
- [Motivation](#motivation)
|
|
34
|
+
- [Installation](#installation)
|
|
35
|
+
- [Quick start](#quick-start)
|
|
36
|
+
- [Output format](#output-format)
|
|
37
|
+
- [How it works](#how-it-works)
|
|
38
|
+
- [Project layout](#project-layout)
|
|
39
|
+
- [Testing](#testing)
|
|
40
|
+
- [Contributing](#contributing)
|
|
41
|
+
- [License](#license)
|
|
42
|
+
|
|
43
|
+
## Motivation
|
|
44
|
+
|
|
45
|
+
Inspecting variable state during debugging usually means scattering `print` calls through the code:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
def solve(data):
|
|
49
|
+
result = []
|
|
50
|
+
for x in data:
|
|
51
|
+
result.append(x * 2)
|
|
52
|
+
print("result:", result) # temporary, easy to forget to remove
|
|
53
|
+
return result
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This clutters the source, is easy to leave behind, and does not scale to hot loops. Interactive debuggers avoid the clutter but interrupt execution, which is impractical when a loop runs thousands of times.
|
|
57
|
+
|
|
58
|
+
`oscilo` takes a different approach: register a variable by name once, and its history is collected automatically.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import oscilo
|
|
62
|
+
|
|
63
|
+
def solve(data):
|
|
64
|
+
result = []
|
|
65
|
+
oscilo.register("result") # every change to `result` from here on is recorded
|
|
66
|
+
for x in data:
|
|
67
|
+
result.append(x * 2)
|
|
68
|
+
return result
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install oscilo
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`oscilo` ships a C extension. A prebuilt wheel is used when available; otherwise the extension is compiled locally, which requires a C compiler.
|
|
78
|
+
|
|
79
|
+
<details>
|
|
80
|
+
<summary>Building from source</summary>
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install -e . # editable install (recommended for development)
|
|
84
|
+
pip install . # standard install
|
|
85
|
+
python setup.py build_ext --inplace # build the C extension only
|
|
86
|
+
```
|
|
87
|
+
</details>
|
|
88
|
+
|
|
89
|
+
**Requirements**
|
|
90
|
+
|
|
91
|
+
- Python 3.11+
|
|
92
|
+
- A C compiler (GCC / Clang / MSVC) when building from source
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
The public API is a single function, `oscilo.register()`, which takes the variable name as a string.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import oscilo
|
|
100
|
+
|
|
101
|
+
def run():
|
|
102
|
+
target = [100, 200]
|
|
103
|
+
oscilo.register("target") # recorded as an "init" event
|
|
104
|
+
|
|
105
|
+
target.append(300) # updated (in-place mutation)
|
|
106
|
+
target = "reassigned" # updated (reassignment)
|
|
107
|
+
del target # deleted
|
|
108
|
+
|
|
109
|
+
run()
|
|
110
|
+
# On exit, the history is written to oscilo.jsonl
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Calling `oscilo.register()` multiple times adds each variable to the same monitor, and all histories are merged into a single output file:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import oscilo
|
|
117
|
+
|
|
118
|
+
def run():
|
|
119
|
+
score = 10
|
|
120
|
+
items = ["potion", "shield"]
|
|
121
|
+
|
|
122
|
+
oscilo.register("score")
|
|
123
|
+
oscilo.register("items")
|
|
124
|
+
|
|
125
|
+
score += 55
|
|
126
|
+
items.append("sword")
|
|
127
|
+
|
|
128
|
+
run()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Notes:
|
|
132
|
+
|
|
133
|
+
- `register()` returns `None`; registration is its only effect.
|
|
134
|
+
- Importing `oscilo` has no side effects — tracing starts only when `register()` is called.
|
|
135
|
+
- If no change is ever recorded, no file is written.
|
|
136
|
+
|
|
137
|
+
## Output format
|
|
138
|
+
|
|
139
|
+
History is written as [JSON Lines](https://jsonlines.org/) (`oscilo.jsonl` by default). Each line represents a single change:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{"name": "target", "var_id": 1, "data": [100, 200], "event": "init", "domain": "LOCAL", "line": 4, "func": "run", "call_id": 1, "parent_call_id": null, "call_depth": 1}
|
|
143
|
+
{"name": "target", "var_id": 1, "data": [100, 200, 300], "event": "updated", "domain": "LOCAL", "line": 6, "func": "run", "call_id": 1, "parent_call_id": null, "call_depth": 1}
|
|
144
|
+
{"name": "target", "var_id": 1, "data": "reassigned", "event": "updated", "domain": "LOCAL", "line": 7, "func": "run", "call_id": 1, "parent_call_id": null, "call_depth": 1}
|
|
145
|
+
{"name": "target", "var_id": 1, "data": null, "event": "deleted", "domain": "LOCAL", "line": 8, "func": "run", "call_id": 1, "parent_call_id": null, "call_depth": 1}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Field | Description |
|
|
149
|
+
|-------|-------------|
|
|
150
|
+
| `name` | The tracked variable name |
|
|
151
|
+
| `var_id` | Identity of the tracked binding; records sharing a `var_id` refer to the same variable (matches `call_id` for locals; stays stable across frames for globals and closure variables) |
|
|
152
|
+
| `data` | The value at the time of the change (`null` for `deleted`) |
|
|
153
|
+
| `event` | `init`, `updated`, or `deleted` |
|
|
154
|
+
| `domain` | Variable scope: `LOCAL` or `GLOBAL` (a closure/enclosing variable is reported as `LOCAL` from its owning frame's point of view and is identified by `var_id`) |
|
|
155
|
+
| `line` | Source line where the change was detected |
|
|
156
|
+
| `func` | Function in which the change occurred (`<module>` at module level) |
|
|
157
|
+
| `call_id` | Unique id of the function call (frame) where the change occurred |
|
|
158
|
+
| `parent_call_id` | `call_id` of the calling frame (`null` for a root call) |
|
|
159
|
+
| `call_depth` | Call depth relative to the registration frame (root = `1`) |
|
|
160
|
+
|
|
161
|
+
The `call_id` / `parent_call_id` / `call_depth` fields let a consumer reconstruct the call tree — i.e. which call each change happened in, and how the calls nest.
|
|
162
|
+
|
|
163
|
+
The line-delimited format streams and parses efficiently for large histories.
|
|
164
|
+
|
|
165
|
+
## How it works
|
|
166
|
+
|
|
167
|
+
`oscilo` watches execution in Python and delegates value comparison to a C extension.
|
|
168
|
+
|
|
169
|
+
1. The first `register()` call lazily creates a single monitor and installs a trace hook via `sys.settrace`.
|
|
170
|
+
2. On each executed line, the C engine (`oscilo_engine`) determines whether a tracked variable changed, using a short-circuiting sequence:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
Variable no longer in scope? → deleted
|
|
174
|
+
Reference identity changed? → updated (reassignment)
|
|
175
|
+
Immutable type (int/str/...)? → unchanged (skip comparison)
|
|
176
|
+
Container size changed? → updated
|
|
177
|
+
Otherwise, compare values → updated / unchanged
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
3. When the process exits, an `atexit` hook flushes the in-memory history to disk in a single write. Records are buffered rather than written per change to avoid I/O overhead.
|
|
181
|
+
|
|
182
|
+
Unchanged values produce no record, and immutable values are skipped entirely when their reference is unchanged — both are central to keeping the per-line cost low.
|
|
183
|
+
|
|
184
|
+
## Project layout
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
src/oscilo/
|
|
188
|
+
├── __init__.py # public API (register) and single-instance management
|
|
189
|
+
├── _core.py # monitor implementation and atexit save logic
|
|
190
|
+
├── CallContext.py # lazy call-context tracking (call_id / parent_call_id / call_depth)
|
|
191
|
+
├── FileWriter.py # JSONL output
|
|
192
|
+
├── HistoryBuffer.py # in-memory history buffer
|
|
193
|
+
├── ScopeResolver.py # LEGB scope resolution (local/enclosing/global/builtin)
|
|
194
|
+
├── TraceDispatcher.py # sys.settrace event handling
|
|
195
|
+
├── VariableTracker.py # per-variable change detection and value snapshots
|
|
196
|
+
└── oscilo_engine.c # C extension for value comparison
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Testing
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
python tests/test.py
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Tests are discovered from `tests/` (`test_*.py`). Component tests run in-process, while the end-to-end scenarios (registration and process lifecycle) spawn a separate interpreter process, since `atexit`-driven saving and `sys.settrace` behavior can only be verified across a full process lifetime.
|
|
206
|
+
|
|
207
|
+
## Contributing
|
|
208
|
+
|
|
209
|
+
Contributions are welcome — bug reports, feature ideas, and pull requests alike.
|
|
210
|
+
|
|
211
|
+
- Open an issue using the bug report or feature request template.
|
|
212
|
+
- Follow the checklist in `.github/pull_request_template.md` when submitting a PR.
|
|
213
|
+
- Include tests with new features or fixes where practical.
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
[MIT](./LICENSE) © 2026 [sdkurjnk](https://github.com/sdkurjnk)
|