indentoken 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rockmizu
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.
@@ -0,0 +1,395 @@
1
+ Metadata-Version: 2.4
2
+ Name: indentoken
3
+ Version: 1.0.0
4
+ Summary: An indentation token which helps you manage text indentations.
5
+ Keywords: indentation,indent,dedent,text formatting,output formatting
6
+ Author: Rockmizu
7
+ Author-email: Rockmizu <Rockmizu@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: Text Processing :: General
16
+ Classifier: Topic :: Utilities
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # indentoken
22
+
23
+ A magical little token to help you manage string indentation.
24
+
25
+ ## Table of Contents
26
+
27
+ * [Motivation](#motivation)
28
+ * [Features](#features)
29
+ * [Requirements](#requirements)
30
+ * [Installation](#installation)
31
+ * [Usage](#usage)
32
+ * [Basic Indentation and Dedentation](#basic-indentation-and-dedentation)
33
+ * [Using Context Manager for Indentation](#using-context-manager-for-indentation)
34
+ * [Multi-line Text Indentation](#multi-line-text-indentation)
35
+ * [Initialization](#initialization)
36
+ * [Padding](#padding)
37
+ * [Indentation Addition](#indentation-addition)
38
+ * [Multi-parameter Print](#multi-parameter-print)
39
+ * [License](#license)
40
+
41
+ ## Motivation
42
+
43
+ Have you ever wanted your program output to look like this?
44
+
45
+ ```text
46
+ food
47
+ fruit
48
+ apple
49
+ banana
50
+ --------
51
+ meat
52
+ pork
53
+ beef
54
+ --------
55
+ ```
56
+
57
+ Normally, you need to manually track the current indentation depth:
58
+
59
+ ```python
60
+ foods = {'fruit': ['apple', 'banana'], 'meat': ['pork', 'beef']}
61
+
62
+ print('food')
63
+ for category, items in foods.items():
64
+ print(f' {category}')
65
+ # ^^ manually track the indentations
66
+ for item in items:
67
+ print(f' {item}')
68
+ # ^^^^ manually track the indentations
69
+ print(f' --------')
70
+ # ^^ manually track the indentations
71
+ ```
72
+
73
+ This method is not only very prone to errors, but it becomes even harder
74
+ to track the preceding whitespace when breaking down sub-loops
75
+ into functions.
76
+
77
+ How great would it be if there was a tool to help you track indentation.
78
+
79
+ This is exactly where indentoken can help!
80
+
81
+ Now you can write:
82
+
83
+ ```python
84
+ from indentoken import Indentation
85
+
86
+ ind = Indentation()
87
+
88
+ foods = {'fruit': ['apple', 'banana'], 'meat': ['pork', 'beef']}
89
+
90
+ print('food')
91
+
92
+ with ind.indented_context():
93
+ for category, items in foods.items():
94
+ print(f'{ind}{category}')
95
+
96
+ with ind.indented_context():
97
+ for item in items:
98
+ print(f'{ind}{item}')
99
+
100
+ print(f'{ind}--------')
101
+ ```
102
+
103
+ Notice how the manual whitespace disappears, replaced by an `ind` object
104
+ that tracks the indentation depth and converts it to a string for you.
105
+
106
+ You can also pass it into a function to continuously track
107
+ the current indentation:
108
+
109
+ ```python
110
+ from collections.abc import Iterable
111
+
112
+ from indentoken import Indentation
113
+
114
+
115
+ def main() -> None:
116
+ ind = Indentation()
117
+
118
+ foods = {'fruit': ['apple', 'banana'], 'meat': ['pork', 'beef']}
119
+
120
+ print('food')
121
+
122
+ with ind.indented_context():
123
+ for category, items in foods.items():
124
+ print(f'{ind}{category}')
125
+
126
+ with ind.indented_context():
127
+ show_food_items(items, ind=ind)
128
+
129
+ print(f'{ind}--------')
130
+
131
+
132
+ def show_food_items(items: Iterable[str], *, ind: Indentation) -> None:
133
+ for item in items:
134
+ print(f'{ind}{item}')
135
+
136
+
137
+ if __name__ == '__main__':
138
+ main()
139
+ ```
140
+
141
+ There are more features, please see [Usage](#usage).
142
+
143
+ ## Features
144
+
145
+ 1. Multifunctional and convenient indentation tracking token.
146
+ 2. Complete type annotations, supporting modern type checking
147
+ and type-safe development.
148
+ 3. Zero package dependencies, so you don't have to worry about conflicts with
149
+ other packages in your virtual environment during installation.
150
+ 4. Pure Python package, no C extensions, usable anywhere Python runs.
151
+
152
+ ## Requirements
153
+
154
+ This package supports Python 3.10 and above.
155
+
156
+ ## Installation
157
+
158
+ You can install this package using pip:
159
+
160
+ ```bash
161
+ pip install indentoken
162
+ ```
163
+
164
+ ## Usage
165
+
166
+ The following examples assume you have imported:
167
+
168
+ ```python
169
+ from indentoken import Indentation
170
+ ```
171
+
172
+ ### Basic Indentation and Dedentation
173
+
174
+ ```python
175
+ ind = Indentation()
176
+
177
+ # Have no indentation at all initally.
178
+ print(f'{ind}Line 1') # |Line 1
179
+
180
+ # Indent by 1 level.
181
+ ind.indent()
182
+ print(f'{ind}Line 2') # | Line 2
183
+
184
+ # Indent by 3 levels, plus the previous 1 level.
185
+ # Now the indentation is 4 levels deep.
186
+ ind.indent(3)
187
+ print(f'{ind}Line 3') # | Line 3
188
+
189
+ # Dedent by 1 level.
190
+ # Now the indentation is 3 levels deep.
191
+ ind.dedent()
192
+ print(f'{ind}Line 4') # | Line 4
193
+
194
+ # Dedent by 99 levels.
195
+ # Note that the indentation level is always non-negative.
196
+ # The final indentation level will be clamped at 0.
197
+ ind.dedent(99)
198
+ assert ind.level == 0
199
+ print(f'{ind}Line 5') # |Line 5
200
+
201
+ # Directly set the current indentation level to 3.
202
+ ind.level = 3
203
+ print(f'{ind}Line 6') # | Line 6
204
+ ```
205
+
206
+ ### Using Context Manager for Indentation
207
+
208
+ If you want to increase indentation within a certain block, I recommend
209
+ using `with` combined with `indented_context()`.
210
+ This ensures that the indentation automatically reverts to
211
+ the correct depth even if an exception is raised.
212
+
213
+ ```python
214
+ ind = Indentation()
215
+
216
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
217
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
218
+ with ind.indented_context():
219
+ print(f'{ind}This line is indented.') # | This line is indented.
220
+ print(f'{ind}This line is indented.') # | This line is indented.
221
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
222
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
223
+ with ind.indented_context(2):
224
+ print(f'{ind}This line is indented by 2 levels.') # | This line is indented by 2 levels.
225
+ print(f'{ind}This line is indented by 2 levels.') # | This line is indented by 2 levels.
226
+ with ind.indented_context(1): # nested indented context will stack
227
+ print(f'{ind}This line is indented by 3 levels.') # | This line is indented by 3 levels.
228
+ print(f'{ind}This line is indented by 3 levels.') # | This line is indented by 3 levels.
229
+ print(f'{ind}This line is indented by 2 levels.') # | This line is indented by 2 levels.
230
+ ```
231
+
232
+ The output you will get is like this:
233
+
234
+ ```text
235
+ This line is NOT indented.
236
+ This line is NOT indented.
237
+ This line is indented.
238
+ This line is indented.
239
+ This line is NOT indented.
240
+ This line is NOT indented.
241
+ This line is indented by 2 levels.
242
+ This line is indented by 2 levels.
243
+ This line is indented by 3 levels.
244
+ This line is indented by 3 levels.
245
+ This line is indented by 2 levels.
246
+ ```
247
+
248
+ ### Multi-line Text Indentation
249
+
250
+ For multi-line text, you can directly wrap it in `ind`
251
+ or use the `apply_to()` method:
252
+
253
+ ```python
254
+ ind = Indentation(level=1)
255
+
256
+ print('No indentation.')
257
+ print(ind('This is a multi-line text.\nAll lines will be indented.\nThe third line.'))
258
+ # ind('something') is equivalent to ind.apply_to('something')
259
+ ```
260
+
261
+ It will output:
262
+
263
+ ```text
264
+ No indentation.
265
+ This is a multi-line text.
266
+ All lines will be indented.
267
+ The third line.
268
+ ```
269
+
270
+ ### Initialization
271
+
272
+ During initialization, you can specify the indentation characters,
273
+ initial indentation depth, and padding (which will be mentioned later).
274
+
275
+ ```python
276
+ ind = Indentation(word='-->', level=2)
277
+ print(f'{ind}Line 1') # |-->-->Line 1
278
+ ind.indent()
279
+ print(f'{ind}Line 2') # |-->-->-->Line 2
280
+ ```
281
+
282
+ ### Padding
283
+
284
+ If you want the starting point of the indentation not to begin
285
+ from the far left, you can specify padding:
286
+
287
+ ```python
288
+ # Use "+++++" as padding for visualization. You can use whitespaces.
289
+ ind = Indentation(word='-->', padding='+++++')
290
+ print(f'{ind}Line 1') # |+++++Line 1
291
+ ind.indent()
292
+ print(f'{ind}Line 2') # |+++++-->Line 2
293
+ ind.indent()
294
+ print(f'{ind}Line 3') # |+++++-->-->Line 3
295
+ ```
296
+
297
+ Padding does not change with indentation depth and it always exists,
298
+ even if the indentation depth is zero.
299
+
300
+ You can also specify the `Indentation` object as padding,
301
+ which gives you dynamic padding effects:
302
+
303
+ ```python
304
+ pad = Indentation(word='++', level=2)
305
+ ind = Indentation(word='-->', padding=pad)
306
+ print(f'{ind}Line 1') # |++++Line 1
307
+ ind.indent()
308
+ print(f'{ind}Line 2') # |++++-->Line 2
309
+ ind.indent()
310
+ print(f'{ind}Line 3') # |++++-->-->Line 3
311
+ pad.dedent() # Note that the `pad` object is changing, not `ind`.
312
+ print(f'{ind}Line 4') # |++-->-->Line 4
313
+
314
+ # Convert the padding to `str` on init if you want a fixed padding.
315
+ pad = Indentation(word='++', level=2)
316
+ ind = Indentation(word='-->', level=1, padding=str(pad))
317
+ print(f'{ind}Line 5') # |++++-->Line 5
318
+ pad.dedent() # Note that the `pad` object is changing, not `ind`.
319
+ print(f'{ind}Line 6') # |++++-->Line 6
320
+ ```
321
+
322
+ ### Indentation Addition
323
+
324
+ If you temporarily need to deepen the indentation
325
+ without using the heavy `with` statement, you can directly add
326
+ the desired indentation depth to the indentation object.
327
+
328
+ Additionally, `Indentation` addition only changes
329
+ the indentation depth and does not change the padding.
330
+
331
+ ```python
332
+ ind = Indentation(word='-->', level=1)
333
+ print(f'{ind}one level indented') # |-->one level indented
334
+ print(f'{ind}one level indented') # |-->one level indented
335
+ print(f'{ind + 1}one level deeper, two levels indented') # |-->-->one level deeper, two levels indented
336
+ print(f'{ind + 2}two levels deeper, three levels indented') # |-->-->-->two levels deeper, three levels indented
337
+ print(f'{ind}one level indented') # |-->one level indented
338
+ print(f'{ind}one level indented') # |-->one level indented
339
+
340
+ ind = Indentation(word='-->', level=1, padding='++')
341
+ print(f'{ind}one level indented') # |++-->one level indented
342
+ print(f'{ind + 1}one level deeper, two levels indented') # |++-->-->one level deeper, two levels indented
343
+ ```
344
+
345
+ Indentation addition is commutative:
346
+
347
+ ```python
348
+ ind = Indentation(word='-->', level=2)
349
+ assert ind + 1 == 1 + ind
350
+ ```
351
+
352
+ ### Multi-parameter Print
353
+
354
+ If you want the following usage of `print` to also receive indentation,
355
+ you can wrap the `print` function with the `Indentation` object.
356
+
357
+ ```python
358
+ print('Alice', 'Bob', sep=' & ') # |Alice & Bob
359
+
360
+ ind = Indentation(word='-->', level=1)
361
+ ind(print)('Alice', 'Bob', sep=' & ') # |-->Alice & Bob
362
+ # Note that the parentheses wrap the `print`, not the entire statement.
363
+ ```
364
+
365
+ Note that if you save `ind(print)` for later use, it will bind
366
+ this `Indentation` object.
367
+ If you do not want the `print` to change with the `Indentation` object,
368
+ you can use the `fixed()` method.
369
+
370
+ ```python
371
+ ind = Indentation(word='-->', level=1)
372
+
373
+ print_indented = ind(print)
374
+
375
+ print_indented('Alice') # |-->Alice
376
+
377
+ ind.indent()
378
+ print_indented('Bob') # |-->-->Bob
379
+
380
+ # ===========================================
381
+ ind = Indentation(word='-->', level=1)
382
+
383
+ # Use ind.fixed() to fixed the indentation.
384
+ print_indented_fixed = ind.fixed(print)
385
+
386
+ print_indented_fixed('Alice') # |-->Alice
387
+
388
+ ind.indent()
389
+ print_indented_fixed('Bob') # |-->Bob
390
+ ```
391
+
392
+ ## License
393
+
394
+ This software is distributed under the MIT License.
395
+ Please see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,375 @@
1
+ # indentoken
2
+
3
+ A magical little token to help you manage string indentation.
4
+
5
+ ## Table of Contents
6
+
7
+ * [Motivation](#motivation)
8
+ * [Features](#features)
9
+ * [Requirements](#requirements)
10
+ * [Installation](#installation)
11
+ * [Usage](#usage)
12
+ * [Basic Indentation and Dedentation](#basic-indentation-and-dedentation)
13
+ * [Using Context Manager for Indentation](#using-context-manager-for-indentation)
14
+ * [Multi-line Text Indentation](#multi-line-text-indentation)
15
+ * [Initialization](#initialization)
16
+ * [Padding](#padding)
17
+ * [Indentation Addition](#indentation-addition)
18
+ * [Multi-parameter Print](#multi-parameter-print)
19
+ * [License](#license)
20
+
21
+ ## Motivation
22
+
23
+ Have you ever wanted your program output to look like this?
24
+
25
+ ```text
26
+ food
27
+ fruit
28
+ apple
29
+ banana
30
+ --------
31
+ meat
32
+ pork
33
+ beef
34
+ --------
35
+ ```
36
+
37
+ Normally, you need to manually track the current indentation depth:
38
+
39
+ ```python
40
+ foods = {'fruit': ['apple', 'banana'], 'meat': ['pork', 'beef']}
41
+
42
+ print('food')
43
+ for category, items in foods.items():
44
+ print(f' {category}')
45
+ # ^^ manually track the indentations
46
+ for item in items:
47
+ print(f' {item}')
48
+ # ^^^^ manually track the indentations
49
+ print(f' --------')
50
+ # ^^ manually track the indentations
51
+ ```
52
+
53
+ This method is not only very prone to errors, but it becomes even harder
54
+ to track the preceding whitespace when breaking down sub-loops
55
+ into functions.
56
+
57
+ How great would it be if there was a tool to help you track indentation.
58
+
59
+ This is exactly where indentoken can help!
60
+
61
+ Now you can write:
62
+
63
+ ```python
64
+ from indentoken import Indentation
65
+
66
+ ind = Indentation()
67
+
68
+ foods = {'fruit': ['apple', 'banana'], 'meat': ['pork', 'beef']}
69
+
70
+ print('food')
71
+
72
+ with ind.indented_context():
73
+ for category, items in foods.items():
74
+ print(f'{ind}{category}')
75
+
76
+ with ind.indented_context():
77
+ for item in items:
78
+ print(f'{ind}{item}')
79
+
80
+ print(f'{ind}--------')
81
+ ```
82
+
83
+ Notice how the manual whitespace disappears, replaced by an `ind` object
84
+ that tracks the indentation depth and converts it to a string for you.
85
+
86
+ You can also pass it into a function to continuously track
87
+ the current indentation:
88
+
89
+ ```python
90
+ from collections.abc import Iterable
91
+
92
+ from indentoken import Indentation
93
+
94
+
95
+ def main() -> None:
96
+ ind = Indentation()
97
+
98
+ foods = {'fruit': ['apple', 'banana'], 'meat': ['pork', 'beef']}
99
+
100
+ print('food')
101
+
102
+ with ind.indented_context():
103
+ for category, items in foods.items():
104
+ print(f'{ind}{category}')
105
+
106
+ with ind.indented_context():
107
+ show_food_items(items, ind=ind)
108
+
109
+ print(f'{ind}--------')
110
+
111
+
112
+ def show_food_items(items: Iterable[str], *, ind: Indentation) -> None:
113
+ for item in items:
114
+ print(f'{ind}{item}')
115
+
116
+
117
+ if __name__ == '__main__':
118
+ main()
119
+ ```
120
+
121
+ There are more features, please see [Usage](#usage).
122
+
123
+ ## Features
124
+
125
+ 1. Multifunctional and convenient indentation tracking token.
126
+ 2. Complete type annotations, supporting modern type checking
127
+ and type-safe development.
128
+ 3. Zero package dependencies, so you don't have to worry about conflicts with
129
+ other packages in your virtual environment during installation.
130
+ 4. Pure Python package, no C extensions, usable anywhere Python runs.
131
+
132
+ ## Requirements
133
+
134
+ This package supports Python 3.10 and above.
135
+
136
+ ## Installation
137
+
138
+ You can install this package using pip:
139
+
140
+ ```bash
141
+ pip install indentoken
142
+ ```
143
+
144
+ ## Usage
145
+
146
+ The following examples assume you have imported:
147
+
148
+ ```python
149
+ from indentoken import Indentation
150
+ ```
151
+
152
+ ### Basic Indentation and Dedentation
153
+
154
+ ```python
155
+ ind = Indentation()
156
+
157
+ # Have no indentation at all initally.
158
+ print(f'{ind}Line 1') # |Line 1
159
+
160
+ # Indent by 1 level.
161
+ ind.indent()
162
+ print(f'{ind}Line 2') # | Line 2
163
+
164
+ # Indent by 3 levels, plus the previous 1 level.
165
+ # Now the indentation is 4 levels deep.
166
+ ind.indent(3)
167
+ print(f'{ind}Line 3') # | Line 3
168
+
169
+ # Dedent by 1 level.
170
+ # Now the indentation is 3 levels deep.
171
+ ind.dedent()
172
+ print(f'{ind}Line 4') # | Line 4
173
+
174
+ # Dedent by 99 levels.
175
+ # Note that the indentation level is always non-negative.
176
+ # The final indentation level will be clamped at 0.
177
+ ind.dedent(99)
178
+ assert ind.level == 0
179
+ print(f'{ind}Line 5') # |Line 5
180
+
181
+ # Directly set the current indentation level to 3.
182
+ ind.level = 3
183
+ print(f'{ind}Line 6') # | Line 6
184
+ ```
185
+
186
+ ### Using Context Manager for Indentation
187
+
188
+ If you want to increase indentation within a certain block, I recommend
189
+ using `with` combined with `indented_context()`.
190
+ This ensures that the indentation automatically reverts to
191
+ the correct depth even if an exception is raised.
192
+
193
+ ```python
194
+ ind = Indentation()
195
+
196
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
197
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
198
+ with ind.indented_context():
199
+ print(f'{ind}This line is indented.') # | This line is indented.
200
+ print(f'{ind}This line is indented.') # | This line is indented.
201
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
202
+ print(f'{ind}This line is NOT indented.') # |This line is NOT indented.
203
+ with ind.indented_context(2):
204
+ print(f'{ind}This line is indented by 2 levels.') # | This line is indented by 2 levels.
205
+ print(f'{ind}This line is indented by 2 levels.') # | This line is indented by 2 levels.
206
+ with ind.indented_context(1): # nested indented context will stack
207
+ print(f'{ind}This line is indented by 3 levels.') # | This line is indented by 3 levels.
208
+ print(f'{ind}This line is indented by 3 levels.') # | This line is indented by 3 levels.
209
+ print(f'{ind}This line is indented by 2 levels.') # | This line is indented by 2 levels.
210
+ ```
211
+
212
+ The output you will get is like this:
213
+
214
+ ```text
215
+ This line is NOT indented.
216
+ This line is NOT indented.
217
+ This line is indented.
218
+ This line is indented.
219
+ This line is NOT indented.
220
+ This line is NOT indented.
221
+ This line is indented by 2 levels.
222
+ This line is indented by 2 levels.
223
+ This line is indented by 3 levels.
224
+ This line is indented by 3 levels.
225
+ This line is indented by 2 levels.
226
+ ```
227
+
228
+ ### Multi-line Text Indentation
229
+
230
+ For multi-line text, you can directly wrap it in `ind`
231
+ or use the `apply_to()` method:
232
+
233
+ ```python
234
+ ind = Indentation(level=1)
235
+
236
+ print('No indentation.')
237
+ print(ind('This is a multi-line text.\nAll lines will be indented.\nThe third line.'))
238
+ # ind('something') is equivalent to ind.apply_to('something')
239
+ ```
240
+
241
+ It will output:
242
+
243
+ ```text
244
+ No indentation.
245
+ This is a multi-line text.
246
+ All lines will be indented.
247
+ The third line.
248
+ ```
249
+
250
+ ### Initialization
251
+
252
+ During initialization, you can specify the indentation characters,
253
+ initial indentation depth, and padding (which will be mentioned later).
254
+
255
+ ```python
256
+ ind = Indentation(word='-->', level=2)
257
+ print(f'{ind}Line 1') # |-->-->Line 1
258
+ ind.indent()
259
+ print(f'{ind}Line 2') # |-->-->-->Line 2
260
+ ```
261
+
262
+ ### Padding
263
+
264
+ If you want the starting point of the indentation not to begin
265
+ from the far left, you can specify padding:
266
+
267
+ ```python
268
+ # Use "+++++" as padding for visualization. You can use whitespaces.
269
+ ind = Indentation(word='-->', padding='+++++')
270
+ print(f'{ind}Line 1') # |+++++Line 1
271
+ ind.indent()
272
+ print(f'{ind}Line 2') # |+++++-->Line 2
273
+ ind.indent()
274
+ print(f'{ind}Line 3') # |+++++-->-->Line 3
275
+ ```
276
+
277
+ Padding does not change with indentation depth and it always exists,
278
+ even if the indentation depth is zero.
279
+
280
+ You can also specify the `Indentation` object as padding,
281
+ which gives you dynamic padding effects:
282
+
283
+ ```python
284
+ pad = Indentation(word='++', level=2)
285
+ ind = Indentation(word='-->', padding=pad)
286
+ print(f'{ind}Line 1') # |++++Line 1
287
+ ind.indent()
288
+ print(f'{ind}Line 2') # |++++-->Line 2
289
+ ind.indent()
290
+ print(f'{ind}Line 3') # |++++-->-->Line 3
291
+ pad.dedent() # Note that the `pad` object is changing, not `ind`.
292
+ print(f'{ind}Line 4') # |++-->-->Line 4
293
+
294
+ # Convert the padding to `str` on init if you want a fixed padding.
295
+ pad = Indentation(word='++', level=2)
296
+ ind = Indentation(word='-->', level=1, padding=str(pad))
297
+ print(f'{ind}Line 5') # |++++-->Line 5
298
+ pad.dedent() # Note that the `pad` object is changing, not `ind`.
299
+ print(f'{ind}Line 6') # |++++-->Line 6
300
+ ```
301
+
302
+ ### Indentation Addition
303
+
304
+ If you temporarily need to deepen the indentation
305
+ without using the heavy `with` statement, you can directly add
306
+ the desired indentation depth to the indentation object.
307
+
308
+ Additionally, `Indentation` addition only changes
309
+ the indentation depth and does not change the padding.
310
+
311
+ ```python
312
+ ind = Indentation(word='-->', level=1)
313
+ print(f'{ind}one level indented') # |-->one level indented
314
+ print(f'{ind}one level indented') # |-->one level indented
315
+ print(f'{ind + 1}one level deeper, two levels indented') # |-->-->one level deeper, two levels indented
316
+ print(f'{ind + 2}two levels deeper, three levels indented') # |-->-->-->two levels deeper, three levels indented
317
+ print(f'{ind}one level indented') # |-->one level indented
318
+ print(f'{ind}one level indented') # |-->one level indented
319
+
320
+ ind = Indentation(word='-->', level=1, padding='++')
321
+ print(f'{ind}one level indented') # |++-->one level indented
322
+ print(f'{ind + 1}one level deeper, two levels indented') # |++-->-->one level deeper, two levels indented
323
+ ```
324
+
325
+ Indentation addition is commutative:
326
+
327
+ ```python
328
+ ind = Indentation(word='-->', level=2)
329
+ assert ind + 1 == 1 + ind
330
+ ```
331
+
332
+ ### Multi-parameter Print
333
+
334
+ If you want the following usage of `print` to also receive indentation,
335
+ you can wrap the `print` function with the `Indentation` object.
336
+
337
+ ```python
338
+ print('Alice', 'Bob', sep=' & ') # |Alice & Bob
339
+
340
+ ind = Indentation(word='-->', level=1)
341
+ ind(print)('Alice', 'Bob', sep=' & ') # |-->Alice & Bob
342
+ # Note that the parentheses wrap the `print`, not the entire statement.
343
+ ```
344
+
345
+ Note that if you save `ind(print)` for later use, it will bind
346
+ this `Indentation` object.
347
+ If you do not want the `print` to change with the `Indentation` object,
348
+ you can use the `fixed()` method.
349
+
350
+ ```python
351
+ ind = Indentation(word='-->', level=1)
352
+
353
+ print_indented = ind(print)
354
+
355
+ print_indented('Alice') # |-->Alice
356
+
357
+ ind.indent()
358
+ print_indented('Bob') # |-->-->Bob
359
+
360
+ # ===========================================
361
+ ind = Indentation(word='-->', level=1)
362
+
363
+ # Use ind.fixed() to fixed the indentation.
364
+ print_indented_fixed = ind.fixed(print)
365
+
366
+ print_indented_fixed('Alice') # |-->Alice
367
+
368
+ ind.indent()
369
+ print_indented_fixed('Bob') # |-->Bob
370
+ ```
371
+
372
+ ## License
373
+
374
+ This software is distributed under the MIT License.
375
+ Please see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "indentoken"
3
+ version = "1.0.0"
4
+ description = "An indentation token which helps you manage text indentations."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.10"
9
+ dependencies = []
10
+ keywords = [
11
+ "indentation",
12
+ "indent",
13
+ "dedent",
14
+ "text formatting",
15
+ "output formatting",
16
+ ]
17
+ classifiers = [
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Topic :: Text Processing :: General",
24
+ "Topic :: Utilities",
25
+ "Typing :: Typed",
26
+ ]
27
+
28
+ [[project.authors]]
29
+ name = "Rockmizu"
30
+ email = "Rockmizu@gmail.com"
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.12.5,<0.13.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "pytest>=9.1.1",
39
+ "ruff>=0.16.5",
40
+ ]
41
+
42
+ [tool.ruff]
43
+ line-length = 120
44
+
45
+ [tool.ruff.format]
46
+ quote-style = "single"
47
+
48
+ [tool.ruff.lint]
49
+ ignore = ["F541"]
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "indentoken"
3
+ version = "1.0.0"
4
+ description = "An indentation token which helps you manage text indentations."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Rockmizu", email = "Rockmizu@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.10"
12
+ dependencies = []
13
+ keywords = [
14
+ "indentation",
15
+ "indent",
16
+ "dedent",
17
+ "text formatting",
18
+ "output formatting",
19
+ ]
20
+ classifiers = [
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: Text Processing :: General",
27
+ "Topic :: Utilities",
28
+ "Typing :: Typed",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.12.5,<0.13.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=9.1.1",
38
+ "ruff>=0.16.5",
39
+ ]
40
+
41
+ [tool.ruff]
42
+ line-length = 120
43
+
44
+ [tool.ruff.format]
45
+ quote-style = "single"
46
+
47
+ [tool.ruff.lint]
48
+ ignore = [
49
+ "F541", # f-string without any placeholders
50
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from .indentation import Indentation
4
+
5
+ __all__ = ['Indentation']
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Final
4
+
5
+ # The method rely on CPython implementation detail to work and should
6
+ # only been re-implemented when PEP 533
7
+ # Deterministic cleanup for iterators becomes available.
8
+ INDENTOKEN_ENABLE_IT_METHOD: Final = False
@@ -0,0 +1,360 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import functools
5
+ import textwrap
6
+ from collections.abc import Callable, Iterable
7
+ from dataclasses import dataclass, field
8
+ from io import StringIO
9
+ from typing import Any, Final, ParamSpec, TypeVar, overload
10
+
11
+ from .feature_flags import INDENTOKEN_ENABLE_IT_METHOD
12
+
13
+ __all__ = ['Indentation']
14
+
15
+
16
+ T = TypeVar('T')
17
+
18
+ P = ParamSpec('P')
19
+
20
+ # A sentinel object
21
+ _MISSING: Final = object()
22
+
23
+
24
+ def _wrap_print_with_post_process(
25
+ print_fn: Callable[P, Any],
26
+ post_process: Callable[[str], str],
27
+ ) -> Callable[P, None]:
28
+ @functools.wraps(print_fn)
29
+ def print_wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
30
+ file = kwargs.pop('file', _MISSING)
31
+ flush = kwargs.pop('flush', _MISSING)
32
+ buffer = StringIO()
33
+ print_fn(*args, **kwargs, file=buffer, flush=False) # type: ignore[arg-type]
34
+
35
+ text = buffer.getvalue()
36
+ text = post_process(text)
37
+
38
+ output_kwargs: dict[str, Any] = {}
39
+ if file is not _MISSING:
40
+ output_kwargs['file'] = file
41
+ if flush is not _MISSING:
42
+ output_kwargs['flush'] = flush
43
+
44
+ print_fn(text, end='', **output_kwargs) # type: ignore[arg-type]
45
+
46
+ return print_wrapper
47
+
48
+
49
+ @dataclass(slots=True, init=False, eq=False)
50
+ class Indentation:
51
+ """
52
+ A magic token that can be used as an indentation string.
53
+ """
54
+
55
+ word: str = ' '
56
+ _level: int = field(init=False)
57
+ padding: str | Indentation = field(default='', kw_only=True)
58
+
59
+ def __init__(
60
+ self,
61
+ word: str = ' ',
62
+ level: int = 0,
63
+ *,
64
+ padding: str | Indentation = '',
65
+ ) -> None:
66
+ """
67
+ Initialize the Indentation object.
68
+
69
+ Args:
70
+ word: The string used for indentation (e.g., ' ').
71
+ level: The initial indentation level. Must be non-negative.
72
+ padding: Optional padding object or string.
73
+ """
74
+ if level < 0:
75
+ raise ValueError('`level` must be non-negative')
76
+ self.word = word
77
+ self.level = level
78
+ self.padding = padding
79
+
80
+ @property
81
+ def level(self) -> int:
82
+ """
83
+ Get the current indentation level.
84
+
85
+ Returns:
86
+ The current indentation level.
87
+ """
88
+ return self._level
89
+
90
+ @level.setter
91
+ def level(self, level: int, /) -> None:
92
+ """
93
+ Set the current indentation level.
94
+
95
+ Args:
96
+ level: The new indentation level.
97
+
98
+ Raises:
99
+ ValueError: If the provided `level` is negative.
100
+ """
101
+ if level < 0:
102
+ raise ValueError('`level` must be non-negative')
103
+ self._level = level
104
+
105
+ @property
106
+ def padding_str(self) -> str:
107
+ """
108
+ Get the padding string representation.
109
+
110
+ Returns:
111
+ The string representation of the padding.
112
+ """
113
+ return str(self.padding)
114
+
115
+ def copy(self) -> Indentation:
116
+ """
117
+ Create a copy of the current Indentation object.
118
+
119
+ Returns:
120
+ A new Indentation instance with the same properties.
121
+ """
122
+ return Indentation(
123
+ word=self.word,
124
+ level=self._level,
125
+ padding=self.padding,
126
+ )
127
+
128
+ def indent(self, delta: int = 1, /) -> None:
129
+ """
130
+ Increase the indentation level by `delta`.
131
+
132
+ * Positive values increase the indentation level.
133
+ * Negative values decrease the indentation level.
134
+ * The level is clamped at zero.
135
+
136
+ Args:
137
+ delta: The amount to increase the level by. Defaults to 1.
138
+ """
139
+ self._level = max(0, self.level + delta)
140
+
141
+ def dedent(self, delta: int = 1, /) -> None:
142
+ """
143
+ Decrease the indentation level by `delta`.
144
+
145
+ * Positive values decrease the indentation level.
146
+ * Negative values increase the indentation level.
147
+ * The level is clamped at zero.
148
+
149
+ Args:
150
+ delta: The amount to decrease the level by. Defaults to 1.
151
+ """
152
+ self._level = max(0, self.level - delta)
153
+
154
+ @contextlib.contextmanager
155
+ def indented_context(self, level: int = 1, /):
156
+ """
157
+ Context manager to temporarily change the indentation level.
158
+
159
+ Args:
160
+ level: The amount to change the indentation level by. Defaults to 1.
161
+
162
+ Yields:
163
+ Indentation: The current Indentation instance within the context.
164
+ """
165
+ if level < -self._level: # noqa: PLR1730
166
+ level = -self._level
167
+
168
+ self.indent(level)
169
+ try:
170
+ yield self
171
+ finally:
172
+ self.dedent(level)
173
+
174
+ def apply_to(self, text: str, /) -> str:
175
+ """
176
+ Apply the current indentation to a given string.
177
+
178
+ Args:
179
+ text: The string to be indented.
180
+
181
+ Returns:
182
+ The indented string.
183
+ """
184
+ return textwrap.indent(text, prefix=str(self))
185
+
186
+ def fixed(self, print_fn: Callable[P, Any], /) -> Callable[P, None]:
187
+ """
188
+ Create a print function wrapper that applies indentation.
189
+
190
+ Args:
191
+ print_fn: The original print function to wrap.
192
+
193
+ Returns:
194
+ A wrapped print function that applies indentation.
195
+ """
196
+ return _wrap_print_with_post_process(
197
+ print_fn,
198
+ functools.partial(textwrap.indent, prefix=str(self)),
199
+ )
200
+
201
+ if INDENTOKEN_ENABLE_IT_METHOD:
202
+
203
+ def it(self, iterable: Iterable[T], /, delta: int = 1) -> Iterable[T]:
204
+ """
205
+ Wrap an iterable to provide the effect of `indented_context()`
206
+ before the iteration completes.
207
+
208
+ This method is particularly useful when you want additional
209
+ indentation during a `for` loop without adding an extra layer of
210
+ indentation to the source code.
211
+
212
+ E.g. You can write
213
+
214
+ ```python
215
+ ind = Indentation()
216
+
217
+ for item in ind.it(items):
218
+ print(f'{ind}{item}')
219
+
220
+ print(f'{ind}no indentation after loop')
221
+ ```
222
+
223
+ instead of
224
+
225
+ ```python
226
+ ind = Indentation()
227
+
228
+ with ind.indented_context():
229
+ for item in items:
230
+ print(f'{ind}{item}')
231
+
232
+ print(f'{ind}no indentation after loop')
233
+ ```
234
+
235
+ Args:
236
+ iterable: The iterable to yield from.
237
+ delta: The amount to change the indentation level by. Defaults to 1.
238
+
239
+ Yields:
240
+ Elements from the iterable, indented.
241
+ """
242
+ with self.indented_context(delta):
243
+ yield from iterable
244
+
245
+ @overload
246
+ def __call__(self, text: str, /) -> str: ...
247
+ @overload
248
+ def __call__(self, print_fn: Callable[P, Any], /) -> Callable[P, None]: ...
249
+ def __call__(self, text_or_print_fn: str | Callable[P, Any], /) -> str | Callable[P, None]:
250
+ """
251
+ Apply the current indentation to a given text or
252
+ wrap a print function to apply indentation to its output.
253
+
254
+ Args:
255
+ text_or_print_fn: The string to indent, or the print function to wrap.
256
+
257
+ Returns:
258
+ The indented string if `text_or_print_fn` is a string, or the wrapped print function if it is a callable.
259
+ """
260
+ if isinstance(text_or_print_fn, str):
261
+ return self.apply_to(text_or_print_fn)
262
+
263
+ return _wrap_print_with_post_process(
264
+ text_or_print_fn,
265
+ self.apply_to,
266
+ )
267
+
268
+ def __add__(self, amount: int, /) -> str:
269
+ """
270
+ Return a string, which is the stringified result
271
+ as if adding `delta` level to this object.
272
+
273
+ ```python
274
+ ind = Indentation('->', level=2)
275
+ s1 = ind + 3 # s1 = '->->->->->'
276
+ assert ind.level == 2 # won't affect the original object
277
+ ind.indent(2)
278
+ s2 = str(ind) # s2 = '->->->->->'
279
+ assert s1 == s2
280
+ ```
281
+
282
+ This is useful when you suddenly need more indentation levels
283
+ but don't want to use a context manager.
284
+
285
+ E.g.:
286
+
287
+ ```python
288
+ ind = Indentation()
289
+
290
+ print(f'{ind}not indented')
291
+ with ind.indented_context():
292
+ print(f'{ind}one level indented')
293
+ print(f'{ind}one level indented')
294
+ print(f'{ind + 1}one level deeper; two level indented')
295
+ print(f'{ind}one level indented')
296
+ print(f'{ind}one level indented')
297
+
298
+ # will output:
299
+ # not indented
300
+ # one level indented
301
+ # one level indented
302
+ # one level deeper; two level indented
303
+ # one level indented
304
+ # one level indented
305
+ ```
306
+
307
+ Args:
308
+ times: The number of times to append the indentation string.
309
+
310
+ Returns:
311
+ The extended indentation string.
312
+ """
313
+ return str(self) + self.word * amount
314
+
315
+ def __radd__(self, amount: int, /) -> str:
316
+ """
317
+ Return a string, which is the stringified result
318
+ as if adding `amount` level to this object.
319
+
320
+ This is useful when you suddenly need more indentation levels
321
+ but don't want to use a context manager.
322
+
323
+ E.g.:
324
+
325
+ ```python
326
+ ind = Indentation()
327
+
328
+ print(f'{ind}not indented')
329
+ with ind.indented_context():
330
+ print(f'{ind}one level indented')
331
+ print(f'{ind}one level indented')
332
+ print(f'{1 + ind}one level deeper; two level indented')
333
+ print(f'{ind}one level indented')
334
+ print(f'{ind}one level indented')
335
+
336
+ # will output:
337
+ # not indented
338
+ # one level indented
339
+ # one level indented
340
+ # one level deeper; two level indented
341
+ # one level indented
342
+ # one level indented
343
+ ```
344
+
345
+ Args:
346
+ times: The number of times to append the indentation string.
347
+
348
+ Returns:
349
+ The extended indentation string.
350
+ """
351
+ return str(self) + self.word * amount
352
+
353
+ def __str__(self) -> str:
354
+ """
355
+ Return the string representation of the indentation.
356
+
357
+ Returns:
358
+ The string representing the indentation (padding + word * level).
359
+ """
360
+ return str(self.padding) + self.word * self._level
File without changes