printsection 0.2.0__tar.gz → 0.3.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.3.0/PKG-INFO +163 -0
- printsection-0.3.0/README.md +150 -0
- {printsection-0.2.0 → printsection-0.3.0}/pyproject.toml +3 -3
- {printsection-0.2.0 → printsection-0.3.0}/src/printsection/__init__.py +13 -2
- printsection-0.3.0/src/printsection/_trace.py +261 -0
- printsection-0.3.0/tests/test_trace.py +112 -0
- printsection-0.3.0/uv.lock +8 -0
- printsection-0.2.0/PKG-INFO +0 -83
- printsection-0.2.0/README.md +0 -70
- {printsection-0.2.0 → printsection-0.3.0}/LICENSE +0 -0
- {printsection-0.2.0 → printsection-0.3.0}/tests/test_printsection.py +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: printsection
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: 출력 구간을 나누는 가운데 정렬 구분선 + 라이브러리 호출 흐름 추적 출력
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: center,console,debug,divider,korean,print,section,trace
|
|
8
|
+
Classifier: Natural Language :: Korean
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# printsection
|
|
15
|
+
|
|
16
|
+
`print("\n", "실습 결과".center(50, "-"), end="\n\n")` 를 한 줄로.
|
|
17
|
+
출력 결과를 구간별로 나눠 보기 좋게 구분해 줍니다.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install printsection
|
|
21
|
+
# uv 사용 시: uv add printsection
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from printsection import print_section
|
|
26
|
+
|
|
27
|
+
print_section("실습 1")
|
|
28
|
+
print("...")
|
|
29
|
+
print_section("실습 2")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
----------------------실습 1----------------------
|
|
35
|
+
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
----------------------실습 2----------------------
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
위아래 빈 줄과 가운데 정렬이 기본입니다. 한글처럼 화면에서 2칸을 차지하는 글자도 폭을 계산해서 실제 화면 기준으로 가운데에 맞춥니다.
|
|
43
|
+
|
|
44
|
+
## 옵션
|
|
45
|
+
|
|
46
|
+
호출할 때 원하는 값만 바꾸면 됩니다.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
print_section("실습 결과", width=60, fillchar="=")
|
|
50
|
+
print_section("왼쪽 정렬", align="left") # "center" / "left" / "right"
|
|
51
|
+
print_section("빈 줄 조절", top=2, bottom=0)
|
|
52
|
+
print_section("str.center처럼", wide=False) # 글자 수 기준으로 계산
|
|
53
|
+
print_section() # 글자 없이 구분선만
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| 옵션 | 기본값 | 설명 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `width` | `50` | 전체 너비 |
|
|
59
|
+
| `fillchar` | `"-"` | 채움 문자 (한 글자) |
|
|
60
|
+
| `align` | `"center"` | `"center"`, `"left"`, `"right"` |
|
|
61
|
+
| `top` | `1` | 위쪽 빈 줄 수 |
|
|
62
|
+
| `bottom` | `1` | 아래쪽 빈 줄 수 |
|
|
63
|
+
| `wide` | `True` | 한글 등 전각 문자를 2칸으로 계산 |
|
|
64
|
+
| `file` | `sys.stdout` | 출력 대상 |
|
|
65
|
+
|
|
66
|
+
## 기본값 한 번에 바꾸기
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from printsection import print_section, set_defaults, reset_defaults
|
|
70
|
+
|
|
71
|
+
set_defaults(width=70, fillchar="*")
|
|
72
|
+
print_section("1번 문제")
|
|
73
|
+
print_section("2번 문제")
|
|
74
|
+
reset_defaults()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 문자열만 받기
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from printsection import format_section
|
|
81
|
+
|
|
82
|
+
s = format_section("실습 결과") # 출력하지 않고 문자열 반환
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 라이브러리 호출 추적: `print_trace_start` / `print_trace_end`
|
|
86
|
+
|
|
87
|
+
두 함수 사이에서 실행된 **라이브러리 내부 호출**을 순서대로 보여줍니다.
|
|
88
|
+
**메소드 · 파일:줄 · 경로**를 들여쓰기 트리로 출력하고, 끝나면 파일·메소드별로 묶은 요약을 출력합니다.
|
|
89
|
+
라이브러리가 내부에서 어떤 순서로 무엇을 호출하는지 공부할 때 쓰면 좋습니다.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from printsection import print_trace_start, print_trace_end
|
|
93
|
+
from packaging.requirements import Requirement
|
|
94
|
+
|
|
95
|
+
print_trace_start()
|
|
96
|
+
Requirement("requests>=2.0; python_version>'3.8'")
|
|
97
|
+
print_trace_end()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
-------------------TRACE START--------------------
|
|
102
|
+
|
|
103
|
+
메소드 · 파일:줄 · 경로
|
|
104
|
+
├ Requirement.__init__ · requirements.py:82 · packaging
|
|
105
|
+
│ ├ parse_requirement · _parser.py:106 · packaging
|
|
106
|
+
│ │ ├ Tokenizer.__init__ · _tokenizer.py:105 · packaging
|
|
107
|
+
│ │ ├ _parse_requirement · _parser.py:110 · packaging
|
|
108
|
+
│ │ │ ├ Tokenizer.consume · _tokenizer.py:116 · packaging
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
--------------------TRACE END---------------------
|
|
112
|
+
|
|
113
|
+
[요약] 호출 87회 · 메소드 29개 · 파일 6개 · 전체 0.008초
|
|
114
|
+
|
|
115
|
+
packaging/requirements.py
|
|
116
|
+
1회 6.3ms Requirement.__init__
|
|
117
|
+
|
|
118
|
+
packaging/_tokenizer.py
|
|
119
|
+
14회 0.2ms Tokenizer.consume
|
|
120
|
+
33회 0.1ms Tokenizer.check
|
|
121
|
+
...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
기본으로는 `site-packages`에 설치된 라이브러리만 보여줍니다. 내가 작성한 코드와 파이썬 표준 라이브러리는 빠집니다.
|
|
125
|
+
|
|
126
|
+
### 추적 옵션
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
print_trace_start(["langchain_core", "openai"]) # 이 패키지들만 보기
|
|
130
|
+
print_trace_start(max_depth=2) # 3단계 깊이까지만
|
|
131
|
+
print_trace_start(user=True) # 내 코드 호출도 함께
|
|
132
|
+
print_trace_start(file="trace.log") # 화면 대신 파일로 저장
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| 옵션 | 기본값 | 설명 |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `packages` | `None` | 추적할 패키지 이름 목록. 지정하면 위치와 상관없이 이 패키지만 출력 |
|
|
138
|
+
| `stdlib` | `False` | 파이썬 표준 라이브러리 호출도 출력 |
|
|
139
|
+
| `user` | `False` | 내가 작성한 프로젝트 코드 호출도 출력 |
|
|
140
|
+
| `max_depth` | `None` | 이 깊이보다 깊은 호출은 생략 (0부터 시작) |
|
|
141
|
+
| `max_lines` | `2000` | 최대 출력 줄 수. 넘으면 생략하고 개수만 알려줌 (`None`이면 무제한) |
|
|
142
|
+
| `file` | 화면 | 파일 객체 또는 파일 경로 |
|
|
143
|
+
| `show_line` | `True` | 파일 이름 뒤에 함수 정의 줄 번호 표시 |
|
|
144
|
+
| `threads` | `False` | 이후 새로 시작되는 스레드도 추적 |
|
|
145
|
+
| `header` | `True` | 시작/종료 구분선 출력 |
|
|
146
|
+
|
|
147
|
+
`print_trace_end(summary=False)`로 요약 출력을 끌 수 있습니다.
|
|
148
|
+
반환값은 호출 목록입니다: `[{"depth", "method", "file", "line", "path"}, ...]`.
|
|
149
|
+
|
|
150
|
+
`with` 문으로도 쓸 수 있습니다.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from printsection import trace
|
|
154
|
+
|
|
155
|
+
with trace(["langchain_core"], max_depth=3):
|
|
156
|
+
chain.invoke({"question": "안녕"})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 참고
|
|
160
|
+
|
|
161
|
+
- LangChain 같은 큰 라이브러리는 호출이 수천 개 나올 수 있습니다. `packages`나 `max_depth`로 범위를 좁혀 쓰세요.
|
|
162
|
+
- 제너레이터/코루틴(스트리밍, async)은 처음 호출될 때 한 번만 표시합니다.
|
|
163
|
+
- 추적하는 동안에는 코드가 느려집니다. 공부하거나 디버깅할 때만 쓰세요.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# printsection
|
|
2
|
+
|
|
3
|
+
`print("\n", "실습 결과".center(50, "-"), end="\n\n")` 를 한 줄로.
|
|
4
|
+
출력 결과를 구간별로 나눠 보기 좋게 구분해 줍니다.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install printsection
|
|
8
|
+
# uv 사용 시: uv add printsection
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from printsection import print_section
|
|
13
|
+
|
|
14
|
+
print_section("실습 1")
|
|
15
|
+
print("...")
|
|
16
|
+
print_section("실습 2")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
----------------------실습 1----------------------
|
|
22
|
+
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
----------------------실습 2----------------------
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
위아래 빈 줄과 가운데 정렬이 기본입니다. 한글처럼 화면에서 2칸을 차지하는 글자도 폭을 계산해서 실제 화면 기준으로 가운데에 맞춥니다.
|
|
30
|
+
|
|
31
|
+
## 옵션
|
|
32
|
+
|
|
33
|
+
호출할 때 원하는 값만 바꾸면 됩니다.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
print_section("실습 결과", width=60, fillchar="=")
|
|
37
|
+
print_section("왼쪽 정렬", align="left") # "center" / "left" / "right"
|
|
38
|
+
print_section("빈 줄 조절", top=2, bottom=0)
|
|
39
|
+
print_section("str.center처럼", wide=False) # 글자 수 기준으로 계산
|
|
40
|
+
print_section() # 글자 없이 구분선만
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| 옵션 | 기본값 | 설명 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `width` | `50` | 전체 너비 |
|
|
46
|
+
| `fillchar` | `"-"` | 채움 문자 (한 글자) |
|
|
47
|
+
| `align` | `"center"` | `"center"`, `"left"`, `"right"` |
|
|
48
|
+
| `top` | `1` | 위쪽 빈 줄 수 |
|
|
49
|
+
| `bottom` | `1` | 아래쪽 빈 줄 수 |
|
|
50
|
+
| `wide` | `True` | 한글 등 전각 문자를 2칸으로 계산 |
|
|
51
|
+
| `file` | `sys.stdout` | 출력 대상 |
|
|
52
|
+
|
|
53
|
+
## 기본값 한 번에 바꾸기
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from printsection import print_section, set_defaults, reset_defaults
|
|
57
|
+
|
|
58
|
+
set_defaults(width=70, fillchar="*")
|
|
59
|
+
print_section("1번 문제")
|
|
60
|
+
print_section("2번 문제")
|
|
61
|
+
reset_defaults()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 문자열만 받기
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from printsection import format_section
|
|
68
|
+
|
|
69
|
+
s = format_section("실습 결과") # 출력하지 않고 문자열 반환
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 라이브러리 호출 추적: `print_trace_start` / `print_trace_end`
|
|
73
|
+
|
|
74
|
+
두 함수 사이에서 실행된 **라이브러리 내부 호출**을 순서대로 보여줍니다.
|
|
75
|
+
**메소드 · 파일:줄 · 경로**를 들여쓰기 트리로 출력하고, 끝나면 파일·메소드별로 묶은 요약을 출력합니다.
|
|
76
|
+
라이브러리가 내부에서 어떤 순서로 무엇을 호출하는지 공부할 때 쓰면 좋습니다.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from printsection import print_trace_start, print_trace_end
|
|
80
|
+
from packaging.requirements import Requirement
|
|
81
|
+
|
|
82
|
+
print_trace_start()
|
|
83
|
+
Requirement("requests>=2.0; python_version>'3.8'")
|
|
84
|
+
print_trace_end()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
-------------------TRACE START--------------------
|
|
89
|
+
|
|
90
|
+
메소드 · 파일:줄 · 경로
|
|
91
|
+
├ Requirement.__init__ · requirements.py:82 · packaging
|
|
92
|
+
│ ├ parse_requirement · _parser.py:106 · packaging
|
|
93
|
+
│ │ ├ Tokenizer.__init__ · _tokenizer.py:105 · packaging
|
|
94
|
+
│ │ ├ _parse_requirement · _parser.py:110 · packaging
|
|
95
|
+
│ │ │ ├ Tokenizer.consume · _tokenizer.py:116 · packaging
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
--------------------TRACE END---------------------
|
|
99
|
+
|
|
100
|
+
[요약] 호출 87회 · 메소드 29개 · 파일 6개 · 전체 0.008초
|
|
101
|
+
|
|
102
|
+
packaging/requirements.py
|
|
103
|
+
1회 6.3ms Requirement.__init__
|
|
104
|
+
|
|
105
|
+
packaging/_tokenizer.py
|
|
106
|
+
14회 0.2ms Tokenizer.consume
|
|
107
|
+
33회 0.1ms Tokenizer.check
|
|
108
|
+
...
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
기본으로는 `site-packages`에 설치된 라이브러리만 보여줍니다. 내가 작성한 코드와 파이썬 표준 라이브러리는 빠집니다.
|
|
112
|
+
|
|
113
|
+
### 추적 옵션
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
print_trace_start(["langchain_core", "openai"]) # 이 패키지들만 보기
|
|
117
|
+
print_trace_start(max_depth=2) # 3단계 깊이까지만
|
|
118
|
+
print_trace_start(user=True) # 내 코드 호출도 함께
|
|
119
|
+
print_trace_start(file="trace.log") # 화면 대신 파일로 저장
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| 옵션 | 기본값 | 설명 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `packages` | `None` | 추적할 패키지 이름 목록. 지정하면 위치와 상관없이 이 패키지만 출력 |
|
|
125
|
+
| `stdlib` | `False` | 파이썬 표준 라이브러리 호출도 출력 |
|
|
126
|
+
| `user` | `False` | 내가 작성한 프로젝트 코드 호출도 출력 |
|
|
127
|
+
| `max_depth` | `None` | 이 깊이보다 깊은 호출은 생략 (0부터 시작) |
|
|
128
|
+
| `max_lines` | `2000` | 최대 출력 줄 수. 넘으면 생략하고 개수만 알려줌 (`None`이면 무제한) |
|
|
129
|
+
| `file` | 화면 | 파일 객체 또는 파일 경로 |
|
|
130
|
+
| `show_line` | `True` | 파일 이름 뒤에 함수 정의 줄 번호 표시 |
|
|
131
|
+
| `threads` | `False` | 이후 새로 시작되는 스레드도 추적 |
|
|
132
|
+
| `header` | `True` | 시작/종료 구분선 출력 |
|
|
133
|
+
|
|
134
|
+
`print_trace_end(summary=False)`로 요약 출력을 끌 수 있습니다.
|
|
135
|
+
반환값은 호출 목록입니다: `[{"depth", "method", "file", "line", "path"}, ...]`.
|
|
136
|
+
|
|
137
|
+
`with` 문으로도 쓸 수 있습니다.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from printsection import trace
|
|
141
|
+
|
|
142
|
+
with trace(["langchain_core"], max_depth=3):
|
|
143
|
+
chain.invoke({"question": "안녕"})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 참고
|
|
147
|
+
|
|
148
|
+
- LangChain 같은 큰 라이브러리는 호출이 수천 개 나올 수 있습니다. `packages`나 `max_depth`로 범위를 좁혀 쓰세요.
|
|
149
|
+
- 제너레이터/코루틴(스트리밍, async)은 처음 호출될 때 한 번만 표시합니다.
|
|
150
|
+
- 추적하는 동안에는 코드가 느려집니다. 공부하거나 디버깅할 때만 쓰세요.
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "printsection"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "출력 구간을 나누는
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "출력 구간을 나누는 가운데 정렬 구분선 + 라이브러리 호출 흐름 추적 출력"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
11
11
|
license = "MIT"
|
|
12
|
-
keywords = ["print", "section", "divider", "center", "korean", "console"]
|
|
12
|
+
keywords = ["print", "section", "divider", "trace", "debug", "center", "korean", "console"]
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Programming Language :: Python :: 3",
|
|
15
15
|
"Operating System :: OS Independent",
|
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
import sys
|
|
11
11
|
import unicodedata
|
|
12
12
|
|
|
13
|
-
__all__ = [
|
|
14
|
-
|
|
13
|
+
__all__ = [
|
|
14
|
+
"print_section",
|
|
15
|
+
"format_section",
|
|
16
|
+
"set_defaults",
|
|
17
|
+
"reset_defaults",
|
|
18
|
+
"print_trace_start",
|
|
19
|
+
"print_trace_end",
|
|
20
|
+
"trace",
|
|
21
|
+
]
|
|
22
|
+
__version__ = "0.3.0"
|
|
15
23
|
|
|
16
24
|
_FACTORY_DEFAULTS = {
|
|
17
25
|
"width": 50,
|
|
@@ -89,3 +97,6 @@ def reset_defaults():
|
|
|
89
97
|
"""set_defaults로 바꾼 기본값을 처음 상태로 되돌린다."""
|
|
90
98
|
_defaults.clear()
|
|
91
99
|
_defaults.update(_FACTORY_DEFAULTS)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
from ._trace import print_trace_end, print_trace_start, trace # noqa: E402
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""print_trace_start() ~ print_trace_end() 사이에 실행된 라이브러리 호출을 추적해 출력한다.
|
|
2
|
+
|
|
3
|
+
>>> from printsection import print_trace_start, print_trace_end
|
|
4
|
+
>>> print_trace_start()
|
|
5
|
+
>>> chain.invoke("안녕") # 이 사이에서 호출된 라이브러리 함수가 출력됨
|
|
6
|
+
>>> print_trace_end()
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import sysconfig
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
|
|
16
|
+
_CO_GENERATOR_LIKE = 0x20 | 0x80 | 0x200 # GENERATOR | COROUTINE | ASYNC_GENERATOR
|
|
17
|
+
_COMPREHENSIONS = {"<listcomp>", "<dictcomp>", "<setcomp>", "<genexpr>"} # 3.11 이하
|
|
18
|
+
_LIB_MARKERS = ("site-packages", "dist-packages")
|
|
19
|
+
_STDLIB_DIRS = tuple(
|
|
20
|
+
{
|
|
21
|
+
os.path.normcase(os.path.realpath(sysconfig.get_paths()[key])) + os.sep
|
|
22
|
+
for key in ("stdlib", "platstdlib")
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_active = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _qualname(frame):
|
|
30
|
+
"""'클래스.메소드' 형태 이름. 3.11+는 co_qualname, 그 이전은 self/cls로 추정."""
|
|
31
|
+
code = frame.f_code
|
|
32
|
+
name = getattr(code, "co_qualname", None)
|
|
33
|
+
if name:
|
|
34
|
+
return name
|
|
35
|
+
if code.co_argcount:
|
|
36
|
+
first = code.co_varnames[0]
|
|
37
|
+
obj = frame.f_locals.get(first)
|
|
38
|
+
if first == "self" and obj is not None:
|
|
39
|
+
return f"{type(obj).__name__}.{code.co_name}"
|
|
40
|
+
if first == "cls" and isinstance(obj, type):
|
|
41
|
+
return f"{obj.__name__}.{code.co_name}"
|
|
42
|
+
return code.co_name
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _location(filename):
|
|
46
|
+
"""(종류, 표시용 경로). 종류는 'library' / 'stdlib' / 'user'."""
|
|
47
|
+
if filename.startswith("<"):
|
|
48
|
+
return ("stdlib" if filename.startswith("<frozen") else "user"), filename
|
|
49
|
+
real = os.path.realpath(filename)
|
|
50
|
+
norm = os.path.normcase(real)
|
|
51
|
+
for marker in _LIB_MARKERS:
|
|
52
|
+
idx = norm.rfind(os.sep + marker + os.sep)
|
|
53
|
+
if idx != -1:
|
|
54
|
+
return "library", real[idx + len(marker) + 2:].replace("\\", "/")
|
|
55
|
+
if norm.startswith(_STDLIB_DIRS):
|
|
56
|
+
for d in _STDLIB_DIRS:
|
|
57
|
+
if norm.startswith(d):
|
|
58
|
+
return "stdlib", real[len(d):].replace("\\", "/")
|
|
59
|
+
return "user", _short_path(real, norm)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _short_path(real, norm):
|
|
63
|
+
"""현재 폴더 기준 경로. 현재 폴더 밖이면 sys.path 기준 경로 (예: editable 설치)."""
|
|
64
|
+
try:
|
|
65
|
+
rel = os.path.relpath(real)
|
|
66
|
+
if not rel.startswith(".."):
|
|
67
|
+
return rel.replace("\\", "/")
|
|
68
|
+
except ValueError: # 다른 드라이브
|
|
69
|
+
pass
|
|
70
|
+
best = ""
|
|
71
|
+
for entry in sys.path:
|
|
72
|
+
base = os.path.normcase(os.path.realpath(entry or ".")) + os.sep
|
|
73
|
+
if norm.startswith(base) and len(base) > len(best):
|
|
74
|
+
best = base
|
|
75
|
+
return (real[len(best):] if best else real).replace("\\", "/")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _Tracer:
|
|
79
|
+
def __init__(self, packages, stdlib, user, max_depth, max_lines, out, show_line):
|
|
80
|
+
self.packages = tuple(packages or ())
|
|
81
|
+
self.kinds = {"library"} | ({"stdlib"} if stdlib else set()) | ({"user"} if user else set())
|
|
82
|
+
self.max_depth = max_depth
|
|
83
|
+
self.max_lines = max_lines
|
|
84
|
+
self.out = out
|
|
85
|
+
self.show_line = show_line
|
|
86
|
+
|
|
87
|
+
self.code_cache = {} # code -> (경로, 파일명, 폴더) 또는 None(출력 안 함)
|
|
88
|
+
self.stacks = {} # 스레드 id -> [(frame, 시작시각, 통계키)]
|
|
89
|
+
self.seen_gens = {} # id(frame) -> frame, 제너레이터 재개 구분용
|
|
90
|
+
self.stats = {} # (경로, 메소드) -> [횟수, 누적초]
|
|
91
|
+
self.records = []
|
|
92
|
+
self.printed = 0
|
|
93
|
+
self.hidden = 0
|
|
94
|
+
self.started = time.perf_counter()
|
|
95
|
+
|
|
96
|
+
def _code_info(self, frame):
|
|
97
|
+
code = frame.f_code
|
|
98
|
+
try:
|
|
99
|
+
return self.code_cache[code]
|
|
100
|
+
except KeyError:
|
|
101
|
+
pass
|
|
102
|
+
module = frame.f_globals.get("__name__", "") or ""
|
|
103
|
+
info = None
|
|
104
|
+
if not module.startswith("printsection") and code.co_name not in _COMPREHENSIONS:
|
|
105
|
+
kind, path = _location(code.co_filename)
|
|
106
|
+
if self.packages:
|
|
107
|
+
wanted = any(module == p or module.startswith(p + ".") for p in self.packages)
|
|
108
|
+
else:
|
|
109
|
+
wanted = kind in self.kinds
|
|
110
|
+
if wanted:
|
|
111
|
+
folder, _, fname = path.rpartition("/")
|
|
112
|
+
info = (path, fname, folder or ".")
|
|
113
|
+
self.code_cache[code] = info
|
|
114
|
+
return info
|
|
115
|
+
|
|
116
|
+
def __call__(self, frame, event, arg):
|
|
117
|
+
if event == "call":
|
|
118
|
+
self._on_call(frame)
|
|
119
|
+
elif event == "return":
|
|
120
|
+
stack = self.stacks.get(threading.get_ident())
|
|
121
|
+
if stack and stack[-1][0] is frame:
|
|
122
|
+
_, start, key = stack.pop()
|
|
123
|
+
self.stats[key][1] += time.perf_counter() - start
|
|
124
|
+
|
|
125
|
+
def _on_call(self, frame):
|
|
126
|
+
info = self._code_info(frame)
|
|
127
|
+
if info is None:
|
|
128
|
+
return
|
|
129
|
+
if frame.f_code.co_flags & _CO_GENERATOR_LIKE:
|
|
130
|
+
if id(frame) in self.seen_gens:
|
|
131
|
+
return # 제너레이터/코루틴 재개 — 처음 호출만 출력
|
|
132
|
+
self.seen_gens[id(frame)] = frame
|
|
133
|
+
|
|
134
|
+
path, fname, folder = info
|
|
135
|
+
method = _qualname(frame)
|
|
136
|
+
stack = self.stacks.setdefault(threading.get_ident(), [])
|
|
137
|
+
depth = len(stack)
|
|
138
|
+
key = (path, method)
|
|
139
|
+
stat = self.stats.get(key)
|
|
140
|
+
if stat is None:
|
|
141
|
+
self.stats[key] = [1, 0.0]
|
|
142
|
+
else:
|
|
143
|
+
stat[0] += 1
|
|
144
|
+
stack.append((frame, time.perf_counter(), key))
|
|
145
|
+
|
|
146
|
+
line = frame.f_code.co_firstlineno
|
|
147
|
+
self.records.append(
|
|
148
|
+
{"depth": depth, "method": method, "file": fname, "line": line, "path": path}
|
|
149
|
+
)
|
|
150
|
+
if self.max_depth is not None and depth > self.max_depth:
|
|
151
|
+
return
|
|
152
|
+
if self.max_lines is not None and self.printed >= self.max_lines:
|
|
153
|
+
self.hidden += 1
|
|
154
|
+
return
|
|
155
|
+
self.printed += 1
|
|
156
|
+
where = f"{fname}:{line}" if self.show_line else fname
|
|
157
|
+
print(f"{'│ ' * depth}├ {method} · {where} · {folder}", file=self.out)
|
|
158
|
+
|
|
159
|
+
def print_summary(self):
|
|
160
|
+
elapsed = time.perf_counter() - self.started
|
|
161
|
+
if self.hidden:
|
|
162
|
+
print(f"... 출력 제한(max_lines={self.max_lines})으로 {self.hidden}개 호출 생략", file=self.out)
|
|
163
|
+
by_file = {}
|
|
164
|
+
for (path, method), (count, total) in self.stats.items():
|
|
165
|
+
by_file.setdefault(path, []).append((method, count, total))
|
|
166
|
+
calls = sum(c for c, _ in self.stats.values())
|
|
167
|
+
print(
|
|
168
|
+
f"[요약] 호출 {calls}회 · 메소드 {len(self.stats)}개 · 파일 {len(by_file)}개"
|
|
169
|
+
f" · 전체 {elapsed:.3f}초",
|
|
170
|
+
file=self.out,
|
|
171
|
+
)
|
|
172
|
+
for path, methods in by_file.items():
|
|
173
|
+
print(f"\n{path}", file=self.out)
|
|
174
|
+
for method, count, total in methods:
|
|
175
|
+
print(f" {count:>5}회 {total * 1000:>9.1f}ms {method}", file=self.out)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def print_trace_start(
|
|
179
|
+
packages=None,
|
|
180
|
+
*,
|
|
181
|
+
stdlib=False,
|
|
182
|
+
user=False,
|
|
183
|
+
max_depth=None,
|
|
184
|
+
max_lines=2000,
|
|
185
|
+
file=None,
|
|
186
|
+
show_line=True,
|
|
187
|
+
threads=False,
|
|
188
|
+
header=True,
|
|
189
|
+
):
|
|
190
|
+
"""지금부터 실행되는 함수 호출을 추적해 '메소드 · 파일:줄 · 경로' 형태로 출력한다.
|
|
191
|
+
|
|
192
|
+
packages 추적할 패키지 이름 목록 (예: ["langchain_core", "openai"]).
|
|
193
|
+
지정하면 위치와 상관없이 이 패키지들만 출력한다.
|
|
194
|
+
생략하면 site-packages에 설치된 모든 라이브러리를 출력한다.
|
|
195
|
+
stdlib True면 파이썬 표준 라이브러리 호출도 출력
|
|
196
|
+
user True면 내가 작성한 코드(프로젝트 파일) 호출도 출력
|
|
197
|
+
max_depth 이 깊이보다 깊은 호출은 출력하지 않음 (0부터 시작)
|
|
198
|
+
max_lines 최대 출력 줄 수. 넘으면 생략하고 마지막에 개수만 알려줌 (None이면 무제한)
|
|
199
|
+
file 출력 대상. 파일 객체 또는 파일 경로 문자열 (기본: 화면)
|
|
200
|
+
show_line 파일 이름 뒤에 함수가 정의된 줄 번호 표시
|
|
201
|
+
threads True면 이후 새로 시작되는 스레드도 추적
|
|
202
|
+
header 시작/종료 구분선 출력
|
|
203
|
+
"""
|
|
204
|
+
global _active
|
|
205
|
+
if _active is not None:
|
|
206
|
+
print_trace_end(summary=False)
|
|
207
|
+
|
|
208
|
+
owns_file = isinstance(file, (str, os.PathLike))
|
|
209
|
+
out = open(file, "w", encoding="utf-8") if owns_file else (file or sys.stdout)
|
|
210
|
+
tracer = _Tracer(packages, stdlib, user, max_depth, max_lines, out, show_line)
|
|
211
|
+
|
|
212
|
+
if header:
|
|
213
|
+
from . import print_section
|
|
214
|
+
|
|
215
|
+
print_section("TRACE START", file=out)
|
|
216
|
+
print("메소드 · 파일:줄 · 경로", file=out)
|
|
217
|
+
|
|
218
|
+
previous = (sys.getprofile(), getattr(threading, "getprofile", lambda: None)())
|
|
219
|
+
_active = (tracer, previous, owns_file, header, threads)
|
|
220
|
+
if threads:
|
|
221
|
+
threading.setprofile(tracer)
|
|
222
|
+
sys.setprofile(tracer)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def print_trace_end(summary=True):
|
|
226
|
+
"""추적을 멈추고, summary=True면 파일·메소드별 호출 횟수와 누적 시간을 출력한다.
|
|
227
|
+
|
|
228
|
+
추적된 호출 목록을 반환한다: [{"depth", "method", "file", "line", "path"}, ...]
|
|
229
|
+
"""
|
|
230
|
+
global _active
|
|
231
|
+
sys.setprofile(None)
|
|
232
|
+
if _active is None:
|
|
233
|
+
return []
|
|
234
|
+
tracer, (prev_sys, prev_thread), owns_file, header, threads = _active
|
|
235
|
+
_active = None
|
|
236
|
+
sys.setprofile(prev_sys)
|
|
237
|
+
if threads:
|
|
238
|
+
threading.setprofile(prev_thread)
|
|
239
|
+
|
|
240
|
+
out = tracer.out
|
|
241
|
+
if header:
|
|
242
|
+
from . import print_section
|
|
243
|
+
|
|
244
|
+
print_section("TRACE END", file=out)
|
|
245
|
+
if summary:
|
|
246
|
+
tracer.print_summary()
|
|
247
|
+
print(file=out)
|
|
248
|
+
if owns_file:
|
|
249
|
+
out.close()
|
|
250
|
+
tracer.seen_gens.clear()
|
|
251
|
+
return tracer.records
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@contextmanager
|
|
255
|
+
def trace(packages=None, **options):
|
|
256
|
+
"""with 문으로 쓰는 버전. with trace(): ... 블록 안의 호출만 추적한다."""
|
|
257
|
+
print_trace_start(packages, **options)
|
|
258
|
+
try:
|
|
259
|
+
yield
|
|
260
|
+
finally:
|
|
261
|
+
print_trace_end()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import io
|
|
3
|
+
import sys
|
|
4
|
+
import textwrap
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from printsection import print_trace_end, print_trace_start, trace
|
|
9
|
+
|
|
10
|
+
FAKELIB = textwrap.dedent(
|
|
11
|
+
"""
|
|
12
|
+
class Client:
|
|
13
|
+
def run(self, n):
|
|
14
|
+
return [self._step(i) for i in range(n)]
|
|
15
|
+
|
|
16
|
+
def _step(self, i):
|
|
17
|
+
return helper(i)
|
|
18
|
+
|
|
19
|
+
def helper(i):
|
|
20
|
+
return i * 2
|
|
21
|
+
|
|
22
|
+
def gen():
|
|
23
|
+
yield 1
|
|
24
|
+
yield 2
|
|
25
|
+
"""
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def fakelib(tmp_path, monkeypatch):
|
|
31
|
+
pkg = tmp_path / "fakelib_ps"
|
|
32
|
+
pkg.mkdir()
|
|
33
|
+
(pkg / "__init__.py").write_text("")
|
|
34
|
+
(pkg / "core.py").write_text(FAKELIB)
|
|
35
|
+
monkeypatch.syspath_prepend(str(tmp_path))
|
|
36
|
+
mod = importlib.import_module("fakelib_ps.core")
|
|
37
|
+
yield mod
|
|
38
|
+
sys.modules.pop("fakelib_ps.core", None)
|
|
39
|
+
sys.modules.pop("fakelib_ps", None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _lines(out):
|
|
43
|
+
return [l for l in out.getvalue().splitlines() if "├" in l]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_tree_with_file_method_path(fakelib):
|
|
47
|
+
out = io.StringIO()
|
|
48
|
+
print_trace_start(["fakelib_ps"], file=out)
|
|
49
|
+
fakelib.Client().run(2)
|
|
50
|
+
records = print_trace_end()
|
|
51
|
+
|
|
52
|
+
lines = _lines(out)
|
|
53
|
+
assert lines[0] == "├ Client.run · core.py:3 · fakelib_ps"
|
|
54
|
+
assert lines[1].startswith("│ ├ Client._step · core.py:6")
|
|
55
|
+
assert lines[2].startswith("│ │ ├ helper · core.py:9")
|
|
56
|
+
assert len(lines) == 5
|
|
57
|
+
assert [r["depth"] for r in records] == [0, 1, 2, 1, 2]
|
|
58
|
+
text = out.getvalue()
|
|
59
|
+
assert "TRACE START" in text and "TRACE END" in text
|
|
60
|
+
assert "[요약] 호출 5회 · 메소드 3개 · 파일 1개" in text
|
|
61
|
+
assert "fakelib_ps/core.py" in text
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_default_skips_non_library_code(fakelib):
|
|
65
|
+
out = io.StringIO()
|
|
66
|
+
print_trace_start(file=out) # tmp 폴더 코드는 site-packages가 아니므로 출력 안 됨
|
|
67
|
+
fakelib.helper(1)
|
|
68
|
+
assert print_trace_end() == []
|
|
69
|
+
assert _lines(out) == []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_user_option_includes_project_code(fakelib):
|
|
73
|
+
out = io.StringIO()
|
|
74
|
+
print_trace_start(user=True, file=out, header=False)
|
|
75
|
+
fakelib.helper(1)
|
|
76
|
+
print_trace_end(summary=False)
|
|
77
|
+
assert any("helper" in l for l in _lines(out))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_generator_resume_printed_once(fakelib):
|
|
81
|
+
out = io.StringIO()
|
|
82
|
+
print_trace_start(["fakelib_ps"], file=out, header=False)
|
|
83
|
+
list(fakelib.gen())
|
|
84
|
+
print_trace_end(summary=False)
|
|
85
|
+
assert [l for l in _lines(out) if "gen" in l] == ["├ gen · core.py:12 · fakelib_ps"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_limits(fakelib):
|
|
89
|
+
out = io.StringIO()
|
|
90
|
+
print_trace_start(["fakelib_ps"], file=out, max_depth=0)
|
|
91
|
+
fakelib.Client().run(3)
|
|
92
|
+
print_trace_end()
|
|
93
|
+
assert len(_lines(out)) == 1
|
|
94
|
+
|
|
95
|
+
out = io.StringIO()
|
|
96
|
+
print_trace_start(["fakelib_ps"], file=out, max_lines=2)
|
|
97
|
+
fakelib.Client().run(3)
|
|
98
|
+
print_trace_end()
|
|
99
|
+
assert len(_lines(out)) == 2
|
|
100
|
+
assert "5개 호출 생략" in out.getvalue()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_file_path_and_context_manager(fakelib, tmp_path):
|
|
104
|
+
log = tmp_path / "trace.log"
|
|
105
|
+
with trace(["fakelib_ps"], file=str(log)):
|
|
106
|
+
fakelib.helper(1)
|
|
107
|
+
assert "├ helper" in log.read_text(encoding="utf-8")
|
|
108
|
+
assert sys.getprofile() is None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_end_without_start_is_safe():
|
|
112
|
+
assert print_trace_end() == []
|
printsection-0.2.0/PKG-INFO
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: printsection
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: 출력 구간을 나누는 위아래 빈 줄 + 가운데 정렬 구분선 (한글 폭 자동 보정)
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Keywords: center,console,divider,korean,print,section
|
|
8
|
-
Classifier: Natural Language :: Korean
|
|
9
|
-
Classifier: Operating System :: OS Independent
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Requires-Python: >=3.8
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
|
-
# printsection
|
|
15
|
-
|
|
16
|
-
`print("\n", "실습 결과".center(50, "-"), end="\n\n")` 를 한 줄로.
|
|
17
|
-
출력 결과를 구간별로 나눠 보기 좋게 구분해 줍니다.
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
pip install printsection
|
|
21
|
-
# uv 사용 시: uv add printsection
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
```python
|
|
25
|
-
from printsection import print_section
|
|
26
|
-
|
|
27
|
-
print_section("실습 1")
|
|
28
|
-
print("...")
|
|
29
|
-
print_section("실습 2")
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
----------------------실습 1----------------------
|
|
35
|
-
|
|
36
|
-
...
|
|
37
|
-
|
|
38
|
-
----------------------실습 2----------------------
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
위아래 빈 줄과 가운데 정렬이 기본입니다. 한글처럼 화면에서 2칸을 차지하는 글자도 폭을 계산해서 실제 화면 기준으로 가운데에 맞춥니다.
|
|
43
|
-
|
|
44
|
-
## 옵션
|
|
45
|
-
|
|
46
|
-
호출할 때 원하는 값만 바꾸면 됩니다.
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
print_section("실습 결과", width=60, fillchar="=")
|
|
50
|
-
print_section("왼쪽 정렬", align="left") # "center" / "left" / "right"
|
|
51
|
-
print_section("빈 줄 조절", top=2, bottom=0)
|
|
52
|
-
print_section("str.center처럼", wide=False) # 글자 수 기준으로 계산
|
|
53
|
-
print_section() # 글자 없이 구분선만
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
| 옵션 | 기본값 | 설명 |
|
|
57
|
-
|---|---|---|
|
|
58
|
-
| `width` | `50` | 전체 너비 |
|
|
59
|
-
| `fillchar` | `"-"` | 채움 문자 (한 글자) |
|
|
60
|
-
| `align` | `"center"` | `"center"`, `"left"`, `"right"` |
|
|
61
|
-
| `top` | `1` | 위쪽 빈 줄 수 |
|
|
62
|
-
| `bottom` | `1` | 아래쪽 빈 줄 수 |
|
|
63
|
-
| `wide` | `True` | 한글 등 전각 문자를 2칸으로 계산 |
|
|
64
|
-
| `file` | `sys.stdout` | 출력 대상 |
|
|
65
|
-
|
|
66
|
-
## 기본값 한 번에 바꾸기
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
from printsection import print_section, set_defaults, reset_defaults
|
|
70
|
-
|
|
71
|
-
set_defaults(width=70, fillchar="*")
|
|
72
|
-
print_section("1번 문제")
|
|
73
|
-
print_section("2번 문제")
|
|
74
|
-
reset_defaults()
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## 문자열만 받기
|
|
78
|
-
|
|
79
|
-
```python
|
|
80
|
-
from printsection import format_section
|
|
81
|
-
|
|
82
|
-
s = format_section("실습 결과") # 출력하지 않고 문자열 반환
|
|
83
|
-
```
|
printsection-0.2.0/README.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# printsection
|
|
2
|
-
|
|
3
|
-
`print("\n", "실습 결과".center(50, "-"), end="\n\n")` 를 한 줄로.
|
|
4
|
-
출력 결과를 구간별로 나눠 보기 좋게 구분해 줍니다.
|
|
5
|
-
|
|
6
|
-
```bash
|
|
7
|
-
pip install printsection
|
|
8
|
-
# uv 사용 시: uv add printsection
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
```python
|
|
12
|
-
from printsection import print_section
|
|
13
|
-
|
|
14
|
-
print_section("실습 1")
|
|
15
|
-
print("...")
|
|
16
|
-
print_section("실습 2")
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
----------------------실습 1----------------------
|
|
22
|
-
|
|
23
|
-
...
|
|
24
|
-
|
|
25
|
-
----------------------실습 2----------------------
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
위아래 빈 줄과 가운데 정렬이 기본입니다. 한글처럼 화면에서 2칸을 차지하는 글자도 폭을 계산해서 실제 화면 기준으로 가운데에 맞춥니다.
|
|
30
|
-
|
|
31
|
-
## 옵션
|
|
32
|
-
|
|
33
|
-
호출할 때 원하는 값만 바꾸면 됩니다.
|
|
34
|
-
|
|
35
|
-
```python
|
|
36
|
-
print_section("실습 결과", width=60, fillchar="=")
|
|
37
|
-
print_section("왼쪽 정렬", align="left") # "center" / "left" / "right"
|
|
38
|
-
print_section("빈 줄 조절", top=2, bottom=0)
|
|
39
|
-
print_section("str.center처럼", wide=False) # 글자 수 기준으로 계산
|
|
40
|
-
print_section() # 글자 없이 구분선만
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
| 옵션 | 기본값 | 설명 |
|
|
44
|
-
|---|---|---|
|
|
45
|
-
| `width` | `50` | 전체 너비 |
|
|
46
|
-
| `fillchar` | `"-"` | 채움 문자 (한 글자) |
|
|
47
|
-
| `align` | `"center"` | `"center"`, `"left"`, `"right"` |
|
|
48
|
-
| `top` | `1` | 위쪽 빈 줄 수 |
|
|
49
|
-
| `bottom` | `1` | 아래쪽 빈 줄 수 |
|
|
50
|
-
| `wide` | `True` | 한글 등 전각 문자를 2칸으로 계산 |
|
|
51
|
-
| `file` | `sys.stdout` | 출력 대상 |
|
|
52
|
-
|
|
53
|
-
## 기본값 한 번에 바꾸기
|
|
54
|
-
|
|
55
|
-
```python
|
|
56
|
-
from printsection import print_section, set_defaults, reset_defaults
|
|
57
|
-
|
|
58
|
-
set_defaults(width=70, fillchar="*")
|
|
59
|
-
print_section("1번 문제")
|
|
60
|
-
print_section("2번 문제")
|
|
61
|
-
reset_defaults()
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## 문자열만 받기
|
|
65
|
-
|
|
66
|
-
```python
|
|
67
|
-
from printsection import format_section
|
|
68
|
-
|
|
69
|
-
s = format_section("실습 결과") # 출력하지 않고 문자열 반환
|
|
70
|
-
```
|
|
File without changes
|
|
File without changes
|