log21 3.1.0__tar.gz → 3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: log21
3
- Version: 3.1.0
3
+ Version: 3.3.0
4
4
  Summary: A simple logging package
5
5
  Keywords: python,log,colorize,color,logging,Python3,CodeWriter21
6
6
  Author: CodeWriter21(Mehrad Pooryoussof)
@@ -94,140 +94,57 @@ pip install git+https://github.com/MPCodeWriter21/log21
94
94
  Changelog
95
95
  ---------
96
96
 
97
- ### v3.1.0
97
+ ### v3.3.0
98
98
 
99
- Change the way `argumentify` handles function parameters to argument-parser arguments
100
- conversion.
99
+ Add `log21.helper_types` module.
101
100
 
102
- + `POSITIONAL_ONLY` and `VAR_POSITIONAL` parameters will be positional arguments.
103
- + `POSITIONAL_OR_KEYWORD` and `KEYWORD_ONLY` parameters have flags assigned to them.
104
- + `POSITIONAL_OR_KEYWORD` parameters will be required if at least one `KEYWORD_ONLY`
105
- parameter is there, otherwise they are optional.
106
- + `VAR_KEYWORD` parameters are still not supported.
101
+ This module contains a collection of useful types meant for using with argument parser
102
+ to parse CLI arguments to more usable formats.
107
103
 
108
- #### Example 1
104
+ + `FileSize`: Can take `str` and `int` values. Will convert human inputs such as "121 KB",
105
+ "21MiB", or "4.56 GB" to bytes. Can also be used to represent bytes value in more
106
+ human-readable formats.
109
107
 
110
- ```python
111
- def main(path: Path, /, output: Path, *, verbose: bool = False):
112
- """Process a file.
113
-
114
- :param path: The input file path
115
- :param output: The output file
116
- :param verbose: Write more logs to the standard output.
117
- """
118
- ...
119
-
120
-
121
- if __name__ == "__main__":
122
- argumentify(main)
123
- ```
124
-
125
- The help looks like this:
126
-
127
- ```help
128
- usage: test.py [-h] --output OUTPUT [--verbose] path
129
-
130
- Process a file.
131
-
132
- positional arguments:
133
- path The input file path
134
-
135
- options:
136
- -h, --help
137
- show this help message and exit
138
- --output OUTPUT, -o OUTPUT
139
- The output file
140
- --verbose, -v
141
- Write more logs to the standard output.
142
-
143
- ```
108
+ For even more control you can still define Logger, Handlers, and Formatters manually.
144
109
 
145
- _Note that `path` and `output` are required._
146
-
147
- #### Example 2
110
+ #### Example
148
111
 
149
112
  ```python
150
- def main(output: Path, /, *inputs: Path):
151
- """Process multiple files into one.
152
-
153
- :param output: The output file
154
- :param inputs: The path to the input files
155
- """
156
- # Since `inputs` is a VAR_POSITIONAL, while being a positional argument, it can have
157
- # zero length which is in many cases not intended.
158
- # You might want to add a check for its length and raise an ArgumentError if it does
159
- # not match your needs
160
-
161
- # Check if at least one input has been passed and mark the argument as required
162
- # if len(inputs) < 1:
163
- # raise RequiredArgumentError("inputs")
164
-
165
- # Raise an error unless at least two inputs are present
166
- if len(inputs) < 2:
167
- raise ArgumentError(message="You need to pass at least two files as input.")
168
- ...
169
- ```
113
+ from pathlib import Path
170
114
 
171
- The help looks like this:
115
+ import log21
116
+ from log21.helper_types import FileSize
172
117
 
