Skip to content

Commit 434337c

Browse files
authored
Merge pull request #3 from kangtegong/feature/python3.10+
Feature/python3.10+
2 parents 692afaa + 07c5fa5 commit 434337c

File tree

16 files changed

+745
-344
lines changed

16 files changed

+745
-344
lines changed

.github/workflows/push-test.yml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@ jobs:
66
test:
77
strategy:
88
matrix:
9-
os: [ubuntu-latest, windows-latest, macos-latest]
10-
python-version: ["3.12", "3.13"]
11-
exclude:
12-
- os: windows-latest
13-
python-version: "3.13"
9+
os: [ubuntu-latest]
10+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1411
runs-on: ${{ matrix.os }}
1512

1613
steps:
@@ -22,11 +19,6 @@ jobs:
2219
with:
2320
python-version: ${{ matrix.python-version }}
2421
check-latest: true
25-
26-
- name: Install dependencies
27-
if: runner.os == 'Windows'
28-
run: pip install -r requirements.txt
29-
3022
- name: Run tests
3123
run: |
3224
python tests/runtests.py

.github/workflows/python-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: Set up Python
2626
uses: actions/setup-python@v3
2727
with:
28-
python-version: '3.12'
28+
python-version: '3.8'
2929
- name: Install dependencies
3030
run: |
3131
python -m pip install --upgrade pip

README-ko.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Introduction
44

5-
**pyftrace**는 Python 스크립트 내에서 함수 호출을 모니터링할 수 있는 경량 Python 함수 추적 도구입니다. Python 3.12의 `sys.monitoring`을 활용하여 Python 이벤트를 모니터링하고, 모니터링 결과를 기반으로 함수 호출/리턴을 추적합니다. pyftrace를 사용하면 여러 모듈에 걸친 함수 호출을 추적할 수 있고, 호출 계층 구조를 시각적으로 나타낼 수 있으며, 추적 결과 보고서를 생성할 수 있습니다.
5+
**pyftrace**는 Python 스크립트 내에서 함수 호출을 모니터링할 수 있는 경량 Python 함수 추적 도구입니다. Python 3.8 ~3.11 버전에서는 `sys.setprofile`을, Python 3.12 이상 버전에서는 `sys.monitoring`을 활용하여 Python 이벤트를 모니터링하고, 모니터링 결과를 기반으로 함수 호출/리턴을 추적합니다. pyftrace를 사용하면 여러 모듈에 걸친 함수 호출을 추적할 수 있고, 호출 계층 구조를 시각적으로 나타낼 수 있으며, 추적 결과 보고서를 생성할 수 있습니다.
66

77
![pyftrace-demo](assets/pyftrace-demo.gif)
88

@@ -36,7 +36,7 @@ options:
3636

3737
### 요구 사항
3838

39-
- **Python 버전**: pyftrace는 Python 3.12 이상이 필요합니다. Python 3.12에 도입된 새로운 `sys.monitoring` 모듈을 사용하기 때문입니다.
39+
- **Python 버전**: pyftrace는 Python 3.8 이상이 필요합니다.
4040

4141
```bash
4242
$ pyftrace [options] /path/to/python/script.py

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
## Introduction
99

10-
**pyftrace** is a lightweight Python function tracing tool designed to monitor and report on function calls within Python scripts. It leverages Python 3.12's sys.monitoring to monitor Python events and trace functions based on the results. With pyftrace, you can trace function calls across multiple modules, visualize call hierarchies, and generate execution time reports.
10+
**pyftrace** is a lightweight Python function tracing tool designed to monitor and report on function calls within Python scripts. It leverages sys.setprofile(Python 3.8 ~ 3.11), sys.monitoring(Python 3.12 ~) to monitor Python events and trace functions based on the results. With pyftrace, you can trace function calls across multiple modules, visualize call hierarchies, and generate execution time reports.
1111

1212
![pyftrace-demo](assets/pyftrace-demo.gif)
1313

@@ -41,7 +41,7 @@ options:
4141

4242
### Requirements
4343

44-
- **Python Version**: pyftrace requires **Python 3.12** or higher due to its use of the new `sys.monitoring` module introduced in Python 3.12.
44+
- **Python Version**: pyftrace requires **Python 3.8+**.
4545

4646
```bash
4747
$ pyftrace [options] /path/to/python/script.py

