Help this project by Donation
Handle percent signs in arguments' help text.
In the older versions, if you had a percent sign in the help text of an argument, it would cause an error when you try to show the help. This is because the argparse module uses percent signs for string formatting, and it would try to format the help text as a string, which would fail if there are any percent signs in it.
The workaround for this issue was to escape the percent signs by doubling them, but it was not a good solution. Now, in v3.3.2, log21 handles percent signs in arguments' help text properly, so you can use percent signs without any issues.
import log21
def show_percentage(a: float, b: float, /) -> None:
"""Takes two numbers and returns the percentage of a in b. E.g. if a is 50 and b is
200, the percentage would be 25.00%.
:param a: The first number. (a %% b)
:param b: The second number. (a %% b)
:return: The percentage of a in b.
"""
if b == 0:
raise log21.ArgumentError("b cannot be zero.")
percentage = (a / b) * 100
print(f"{a} is {percentage:.2f}% of {b}.")
if __name__ == "__main__":
log21.argumentify(show_percentage)import log21
def show_percentage(a: float, b: float, /) -> None:
"""Takes two numbers and returns the percentage of a in b. E.g. if a is 50 and b is
200, the percentage would be 25.00%.
:param a: The first number. (a % b)
:param b: The second number. (a % b)
:return: The percentage of a in b.
"""
if b == 0:
raise log21.ArgumentError("b cannot be zero.")
percentage = (a / b) * 100
print(f"{a} is {percentage:.2f}% of {b}.")
if __name__ == "__main__":
log21.argumentify(show_percentage)Get rid of invalid NoneType value error message.
In the previous versions, if you used an optional union type such as int | float | None
which ended with None, you'd get an error saying invalid NoneType value that didn't
make any sense to the user. The code has been updated to use the name of the last
non-None type for the error message in these situations.
import log21
from log21.helper_types import FileSize
def main(min_size: FileSize | None = None, max_size: FileSize | None = None) -> None:
log21.info("Min Size: %s, Max Size: %s", args=(min_size, max_size))
if __name__ == "__main__":
log21.argumentify(main)Before v3.3.1:
$ python test.py -m Hello
usage: test.py [-h] [--min-size MIN_SIZE] [--max-size MAX_SIZE]
test.py: error: argument --min-size/-m: invalid NoneType value: 'Hello'With v3.3.1 update:
$ python test.py -m Hello
usage: test.py [-h] [--min-size MIN_SIZE] [--max-size MAX_SIZE]
test.py: error: argument --min-size/-m: invalid FileSize value: 'Hello'Add log21.helper_types module.
This module contains a collection of useful types meant for using with argument parser to parse CLI arguments to more usable formats.
FileSize: Can takestrandintvalues. Will convert human inputs such as "121 KB", "21MiB", or "4.56 GB" to bytes. Can also be used to represent bytes value in more human-readable formats.
For even more control you can still define Logger, Handlers, and Formatters manually.
from pathlib import Path
import log21
from log21.helper_types import FileSize
def main(path: Path, min_size: FileSize, max_size: FileSize, /):
log21.info(
"Files that are smaller than %s or bigger than %s will be ignored.",
args=(min_size, max_size),
)
for file in path.iterdir():
if not file.is_file():
continue
if min_size <= (size := file.stat().st_size) <= max_size:
log21.print(
"`%s` is %s.",
args=(file, FileSize(size).humanize(binary=False, fmt="%.4f")),
)
if __name__ == "__main__":
log21.argumentify(main)Example usage and output:
$ uv run test.py . "1.23MiB" "0.5 GB"
[21:21:21] [INFO] Files that are smaller than 1.23 MiB or bigger than 476.84 MiB will be
ignored.
`myfile21.zip` is 35.1856 MB.Add file_mode and file_encoding parameters to get_logger for finer control over
the way a simple logger handles files.
For even more control you can still define Logger, Handlers, and Formatters manually.
import log21
logger = log21.get_logger(
"My File Logger", show_level=False, show_time=True, file="myapp.log", file_mode="a",
file_encoding="utf-8"
)
logger.info("Hello World!")Change the way argumentify handles function parameters to argument-parser arguments
conversion.
POSITIONAL_ONLYandVAR_POSITIONALparameters will be positional arguments.POSITIONAL_OR_KEYWORDandKEYWORD_ONLYparameters have flags assigned to them.POSITIONAL_OR_KEYWORDparameters will be required if at least oneKEYWORD_ONLYparameter is there, otherwise they are optional.VAR_KEYWORDparameters are still not supported.
def main(path: Path, /, output: Path, *, verbose: bool = False):
"""Process a file.
:param path: The input file path
:param output: The output file
:param verbose: Write more logs to the standard output.
"""
...
if __name__ == "__main__":
argumentify(main)The help looks like this:
usage: test.py [-h] --output OUTPUT [--verbose] path
Process a file.
positional arguments:
path The input file path
options:
-h, --help
show this help message and exit
--output OUTPUT, -o OUTPUT
The output file
--verbose, -v
Write more logs to the standard output.
Note that path and output are required.
def main(output: Path, /, *inputs: Path):
"""Process multiple files into one.
:param output: The output file
:param inputs: The path to the input files
"""
# Since `inputs` is a VAR_POSITIONAL, while being a positional argument, it can have
# zero length which is in many cases not intended.
# You might want to add a check for its length and raise an ArgumentError if it does
# not match your needs
# Check if at least one input has been passed and mark the argument as required
# if len(inputs) < 1:
# raise RequiredArgumentError("inputs")
# Raise an error unless at least two inputs are present
if len(inputs) < 2:
raise ArgumentError(message="You need to pass at least two files as input.")
...The help looks like this:
usage: test.py [-h] output [inputs ...]
Process multiple files into one.
positional arguments:
output The output file
inputs The path to the input files
options:
-h, --help
show this help message and exit
def main(first_name: str, last_name: str, output: Path, verbose: bool = False):
"""Write a greeting message.
:param first_name: The first name of the user to greet (optional)
:param last_name: The last name of the user to greet (optional)
:param output: The output file (stdout if none is provided)
:param verbose: If provided, will write the debug logs to stdout
"""
...
if __name__ == "__main__":
argumentify(main)The help looks like this:
usage: test.py [-h] [--first-name FIRST_NAME] [--last-name LAST_NAME] [--output OUTPUT]
[--verbose]
Write a greeting message.
options:
-h, --help
show this help message and exit
--first-name FIRST_NAME, -f FIRST_NAME
The first name of the user to greet (optional)
--last-name LAST_NAME, -l LAST_NAME
The last name of the user to greet (optional)
--output OUTPUT, -o OUTPUT
The output file (stdout if none is provided)
--verbose, -v
If provided, will write the debug logs to stdout
Note that all the options are optional and default to None. verbose is False by
default since a default value is provided for it in function definition.
Change argumentify to use the whole function description as the argument-parser
description instead of the one-line short description.
- Example:
def main(verbose: bool = False) -> None:
"""This is a very useful tool and I will describe it thoroughly. It is so good that
we have a second line in the first part of the description.
And now we can talk more about the tool...
:param verbose: This flag will make the logs more verbose!
"""
argumentify(main)The way old versions would look:
usage: test.py [-h] [--verbose]
This is a very useful tool and I will describe it thoroughly. It is so good that
options:
-h, --help
show this help message and exit
--verbose, -v
This flag will make the logs more verbose!
Now at v3.0.2:
usage: test.py [-h] [--verbose]
This is a very useful tool and I will describe it thoroughly. It is so good that we have
second line in the first part of the description. And now we can talk more about the tool...
options:
-h, --help
show this help message and exit
--verbose, -v
This flag will make the logs more verbose!
Fix the issue with argumentify which would result in falsy default values to be
replaced with None.
- Example:
def main(offset: int = 0) -> None:
...
argumentify(main)if no value was provided for --offset, the default would be None instead of 0
which was unexpected and can lead to issues.
This release introduces a cleaned-up internal structure, stricter naming conventions, and several quality-of-life improvements. While most users will not notice behavioral changes, v3 contains breaking changes for code that relies on internal imports or specific exception names.
-
Internal module renaming and normalization
- All internal modules were renamed to lowercase and, in some cases, split or reorganized.
- Imports such as
log21.Colors,log21.Logger,log21.ProgressBar, etc. are no longer valid. - Users importing from internal modules must update their imports to the new module names.
- Public imports from
log21remain supported.
-
Argumentify exception renames
- Several exceptions were renamed to follow a consistent
*Errornaming convention:TooFewArguments→TooFewArgumentsErrorRequiredArgument→RequiredArgumentErrorIncompatibleArguments→IncompatibleArgumentsError
- Code that explicitly raises or catches these exceptions must be updated.
- Several exceptions were renamed to follow a consistent
-
Crash reporter behavior improvement
- Prevented the default file crash reporter from creating
.crash_reportfiles when it is not actually used. - Implemented using an internal
FakeModulehelper.
- Prevented the default file crash reporter from creating
-
Argparse compatibility update
- Bundled and used the Python 3.13
argparseimplementation to ensure consistent behavior across supported Python versions.
- Bundled and used the Python 3.13
-
Progress bar module rename
- Renamed the internal progress bar module to
progress_barfor consistency with the new naming scheme. - This will not break the usages of
log21.progress_bar(...)since the call functionality was added to the module using theFakeModulehelper.
- Renamed the internal progress bar module to
-
Examples added and updated
- Added new example code files.
- Updated existing examples to match the v3 API and conventions.
- Resolved various linting and static-analysis issues across the codebase.
- Addressed minor compatibility issues uncovered by running linters and pre-commit hooks.
- Resolved errors occurring in environments with newer versions of argparse.
- Migrated the build system configuration to
uv. - Updated Python version classifiers and set the supported Python version to 3.9+.
- Added
verminto the pre-commit configuration. - Updated
.gitignore, license metadata, and tool configurations. - Silenced and resolved a large number of linter warnings.
- General internal refactoring with no intended user-visible behavioral changes.
- There are no intentional behavioral changes in logging output, argument parsing logic, or UI components.
- Most projects will require minimal or no changes unless they depend on internal modules or renamed exceptions.
- See MIGRATION-V2-V3.md for detailed upgrade instructions.
- Update README.md and CHANGELOG.md.
- Updated the Argparse module to be usable with python 3.12.3.
- Added some exception classes to raise in the "argumentified" functions to show
parser error to the user:
ArgumentError,IncompatibleArguments,RequiredArgument,TooFewArguments
- Added
Sequence[T]as a supported type to the ColorizingArgumentParser. - Bug fixes.
- Update
README.md.
- Added
<<and>>(left shift and right shift operators) tolog21.Logger.Logger.
- Fixed Carriage Return Handling.
- Fixed setting level using
log21.basic_config - Added more configuration for developer tools to the
pyproject.tomlfile. - Added pre-commit.
- Fixed setting level using
log21.basic_config
- Fixed Carriage Return Handling.
- Update python version
- Renamed
crash_report.logto.crash_report.log. - Added "force" error handling method to
Logger.add_level. - Changed the adding level error handling method to "ignore".
- Ability to add new methods to the Logger object for each custom level.
- Renamed
crash_report.logto.crash_report.log.
- Changed the adding level error handling method to "ignore".
- Ability to add new methods to the Logger object for each custom level.
- Update python version
- Added "force" error handling method to
Logger.add_level.
- Improved compatibility
- Modified
automatic-release.ymlandpypi.ymlworkflows to check the version - Added the support for more
types to pass toColorizingArgumentParser().add_argument(...):typing.Union,typing.Optional,typing.Literal,enum.Enum,tupleandtyping.Required. - Modified the way
Enums are handled in the Argument Parser. - Handled some
typing._SpecialForms. - A normal ArgumentGroup can now be required! (Unlike MutuallyExclusiveGroup it can have more than 1 option used at the same time)
argumentifynow supports async functions as the entry point.
Change in README.md.
- Added
encodingtolog21.crash_reporter.FileReporter. - Added configs for
pylint,yapfandisorttopyproject.toml. - Added optional
devdependencies topyproject.toml. - Improved overall code quality.
Added the Argumentify module. Check the examples.
Fixed a bug in the TreePrint class.
Added constant colors directly to the Colors module. Now you can do this:
from log21 import print
from log21.colors import GREEN, WHITE, RED
print(GREEN + 'This' + WHITE + ' is' + RED + ' Red')Moved some dictionaries to __init__ methods.
colorsinArgparse.ColorizingHelpFormatterclass._level_nameinFormatters._Formatterclass andlevel_colorsinFormatters.ColorizingFormatterclass.sign_colorsinPPrint.PrettyPrinterclass.colorsinTreePrint.TreePrint.Nodeclass.
Improved type-hintings.
Switched from setup.py build system to pyproject.toml
Added level_colors argument to log21.get_logger function with will be passed to the
formatter and allows user to set custom level colors while making a new logger.
Also changed most Dict type hints to be Mapping and list to Sequence to make the
functions more general and less strict.
Added extra_values argument to crash_reporter.Formatter which will let you pass extra
static or dynamic values to the report formatter.
They can be used in the format string. For dynamic values you can pass a function that
takes no arguments as the value.
Shortened the usage syntax for the CrashReporters:
import log21
# Define a ConsoleReporter object
console_reporter = log21.crash_reporter.ConsoleReporter()
# This works with other `log21.crash_reporter.reporter` subclasses as well.
# Old syntax (still supported)
@console_reporter.reporter
def divide_old(a, b):
return a / b
# New Syntax
@console_reporter.reporter
def divide_new(a, b):
return a / bconsole_crash_reporter and file_crash_reporter are removed!
Added no_color parameter to ProgressBar.
Some bug fixes.
Improvements.
Bug Fixes.
Bug fixes and improvements.
- Made it more compatible with multi-threading.
- Fixed some bugs.
Minor fixes and improvements.
Minor fixes and improvements.
Added catch and ignore methods to log21.CrashReporter.Reporter.
Added exceptions_to_catch and exceptions_to_ignore arguments to
log21.CrashReporter.Reporter class.
Added Print logging level.
Minor improvements.
Added a new method to log21.Logger class: log21.Logger.clear_line. This method
clears the current line in the console and moves the cursor to the beginning of the line.
Fixed a bug that would cause an error creating a progress bar with no value set for
width in systems without support for os.get_terminal_size().
Added additional_variables argument to log21.ProgressBar class. You can use it in
order to add additional variables to the progress bar:
import log21, time
progress_bar = log21.ProgressBar(
format_='Iteration: {i} {prefix}{bar}{suffix} {percentage}%',
style='{',
additional_variables={"i": 0}
)
for i in range(100):
progress_bar(i + 1, 100, i=i)
time.sleep(0.1)
# Iteration: 99 |████████████████████████████████████████████████████████████████| 100%Added formatter argument to StreamHandler and FileHandler. You can use it to set
the formatter of the handler when you create it. Added handlers argument to Logger.
You can use it to add handlers to the logger when you create it.
Added progressbar custom formatting.
Now you can use your own formatting for the progressbar instead of the default one.
Let's see an example:
# We import the ProgressBar class from log21
from log21 import ProgressBar
# psutil is a module that can be used to get the current memory usage or cpu usage of
# your system
# If you want to try this example, you need to install psutil: pip install psutil
import psutil
# We use the time module to make a delay between the progressbar updates
import time
cpu_bar = ProgressBar(format_='CPU Usage: {prefix}{bar}{suffix} {percentage}%',
style='{', new_line_when_complete=False)
while True:
cpu_bar.update(psutil.cpu_percent(), 100)
time.sleep(0.1)Added CrashReporter!
You can use Reporter classes to monitor your program and send crash reports to the developer. It can help you fix the bugs and improve your program before your users get upset about it. See some examples in the log21/crash_reporter/reporters.py file.
Bug fixes.
Added getpass method to log21.Logger class and added log21.getpass function.
Bug fixes.
Added log21.input function.
Fixed import error in tkinter-less environments.
Minor changes.
Added optional shell support to the LoggingWindow.
Added LoggingWindow!
Added ProgressBar class!
You can directly print a progress bar to the console using print_progress method of
log21.Logger class.
OR
Use log21.ProgressBar class witch is specifically designed for this purpose.
OR
Use log21.progress_bar function (I don't recommend it!).
Minor changes.
Added log21.log, log21.debug, log21.info, log21.warning, log21.error and some
other functions.
Added log21.tree_print() function.
Added log21.pprint() function. It is similar to pprint.pprint() function.
Added level_names argument to Formatter classes.
level_names can be used to change the name of logging level that appears while logging
messages.
Minor changes.
log21.print function added!
More description added.
ColorizingArgumentParser improvements.
Setting custom formatting style and custom date-time formatting added to
log21.get_logger function.
Logger.write edited. It's same as Logger.warning but its default end argument
value is an empty string.
Logger.write added. It's same as Logger.warning
Bug fixed:
>>> log21.get_logger()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\...\Python37-32\lib\site-packages\log21\__init__.py", line 44, in get_logger
raise TypeError('A logger name must be a string')
TypeError: A logger name must be a stringget_logger improved.
Logger.print added.
You can use Logger.print to print a message using the current level of the logger
class.
It gets printed with any level.
ColorizingArgumentParser added.
You can use ColorizingArgumentParser to have a colorful ArgumentParser.
StreamHandler can handle new-line characters at the beginning of the message.
get_color function now supports hexadecimal and decimal RGB values.