printsection 0.4.0__tar.gz → 0.5.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.
- {printsection-0.4.0 → printsection-0.5.0}/PKG-INFO +37 -4
- {printsection-0.4.0 → printsection-0.5.0}/README.md +36 -3
- {printsection-0.4.0 → printsection-0.5.0}/pyproject.toml +1 -1
- {printsection-0.4.0 → printsection-0.5.0}/src/printsection/__init__.py +1 -1
- {printsection-0.4.0 → printsection-0.5.0}/src/printsection/_trace.py +91 -28
- {printsection-0.4.0 → printsection-0.5.0}/tests/test_trace.py +43 -4
- {printsection-0.4.0 → printsection-0.5.0}/LICENSE +0 -0
- {printsection-0.4.0 → printsection-0.5.0}/tests/test_printsection.py +0 -0
- {printsection-0.4.0 → printsection-0.5.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: printsection
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: 출력 구간을 나누는 가운데 정렬 구분선 + 라이브러리 호출 흐름 추적 출력
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -85,7 +85,8 @@ s = format_section("실습 결과") # 출력하지 않고 문자열 반환
|
|
|
85
85
|
## 라이브러리 호출 추적: `print_trace_start` / `print_trace_end`
|
|
86
86
|
|
|
87
87
|
두 함수 사이에서 실행된 **라이브러리 내부 호출**을 순서대로 보여줍니다.
|
|
88
|
-
**메소드 · 파일:줄 · 경로**를 들여쓰기 트리로 출력하고,
|
|
88
|
+
`print_trace_end()`를 호출하면 **메소드 · 파일:줄 · 경로**를 들여쓰기 트리로 출력하고, 이어서 파일·메소드별로 묶은 요약을 출력합니다.
|
|
89
|
+
(같은 호출 반복을 접으려면 끝까지 모아야 해서, 트리는 추적이 끝날 때 한꺼번에 나옵니다.)
|
|
89
90
|
라이브러리가 내부에서 어떤 순서로 무엇을 호출하는지 공부할 때 쓰면 좋습니다.
|
|
90
91
|
|
|
91
92
|
```python
|
|
@@ -106,6 +107,10 @@ print_trace_end()
|
|
|
106
107
|
│ │ ├ Tokenizer.__init__ · _tokenizer.py:105 · packaging
|
|
107
108
|
│ │ ├ _parse_requirement · _parser.py:110 · packaging
|
|
108
109
|
│ │ │ ├ Tokenizer.consume · _tokenizer.py:116 · packaging
|
|
110
|
+
│ │ │ │ ├ Tokenizer.check · _tokenizer.py:121 · packaging
|
|
111
|
+
│ │ │ ├ Tokenizer.expect · _tokenizer.py:142 · packaging
|
|
112
|
+
│ │ │ │ ├ Tokenizer.check · _tokenizer.py:121 · packaging
|
|
113
|
+
│ │ │ │ ├ Tokenizer.read · _tokenizer.py:151 · packaging
|
|
109
114
|
...
|
|
110
115
|
|
|
111
116
|
--------------------TRACE END---------------------
|
|
@@ -123,6 +128,30 @@ packaging/_tokenizer.py
|
|
|
123
128
|
|
|
124
129
|
기본으로는 `site-packages`에 설치된 라이브러리만 보여줍니다. 내가 작성한 코드와 파이썬 표준 라이브러리는 빠집니다.
|
|
125
130
|
|
|
131
|
+
### 출력이 너무 많을 때
|
|
132
|
+
|
|
133
|
+
LangChain 같은 큰 라이브러리는 한 번 호출에도 수백~수천 줄이 나옵니다. 아래 순서대로 좁혀 보세요.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
print_trace_start(
|
|
137
|
+
["langchain_core"], # 1. 공부할 패키지만 (pydantic, httpx 등 제외)
|
|
138
|
+
hide_private=True, # 2. _로 시작하는 내부 함수 숨기기
|
|
139
|
+
unique=True, # 3. 같은 메소드는 처음 한 번만
|
|
140
|
+
max_depth=3, # 4. 깊이 제한
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`langchain-core`의 `prompt | RunnableLambda(...)` 체인 `invoke` 한 번 기준:
|
|
145
|
+
|
|
146
|
+
| 설정 | 줄 수 |
|
|
147
|
+
|---|---|
|
|
148
|
+
| 접기 없음 (`collapse=False`) | 231 |
|
|
149
|
+
| 기본값 (연속 반복 접기) | 193 |
|
|
150
|
+
| `+ hide_private=True` | 153 |
|
|
151
|
+
| `+ unique=True` | 48 |
|
|
152
|
+
| `+ ["langchain_core"]` | 38 |
|
|
153
|
+
| `+ max_depth=3` | 29 |
|
|
154
|
+
|
|
126
155
|
### 추적 옵션
|
|
127
156
|
|
|
128
157
|
```python
|
|
@@ -131,6 +160,8 @@ print_trace_start(max_depth=2) # 3단계 깊이까지만
|
|
|
131
160
|
print_trace_start(user=True) # 내 코드 호출도 함께
|
|
132
161
|
print_trace_start(file="trace.log") # 화면 대신 파일로 저장
|
|
133
162
|
print_trace_start(hide_private=True) # _로 시작하는 내부 함수 숨기기
|
|
163
|
+
print_trace_start(unique=True) # 메소드마다 처음 호출만
|
|
164
|
+
print_trace_start(collapse=False) # 반복 호출도 접지 않고 전부
|
|
134
165
|
```
|
|
135
166
|
|
|
136
167
|
| 옵션 | 기본값 | 설명 |
|
|
@@ -143,6 +174,8 @@ print_trace_start(hide_private=True) # _로 시작하는 내부
|
|
|
143
174
|
| `file` | 화면 | 파일 객체 또는 파일 경로 |
|
|
144
175
|
| `show_line` | `True` | 파일 이름 뒤에 함수 정의 줄 번호 표시 |
|
|
145
176
|
| `hide_private` | `False` | `_parse`, `_Foo.run`처럼 `_`로 시작하는 내부 함수·클래스 숨김 (`__init__` 같은 특수 메소드는 표시) |
|
|
177
|
+
| `collapse` | `True` | 같은 메소드가 연달아 호출되면 `Foo.bar ×3` 한 줄로 접음 (하위 호출은 첫 번째 것만 표시) |
|
|
178
|
+
| `unique` | `False` | 메소드마다 처음 호출만 표시. 이후 같은 메소드는 줄을 생략하고, 그 안에서 처음 등장하는 메소드만 위 단계에 붙여 표시 |
|
|
146
179
|
| `threads` | `False` | 이후 새로 시작되는 스레드도 추적 |
|
|
147
180
|
| `header` | `True` | 시작/종료 구분선 출력 |
|
|
148
181
|
|
|
@@ -160,7 +193,7 @@ with trace(["langchain_core"], max_depth=3):
|
|
|
160
193
|
|
|
161
194
|
### 참고
|
|
162
195
|
|
|
163
|
-
-
|
|
164
|
-
-
|
|
196
|
+
- `hide_private`나 `unique`로 숨긴 호출 안에서 불린 새 함수는 그대로 보이고, 가장 가까운 상위 호출 아래에 붙습니다.
|
|
197
|
+
- 요약의 호출 횟수·시간에는 접히거나 생략된 호출까지 모두 포함됩니다 (`hide_private`로 숨긴 함수는 제외).
|
|
165
198
|
- 제너레이터/코루틴(스트리밍, async)은 처음 호출될 때 한 번만 표시합니다.
|
|
166
199
|
- 추적하는 동안에는 코드가 느려집니다. 공부하거나 디버깅할 때만 쓰세요.
|
|
@@ -72,7 +72,8 @@ s = format_section("실습 결과") # 출력하지 않고 문자열 반환
|
|
|
72
72
|
## 라이브러리 호출 추적: `print_trace_start` / `print_trace_end`
|
|
73
73
|
|
|
74
74
|
두 함수 사이에서 실행된 **라이브러리 내부 호출**을 순서대로 보여줍니다.
|
|
75
|
-
**메소드 · 파일:줄 · 경로**를 들여쓰기 트리로 출력하고,
|
|
75
|
+
`print_trace_end()`를 호출하면 **메소드 · 파일:줄 · 경로**를 들여쓰기 트리로 출력하고, 이어서 파일·메소드별로 묶은 요약을 출력합니다.
|
|
76
|
+
(같은 호출 반복을 접으려면 끝까지 모아야 해서, 트리는 추적이 끝날 때 한꺼번에 나옵니다.)
|
|
76
77
|
라이브러리가 내부에서 어떤 순서로 무엇을 호출하는지 공부할 때 쓰면 좋습니다.
|
|
77
78
|
|
|
78
79
|
```python
|
|
@@ -93,6 +94,10 @@ print_trace_end()
|
|
|
93
94
|
│ │ ├ Tokenizer.__init__ · _tokenizer.py:105 · packaging
|
|
94
95
|
│ │ ├ _parse_requirement · _parser.py:110 · packaging
|
|
95
96
|
│ │ │ ├ Tokenizer.consume · _tokenizer.py:116 · packaging
|
|
97
|
+
│ │ │ │ ├ Tokenizer.check · _tokenizer.py:121 · packaging
|
|
98
|
+
│ │ │ ├ Tokenizer.expect · _tokenizer.py:142 · packaging
|
|
99
|
+
│ │ │ │ ├ Tokenizer.check · _tokenizer.py:121 · packaging
|
|
100
|
+
│ │ │ │ ├ Tokenizer.read · _tokenizer.py:151 · packaging
|
|
96
101
|
...
|
|
97
102
|
|
|
98
103
|
--------------------TRACE END---------------------
|
|
@@ -110,6 +115,30 @@ packaging/_tokenizer.py
|
|
|
110
115
|
|
|
111
116
|
기본으로는 `site-packages`에 설치된 라이브러리만 보여줍니다. 내가 작성한 코드와 파이썬 표준 라이브러리는 빠집니다.
|
|
112
117
|
|
|
118
|
+
### 출력이 너무 많을 때
|
|
119
|
+
|
|
120
|
+
LangChain 같은 큰 라이브러리는 한 번 호출에도 수백~수천 줄이 나옵니다. 아래 순서대로 좁혀 보세요.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
print_trace_start(
|
|
124
|
+
["langchain_core"], # 1. 공부할 패키지만 (pydantic, httpx 등 제외)
|
|
125
|
+
hide_private=True, # 2. _로 시작하는 내부 함수 숨기기
|
|
126
|
+
unique=True, # 3. 같은 메소드는 처음 한 번만
|
|
127
|
+
max_depth=3, # 4. 깊이 제한
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`langchain-core`의 `prompt | RunnableLambda(...)` 체인 `invoke` 한 번 기준:
|
|
132
|
+
|
|
133
|
+
| 설정 | 줄 수 |
|
|
134
|
+
|---|---|
|
|
135
|
+
| 접기 없음 (`collapse=False`) | 231 |
|
|
136
|
+
| 기본값 (연속 반복 접기) | 193 |
|
|
137
|
+
| `+ hide_private=True` | 153 |
|
|
138
|
+
| `+ unique=True` | 48 |
|
|
139
|
+
| `+ ["langchain_core"]` | 38 |
|
|
140
|
+
| `+ max_depth=3` | 29 |
|
|
141
|
+
|
|
113
142
|
### 추적 옵션
|
|
114
143
|
|
|
115
144
|
```python
|
|
@@ -118,6 +147,8 @@ print_trace_start(max_depth=2) # 3단계 깊이까지만
|
|
|
118
147
|
print_trace_start(user=True) # 내 코드 호출도 함께
|
|
119
148
|
print_trace_start(file="trace.log") # 화면 대신 파일로 저장
|
|
120
149
|
print_trace_start(hide_private=True) # _로 시작하는 내부 함수 숨기기
|
|
150
|
+
print_trace_start(unique=True) # 메소드마다 처음 호출만
|
|
151
|
+
print_trace_start(collapse=False) # 반복 호출도 접지 않고 전부
|
|
121
152
|
```
|
|
122
153
|
|
|
123
154
|
| 옵션 | 기본값 | 설명 |
|
|
@@ -130,6 +161,8 @@ print_trace_start(hide_private=True) # _로 시작하는 내부
|
|
|
130
161
|
| `file` | 화면 | 파일 객체 또는 파일 경로 |
|
|
131
162
|
| `show_line` | `True` | 파일 이름 뒤에 함수 정의 줄 번호 표시 |
|
|
132
163
|
| `hide_private` | `False` | `_parse`, `_Foo.run`처럼 `_`로 시작하는 내부 함수·클래스 숨김 (`__init__` 같은 특수 메소드는 표시) |
|
|
164
|
+
| `collapse` | `True` | 같은 메소드가 연달아 호출되면 `Foo.bar ×3` 한 줄로 접음 (하위 호출은 첫 번째 것만 표시) |
|
|
165
|
+
| `unique` | `False` | 메소드마다 처음 호출만 표시. 이후 같은 메소드는 줄을 생략하고, 그 안에서 처음 등장하는 메소드만 위 단계에 붙여 표시 |
|
|
133
166
|
| `threads` | `False` | 이후 새로 시작되는 스레드도 추적 |
|
|
134
167
|
| `header` | `True` | 시작/종료 구분선 출력 |
|
|
135
168
|
|
|
@@ -147,7 +180,7 @@ with trace(["langchain_core"], max_depth=3):
|
|
|
147
180
|
|
|
148
181
|
### 참고
|
|
149
182
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
183
|
+
- `hide_private`나 `unique`로 숨긴 호출 안에서 불린 새 함수는 그대로 보이고, 가장 가까운 상위 호출 아래에 붙습니다.
|
|
184
|
+
- 요약의 호출 횟수·시간에는 접히거나 생략된 호출까지 모두 포함됩니다 (`hide_private`로 숨긴 함수는 제외).
|
|
152
185
|
- 제너레이터/코루틴(스트리밍, async)은 처음 호출될 때 한 번만 표시합니다.
|
|
153
186
|
- 추적하는 동안에는 코드가 느려집니다. 공부하거나 디버깅할 때만 쓰세요.
|
|
@@ -84,22 +84,45 @@ def _short_path(real, norm):
|
|
|
84
84
|
return (real[len(best):] if best else real).replace("\\", "/")
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
class _Node:
|
|
88
|
+
__slots__ = ("key", "method", "fname", "line", "folder", "depth", "count", "children")
|
|
89
|
+
|
|
90
|
+
def __init__(self, key, method, fname, line, folder, depth):
|
|
91
|
+
self.key = key
|
|
92
|
+
self.method = method
|
|
93
|
+
self.fname = fname
|
|
94
|
+
self.line = line
|
|
95
|
+
self.folder = folder
|
|
96
|
+
self.depth = depth
|
|
97
|
+
self.count = 1
|
|
98
|
+
self.children = []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_DROP = object() # 출력하지 않는 하위 트리 (반복 호출, 깊이 제한)
|
|
102
|
+
_OVER = object() # max_lines를 넘어 생략된 하위 트리 (개수 집계)
|
|
103
|
+
|
|
104
|
+
|
|
87
105
|
class _Tracer:
|
|
88
|
-
def __init__(
|
|
106
|
+
def __init__(
|
|
107
|
+
self, packages, stdlib, user, max_depth, max_lines, show_line, hide_private, collapse, unique
|
|
108
|
+
):
|
|
89
109
|
self.packages = tuple(packages or ())
|
|
90
110
|
self.kinds = {"library"} | ({"stdlib"} if stdlib else set()) | ({"user"} if user else set())
|
|
91
111
|
self.max_depth = max_depth
|
|
92
112
|
self.max_lines = max_lines
|
|
93
|
-
self.out = out
|
|
94
113
|
self.show_line = show_line
|
|
95
114
|
self.hide_private = hide_private
|
|
115
|
+
self.collapse = collapse
|
|
116
|
+
self.unique = unique
|
|
96
117
|
|
|
97
118
|
self.code_cache = {} # code -> (경로, 파일명, 폴더) 또는 None(출력 안 함)
|
|
98
|
-
self.stacks = {} # 스레드 id -> [(frame, 시작시각,
|
|
119
|
+
self.stacks = {} # 스레드 id -> [(frame, 시작시각, 통계키, 하위 호출이 붙을 곳)]
|
|
99
120
|
self.seen_gens = {} # id(frame) -> frame, 제너레이터 재개 구분용
|
|
100
121
|
self.stats = {} # (경로, 메소드) -> [횟수, 누적초]
|
|
122
|
+
self.shown = set() # 트리에 한 번이라도 나온 (경로, 메소드)
|
|
123
|
+
self.root = _Node(None, "", "", 0, "", -1)
|
|
101
124
|
self.records = []
|
|
102
|
-
self.
|
|
125
|
+
self.nodes = 0
|
|
103
126
|
self.hidden = 0
|
|
104
127
|
self.started = time.perf_counter()
|
|
105
128
|
|
|
@@ -129,7 +152,7 @@ class _Tracer:
|
|
|
129
152
|
elif event == "return":
|
|
130
153
|
stack = self.stacks.get(threading.get_ident())
|
|
131
154
|
if stack and stack[-1][0] is frame:
|
|
132
|
-
_, start, key = stack.pop()
|
|
155
|
+
_, start, key, _ = stack.pop()
|
|
133
156
|
self.stats[key][1] += time.perf_counter() - start
|
|
134
157
|
|
|
135
158
|
def _on_call(self, frame):
|
|
@@ -145,33 +168,62 @@ class _Tracer:
|
|
|
145
168
|
self.seen_gens[id(frame)] = frame
|
|
146
169
|
|
|
147
170
|
path, fname, folder = info
|
|
148
|
-
stack = self.stacks.setdefault(threading.get_ident(), [])
|
|
149
|
-
depth = len(stack)
|
|
150
171
|
key = (path, method)
|
|
151
172
|
stat = self.stats.get(key)
|
|
152
173
|
if stat is None:
|
|
153
174
|
self.stats[key] = [1, 0.0]
|
|
154
175
|
else:
|
|
155
176
|
stat[0] += 1
|
|
156
|
-
stack.append((frame, time.perf_counter(), key))
|
|
157
177
|
|
|
178
|
+
stack = self.stacks.setdefault(threading.get_ident(), [])
|
|
179
|
+
parent = stack[-1][3] if stack else self.root
|
|
158
180
|
line = frame.f_code.co_firstlineno
|
|
181
|
+
child = self._place(parent, key, method, fname, line, folder)
|
|
182
|
+
stack.append((frame, time.perf_counter(), key, child))
|
|
183
|
+
|
|
184
|
+
depth = parent.depth + 1 if isinstance(parent, _Node) else -1
|
|
159
185
|
self.records.append(
|
|
160
186
|
{"depth": depth, "method": method, "file": fname, "line": line, "path": path}
|
|
161
187
|
)
|
|
188
|
+
|
|
189
|
+
def _place(self, parent, key, method, fname, line, folder):
|
|
190
|
+
"""이 호출을 트리 어디에 둘지 정한다. 반환값은 이 호출의 하위 호출이 붙을 곳."""
|
|
191
|
+
if parent is _DROP:
|
|
192
|
+
return _DROP
|
|
193
|
+
if parent is _OVER:
|
|
194
|
+
self.hidden += 1
|
|
195
|
+
return _OVER
|
|
196
|
+
if self.unique and key in self.shown:
|
|
197
|
+
return parent # 이미 나온 메소드: 줄은 생략하고 하위 호출은 위 단계에 붙임
|
|
198
|
+
siblings = parent.children
|
|
199
|
+
if self.collapse and siblings and siblings[-1].key == key:
|
|
200
|
+
siblings[-1].count += 1
|
|
201
|
+
return _DROP # 바로 앞과 같은 호출: ×N으로 접고 하위 호출은 첫 번째 것만 표시
|
|
202
|
+
depth = parent.depth + 1
|
|
162
203
|
if self.max_depth is not None and depth > self.max_depth:
|
|
163
|
-
return
|
|
164
|
-
if self.max_lines is not None and self.
|
|
204
|
+
return _DROP
|
|
205
|
+
if self.max_lines is not None and self.nodes >= self.max_lines:
|
|
165
206
|
self.hidden += 1
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
207
|
+
return _OVER
|
|
208
|
+
node = _Node(key, method, fname, line, folder, depth)
|
|
209
|
+
siblings.append(node)
|
|
210
|
+
self.nodes += 1
|
|
211
|
+
self.shown.add(key)
|
|
212
|
+
return node
|
|
213
|
+
|
|
214
|
+
def print_tree(self, out):
|
|
215
|
+
todo = list(reversed(self.root.children))
|
|
216
|
+
while todo:
|
|
217
|
+
node = todo.pop()
|
|
218
|
+
where = f"{node.fname}:{node.line}" if self.show_line else node.fname
|
|
219
|
+
times = f" ×{node.count}" if node.count > 1 else ""
|
|
220
|
+
print(f"{'│ ' * node.depth}├ {node.method}{times} · {where} · {node.folder}", file=out)
|
|
221
|
+
todo.extend(reversed(node.children))
|
|
222
|
+
if self.hidden:
|
|
223
|
+
print(f"... 출력 제한(max_lines={self.max_lines})으로 {self.hidden}개 호출 생략", file=out)
|
|
170
224
|
|
|
171
|
-
def print_summary(self):
|
|
225
|
+
def print_summary(self, out):
|
|
172
226
|
elapsed = time.perf_counter() - self.started
|
|
173
|
-
if self.hidden:
|
|
174
|
-
print(f"... 출력 제한(max_lines={self.max_lines})으로 {self.hidden}개 호출 생략", file=self.out)
|
|
175
227
|
by_file = {}
|
|
176
228
|
for (path, method), (count, total) in self.stats.items():
|
|
177
229
|
by_file.setdefault(path, []).append((method, count, total))
|
|
@@ -179,12 +231,12 @@ class _Tracer:
|
|
|
179
231
|
print(
|
|
180
232
|
f"[요약] 호출 {calls}회 · 메소드 {len(self.stats)}개 · 파일 {len(by_file)}개"
|
|
181
233
|
f" · 전체 {elapsed:.3f}초",
|
|
182
|
-
file=
|
|
234
|
+
file=out,
|
|
183
235
|
)
|
|
184
236
|
for path, methods in by_file.items():
|
|
185
|
-
print(f"\n{path}", file=
|
|
237
|
+
print(f"\n{path}", file=out)
|
|
186
238
|
for method, count, total in methods:
|
|
187
|
-
print(f" {count:>5}회 {total * 1000:>9.1f}ms {method}", file=
|
|
239
|
+
print(f" {count:>5}회 {total * 1000:>9.1f}ms {method}", file=out)
|
|
188
240
|
|
|
189
241
|
|
|
190
242
|
def print_trace_start(
|
|
@@ -197,10 +249,13 @@ def print_trace_start(
|
|
|
197
249
|
file=None,
|
|
198
250
|
show_line=True,
|
|
199
251
|
hide_private=False,
|
|
252
|
+
collapse=True,
|
|
253
|
+
unique=False,
|
|
200
254
|
threads=False,
|
|
201
255
|
header=True,
|
|
202
256
|
):
|
|
203
|
-
"""지금부터 실행되는 함수 호출을
|
|
257
|
+
"""지금부터 실행되는 함수 호출을 추적한다. print_trace_end()에서 '메소드 · 파일:줄 · 경로'
|
|
258
|
+
트리로 한꺼번에 출력한다 (반복 호출을 접으려면 끝까지 모아야 하므로).
|
|
204
259
|
|
|
205
260
|
packages 추적할 패키지 이름 목록 (예: ["langchain_core", "openai"]).
|
|
206
261
|
지정하면 위치와 상관없이 이 패키지들만 출력한다.
|
|
@@ -213,6 +268,10 @@ def print_trace_start(
|
|
|
213
268
|
show_line 파일 이름 뒤에 함수가 정의된 줄 번호 표시
|
|
214
269
|
hide_private True면 _parse, _Foo.run처럼 '_'로 시작하는 내부용 함수/클래스를 숨김
|
|
215
270
|
(__init__ 같은 특수 메소드는 표시). 숨긴 함수 안에서 부른 공개 함수는 계속 표시
|
|
271
|
+
collapse True면 바로 앞과 같은 메소드가 연달아 호출될 때 'Foo.bar ×3' 한 줄로 접음
|
|
272
|
+
(하위 호출은 첫 번째 것만 표시)
|
|
273
|
+
unique True면 메소드마다 처음 호출만 표시. 이후 같은 메소드는 줄을 생략하고,
|
|
274
|
+
그 안에서 처음 등장하는 메소드만 위 단계에 붙여 표시 → 전체 흐름만 빠르게 파악
|
|
216
275
|
threads True면 이후 새로 시작되는 스레드도 추적
|
|
217
276
|
header 시작/종료 구분선 출력
|
|
218
277
|
"""
|
|
@@ -222,23 +281,25 @@ def print_trace_start(
|
|
|
222
281
|
|
|
223
282
|
owns_file = isinstance(file, (str, os.PathLike))
|
|
224
283
|
out = open(file, "w", encoding="utf-8") if owns_file else (file or sys.stdout)
|
|
225
|
-
tracer = _Tracer(
|
|
284
|
+
tracer = _Tracer(
|
|
285
|
+
packages, stdlib, user, max_depth, max_lines, show_line, hide_private, collapse, unique
|
|
286
|
+
)
|
|
226
287
|
|
|
227
288
|
if header:
|
|
228
289
|
from . import print_section
|
|
229
290
|
|
|
230
291
|
print_section("TRACE START", file=out)
|
|
231
|
-
print("메소드 · 파일:줄 · 경로", file=out)
|
|
232
292
|
|
|
233
293
|
previous = (sys.getprofile(), getattr(threading, "getprofile", lambda: None)())
|
|
234
|
-
_active = (tracer, previous, owns_file, header, threads)
|
|
294
|
+
_active = (tracer, out, previous, owns_file, header, threads)
|
|
235
295
|
if threads:
|
|
236
296
|
threading.setprofile(tracer)
|
|
237
297
|
sys.setprofile(tracer)
|
|
238
298
|
|
|
239
299
|
|
|
240
300
|
def print_trace_end(summary=True):
|
|
241
|
-
"""추적을
|
|
301
|
+
"""추적을 멈추고 호출 트리를 출력한다. summary=True면 파일·메소드별 호출 횟수와
|
|
302
|
+
누적 시간도 출력한다.
|
|
242
303
|
|
|
243
304
|
추적된 호출 목록을 반환한다: [{"depth", "method", "file", "line", "path"}, ...]
|
|
244
305
|
"""
|
|
@@ -246,19 +307,21 @@ def print_trace_end(summary=True):
|
|
|
246
307
|
sys.setprofile(None)
|
|
247
308
|
if _active is None:
|
|
248
309
|
return []
|
|
249
|
-
tracer, (prev_sys, prev_thread), owns_file, header, threads = _active
|
|
310
|
+
tracer, out, (prev_sys, prev_thread), owns_file, header, threads = _active
|
|
250
311
|
_active = None
|
|
251
312
|
sys.setprofile(prev_sys)
|
|
252
313
|
if threads:
|
|
253
314
|
threading.setprofile(prev_thread)
|
|
254
315
|
|
|
255
|
-
|
|
316
|
+
if header:
|
|
317
|
+
print("메소드 · 파일:줄 · 경로", file=out)
|
|
318
|
+
tracer.print_tree(out)
|
|
256
319
|
if header:
|
|
257
320
|
from . import print_section
|
|
258
321
|
|
|
259
322
|
print_section("TRACE END", file=out)
|
|
260
323
|
if summary:
|
|
261
|
-
tracer.print_summary()
|
|
324
|
+
tracer.print_summary(out)
|
|
262
325
|
print(file=out)
|
|
263
326
|
if owns_file:
|
|
264
327
|
out.close()
|
|
@@ -54,7 +54,7 @@ def _lines(out):
|
|
|
54
54
|
|
|
55
55
|
def test_tree_with_file_method_path(fakelib):
|
|
56
56
|
out = io.StringIO()
|
|
57
|
-
print_trace_start(["fakelib_ps"], file=out)
|
|
57
|
+
print_trace_start(["fakelib_ps"], file=out, collapse=False)
|
|
58
58
|
fakelib.Client().run(2)
|
|
59
59
|
records = print_trace_end()
|
|
60
60
|
|
|
@@ -102,7 +102,7 @@ def test_limits(fakelib):
|
|
|
102
102
|
assert len(_lines(out)) == 1
|
|
103
103
|
|
|
104
104
|
out = io.StringIO()
|
|
105
|
-
print_trace_start(["fakelib_ps"], file=out, max_lines=2)
|
|
105
|
+
print_trace_start(["fakelib_ps"], file=out, max_lines=2, collapse=False)
|
|
106
106
|
fakelib.Client().run(3)
|
|
107
107
|
print_trace_end()
|
|
108
108
|
assert len(_lines(out)) == 2
|
|
@@ -131,8 +131,7 @@ def test_hide_private(fakelib):
|
|
|
131
131
|
lines = _lines(out)
|
|
132
132
|
assert lines == [
|
|
133
133
|
"├ Client.run · core.py:3 · fakelib_ps",
|
|
134
|
-
"│ ├ helper · core.py:9 · fakelib_ps", # _step은 숨기고 helper는 run 바로 아래에
|
|
135
|
-
"│ ├ helper · core.py:9 · fakelib_ps",
|
|
134
|
+
"│ ├ helper ×2 · core.py:9 · fakelib_ps", # _step은 숨기고 helper는 run 바로 아래에
|
|
136
135
|
"├ Box.__init__ · core.py:21 · fakelib_ps", # __init__은 표시
|
|
137
136
|
"│ ├ helper · core.py:9 · fakelib_ps", # _Inner.go는 숨김
|
|
138
137
|
]
|
|
@@ -154,3 +153,43 @@ def test_hide_private(fakelib):
|
|
|
154
153
|
)
|
|
155
154
|
def test_is_private(name, private):
|
|
156
155
|
assert _is_private(name) is private
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_collapse_repeats_by_default(fakelib):
|
|
159
|
+
out = io.StringIO()
|
|
160
|
+
print_trace_start(["fakelib_ps"], file=out)
|
|
161
|
+
fakelib.Client().run(3)
|
|
162
|
+
print_trace_end()
|
|
163
|
+
|
|
164
|
+
assert _lines(out) == [
|
|
165
|
+
"├ Client.run · core.py:3 · fakelib_ps",
|
|
166
|
+
"│ ├ Client._step ×3 · core.py:6 · fakelib_ps",
|
|
167
|
+
"│ │ ├ helper · core.py:9 · fakelib_ps", # 하위 호출은 첫 번째 것만
|
|
168
|
+
]
|
|
169
|
+
assert "[요약] 호출 7회" in out.getvalue() # 요약은 접힌 호출까지 모두 셈
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_unique_shows_first_call_only(fakelib):
|
|
173
|
+
out = io.StringIO()
|
|
174
|
+
print_trace_start(["fakelib_ps"], file=out, unique=True, collapse=False)
|
|
175
|
+
fakelib.Client().run(2)
|
|
176
|
+
fakelib.Box()
|
|
177
|
+
print_trace_end()
|
|
178
|
+
|
|
179
|
+
assert _lines(out) == [
|
|
180
|
+
"├ Client.run · core.py:3 · fakelib_ps",
|
|
181
|
+
"│ ├ Client._step · core.py:6 · fakelib_ps",
|
|
182
|
+
"│ │ ├ helper · core.py:9 · fakelib_ps",
|
|
183
|
+
"├ Box.__init__ · core.py:21 · fakelib_ps",
|
|
184
|
+
"│ ├ _Inner.go · core.py:17 · fakelib_ps", # 안의 helper는 이미 나왔으므로 생략
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_tree_printed_at_end(fakelib):
|
|
189
|
+
out = io.StringIO()
|
|
190
|
+
print_trace_start(["fakelib_ps"], file=out)
|
|
191
|
+
fakelib.helper(1)
|
|
192
|
+
print("내 출력", file=out)
|
|
193
|
+
print_trace_end(summary=False)
|
|
194
|
+
text = out.getvalue()
|
|
195
|
+
assert text.index("TRACE START") < text.index("내 출력") < text.index("├ helper") < text.index("TRACE END")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|