progress-table 3.0.2__tar.gz → 3.1.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.
@@ -3,6 +3,8 @@
3
3
  *.ipynb
4
4
  __pycache__
5
5
  *.egg-info
6
+ .coverage
7
+ coverage.xml
6
8
 
7
9
  devel/
8
10
  dist
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: progress-table
3
- Version: 3.0.2
3
+ Version: 3.1.0
4
4
  Summary: Display progress as a pretty table in the command line.
5
5
  Project-URL: Home, https://github.com/gahaalt/progress-table
6
6
  Project-URL: Documentation, https://github.com/sjmikler/progress-table/blob/main/docs
@@ -34,7 +34,8 @@ Description-Content-Type: text/markdown
34
34
  # Progress Table
35
35
 
36
36
  [![PyPi version](https://img.shields.io/badge/dynamic/json?label=latest&query=info.version&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table)
37
- [![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table)
37
+ [![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://github.com/sjmikler/progress-table/blob/main/LICENSE.txt)
38
+ [![codecov](https://codecov.io/gh/sjmikler/progress-table/graph/badge.svg?token=CDJKF0FFAQ)](https://codecov.io/gh/sjmikler/progress-table)
38
39
 
39
40
  Lightweight utility to display the progress of your process as a pretty table in the command line.
40
41
 
@@ -7,7 +7,8 @@
7
7
  # Progress Table
8
8
 
9
9
  [![PyPi version](https://img.shields.io/badge/dynamic/json?label=latest&query=info.version&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table)
10
- [![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table)
10
+ [![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://github.com/sjmikler/progress-table/blob/main/LICENSE.txt)
11
+ [![codecov](https://codecov.io/gh/sjmikler/progress-table/graph/badge.svg?token=CDJKF0FFAQ)](https://codecov.io/gh/sjmikler/progress-table)
11
12
 
12
13
  Lightweight utility to display the progress of your process as a pretty table in the command line.
13
14
 
@@ -7,7 +7,8 @@
7
7
  # Progress Table
8
8
 
9
9
  [![PyPi version](https://img.shields.io/badge/dynamic/json?label=latest&query=info.version&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table)
10
- [![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table)
10
+ [![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://github.com/sjmikler/progress-table/blob/main/LICENSE.txt)
11
+ [![codecov](https://codecov.io/gh/sjmikler/progress-table/graph/badge.svg?token=CDJKF0FFAQ)](https://codecov.io/gh/sjmikler/progress-table)
11
12
 
12
13
  Lightweight utility to display the progress of your process as a pretty table in the command line.
13
14
 
@@ -0,0 +1,290 @@
1
+ # Advanced usage
2
+
3
+ ## Indexing
4
+
5
+ Progress Table with `interactive>=2` supports modyfing already closed rows (rows above the current row).
6
+ This can be done either with `.update` method or using `.at` indexing which is a shorthand for `AtIndexer` object.
7
+
8
+
9
+ > When using `interactive<2` mode, you can only modify the current row.
10
+ > Any changes made to other rows will not be displayed.
11
+
12
+ ### `.update` method
13
+
14
+ When using `.update` method you update one value at once.
15
+ Use column name and row index to specify the cell location.
16
+ Example of using `.update` method:
17
+
18
+ ```python
19
+ from progress_table import ProgressTable
20
+
21
+ table = ProgressTable()
22
+ table.add_column("Value")
23
+ table.add_rows(3) # Adding empty rows
24
+
25
+ table.update(name="Value", value=1.0, row=1)
26
+ table.update(name="Value", value=2.0, row=0, cell_color="red bold")
27
+ table.update(name="Value", value=3.0, row=2) # modify last-but-one row
28
+ table.close()
29
+ ```
30
+
31
+ Which might give you the following:
32
+
33
+ ```output
34
+ ╭──────────╮
35
+ │ Value │
36
+ ├──────────┤
37
+ │ 2.0000 │
38
+ │ 1.0000 │
39
+ │ 3.0000 │
40
+ ╰──────────╯
41
+ ```
42
+
43
+ ### `.at` indexing
44
+
45
+ When using `.at` indexing you do not use column names, instead you use column indices.
46
+ First column has index 0, second 1, etc. Slicing operations are also supported with `.at` indexing.
47
+ Treat the table similarly to a numpy array:
48
+
49
+ ```python
50
+ from progress_table import ProgressTable
51
+
52
+ table = ProgressTable()
53
+ table.add_columns(4) # Add more columns with automatic names
54
+ table.add_rows(4) # Adding empty rows
55
+
56
+ table.at[:] = 0.0 # Initialize all values to 0.0
57
+ table.at[0, :] = 2.0 # Set all values in the first row to 2.0
58
+ table.at[:, 1] = 2.0 # Set all values in the second column to 2.0
59
+ table.at[2, 0] = 3.0 # Set the first column in the second-to-last row to 3.0
60
+
61
+ table.close()
62
+ ```
63
+
64
+ Which should give you the following:
65
+
66
+ ```
67
+ ╭──────────┬──────────┬──────────┬──────────╮
68
+ │ 0 │ 1 │ 2 │ 3 │
69
+ ├──────────┼──────────┼──────────┼──────────┤
70
+ │ 2.0000 │ 2.0000 │ 2.0000 │ 2.0000 │
71
+ │ 0.0000 │ 2.0000 │ 0.0000 │ 0.0000 │
72
+ │ 3.0000 │ 2.0000 │ 0.0000 │ 0.0000 │
73
+ │ 0.0000 │ 2.0000 │ 0.0000 │ 0.0000 │
74
+ ╰──────────┴──────────┴──────────┴──────────╯
75
+ ```
76
+
77
+ ## Progress Bars
78
+
79
+ There are two types of progress bars in Progress Table: embedded and non-embedded.
80
+
81
+
82
+ > When using `interactive=0` mode, the progress bars will not be displayed.
83
+
84
+ > When using `interactive=1` mode, progress bars are be displayed only in the bottom row.
85
+
86
+ ### Embedded progress bars
87
+
88
+ Embedded progress bars are displayed as an overlay under a table row that contains data.
89
+ This allows you to see the progress and the new data all at once.
90
+ Those progress bars are displayed 'under' the row and are aligned with the data columns.
91
+ It is possible to customize the looks of the embedded progress bar by specyfing an argument when creating a table:
92
+
93
+ ```python
94
+ from progress_table import ProgressTable
95
+
96
+ table = ProgressTable(pbar_style_embed="dash")
97
+ ```
98
+
99
+ Below we show a sample of available styles for embedded progress bars:
100
+
101
+ `dash`
102
+
103
+ ```
104
+ | Name | Value | Number |
105
+ |-----------------------------------|
106
+ | test1 | 1.0 | 42 |
107
+ |---test2--|----2.0-> | 37 |
108
+ ```
109
+
110
+ `rich`
111
+
112
+ ```
113
+ | Name | Value | Number |
114
+ |-----------------------------------|
115
+ | test1 | 1.0 | 42 |
116
+ |━━━test2━━|━━━━2.0━━ | 37 |
117
+ ```
118
+
119
+ `under`
120
+
121
+ ```
122
+ | Name | Value | Number |
123
+ |-----------------------------------|
124
+ | test1 | 1.0 | 42 |
125
+ |___test2__|____2.0__ | 37 |
126
+ ```
127
+
128
+ ---
129
+
130
+ To see the exahustive list of available options, one can use the following code:
131
+
132
+ ```python
133
+ from progress_table import styles
134
+
135
+ print(styles.available_pbar_styles())
136
+ ```
137
+
138
+ ### Non-embedded progress bars
139
+
140
+ Non-embedded progress bars are displayed as a separate row under the row that contains data.
141
+ Optionally, you can force every progress bar, including the embedded ones, to look like a non-embedded and cover the data.
142
+ You might want to do that if you aim for specific looks of your progress bars.
143
+
144
+ ```python
145
+ from progress_table import ProgressTable
146
+
147
+ table = ProgressTable(
148
+ pbar_style="square", # specify specific style
149
+ pbar_embedded=False, # force all progress bars to be non-embedded
150
+ )
151
+ ```
152
+
153
+ Below we show a sample of available styles for non-embedded progress bars
154
+
155
+ ---
156
+
157
+ `square`
158
+
159
+ ```
160
+ | Name | Value | Number |
161
+ |-----------------------------------|
162
+ | test1 | 1.0 | 42 |
163
+ | test2 | 2.0 | 37 |
164
+ |■■■■■■■■■■■■■■■■■■■■□□□□□□□□□□□□□□□|
165
+ ```
166
+
167
+ `circle`
168
+
169
+ ```
170
+ | Name | Value | Number |
171
+ |-----------------------------------|
172
+ | test1 | 1.0 | 42 |
173
+ | test2 | 2.0 | 37 |
174
+ | ●●●●●●●●●●●●●●●●●●◉○○○○○○○○○○○○○○○|
175
+ ```
176
+
177
+ ---
178
+
179
+ As previously, you can see the exhaustive list of available options by using the following code:
180
+
181
+ ```python
182
+ from progress_table import styles
183
+
184
+ print(styles.available_pbar_styles())
185
+ ```
186
+
187
+ > You can create custom styles by following code in `styles.py` file.
188
+
189
+ ### Infobar
190
+
191
+ You can display detailed information like:
192
+
193
+ * throughput
194
+ * progress in percents
195
+ * progress as the current step and total steps
196
+ * ETA (estimated time arrival) - estimated time in seconds, minutes or hours when the progress bar will finish
197
+ * custom description
198
+
199
+ To add or remove some of this information, specify the arguments like below.
200
+ Additionaly, you can create a progress bar object and specify
201
+ all the options only for this specific progress bar.
202
+
203
+ ```python
204
+ from progress_table import ProgressTable
205
+
206
+ table = ProgressTable(
207
+ pbar_show_throughput=True,
208
+ pbar_show_progress=False,
209
+ pbar_show_percents=False,
210
+ pbar_show_eta=False,
211
+ )
212
+
213
+ pbar = table.pbar(
214
+ range(1000),
215
+ description="train epoch",
216
+ show_throughput=True,
217
+ show_progress=False,
218
+ show_percents=False,
219
+ show_eta=True,
220
+ style="circle",
221
+ style_embed="cdots",
222
+ )
223
+ ```
224
+
225
+ Which, when used, might look like this:
226
+
227
+ ```
228
+ │ │ │ │ │
229
+ │[train epoch, 24.1 it/s, ETA 53s] ●●●●●●●◉○○○○○│
230
+ ```
231
+
232
+ ### More progress bar customization
233
+
234
+ There is more ways to customize the look and feel of your progress bars
235
+ by adding special keywords to the style string:
236
+
237
+ ```python
238
+ from progress_table import ProgressTable
239
+
240
+ table = ProgressTable(pbar_style="angled alt red blue")
241
+ ```
242
+
243
+ The example above would result in an angled-type pbar with alternative way of displaying empty bars,
244
+ with filled bars colored red and empty bars colored blue. There's a separate style string
245
+ for the embedded type progress bars - styling of embedded and non-embedded progress bars is indepedent.
246
+
247
+ ```python
248
+ from progress_table import ProgressTable
249
+
250
+ table = ProgressTable(pbar_style_embed="cdots red blue")
251
+ ```
252
+
253
+ The available keywords are:
254
+
255
+ * `alt` - alternative way of displaying empty bars, for example `■` instead of `□`.
256
+ This is only useful when using colors, otherwise you might not be able to differentiate between filled and empty bars.
257
+ * `clean` - removes empty bars from the progress bar. Spaces are used instead.
258
+ * `red`, `green`, `blue`, `yellow` and other colors available in colorama. If more than one is provided,
259
+ the first one is used for filled bars and the last one is used for empty bars.
260
+
261
+ ---
262
+
263
+ Additionaly, you can specify the color of the progress bar using
264
+ `color` and `color_empty` arguments when creating a progress bar object.
265
+ This will override whatever color is set in `style` or `style_embed`.
266
+ We can combine this option with `colorama.Back` to modify colors
267
+ of the background instead of the foreground symbols.
268
+
269
+ ```python
270
+ from progress_table import ProgressTable
271
+ import colorama
272
+ import time
273
+
274
+ table = ProgressTable("a", "b", "c")
275
+ table.add_rows(1)
276
+
277
+ pbar = table.pbar(
278
+ range(100),
279
+ style_embed="hidden",
280
+ color=colorama.Back.RED,
281
+ color_empty=colorama.Back.BLUE,
282
+ )
283
+
284
+ for _ in pbar:
285
+ time.sleep(0.1)
286
+ ```
287
+
288
+ Try the example above. It contains a different type of embedded progress bar.
289
+ Here the typical progress bar symbols will are hidden,
290
+ but the background color will show us the progress of the process.
@@ -0,0 +1,114 @@
1
+ # Integrations
2
+
3
+ Progress Table is not tied up to any specific deep learning framework.
4
+ Because of it, it's lightweight and minimal. You can still easily integrate it with your code and your favourite framework. Below you will find examples of how to do this.
5
+
6
+ # PyTorch
7
+
8
+ This is an example of a simple custom training loop in PyTorch. We present two versions: without and with Progress Bar.
9
+
10
+ ### Without Progress Table
11
+
12
+ Manual logging is often done in haste. It can look like this:
13
+
14
+ ```python
15
+ ...
16
+
17
+ for epoch in range(n_epochs):
18
+ print(f'Epoch {epoch}')
19
+ cumulated_loss = 0
20
+ cumulated_accuracy = 0
21
+
22
+ for batch_idx, (x, y) in enumerate(train_loader):
23
+ loss, predictions = training_step(x, y)
24
+ accuracy = torch.mean((predictions == y).float()).item()
25
+
26
+ cumulated_loss += loss.item()
27
+ cumulated_accuracy += accuracy
28
+
29
+ if (batch_idx) % 100 == 0:
30
+ print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}, Accuracy: {:.4f}'.format(
31
+ epoch,
32
+ n_epochs,
33
+ batch_idx,
34
+ len(train_loader),
35
+ cumulated_loss / (batch_idx + 1),
36
+ cumulated_accuracy / (batch_idx + 1),
37
+ ))
38
+
39
+ print('Epoch [{}/{}], Step [FINISHED], Loss: {:.4f}, Accuracy: {:.4f}'.format(
40
+ epoch,
41
+ n_epochs,
42
+ cumulated_loss / len(train_loader),
43
+ cumulated_accuracy / len(train_loader),
44
+ ))
45
+ ```
46
+
47
+ ### With Progress Table
48
+
49
+ When using Progress Table, you get detailed and clean logs. Moreover, your code is shorter, simpler and you get more functionality out of it.
50
+
51
+ ```python
52
+ ...
53
+
54
+ table = ProgressTable("Epoch", "Step")
55
+ table.add_columns(["Loss", "Accuracy"], aggregate="mean")
56
+
57
+ for epoch in range(n_epochs):
58
+ table["Epoch"] = f"{epoch}/{n_epochs}"
59
+
60
+ for x, y in table(train_loader):
61
+ loss, predictions = training_step(x, y)
62
+ accuracy = torch.mean((predictions == y).float()).item()
63
+
64
+ table["Loss"] = loss
65
+ table["Accuracy"] = accuracy
66
+ table.next_row()
67
+
68
+ table.close()
69
+ ```
70
+
71
+ # Keras
72
+
73
+ In case of PyTorch or TensorFlow, we often use custom training loops
74
+ where we can integrate Progress Table as shown above.
75
+ What about Keras, where the progress bar is built-in the `model.fit` method?
76
+ You can create a callback that will replace the progress bar built-in Keras
77
+ with a progress table. Callback should inherit from `ProgbarLogger`.
78
+
79
+ Here's an example:
80
+
81
+ ```python
82
+ ...
83
+
84
+ table = ProgressTable()
85
+ table.add_columns(["loss", "accuracy", "val_loss", "val_accuracy"], aggregate="mean")
86
+
87
+
88
+ class TableCallback(tf.keras.callbacks.ProgbarLogger):
89
+ def __init__(self, table):
90
+ super().__init__()
91
+ self.table = table
92
+
93
+ def on_epoch_begin(self, epoch, logs=None):
94
+ pass
95
+
96
+ def on_epoch_end(self, epoch, logs=None):
97
+ self.table.update_from_dict(logs)
98
+ self.table.next_row()
99
+
100
+ def on_train_end(self, logs=None):
101
+ self.table.close()
102
+
103
+ def on_train_batch_end(self, batch, logs=None):
104
+ self.table.update_from_dict(logs)
105
+
106
+
107
+ model.fit(
108
+ x_train,
109
+ y_train,
110
+ validation_data=(x_train, y_train),
111
+ callbacks=[TableCallback(table)],
112
+ epochs=10,
113
+ )
114
+ ```
@@ -0,0 +1,52 @@
1
+ # Version 3.0.0
2
+
3
+ Version 3.0.0 of Progress Table is now available.
4
+
5
+ It introduces:
6
+
7
+ * Important internal changes to increase the stability.
8
+ * Increased compatibility with some types of terminals.
9
+ * Minor breaking changes in the API.
10
+
11
+ ## BREAKING API CHANGES
12
+
13
+ ---
14
+
15
+ ```
16
+ ProgressTable(["c1", "c2", "c3"])
17
+ ```
18
+
19
+ ↑ becomes now ↓
20
+
21
+ ```
22
+ ProgressTable("c1", "c2", "c3")
23
+ # or
24
+ ProgressTable(columns=["c1", "c2", "c3"])
25
+ ```
26
+
27
+ ---
28
+
29
+ ```
30
+ ProgressTable([], 1, 20)
31
+ ```
32
+
33
+ ↑ will raise an error. Use named arguments instead ↓
34
+
35
+ ```
36
+ ProgressTable(interactive=1, refresh_rate=20)
37
+ ```
38
+
39
+ ---
40
+
41
+ ```
42
+ from progress_table import ProgressTableV1, ProgressTableV2
43
+ ```
44
+
45
+ ↑ multiple versions of ProgressTable are not available anymore. Use ↓
46
+
47
+ ```
48
+ from progress_table import ProgressTable
49
+ ```
50
+
51
+ ---
52
+
@@ -0,0 +1,115 @@
1
+ # Copyright (c) 2022-2025 Szymon Mikler
2
+ # Licensed under the MIT License
3
+
4
+ import math
5
+ import random
6
+ import time
7
+
8
+ from progress_table import ProgressTable
9
+
10
+
11
+ def calc_distance(pos):
12
+ return (pos[0] ** 2 + pos[1] ** 2) ** 0.5
13
+
14
+
15
+ def get_color_by_distance(distance):
16
+ if distance < 20:
17
+ return "white"
18
+ elif distance < 40:
19
+ return "cyan"
20
+ elif distance < 60:
21
+ return "blue"
22
+ elif distance < 80:
23
+ return "magenta"
24
+ elif distance < 100:
25
+ return "red"
26
+ else:
27
+ return "yellow bold"
28
+
29
+
30
+ def shift_rows_up(table, last_row_color):
31
+ num_rows = table.num_rows()
32
+ num_columns = table.num_columns()
33
+
34
+ for row in range(num_rows - 1):
35
+ for col in range(num_columns):
36
+ table.at[row, col] = table.at[row + 1, col]
37
+ table.at[row, col, "color"] = table.at[row + 1, col, "color"]
38
+ for col in range(num_columns):
39
+ table.at[-2, col, "color"] = last_row_color
40
+
41
+
42
+ def main(random_seed=None, sleep_duration=0.001, **overrides):
43
+ if random_seed is None:
44
+ random_seed = random.randint(0, 100)
45
+ random.seed(random_seed)
46
+
47
+ table = ProgressTable(
48
+ pbar_embedded=False,
49
+ pbar_style="full clean blue",
50
+ **overrides,
51
+ )
52
+
53
+ TARGET_DISTANCE = 100
54
+
55
+ PARTICLE_VELOCITY = 1
56
+ PARTICLE_MOMENTUM = 0.999
57
+
58
+ MAX_ROWS = 20
59
+ STEP_SIZE = 100
60
+
61
+ distance_pbar = table.pbar(
62
+ TARGET_DISTANCE,
63
+ description="Distance",
64
+ show_throughput=False,
65
+ show_progress=True,
66
+ )
67
+
68
+ current_position = (0, 0)
69
+ current_velocity = PARTICLE_VELOCITY
70
+
71
+ print("Particle will move randomly in 2D space.")
72
+ print(f"Simulation stops when it reaches distance of {TARGET_DISTANCE} from the center.")
73
+
74
+ tick = 0
75
+ pbar_momentum = 0
76
+ while calc_distance(current_position) < TARGET_DISTANCE:
77
+ random_direction = random.uniform(0, 2 * 3.1415)
78
+ new_velocity = random.uniform(0, PARTICLE_VELOCITY * 2)
79
+ current_velocity = current_velocity * PARTICLE_MOMENTUM + new_velocity * (1 - PARTICLE_MOMENTUM)
80
+ move_vector = (
81
+ current_velocity * math.cos(random_direction),
82
+ current_velocity * math.sin(random_direction),
83
+ )
84
+ current_position = (
85
+ current_position[0] + move_vector[0],
86
+ current_position[1] + move_vector[1],
87
+ )
88
+ distance_from_center = calc_distance(current_position)
89
+
90
+ tick += 1
91
+ table["tick"] = tick
92
+ table["velocity"] = current_velocity
93
+ table["position X"] = current_position[0]
94
+ table["position Y"] = current_position[1]
95
+ table["distance from center"] = distance_from_center
96
+
97
+ pbar_momentum = pbar_momentum * 0.95 + int(distance_from_center) * 0.05
98
+ distance_pbar.set_step(round(pbar_momentum))
99
+
100
+ if tick % STEP_SIZE == 0:
101
+ color = get_color_by_distance(distance_from_center)
102
+ if table.num_rows() > MAX_ROWS:
103
+ shift_rows_up(table, last_row_color=color)
104
+ else:
105
+ table.next_row(color=color)
106
+
107
+ time.sleep(sleep_duration)
108
+
109
+ color = get_color_by_distance(100)
110
+ table.next_row(color=color)
111
+ table.close()
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
@@ -0,0 +1,92 @@
1
+ # Copyright (c) 2022-2025 Szymon Mikler
2
+ # Licensed under the MIT License
3
+ import random
4
+ import time
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+ import colorama
8
+
9
+ from progress_table import ProgressTable
10
+
11
+
12
+ def create_random_file_info():
13
+ info = {}
14
+ index = random.randint(0, 1000)
15
+ types = ["image_.jpg", "video_.mp4", "archive_.zip", "movie_.avi"]
16
+ info["name"] = random.choice(types).replace("_", str(index))
17
+ info["size int"] = random.randint(1, 1000)
18
+ info["size unit"] = random.choice(["KB", "MB", "GB"])
19
+ info["time"] = 2 + random.random() * 10
20
+ return info
21
+
22
+
23
+ NUM_FILES = 15
24
+ files_to_download = [create_random_file_info() for _ in range(NUM_FILES)]
25
+
26
+
27
+ table = ProgressTable(
28
+ pbar_show_progress=False,
29
+ pbar_show_throughput=False,
30
+ pbar_show_eta=True,
31
+ default_column_width=8,
32
+ default_header_color="bold",
33
+ )
34
+
35
+ main_pbar = table.pbar(
36
+ NUM_FILES,
37
+ position=1,
38
+ show_progress=True,
39
+ style="square clean blue",
40
+ )
41
+
42
+
43
+ def fake_download(idx, file_info):
44
+ # Modify pbar styling so that emebedded pbar shows color only
45
+ pbar = table.pbar(1, position=idx, static=True, style_embed="hidden", color=colorama.Back.BLACK)
46
+
47
+ # Update table values in a specifc row
48
+ table.update("total size", str(file_info["size int"]) + " " + file_info["size unit"], row=idx)
49
+ table.update("name", file_info["name"], row=idx)
50
+ table.update("seeds", random.randint(1, 30), row=idx)
51
+ table.update("peers", random.randint(0, 5), row=idx)
52
+
53
+ number_of_errors = 0
54
+
55
+ t0 = time.time()
56
+ td = 0
57
+ while True:
58
+ pbar.set_step(td / file_info["time"]) # Set specific pbar progress
59
+
60
+ # Maybe add an error to the error counter
61
+ if random.random() < 0.004:
62
+ number_of_errors += 1
63
+ random_delay = random.random()
64
+ time.sleep(random_delay)
65
+ t0 += random_delay
66
+
67
+ downloaded_units = int(td / file_info["time"] * file_info["size int"])
68
+ table.update("downloaded", str(downloaded_units) + " " + file_info["size unit"], row=idx)
69
+ table.update("warnings", number_of_errors, row=idx)
70
+ time.sleep(0.01)
71
+
72
+ td = time.time() - t0
73
+ if td > file_info["time"]:
74
+ break
75
+ pbar.close()
76
+ main_pbar.update()
77
+
78
+
79
+ table.add_column("name", alignment="right", width=25)
80
+ table.add_columns("total size", "downloaded", "seeds", "peers", "warnings")
81
+ table.add_rows(len(files_to_download), color="blue")
82
+
83
+ threads = []
84
+ executor = ThreadPoolExecutor()
85
+
86
+ for idx, pkg in enumerate(files_to_download):
87
+ threads.append(executor.submit(fake_download, idx, pkg))
88
+
89
+ for thread in threads:
90
+ thread.result()
91
+
92
+ table.close()
@@ -0,0 +1,96 @@
1
+ # Copyright (c) 2022-2025 Szymon Mikler
2
+ # Licensed under the MIT License
3
+ import random
4
+ import time
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+ import colorama
8
+
9
+ from progress_table import ProgressTable
10
+
11
+
12
+ def create_random_file_info():
13
+ info = {}
14
+ index = random.randint(0, 1000)
15
+ types = ["image_.jpg", "video_.mp4", "archive_.zip", "movie_.avi"]
16
+ info["name"] = random.choice(types).replace("_", str(index))
17
+ info["size int"] = random.randint(1, 1000)
18
+ info["size unit"] = random.choice(["KB", "MB", "GB"])
19
+ info["time"] = 2 + random.random() * 10
20
+ return info
21
+
22
+
23
+ NUM_FILES = 40
24
+ files_to_download = [create_random_file_info() for _ in range(NUM_FILES)]
25
+
26
+
27
+ table = ProgressTable(
28
+ pbar_show_progress=False,
29
+ pbar_show_throughput=False,
30
+ pbar_show_eta=True,
31
+ default_column_width=8,
32
+ default_header_color="bold",
33
+ )
34
+
35
+ main_pbar = table.pbar(
36
+ NUM_FILES,
37
+ position=1,
38
+ show_progress=True,
39
+ style="square clean blue",
40
+ )
41
+
42
+
43
+ def fake_download(file_info):
44
+ # Modify pbar styling so that emebedded pbar shows color only
45
+ idx = table.num_rows() - 1
46
+ table.next_row()
47
+
48
+ pbar = table.pbar(1, position=idx, static=True, style_embed="hidden", color=colorama.Back.BLACK)
49
+
50
+ # Update table values in a specifc row
51
+ table.update("total size", str(file_info["size int"]) + " " + file_info["size unit"], row=idx)
52
+ table.update("name", file_info["name"], row=idx)
53
+ table.update("seeds", random.randint(1, 30), row=idx)
54
+ table.update("peers", random.randint(0, 5), row=idx)
55
+
56
+ number_of_errors = 0
57
+
58
+ t0 = time.time()
59
+ td = 0
60
+ while True:
61
+ pbar.set_step(td / file_info["time"]) # Set specific pbar progress
62
+
63
+ # Maybe add an error to the error counter
64
+ if random.random() < 0.004:
65
+ number_of_errors += 1
66
+ random_delay = random.random()
67
+ time.sleep(random_delay)
68
+ t0 += random_delay
69
+
70
+ downloaded_units = int(td / file_info["time"] * file_info["size int"])
71
+ table.update("downloaded", str(downloaded_units) + " " + file_info["size unit"], row=idx)
72
+ table.update("warnings", number_of_errors, row=idx)
73
+ time.sleep(0.01)
74
+
75
+ td = time.time() - t0
76
+ if td > file_info["time"]:
77
+ break
78
+ pbar.close()
79
+ main_pbar.update()
80
+
81
+
82
+ table.add_column("name", alignment="right", width=25)
83
+ table.add_columns("total size", "downloaded", "seeds", "peers", "warnings")
84
+
85
+ threads = []
86
+ executor = ThreadPoolExecutor()
87
+
88
+ for _idx, pkg in enumerate(files_to_download):
89
+ threads.append(executor.submit(fake_download, pkg))
90
+ random_wait_time = random.randint(1, 10)
91
+ time.sleep(random_wait_time / 10)
92
+
93
+ for thread in threads:
94
+ thread.result()
95
+
96
+ table.close()
@@ -0,0 +1,85 @@
1
+ # Copyright (c) 2022-2025 Szymon Mikler
2
+ # Licensed under the MIT License
3
+
4
+ import random
5
+ import time
6
+
7
+ from progress_table import ProgressTable
8
+
9
+ BOARD_SIZE = 10
10
+ STREAK_LENGTH = 5
11
+
12
+
13
+ def main(random_seed=None, sleep_duration=0.05, **overrides):
14
+ if random_seed is None:
15
+ random_seed = random.randint(0, 100)
16
+ random.seed(random_seed)
17
+
18
+ table = ProgressTable(
19
+ pbar_embedded=False,
20
+ default_column_width=1,
21
+ print_header_on_top=False,
22
+ pbar_style="circle alt red",
23
+ **overrides,
24
+ )
25
+
26
+ print("Two players are playing tic-tac-toe.")
27
+ print(f"The first to get a streak of {STREAK_LENGTH} wins.")
28
+
29
+ # Use convenience method to add multiple columns at once
30
+ table.add_columns(BOARD_SIZE)
31
+
32
+ # Adding multiple new rows at once is also possible
33
+ # We get the 1-st row automatically, so we add one less
34
+ table.add_rows(BOARD_SIZE, split=True)
35
+
36
+ sign = 0
37
+ x = 0
38
+ y = 0
39
+
40
+ win_row_slice = None
41
+ win_col_slice = None
42
+ for _i in table(BOARD_SIZE * BOARD_SIZE, show_throughput=False, show_progress=True):
43
+ current_symbol = True
44
+ while current_symbol:
45
+ x = random.randint(0, BOARD_SIZE - 1)
46
+ y = random.randint(0, BOARD_SIZE - 1)
47
+ current_symbol = table.at[y, x]
48
+ sign = 1 - sign
49
+
50
+ table.at[y, x] = "X" if sign else "O"
51
+ table.at[y, x, "COLOR"] = "CYAN" if sign else "BLUE"
52
+
53
+ finished = False
54
+ for row_idx in range(BOARD_SIZE):
55
+ row = table.at[row_idx, :]
56
+ for j in range(BOARD_SIZE - STREAK_LENGTH):
57
+ values = set(row[j : j + STREAK_LENGTH])
58
+ if values == {"X"} or values == {"O"}:
59
+ win_row_slice = slice(row_idx, row_idx + 1)
60
+ win_col_slice = slice(j, j + STREAK_LENGTH)
61
+ finished = True
62
+ for col_idx in range(BOARD_SIZE):
63
+ col = table.at[:, col_idx]
64
+
65
+ for j in range(BOARD_SIZE - STREAK_LENGTH):
66
+ values = set(col[j : j + STREAK_LENGTH])
67
+ if values == {"X"} or values == {"O"}:
68
+ win_row_slice = slice(j, j + STREAK_LENGTH)
69
+ win_col_slice = slice(col_idx, col_idx + 1)
70
+ finished = True
71
+
72
+ if finished:
73
+ break
74
+ time.sleep(sleep_duration)
75
+
76
+ # Flashing the winner
77
+ for color in ["bold white", "bold red"] * 8:
78
+ table.at[win_row_slice, win_col_slice, "COLOR"] = color
79
+ time.sleep(0.1)
80
+
81
+ table.close()
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,138 @@
1
+ # Copyright (c) 2022-2025 Szymon Mikler
2
+ # Licensed under the MIT License
3
+
4
+ import random
5
+ import time
6
+
7
+ try:
8
+ import numpy as np
9
+ except ImportError as ex:
10
+ raise ImportError("Numpy is required to run the example!") from ex
11
+
12
+ try:
13
+ from sklearn.datasets import load_iris
14
+ from sklearn.model_selection import train_test_split
15
+ from sklearn.utils import shuffle
16
+ except ImportError as ex:
17
+ raise ImportError("Scikit-learn is required to run the example!") from ex
18
+
19
+ from progress_table import ProgressTable
20
+
21
+ # Training parameters
22
+ SGD_LR = 0.01
23
+ NUM_EPOCHS = 15
24
+ SLEEP_DURATION = 0.04
25
+
26
+
27
+ def softmax(x):
28
+ exp = np.exp(x)
29
+ return exp / np.sum(exp, axis=1, keepdims=True)
30
+
31
+
32
+ def log_softmax(x):
33
+ bot = np.sum(np.exp(x), axis=1, keepdims=True)
34
+ return x - np.log(bot)
35
+
36
+
37
+ def cross_entropy_loss(targets, logits):
38
+ # Simulate heavy computation
39
+ time.sleep(SLEEP_DURATION)
40
+
41
+ assert len(targets) == len(logits)
42
+ num_elements = len(targets)
43
+
44
+ logits = log_softmax(logits)
45
+ return -logits[np.arange(num_elements), targets]
46
+
47
+
48
+ def cross_entropy_loss_grads(targets, logits):
49
+ one_hot_targets = np.zeros_like(logits)
50
+ one_hot_targets[np.arange(len(targets)), targets] = 1
51
+ return one_hot_targets - softmax(logits)
52
+
53
+
54
+ def model_grads(targets, logits, inputs):
55
+ cross_entropy_grads = cross_entropy_loss_grads(targets, logits)
56
+ return inputs.T @ cross_entropy_grads
57
+
58
+
59
+ def main(random_seed=None, sleep_duration=SLEEP_DURATION, **overrides):
60
+ if random_seed is None:
61
+ random_seed = random.randint(0, 100)
62
+ global SLEEP_DURATION
63
+ SLEEP_DURATION = sleep_duration
64
+
65
+ random.seed(random_seed)
66
+ np.random.seed(random_seed)
67
+
68
+ table = ProgressTable(
69
+ pbar_embedded=False, # Do not use embedded pbar
70
+ pbar_style="angled alt red blue",
71
+ **overrides,
72
+ )
73
+ print("Training a simple linear model on the Iris dataset.")
74
+
75
+ # Loading dataset
76
+ X, Y = load_iris(return_X_y=True)
77
+ X_train, X_valid, Y_train, Y_valid = train_test_split(X, Y)
78
+ weights = np.random.rand(4, 3)
79
+
80
+ for epoch in table(NUM_EPOCHS, show_throughput=False, show_eta=True):
81
+ table["epoch"] = epoch
82
+ # Shuffling training dataset each epoch
83
+ X_train, Y_train = shuffle(X_train, Y_train) # type: ignore
84
+
85
+ NUM_BATCHES = 16
86
+ X_batches = np.array_split(X_train, NUM_BATCHES)
87
+ Y_batches = np.array_split(Y_train, NUM_BATCHES)
88
+
89
+ for batch in table(zip(X_batches, Y_batches), total=NUM_BATCHES, description="train epoch"):
90
+ x, y = batch
91
+ logits = x @ weights
92
+
93
+ # Computing and applying gradient update
94
+ weights_updates = model_grads(y, logits, x)
95
+ weights = weights + SGD_LR * weights_updates
96
+
97
+ # Computing loss function for the logging
98
+ accuracy = np.mean(np.argmax(logits, axis=1) == y)
99
+ loss_value = np.mean(cross_entropy_loss(y, logits))
100
+
101
+ # We're using .update instead of __setitem__ so that we can specify column details
102
+ table.update("train loss", loss_value, aggregate="mean", color="blue")
103
+ table.update("train accuracy", accuracy, aggregate="mean", color="blue bold")
104
+
105
+ run_validation = epoch % 5 == 4 or epoch == NUM_EPOCHS - 1
106
+ if run_validation:
107
+ NUM_BATCHES = 32
108
+ X_batches = np.array_split(X_valid, NUM_BATCHES)
109
+ Y_batches = np.array_split(Y_valid, NUM_BATCHES)
110
+
111
+ for batch in table(zip(X_batches, Y_batches), total=NUM_BATCHES, description="valid epoch"):
112
+ x, y = batch
113
+ logits = x @ weights
114
+ accuracy = np.mean(np.argmax(logits, axis=1) == y)
115
+ loss_value = np.mean(cross_entropy_loss(y, logits))
116
+
117
+ # Use aggregation weight equal to batch size to get real mean over the validation dataset
118
+ batch_size = x.shape[0]
119
+ table.update(
120
+ "valid loss",
121
+ loss_value,
122
+ weight=batch_size,
123
+ aggregate="mean",
124
+ color="red",
125
+ )
126
+ table.update(
127
+ "valid accuracy",
128
+ accuracy,
129
+ weight=batch_size,
130
+ aggregate="mean",
131
+ color="red bold",
132
+ )
133
+ table.next_row(split=run_validation)
134
+ table.close()
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -10,7 +10,7 @@ Supported features:
10
10
  """
11
11
 
12
12
  __license__ = "MIT"
13
- __version__ = "3.0.2"
13
+ __version__ = "3.1.0"
14
14
  __author__ = "Szymon Mikler"
15
15
 
16
16
  from progress_table.progress_table import ProgressTable, styles
@@ -15,12 +15,17 @@ import time
15
15
  from collections.abc import Callable, Iterable, Iterator, Sized
16
16
  from dataclasses import dataclass
17
17
  from threading import Thread
18
- from typing import TextIO
18
+ from typing import Any, TextIO
19
19
 
20
20
  from colorama import Style
21
21
 
22
22
  from progress_table import styles
23
- from progress_table.common import CURSOR_UP, ColorFormat, ColorFormatTuple, maybe_convert_to_colorama
23
+ from progress_table.common import (
24
+ CURSOR_UP,
25
+ ColorFormat,
26
+ ColorFormatTuple,
27
+ maybe_convert_to_colorama,
28
+ )
24
29
 
25
30
  ######################
26
31
  ## HELPER FUNCTIONS ##
@@ -83,8 +88,8 @@ def get_aggregate_fn(aggregate: None | str | Callable) -> Callable:
83
88
  raise ValueError(msg)
84
89
 
85
90
 
86
- def get_default_format_fn(decimal_places: int) -> Callable[[object], str]:
87
- def fmt(x: object) -> str:
91
+ def get_default_format_fn(decimal_places: int) -> Callable[[Any], str]:
92
+ def fmt(x: Any) -> str:
88
93
  if isinstance(x, int):
89
94
  return str(x)
90
95
  try:
@@ -104,13 +109,14 @@ def get_default_format_fn(decimal_places: int) -> Callable[[object], str]:
104
109
  class DataRow:
105
110
  """Basic unit of data storage for the table."""
106
111
 
107
- values: dict[str, object]
112
+ values: dict[str, Any]
108
113
  weights: dict[str, float]
109
114
  colors: dict[str, str]
115
+ user: bool = False
110
116
 
111
117
  def is_empty(self) -> bool:
112
118
  """Check if the row is empty."""
113
- return not any(self.values)
119
+ return not any(self.values) and not self.user
114
120
 
115
121
 
116
122
  class ProgressTable:
@@ -144,7 +150,7 @@ class ProgressTable:
144
150
  pbar_style_embed: str | styles.PbarStyleBase = "cdots",
145
151
  print_header_on_top: bool = True,
146
152
  print_header_every_n_rows: int = 30,
147
- custom_cell_format: Callable[[object], str] | None = None,
153
+ custom_cell_format: Callable[[Any], str] | None = None,
148
154
  table_style: str | styles.TableStyleBase = "round",
149
155
  file: TextIO | list[TextIO] | tuple[TextIO] | None = None,
150
156
  # DEPRECATED ARGUMENTS
@@ -387,7 +393,7 @@ class ProgressTable:
387
393
  def update(
388
394
  self,
389
395
  name: str,
390
- value: object,
396
+ value: Any,
391
397
  *,
392
398
  row: int = -1,
393
399
  weight: float = 1.0,
@@ -429,8 +435,12 @@ class ProgressTable:
429
435
  if self.interactive > 0:
430
436
  self._append_or_update_display_row(data_row_index)
431
437
 
432
- def __setitem__(self, key: str | tuple[str, int], value: object) -> None:
438
+ def __setitem__(self, key: str | tuple[str, int], value: Any) -> None:
433
439
  """Update value in the current row. Calls 'update'."""
440
+ if isinstance(key, slice):
441
+ msg = "slicing not supported! Did you want to use 'table.at[:]' indexer?"
442
+ raise IndexError(msg)
443
+
434
444
  if isinstance(key, tuple):
435
445
  name, row = key
436
446
  if isinstance(name, int) and isinstance(row, str):
@@ -441,8 +451,12 @@ class ProgressTable:
441
451
  assert isinstance(row, int), f"Row {row} has to be an integer, not {type(row)}!"
442
452
  self.update(name, value, row=row, weight=1)
443
453
 
444
- def __getitem__(self, key: str | tuple[str, int]) -> object | None:
454
+ def __getitem__(self, key: str | tuple[str, int]) -> Any:
445
455
  """Get the value from the current row in table."""
456
+ if isinstance(key, slice):
457
+ msg = "slicing not supported! Did you want to use 'table.at[:]' indexer?"
458
+ raise IndexError(msg)
459
+
446
460
  if isinstance(key, tuple):
447
461
  name, row = key
448
462
  if isinstance(name, int) and isinstance(row, int):
@@ -454,7 +468,7 @@ class ProgressTable:
454
468
  assert isinstance(row, int), f"Row {row} has to be an integer, not {type(row)}!"
455
469
  return self._data_rows[row].values.get(name, None)
456
470
 
457
- def update_from_dict(self, dictionary: dict[str, object]) -> None:
471
+ def update_from_dict(self, dictionary: dict[str, Any]) -> None:
458
472
  """Update multiple values in the current row."""
459
473
  for key, value in dictionary.items():
460
474
  self.update(key, value)
@@ -500,7 +514,7 @@ class ProgressTable:
500
514
  row.colors = {**self._resolve_row_color_dict(color), **row.colors}
501
515
 
502
516
  # Refreshing the existing row is necessary to apply colors
503
- # Or - if row is empty, this will cause the first addition to display rows
517
+ # Or - if row is new - this will add it to display rows
504
518
  self._append_or_update_display_row(data_row_index)
505
519
 
506
520
  self._append_new_empty_data_row()
@@ -513,9 +527,18 @@ class ProgressTable:
513
527
 
514
528
  def add_row(self, *values, **kwds) -> None:
515
529
  """Mimicking rich.table behavior for adding full rows in one call."""
530
+ if not self._data_rows[-1].is_empty():
531
+ self.next_row(**kwds)
532
+
516
533
  for key, value in zip(self.column_names, values):
517
534
  self.update(key, value)
518
- self.next_row(**kwds)
535
+
536
+ # The row was explicitly added, make sure it is displayed
537
+ data_row_index = len(self._data_rows) - 1
538
+ self._append_or_update_display_row(data_row_index)
539
+
540
+ # Mark row as explicitly added by the user
541
+ self._data_rows[data_row_index].user = True
519
542
 
520
543
  def add_rows(self, *rows, **kwds) -> None:
521
544
  """Like `add_row` but adds multiple rows at once.
@@ -574,14 +597,14 @@ class ProgressTable:
574
597
 
575
598
  self._append_or_update_display_row("USER WRITE " + line)
576
599
 
577
- def to_list(self) -> list[list[object]]:
600
+ def to_list(self) -> list[list[Any]]:
578
601
  """Convert to Python nested list."""
579
602
  values = [[row.values.get(col, None) for col in self.column_names] for row in self._data_rows]
580
603
  if self._data_rows[-1].is_empty():
581
604
  values.pop(-1)
582
605
  return values
583
606
 
584
- def to_numpy(self) -> object:
607
+ def to_numpy(self) -> Any:
585
608
  """Convert to numpy array.
586
609
 
587
610
  Numpy library is required.
@@ -590,7 +613,7 @@ class ProgressTable:
590
613
 
591
614
  return np.array(self.to_list())
592
615
 
593
- def to_df(self) -> object:
616
+ def to_df(self) -> Any:
594
617
  """Convert to pandas DataFrame.
595
618
 
596
619
  Pandas library is required.
@@ -809,7 +832,7 @@ class ProgressTable:
809
832
  color_colorama = {column: maybe_convert_to_colorama(color) for column, color in color.items()}
810
833
  return {col: self.column_colors[col] + color_colorama[col] for col in color}
811
834
 
812
- def _apply_cell_formatting(self, value: object, column_name: str, color: str) -> str:
835
+ def _apply_cell_formatting(self, value: Any, column_name: str, color: str) -> str:
813
836
  str_value = self.custom_cell_format(value)
814
837
  width = self.column_widths[column_name]
815
838
  alignment = self.column_alignments[column_name]
@@ -1284,7 +1307,7 @@ class TableAtIndexer:
1284
1307
  column_names = self.table.column_names[cols] if isinstance(cols, slice) else [self.table.column_names[cols]]
1285
1308
  return row_indices, column_names, mode
1286
1309
 
1287
- def __setitem__(self, key: slice | tuple, value: object) -> None:
1310
+ def __setitem__(self, key: slice | tuple, value: Any) -> None:
1288
1311
  """Set the values, colors, or weights of a slice in the table."""
1289
1312
  row_indices, column_names, edit_mode = self._parse_index(key)
1290
1313
  if edit_mode == "colors":
@@ -32,8 +32,6 @@ def _parse_colors_from_description(description: str) -> tuple[str, str, str]:
32
32
  class UnknownStyleError(ValueError):
33
33
  """Raised when style description is not recognized."""
34
34
 
35
- pass
36
-
37
35
 
38
36
  def parse_pbar_style(description: str | PbarStyleBase) -> PbarStyleBase:
39
37
  """Parse progress bar style description and return a style object.
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.12",
24
24
  "Programming Language :: Python :: 3.13",
25
25
  ]
26
- readme = "README.md"
26
+ readme = "docs/README.md"
27
27
 
28
28
  [project.urls]
29
29
  Home = "https://github.com/gahaalt/progress-table"
@@ -36,7 +36,7 @@ dev = ["black", "isort", "build", "twine"]
36
36
  path = "progress_table/__init__.py"
37
37
 
38
38
  [tool.hatch.build.targets.sdist]
39
- include = ["progress_table", "README.md"]
39
+ include = ["progress_table", "examples", "docs", "README.md"]
40
40
 
41
41
  [tool.hatch.build.targets.wheel]
42
42
  packages = ["progress_table"]
@@ -44,8 +44,7 @@ packages = ["progress_table"]
44
44
  [tool.hatch.metadata.hooks.custom]
45
45
  path = "hooks.py"
46
46
 
47
-
48
- # %%
47
+ # %% TOOLS
49
48
 
50
49
  [tool.pyright]
51
50
  typeCheckingMode = "standard"
@@ -53,8 +52,23 @@ exclude = ["devel", "build", "dist"]
53
52
 
54
53
  [tool.ruff]
55
54
  line-length = 120
56
- fix = true
57
55
  target-version = "py37"
58
56
 
59
57
  [tool.ruff.lint]
60
58
  select = ["E", "F", "I", "B"]
59
+
60
+ # %%
61
+
62
+ [tool.pytest.ini_options]
63
+ pythonpath = ["."]
64
+
65
+ [tool.mypy]
66
+ ignore_missing_imports = true
67
+ exclude = ["devel", "build", "dist"]
68
+
69
+ [tool.isort]
70
+ profile = "black"
71
+ line_length = 120
72
+
73
+ [tool.black]
74
+ line_length = 120