173
- ```help
174
- usage: test.py [-h] output [inputs ...]
175
118
 
176
- Process multiple files into one.
119
+ def main(path: Path, min_size: FileSize, max_size: FileSize, /):
120
+ log21.info(
121
+ "Files that are smaller than %s or bigger than %s will be ignored.",
122
+ args=(min_size, max_size),
123
+ )
177
124
 
178
- positional arguments:
179
- output The output file
180
- inputs The path to the input files
181
-
182
- options:
183
- -h, --help
184
- show this help message and exit
185
-
186
- ```
187
-
188
- #### Example 3
189
-
190
- ```python
191
- def main(first_name: str, last_name: str, output: Path, verbose: bool = False):
192
- """Write a greeting message.
193
-
194
- :param first_name: The first name of the user to greet (optional)
195
- :param last_name: The last name of the user to greet (optional)
196
- :param output: The output file (stdout if none is provided)
197
- :param verbose: If provided, will write the debug logs to stdout
198
- """
199
- ...
125
+ for file in path.iterdir():
126
+ if not file.is_file():
127
+ continue
128
+ if min_size <= (size := file.stat().st_size) <= max_size:
129
+ log21.print(
130
+ "`%s` is %s.",
131
+ args=(file, FileSize(size).humanize(binary=False, fmt="%.4f")),
132
+ )
200
133
 
201
134
 
202
135
  if __name__ == "__main__":
203
- argumentify(main)
136
+ log21.argumentify(main)
204
137
  ```
205
138
 
206
- The help looks like this:
207
-
208
- ```help
209
- usage: test.py [-h] [--first-name FIRST_NAME] [--last-name LAST_NAME] [--output OUTPUT]
210
- [--verbose]
211
-
212
- Write a greeting message.
213
-
214
- options:
215
- -h, --help
216
- show this help message and exit
217
- --first-name FIRST_NAME, -f FIRST_NAME
218
- The first name of the user to greet (optional)
219
- --last-name LAST_NAME, -l LAST_NAME
220
- The last name of the user to greet (optional)
221
- --output OUTPUT, -o OUTPUT
222
- The output file (stdout if none is provided)
223
- --verbose, -v
224
- If provided, will write the debug logs to stdout
139
+ Example usage and output:
225
140
 
141
+ ```shell
142
+ $ uv run test.py . "1.23MiB" "0.5 GB"
143
+ [21:21:21] [INFO] Files that are smaller than 1.23 MiB or bigger than 476.84 MiB will be
144
+ ignored.
145
+ `myfile21.zip` is 35.1856 MB.
226
146
  ```
227
147
 
228
- _Note that all the options are optional and default to None. `verbose` is False by
229
- default since a default value is provided for it in function definition._
230
-
231
148
  [Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md)
232
149
 
233
150
  Usage Examples
@@ -69,140 +69,57 @@ pip install git+https://github.com/MPCodeWriter21/log21
69
69
  Changelog
70
70
  ---------
71
71
 
72
- ### v3.1.0
72
+ ### v3.3.0
73
73
 
74
- Change the way `argumentify` handles function parameters to argument-parser arguments
75
- conversion.
74
+ Add `log21.helper_types` module.
76
75
 
77
- + `POSITIONAL_ONLY` and `VAR_POSITIONAL` parameters will be positional arguments.
78
- + `POSITIONAL_OR_KEYWORD` and `KEYWORD_ONLY` parameters have flags assigned to them.
79
- + `POSITIONAL_OR_KEYWORD` parameters will be required if at least one `KEYWORD_ONLY`
80
- parameter is there, otherwise they are optional.
81
- + `VAR_KEYWORD` parameters are still not supported.
76
+ This module contains a collection of useful types meant for using with argument parser
77
+ to parse CLI arguments to more usable formats.
82
78
 
83
- #### Example 1
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.
84
82
 
85
- ```python
86
- def main(path: Path, /, output: Path, *, verbose: bool = False):
87
- """Process a file.
88
-
89
- :param path: The input file path
90
- :param output: The output file
91
- :param verbose: Write more logs to the standard output.
92
- """
93
- ...
94
-
95
-
96
- if __name__ == "__main__":
97
- argumentify(main)
98
- ```
99
-
100
- The help looks like this:
101
-
102
- ```help
103
- usage: test.py [-h] --output OUTPUT [--verbose] path
104
-
105
- Process a file.
106
-
107
- positional arguments:
108
- path The input file path
109
-
110
- options:
111
- -h, --help
112
- show this help message and exit
113
- --output OUTPUT, -o OUTPUT
114
- The output file
115
- --verbose, -v
116
- Write more logs to the standard output.
117
-
118
- ```
83
+ For even more control you can still define Logger, Handlers, and Formatters manually.
119
84
 
120
- _Note that `path` and `output` are required._
121
-
122
- #### Example 2
85
+ #### Example
123
86
 
124
87
  ```python
