Skip to content

Commit fbe3499

Browse files
feat: Add log21.helper_types module and FileSize type.
1 parent 1d5617f commit fbe3499

File tree

5 files changed

+222
-12
lines changed

5 files changed

+222
-12
lines changed

CHANGELOG.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,65 @@ Help this project by [Donation](DONATE.md)
66
Changes
77
-------
88

9+
### v3.3.0
10+
11+
Add `log21.helper_types` module.
12+
13+
This module contains a collection of useful types meant for using with argument parser
14+
to parse CLI arguments to more usable formats.
15+
16+
- `FileSize`: Can take `str` and `int` values. Will convert human inputs such as "121 KB",
17+
"21MiB", or "4.56 GB" to bytes. Can also be used to represent bytes value in more
18+
human-readable formats.
19+
20+
For even more control you can still define Logger, Handlers, and Formatters manually.
21+
22+
#### Example of `FileSize` usage
23+
24+
```python
25+
from pathlib import Path
26+
27+
import log21
28+
from log21.helper_types import FileSize
29+
30+
31+
def main(path: Path, min_size: FileSize, max_size: FileSize, /):
32+
log21.info(
33+
"Files that are smaller than %s or bigger than %s will be ignored.",
34+
args=(min_size, max_size),
35+
)
36+
37+
for file in path.iterdir():
38+
if not file.is_file():
39+
continue
40+
if min_size <= (size := file.stat().st_size) <= max_size:
41+
log21.print(
42+
"`%s` is %s.",
43+
args=(file, FileSize(size).humanize(binary=False, fmt="%.4f")),
44+
)
45+
46+
47+
if __name__ == "__main__":
48+
log21.argumentify(main)
49+
```
50+
51+
Example usage and output:
52+
53+
```shell
54+
$ uv run test.py . "1.23MiB" "0.5 GB"
55+
[21:21:21] [INFO] Files that are smaller than 1.23 MiB or bigger than 476.84 MiB will be
56+
ignored.
57+
`myfile21.zip` is 35.1856 MB.
58+
```
59+
960
### v3.2.0
1061

1162
Add `file_mode` and `file_encoding` parameters to `get_logger` for finer control over
1263
the way a simple logger handles files.
1364

1465
For even more control you can still define Logger, Handlers, and Formatters manually.
1566

16-
#### Example
67+
#### Example of using `get_logger` with a file
1768

