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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: printsection
3
- Version: 0.4.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
- - 출력이 너무 많으면 `packages`로 패키지를 좁히고, `hide_private=True`로 내부 함수를 숨기고, `max_depth`로 깊이를 제한해 보세요. 숨긴 함수 안에서 호출된 공개 함수는 그대로 보이고, 가장 가까운 상위 호출 아래에 붙습니다.
164
- - LangChain 같은 큰 라이브러리는 호출이 수천 개 나올 수 있습니다. `packages`나 `max_depth`로 범위를 좁혀 쓰세요.
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
- - 출력이 너무 많으면 `packages`로 패키지를 좁히고, `hide_private=True`로 내부 함수를 숨기고, `max_depth`로 깊이를 제한해 보세요. 숨긴 함수 안에서 호출된 공개 함수는 그대로 보이고, 가장 가까운 상위 호출 아래에 붙습니다.
151
- - LangChain 같은 큰 라이브러리는 호출이 수천 개 나올 수 있습니다. `packages`나 `max_depth`로 범위를 좁혀 쓰세요.
183
+ - `hide_private`나 `unique`로 숨긴 호출 안에서 불린 새 함수는 그대로 보이고, 가장 가까운 상위 호출 아래에 붙습니다.
184
+ - 요약의 호출 횟수·시간에는 접히거나 생략된 호출까지 모두 포함됩니다 (`hide_private`로 숨긴 함수는 제외).
152
185
  - 제너레이터/코루틴(스트리밍, async)은 처음 호출될 때 한 번만 표시합니다.
153
186
  - 추적하는 동안에는 코드가 느려집니다. 공부하거나 디버깅할 때만 쓰세요.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "printsection"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "출력 구간을 나누는 가운데 정렬 구분선 + 라이브러리 호출 흐름 추적 출력"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -19,7 +19,7 @@ __all__ = [
19
19
  "print_trace_end",
20
20
  "trace",
21
21
  ]
22
- __version__ = "0.4.0"
22
+ __version__ = "0.5.0"
23
23
 
24
24
  _FACTORY_DEFAULTS = {
25
25
  "width": 50,
@@ -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__(self, packages, stdlib, user, max_depth, max_lines, out, show_line, hide_private):
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.printed = 0
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.printed >= self.max_lines:
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
- self.printed += 1
168
- where = f"{fname}:{line}" if self.show_line else fname
169
- print(f"{'│ ' * depth}├ {method} · {where} · {folder}", file=self.out)
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=self.out,
234
+ file=out,
183
235
  )
184
236
  for path, methods in by_file.items():
185
- print(f"\n{path}", file=self.out)
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=self.out)
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(packages, stdlib, user, max_depth, max_lines, out, show_line, hide_private)
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
- """추적을 멈추고, summary=True면 파일·메소드별 호출 횟수와 누적 시간을 출력한다.
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
- out = tracer.out
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