TODO

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
- add -vv option for large codebase (e.g. Django, Pytorch, etc)
2-
- -v option: Additionally enable built-in functions tracing
3-
- -vv option: Additionally enable stdlib tracing
4-
- stdlib path: sysconfig.get_paths()["stdlib"]
1+
- depth limit: --depth/-D <int>
52
- trace format using ├, │, ─
63
- tui: fold/unfold
74
- tui: '/' (search function)
85
- function filtering: --function/-f <string>
96
- function-exclude filtering: --exclude-function/-e <string>
10-
- depth limit: --depth/-D <int>
117
- output format: -o/--output -O <filename> / sqlite dump
128
- multiprocessing(pid) & multithreading(tid) tracing
139
- argument tracing

examples/requests_example.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import requests
2+
3+
x = requests.get('https://example.com')
4+
5+
print(x.text)
6+

examples/torch_example.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Example from: https://pytorch.org/tutorials/beginner/pytorch_with_examples.html
2+
# -*- coding: utf-8 -*-
3+
import torch
4+
import math
5+
6+
dtype = torch.float
7+
device = "cuda" if torch.cuda.is_available() else "cpu"
8+
torch.set_default_device(device)
9+
10+
# Create Tensors to hold input and outputs.
11+
# By default, requires_grad=False, which indicates that we do not need to
12+
# compute gradients with respect to these Tensors during the backward pass.
13+
x = torch.linspace(-math.pi, math.pi, 2000, dtype=dtype)
14+
y = torch.sin(x)
15+
16+
# Create random Tensors for weights. For a third order polynomial, we need
17+
# 4 weights: y = a + b x + c x^2 + d x^3
18+
# Setting requires_grad=True indicates that we want to compute gradients with
19+
# respect to these Tensors during the backward pass.
20+
a = torch.randn((), dtype=dtype, requires_grad=True)
21+
b = torch.randn((), dtype=dtype, requires_grad=True)
22+
c = torch.randn((), dtype=dtype, requires_grad=True)
23+
d = torch.randn((), dtype=dtype, requires_grad=True)
24+
25+
learning_rate = 1e-6
26+
for t in range(100):
27+
# Forward pass: compute predicted y using operations on Tensors.
28+
y_pred = a + b * x + c * x ** 2 + d * x ** 3
29+
30+
# Compute and print loss using operations on Tensors.
31+
# Now loss is a Tensor of shape (1,)
32+
# loss.item() gets the scalar value held in the loss.
33+
loss = (y_pred - y).pow(2).sum()
34+
if t % 100 == 99:
35+
print(t, loss.item())
36+
37+
# Use autograd to compute the backward pass. This call will compute the
38+
# gradient of loss with respect to all Tensors with requires_grad=True.
39+
# After this call a.grad, b.grad. c.grad and d.grad will be Tensors holding
40+
# the gradient of the loss with respect to a, b, c, d respectively.
41+
loss.backward()
42+
43+
# Manually update weights using gradient descent. Wrap in torch.no_grad()
44+
# because weights have requires_grad=True, but we don't need to track this
45+
# in autograd.
46+
with torch.no_grad():
47+
a -= learning_rate * a.grad
48+
b -= learning_rate * b.grad
49+
c -= learning_rate * c.grad
50+
d -= learning_rate * d.grad
51+
52+
# Manually zero the gradients after updating weights
53+
a.grad = None
54+
b.grad = None
55+
c.grad = None
56+
d.grad = None
57+
58+
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')
59+

pyftrace/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
__version__ = "0.2.0"
22

3-
from .tracer import Pyftrace
3+
from .tracer import get_tracer
4+
5+
__all__ = ['get_tracer']

pyftrace/engine/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import sys
2+
import os
3+
import time
4+
import weakref
5+
import sysconfig
6+
from ..tracer import PyftraceBase
7+
from ..utils import get_site_packages_modules, resolve_filename, get_line_number
8+
9+
class PyftraceMonitoring(PyftraceBase):
10+
"""
11+
sys.monitoring based tracer.
12+
"""
13+
def setup_tracing(self):
14+
self.tool_id = 1
15+
sys.monitoring.use_tool_id(self.tool_id, "pyftrace")
16+
sys.monitoring.register_callback(self.tool_id, sys.monitoring.events.CALL, self.monitor_call)
17+
sys.monitoring.register_callback(self.tool_id, sys.monitoring.events.PY_RETURN, self.monitor_py_return)
18+
sys.monitoring.register_callback(self.tool_id, sys.monitoring.events.C_RETURN, self.monitor_c_return)
19+
sys.monitoring.register_callback(self.tool_id, sys.monitoring.events.C_RAISE, self.monitor_c_raise)
20+
sys.monitoring.set_events(
21+
self.tool_id,
22+
sys.monitoring.events.CALL |
23+
sys.monitoring.events.PY_RETURN |
24+
sys.monitoring.events.C_RETURN |
25+
sys.monitoring.events.C_RAISE
26+
)
27+
28+
def cleanup_tracing(self):
29+
sys.monitoring.free_tool_id(self.tool_id)
30+
self.output_stream = None
31+
32+
def run_python_script(self, script_path, script_args):
33+
if self.output_stream:
34+
print(f"Running script: {script_path}", file=self.output_stream)
35+
36+
self.script_name = os.path.abspath(script_path)
37+
self.script_dir = os.path.dirname(self.script_name)
38+
39+
with open(script_path, "r") as file:
40+
script_code = file.read()
41+
code_object = compile(script_code, script_path, 'exec')
42+
43+
old_sys_path = sys.path.copy()
44+
old_sys_argv = sys.argv.copy()
45+
sys.path.insert(0, self.script_dir)
46+
sys.argv = [script_path] + script_args
47+
48+
self.tracing_started = False
49+
50+
self.setup_tracing()
51+
52+
try:
53+
exec(code_object, {"__file__": script_path, "__name__": "__main__"})
54+
finally:
55+
self.cleanup_tracing()
56+
sys.path = old_sys_path
57+
sys.argv = old_sys_argv
58+
59+
def monitor_call(self, code, instruction_offset, callable_obj, arg0):
60+
self.handle_call_event(code, instruction_offset, callable_obj)
61+
62+
def monitor_py_return(self, code, instruction_offset, retval):
63+
self.handle_py_return_event(code, instruction_offset, retval)
64+
65+
def monitor_c_return(self, code, instruction_offset, callable_obj, arg0):
66+
self.handle_c_return_event(code, instruction_offset, callable_obj)
67+
68+
def monitor_c_raise(self, code, instruction_offset, callable_obj, arg0):
69+
pass # Placeholder for handling C_RAISE events
70+
71+
def handle_call_event(self, code, instruction_offset, callable_obj):
72+
if not self.tracing_started:
73+
# Start tracing when enter script's '<module>' code
74+
if code and os.path.abspath(code.co_filename) == os.path.abspath(self.script_name) and code.co_name == '<module>':
75+
self.tracing_started = True
76+
else:
77+
return
78+
79+
call_lineno = get_line_number(code, instruction_offset)
80+
call_filename = resolve_filename(code, None)
81+
if call_filename:
82+
call_filename = os.path.abspath(call_filename)
83+
84+
if isinstance(callable_obj, weakref.ReferenceType):
85+
callable_obj = callable_obj()
86+
87+
func_name = getattr(callable_obj, '__name__', str(callable_obj))
88+
module_name = getattr(callable_obj, '__module__', None)
89+
is_builtin = module_name in (None, 'builtins')
90+
91+
# Exclude stdlib and frozen modules
92+
def_filename = ''
93+
func_def_lineno = ''
94+
trace_this = False
95+
96+
if hasattr(callable_obj, '__code__'):
97+
func_def_lineno = callable_obj.__code__.co_firstlineno
98+
def_filename = os.path.abspath(callable_obj.__code__.co_filename)
99+
if not self.is_stdlib_code(def_filename):
100+
trace_this = self.should_trace(def_filename) or self.verbose
101+
else:
102+
trace_this = False # Exclude stdlib
103+
else:
104+
def_filename = resolve_filename(None, callable_obj)
105+
if def_filename:
106+
def_filename = os.path.abspath(def_filename)
107+
if is_builtin:
108+
# Only trace built-in functions for `verbose`
109+
if self.verbose and self.should_trace(call_filename):
110+
trace_this = True
111+
else:
112+
trace_this = False
113+
else:
114+
if self.verbose and self.should_trace(def_filename):
115+
trace_this = True
116+
117+
if trace_this and not self.is_tracer_code(call_filename):
118+
indent = " " * self.current_depth()
119+
if self.show_path:
120+
if is_builtin or not def_filename:
121+
func_location = f"{func_name}@{module_name or '<builtin>'}"
122+
else:
123+
func_location = f"{func_name}@{def_filename}:{func_def_lineno}"
124+
call_location = f"from {call_filename}:{call_lineno}"
125+
else:
126+
func_location = func_name
127+
call_location = f"from line {call_lineno}"
128+
if not self.report_mode and self.output_stream:
129+
print(f"{indent}Called {func_location} {call_location}", file=self.output_stream)
130+
self.call_stack.append((func_name, is_builtin))
131+
if self.report_mode:
132+
start_time = time.time()
133+
if func_name in self.execution_report:
134+
_, total_time, call_count = self.execution_report[func_name]
135+
self.execution_report[func_name] = (start_time, total_time, call_count + 1)
136+
else:
137+
self.execution_report[func_name] = (start_time, 0, 1)
138+
139+
def handle_py_return_event(self, code, instruction_offset, retval):
140+
if not self.tracing_started:
141+
return
142+
143+
filename = resolve_filename(code, None)
144+
if filename:
145+
filename = os.path.abspath(filename)
146+
func_name = code.co_name if code else "<unknown>"
147+
148+
# Skip tracing the '<module>' function's return event
149+
if func_name == '<module>':
150+
return
151+
152+
trace_this = self.should_trace(filename) or self.verbose
153+
154+
if trace_this and not self.is_tracer_code(filename):
155+
if self.call_stack:
156+
stack_func_name, _ = self.call_stack[-1]
157+
else:
158+
stack_func_name = "<unknown>"
159+
160+
indent = " " * (self.current_depth() - 1)
161+
162+
if self.show_path:
163+
file_info = f" @ {filename}" if filename else ""
164+
else:
165+
file_info = ""
166+
167+
if stack_func_name == func_name:
168+
if not self.report_mode and self.output_stream:
169+
print(f"{indent}Returning {func_name}-> {retval}{file_info}", file=self.output_stream)
170+
171+
if self.report_mode and func_name in self.execution_report:
172+
start_time, total_time, call_count = self.execution_report[func_name]
173+
exec_time = time.time() - start_time
174+
self.execution_report[func_name] = (start_time, total_time + exec_time, call_count)
175+
176+
if self.call_stack and self.call_stack[-1][0] == func_name:
177+
self.call_stack.pop()
178+
179+
def handle_c_return_event(self, code, instruction_offset, callable_obj):
180+
if not self.tracing_started:
181+
return
182+
183+
func_name = getattr(callable_obj, '__name__', str(callable_obj))
184+
module_name = getattr(callable_obj, '__module__', None)
185+
is_builtin = module_name in (None, 'builtins')
186+
filename = resolve_filename(code, callable_obj)
187+
if filename:
188+
filename = os.path.abspath(filename)
189+
190+
# Exclude stdlib and frozen modules
191+
if self.is_stdlib_code(filename):
192+
return
193+
194+
trace_this = False
195+
if is_builtin:
196+
# Only trace built-in functions if verbose and called from script
197+
if self.verbose and self.call_stack:
198+
# Check if the caller is from script
199+
caller_filename = filename
200+
if caller_filename and self.should_trace(caller_filename):
201+
trace_this = True
202+
else:
203+
if self.verbose and self.should_trace(filename):
204+
trace_this = True
205+
206+
if trace_this and not self.is_tracer_code(filename):
207+
if self.call_stack:
208+
stack_func_name, _ = self.call_stack[-1]
209+
else:
210+
stack_func_name = "<unknown>"
211+
212+
indent = " " * (self.current_depth() - 1)
213+
214+
if self.show_path:
215+
file_info = f" @ {filename}" if filename else ""
216+
else:
217+
file_info = ""
218+
219+
if stack_func_name == func_name:
220+
if not self.report_mode and self.output_stream:
221+
print(f"{indent}Returning {func_name}{file_info}", file=self.output_stream)
222+
if self.report_mode and func_name in self.execution_report:
223+
start_time, total_time, call_count = self.execution_report[func_name]
224+
exec_time = time.time() - start_time
225+
self.execution_report[func_name] = (start_time, total_time + exec_time, call_count)
226+
if self.call_stack and self.call_stack[-1][0] == func_name:
227+
self.call_stack.pop()
228+

0 commit comments

Comments
 (0)