1869
```python
1970
import log21

README.md

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,55 @@ pip install git+https://github.com/MPCodeWriter21/log21
6969
Changelog
7070
---------
7171

72-
### v3.2.0
72+
### v3.3.0
7373

74-
Add `file_mode` and `file_encoding` parameters to `get_logger` for finer control over
75-
the way a simple logger handles files.
74+
Add `log21.helper_types` module.
75+
76+
This module contains a collection of useful types meant for using with argument parser
77+
to parse CLI arguments to more usable formats.
78+
79+
+ `FileSize`: Can take `str` and `int` values. Will convert human inputs such as "121 KB",
80+
"21MiB", or "4.56 GB" to bytes. Can also be used to represent bytes value in more
81+
human-readable formats.
7682

7783
For even more control you can still define Logger, Handlers, and Formatters manually.
7884

7985
#### Example
8086

8187
```python
88+
from pathlib import Path
89+
8290
import log21
91+
from log21.helper_types import FileSize
92+
93+
94+
def main(path: Path, min_size: FileSize, max_size: FileSize, /):
95+
log21.info(
96+
"Files that are smaller than %s or bigger than %s will be ignored.",
97+
args=(min_size, max_size),
98+
)
99+
100+
for file in path.iterdir():
101+
if not file.is_file():
102+
continue
103+
if min_size <= (size := file.stat().st_size) <= max_size:
104+
log21.print(
105+
"`%s` is %s.",
106+
args=(file, FileSize(size).humanize(binary=False, fmt="%.4f")),
107+
)
108+
109+
110+
if __name__ == "__main__":
111+
log21.argumentify(main)
112+
```
83113

84-
logger = log21.get_logger(
85-
"My File Logger", show_level=False, show_time=True, file="myapp.log", file_mode="a",
86-
file_encoding="utf-8"
87-
)
114+
Example usage and output:
88115

89-
logger.info("Hello World!")
116+
```shell
117+
$ uv run test.py . "1.23MiB" "0.5 GB"
118+
[21:21:21] [INFO] Files that are smaller than 1.23 MiB or bigger than 476.84 MiB will be
119+
ignored.
120+
`myfile21.zip` is 35.1856 MB.
90121
```
91122

92123
[Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dependencies = [
2323
"webcolors",
2424
"docstring-parser"
2525
]
26-
version = "3.2.0"
26+
version = "3.3.0"
2727

2828
[build-system]
2929
requires = ["uv_build>=0.8.15,<0.9.0"]

src/log21/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from typing import (Type as _Type, Union as _Union, Literal as _Literal,
1111
Mapping as _Mapping, Iterable as _Iterable, Optional as _Optional)
1212

13+
import log21.helper_types
14+
1315
from . import crash_reporter
1416
from .colors import (Colors, get_color, get_colors, ansi_escape, closest_color,
1517
get_color_name)
@@ -31,7 +33,7 @@
3133
# yapf: enable
3234

3335
__author__ = 'CodeWriter21 (Mehrad Pooryoussof)'
34-
__version__ = '3.2.0'
36+
__version__ = '3.3.0'
3537
__github__ = 'https://GitHub.com/MPCodeWriter21/log21'
3638
__all__ = [
3739
'ColorizingStreamHandler', 'DecolorizingFileHandler', 'ColorizingFormatter',
@@ -44,7 +46,7 @@
4446
'log', 'basic_config', 'basicConfig', 'ProgressBar', 'LoggingWindow',
4547
'LoggingWindowHandler', 'get_logging_window', 'crash_reporter', 'console_reporter',
4648
'file_reporter', 'argumentify', 'ArgumentError', 'IncompatibleArgumentsError',
47-
'RequiredArgumentError', 'TooFewArgumentsError'
49+
'RequiredArgumentError', 'TooFewArgumentsError', 'FileHandler'
4850
]
4951

5052
_manager = Manager()

src/log21/helper_types.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# log21.helper_types.py
2+
# CodeWriter21
3+
"""A collection of useful types meant for using with argument parser to parse CLI
4+
arguments to more usable formats.
5+
6+
+ FileSize: Can take `str` and `int` values. Will convert human inputs such as "121 KB",
7+
"21MiB", or "4.56 GB" to bytes. Can also be used to represent bytes value in more
8+
human-readable formats.
9+
"""
10+
11+
# yapf: disable
12+
13+
import re as _re
14+
from math import log as _log
15+
from typing import Union as _Union, SupportsInt as _SupportsInt
16+
17+
# yapf: enable
18+
19+
__all__ = ["FileSize"]
20+
21+
POWERS = "KMGTPEZYRQ"
22+
FILE_SIZE_PATTERN = _re.compile(rf"^([+-]?[0-9]+(?:\.[0-9]+)?)\s*(|[{POWERS}])(i?)B$")
23+
24+
25+
class FileSize:
26+
27+
def __init__(self, value: _Union[int, str]) -> None:
28+
"""An interface for converting different inputs to file-size (bytes).
29+
30+
:param value: int value in bytes or a string such as "100 KB", "20MiB", or "1.23
31+
GB"
32+
:raises TypeError: If the value is not of type int or str
33+
:raises ValueError: If the str value does not match the file-size pattern:
34+
^([+-]?[0-9]+(?:\\.[0-9]+)?)\\s*(|[KMGTPEZYRQ])(i?)B$
35+
"""
36+
if isinstance(value, int):
37+
self.bytes = value
38+
elif isinstance(value, str):
39+
match = FILE_SIZE_PATTERN.match(value)
40+
if not match:
41+
raise ValueError(f"Input does not match the file-size pattern: {value}")
42+
val, prefix, binary = match.groups()
43+
power = POWERS.index(prefix) + 1
44+
assert power is not None
45+
self.bytes = int(float(val) * (1024 if binary else 1000)**power)
46+
else:
47+
raise TypeError(f"Input to FileSize() can be int or str, not {type(value)}")
48+
49+
def humanize(
50+
self,
51+
binary: bool = False,
52+
gnu: bool = False,
53+
fmt: str = "%.2f",
54+
) -> str:
55+
"""Returns the size in a human readable way."""
56+
base = 1024 if (gnu or binary) else 1000
57+
abs_bytes = abs(self.bytes)
58+
59+
if abs_bytes == 1 and not gnu:
60+
return f"{self.bytes} Byte"
61+
62+
if abs_bytes < base:
63+
return f"{self.bytes}B" if gnu else f"{self.bytes} Bytes"
64+
65+
power = int(min(_log(abs_bytes, base), len(POWERS)))
66+
result: str = fmt % (self.bytes / (base**power))
67+
if gnu:
68+
return result + POWERS[power - 1]
69+
result += " " + POWERS[power - 1]
70+
if binary:
71+
result += "i"
72+
result += "B"
73+
return result
74+
75+
@property
76+
def KB(self) -> float:
77+
return self.bytes / 1000
78+
79+
@property
80+
def MB(self) -> float:
81+
return self.bytes / 1000_000
82+
83+
@property
84+
def GB(self) -> float:
85+
return self.bytes / 1000_000_000
86+
87+
@property
88+
def KiB(self) -> float:
89+
return self.bytes / 1024
90+
91+
@property
92+
def MiB(self) -> float:
93+
return self.bytes / 1048576
94+
95+
@property
96+
def GiB(self) -> float:
97+
return self.bytes / 1073741824
98+
99+
def __int__(self) -> int:
100+
return self.bytes
101+
102+
def __eq__(self, value: object) -> bool:
103+
if not isinstance(value, _SupportsInt):
104+
return False
105+
return self.bytes == int(value)
106+
107+
def __lt__(self, value: _SupportsInt) -> bool:
108+
return self.bytes < int(value)
109+
110+
def __le__(self, value: _SupportsInt) -> bool:
111+
return self.bytes <= int(value)
112+
113+
def __gt__(self, value: _SupportsInt) -> bool:
114+
return int(value) < self.bytes
115+
116+
def __ge__(self, value: _SupportsInt) -> bool:
117+
return int(value) <= self.bytes
118+
119+
def __add__(self, value: _SupportsInt) -> "FileSize":
120+
return FileSize(self.bytes + int(value))
121+
122+
def __str__(self) -> str:
123+
return self.humanize(binary=True)
124+
125+
def __repr__(self) -> str:
126+
return f"<{self.__class__.__name__}: '{self!s}'>"

0 commit comments

Comments
 (0)