125
- def main(output: Path, /, *inputs: Path):
126
- """Process multiple files into one.
127
-
128
- :param output: The output file
129
- :param inputs: The path to the input files
130
- """
131
- # Since `inputs` is a VAR_POSITIONAL, while being a positional argument, it can have
132
- # zero length which is in many cases not intended.
133
- # You might want to add a check for its length and raise an ArgumentError if it does
134
- # not match your needs
135
-
136
- # Check if at least one input has been passed and mark the argument as required
137
- # if len(inputs) < 1:
138
- # raise RequiredArgumentError("inputs")
139
-
140
- # Raise an error unless at least two inputs are present
141
- if len(inputs) < 2:
142
- raise ArgumentError(message="You need to pass at least two files as input.")
143
- ...
144
- ```
88
+ from pathlib import Path
145
89
 
146
- The help looks like this:
90
+ import log21
91
+ from log21.helper_types import FileSize
147
92
 
148
- ```help
149
- usage: test.py [-h] output [inputs ...]
150
93
 
151
- Process multiple files into one.
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
+ )
152
99
 
153
- positional arguments:
154
- output The output file
155
- inputs The path to the input files
156
-
157
- options:
158
- -h, --help
159
- show this help message and exit
160
-
161
- ```
162
-
163
- #### Example 3
164
-
165
- ```python
166
- def main(first_name: str, last_name: str, output: Path, verbose: bool = False):
167
- """Write a greeting message.
168
-
169
- :param first_name: The first name of the user to greet (optional)
170
- :param last_name: The last name of the user to greet (optional)
171
- :param output: The output file (stdout if none is provided)
172
- :param verbose: If provided, will write the debug logs to stdout
173
- """
174
- ...
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
+ )
175
108
 
176
109
 
177
110
  if __name__ == "__main__":
