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.
Files changed (43) hide show
  1. oscilo-1.0.0/.github/ISSUE_TEMPLATE/bug_report.yml +61 -0
  2. oscilo-1.0.0/.github/ISSUE_TEMPLATE/feature_request.yml +49 -0
  3. oscilo-1.0.0/.github/pull_request_template.md +23 -0
  4. oscilo-1.0.0/.github/workflows/publish.yml +94 -0
  5. oscilo-1.0.0/.github/workflows/test.yml +44 -0
  6. oscilo-1.0.0/.gitignore +14 -0
  7. oscilo-1.0.0/LICENSE +21 -0
  8. oscilo-1.0.0/PKG-INFO +217 -0
  9. oscilo-1.0.0/README.ko.md +199 -0
  10. oscilo-1.0.0/README.md +199 -0
  11. oscilo-1.0.0/pyproject.toml +33 -0
  12. oscilo-1.0.0/setup.cfg +4 -0
  13. oscilo-1.0.0/setup.py +11 -0
  14. oscilo-1.0.0/src/oscilo/CallContext.py +143 -0
  15. oscilo-1.0.0/src/oscilo/FileWriter.py +10 -0
  16. oscilo-1.0.0/src/oscilo/HistoryBuffer.py +39 -0
  17. oscilo-1.0.0/src/oscilo/ScopeResolver.py +32 -0
  18. oscilo-1.0.0/src/oscilo/TraceDispatcher.py +504 -0
  19. oscilo-1.0.0/src/oscilo/VariableTracker.py +121 -0
  20. oscilo-1.0.0/src/oscilo/__init__.py +32 -0
  21. oscilo-1.0.0/src/oscilo/_core.py +47 -0
  22. oscilo-1.0.0/src/oscilo/_version.py +24 -0
  23. oscilo-1.0.0/src/oscilo/oscilo_engine.c +151 -0
  24. oscilo-1.0.0/src/oscilo.egg-info/PKG-INFO +217 -0
  25. oscilo-1.0.0/src/oscilo.egg-info/SOURCES.txt +41 -0
  26. oscilo-1.0.0/src/oscilo.egg-info/dependency_links.txt +1 -0
  27. oscilo-1.0.0/src/oscilo.egg-info/not-zip-safe +1 -0
  28. oscilo-1.0.0/src/oscilo.egg-info/scm_file_list.json +37 -0
  29. oscilo-1.0.0/src/oscilo.egg-info/scm_version.json +8 -0
  30. oscilo-1.0.0/src/oscilo.egg-info/top_level.txt +1 -0
  31. oscilo-1.0.0/tests/test.py +36 -0
  32. oscilo-1.0.0/tests/test_call_context.py +242 -0
  33. oscilo-1.0.0/tests/test_log_register.py +125 -0
  34. oscilo-1.0.0/tests/test_oscilo_behavior.py +305 -0
  35. oscilo-1.0.0/tests/test_oscilo_components.py +173 -0
  36. oscilo-1.0.0/tests/test_oscilo_edge_cases.py +193 -0
  37. oscilo-1.0.0/tests/test_oscilo_engine.py +77 -0
  38. oscilo-1.0.0/tests/test_oscilo_process_lifecycle.py +163 -0
  39. oscilo-1.0.0/tests/test_scope_resolver_legb.py +125 -0
  40. oscilo-1.0.0/tests/test_trace_dispatcher.py +358 -0
  41. oscilo-1.0.0/tests/test_trace_dispatcher_enclosing.py +207 -0
  42. oscilo-1.0.0/tests/test_tracker_selection.py +161 -0
  43. 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
@@ -0,0 +1,14 @@
1
+ *.jsonl
2
+
3
+ .vscode/
4
+
5
+ build/
6
+
7
+ *.pyd
8
+
9
+ oscilo.egg-info/
10
+
11
+ __pycache__/
12
+
13
+ # setuptools-scm generated version file
14
+ src/oscilo/_version.py
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
+ [![PyPI version](https://img.shields.io/pypi/v/oscilo)](https://pypi.org/project/oscilo/)
24
+ [![Python](https://img.shields.io/pypi/pyversions/oscilo)](https://pypi.org/project/oscilo/)
25
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./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)