178
- argumentify(main)
111
+ log21.argumentify(main)
179
112
  ```
180
113
 
181
- The help looks like this:
182
-
183
- ```help
184
- usage: test.py [-h] [--first-name FIRST_NAME] [--last-name LAST_NAME] [--output OUTPUT]
185
- [--verbose]
186
-
187
- Write a greeting message.
188
-
189
- options:
190
- -h, --help
191
- show this help message and exit
192
- --first-name FIRST_NAME, -f FIRST_NAME
193
- The first name of the user to greet (optional)
194
- --last-name LAST_NAME, -l LAST_NAME
195
- The last name of the user to greet (optional)
196
- --output OUTPUT, -o OUTPUT
197
- The output file (stdout if none is provided)
198
- --verbose, -v
199
- If provided, will write the debug logs to stdout
114
+ Example usage and output:
200
115
 
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.
201
121
  ```
202
122
 
203
- _Note that all the options are optional and default to None. `verbose` is False by
204
- default since a default value is provided for it in function definition._
205
-
206
123
  [Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md)
207
124
 
208
125
  Usage Examples
@@ -23,7 +23,7 @@ dependencies = [
23
23
  "webcolors",
24
24
  "docstring-parser"
25
25
  ]
26
- version = "3.1.0"
26
+ version = "3.3.0"
27
27
 
28
28
  [build-system]
29
29
  requires = ["uv_build>=0.8.15,<0.9.0"]
@@ -10,6 +10,8 @@ from types import ModuleType as _ModuleType
10
10
  from typing import (Type as _Type, Union as _Union, Literal as _Literal,
11
11
  Mapping as _Mapping, Iterable as _Iterable, Optional as _Optional)
12
12
 
13
+ import log21.helper_types
14
+
13
15
  from . import crash_reporter
14
16
  from .colors import (Colors, get_color, get_colors, ansi_escape, closest_color,
15
17
  get_color_name)
@@ -31,7 +33,7 @@ from .stream_handler import StreamHandler, ColorizingStreamHandler
31
33
  # yapf: enable
32
34
 
33
35
  __author__ = 'CodeWriter21 (Mehrad Pooryoussof)'
34
- __version__ = '3.1.0'
36
+ __version__ = '3.3.0'
35
37
  __github__ = 'https://GitHub.com/MPCodeWriter21/log21'
36
38
  __all__ = [
37
39
  'ColorizingStreamHandler', 'DecolorizingFileHandler', 'ColorizingFormatter',
@@ -44,7 +46,7 @@ __all__ = [
44
46
  'log', 'basic_config', 'basicConfig', 'ProgressBar', 'LoggingWindow',
45
47
  'LoggingWindowHandler', 'get_logging_window', 'crash_reporter', 'console_reporter',
46
48
  'file_reporter', 'argumentify', 'ArgumentError', 'IncompatibleArgumentsError',
47
- 'RequiredArgumentError', 'TooFewArgumentsError'
49
+ 'RequiredArgumentError', 'TooFewArgumentsError', 'FileHandler'
48
50
  ]
49
51
 
50
52
  _manager = Manager()
@@ -60,7 +62,8 @@ def _prepare_formatter(
60
62
  colorize_time_and_level: bool = True,
61
63
  level_names: _Optional[_Mapping[int, str]] = None,
62
64
  level_colors: _Optional[_Mapping[int, tuple[str, ...]]] = None,
63
- formatter_class: _Type[_logging.Formatter] = ColorizingFormatter
65
+ formatter_class: _Type[_logging.Formatter] = ColorizingFormatter,
66
+ prefix_carriage_return: bool = True,
64
67
  ) -> _logging.Formatter:
65
68
  # Prepares a formatting if the fmt was None
66
69
  if not fmt:
@@ -70,7 +73,8 @@ def _prepare_formatter(
70
73
  fmt = '[%(levelname)s] ' + fmt
71
74
  if show_time:
72
75
  fmt = '[%(asctime)s] ' + fmt
73
- fmt = '\r' + fmt
76
+ if prefix_carriage_return:
77
+ fmt = '\r' + fmt
74
78
 
75
79
  if level_colors and not issubclass(formatter_class, ColorizingFormatter):
76
80
  warning(
@@ -116,7 +120,10 @@ def get_logger(
116
120
  override: bool = False,
117
121
  level_names: _Optional[_Mapping[int, str]] = None,
118
122
  level_colors: _Optional[_Mapping[int, tuple[str, ...]]] = None,
119
- file: _Optional[_Union[_os.PathLike, str]] = None
123
+ # TODO: Rename file to file_path in one future update
124
+ file: _Optional[_Union[_os.PathLike, str]] = None,
125
+ file_mode: _Optional[str] = None,
126
+ file_encoding: _Optional[str] = None,
120
127
  ) -> Logger:
121
128
  """Returns a logging.Logger with colorizing support.
122
129
 
@@ -182,7 +189,9 @@ def get_logger(
182
189
  :param level_names: Mapping[int, str] = None: You can specify custom level names.
183
190
  :param level_colors: Mapping[int, Tuple[str, ...]] = None: You can specify custom
184
191
  level colors.
185
- :param file: Union[os.PathLike, str] = None: The file to log to
192
+ :param file: Union[os.PathLike, str] = None: The file path to log to
193
+ :param file_mode: str = None: The mode to open file at (Defaults to 'a')
194
+ :param file_encoding: str = None: The file encoding
186
195
  :return: log21.Logger
187
196
  """
188
197
  if not isinstance(name, str):
@@ -209,7 +218,9 @@ def get_logger(
209
218
  _manager.addLogger(name, logger)
210
219
 
211
220
  if file:
212
- file_handler = FileHandler(file)
221
+ file_handler = DecolorizingFileHandler(
222
+ file, mode=file_mode or 'a', encoding=file_encoding
223
+ )
213
224
  file_formatter = _prepare_formatter(
214
225
  fmt,
215
226
  style,
@@ -218,7 +229,8 @@ def get_logger(
218
229
  show_time,
219
230
  False,
220
231
  level_names,
221
- formatter_class=DecolorizingFormatter
232
+ formatter_class=DecolorizingFormatter,
233
+ prefix_carriage_return=False,
222
234
  )
223
235
  file_handler.setFormatter(file_formatter)
224
236
  logger.addHandler(file_handler)
@@ -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}'>"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes