diff --git a/CHANGELOG.md b/CHANGELOG.md index f906f81..deb099a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Help this project by [Donation](DONATE.md) Changes ----------- +### 2.6.0 + +Added the `Argumentify` module. Check the examples. + ### 2.5.5 Fixed a bug in the `TreePrint` class. diff --git a/README.md b/README.md index 302f7bc..d56a27b 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,12 @@ Features structure. It's also colorized XD. + ProgressBar : log21's progress bar can be used to show progress of a process in a beautiful way. + LoggingWindow : Helps you to log messages and debug your code in a window other than the console. -+ CrashReporter : log21's crash reporter can be used to report crashes in different ways. You can use it to log crashes - to console or files or use it to receive crash reports of your program through email. And you can also define your own - crash reporter functions and use them instead! ++ CrashReporter : log21's crash reporter can be used to report crashes in different + ways. You can use it to log crashes to console or files or use it to receive crash + reports of your program through email. And you can also define your own crash + reporter functions and use them instead! ++ Argumentify : You can use the argumentify feature to decrease the number of lines you + need to write to parse command-line arguments. It's colored by the way! + Any idea? Feel free to [open an issue](https://github.com/MPCodeWriter21/log21/issues) or submit a pull request. ![issues](https://img.shields.io/github/issues/MPCodeWriter21/log21) @@ -40,22 +43,27 @@ python. Then you can install log21 using pip module: -```shell +```bash python -m pip install log21 -U ``` Or you can clone [the repository](https://github.com/MPCodeWriter21/log21) and run: -```shell -python setup.py install +```bash +pip install . +``` + +Or let the pip get it using git: +```bash +pip install git+https://github.com/MPCodeWriter21/log21 ``` Changes ------- -### 2.5.5 +### 2.6.0 -Fixed a bug in the `TreePrint` class. +Added the `Argumentify` module. Check the examples. [Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md) @@ -63,7 +71,9 @@ Fixed a bug in the `TreePrint` class. Usage Examples: --------------- -```python3 +### Basic Logging + +```python import log21 log21.print(log21.get_color('#FF0000') + 'This' + log21.get_color((0, 255, 0)) + ' is' + log21.get_color('Blue') + @@ -87,7 +97,9 @@ logger.error(log21.get_colors('LightRed') + "I'm still here ;1") ---------------- -```python3 +### Argument Parsing (See Also: [Argumentify](https://github.com/MPCodeWriter21/log21#argumentify)) + +```python import log21 from log21 import ColorizingArgumentParser, get_logger, get_colors as gc @@ -133,7 +145,9 @@ logger.info(gc('LightWhite') + 'Done!') ------------------ -```python3 +### Pretty-Printing and Tree-Printing + +```python import json import log21 @@ -156,7 +170,9 @@ log21.tree_print(data) ------------------ -```python3 +### Logging Window + +```python import log21 window = log21.get_logging_window('My Logging Window', width=80) @@ -197,7 +213,9 @@ window.error(colored_text) ------------------ -```python3 +### ProgressBar + +```python # Example 1 import log21, time @@ -235,6 +253,199 @@ for i in range(84): ![ProgressBar - Example 1](https://github.com/MPCodeWriter21/log21/raw/master/screen-shots/example-5.1.gif) ![ProgressBar - Example 2](https://github.com/MPCodeWriter21/log21/raw/master/screen-shots/example-5.2.gif) +------------------ + +### Argumentify (Check out [the manual way](https://github.com/MPCodeWriter21/log21#argument-parsing)) + +```python +# Common Section +import log21 + + +class ReversedText: + def __init__(self, text: str): + self._text = text[::-1] + + def __str__(self): + return self._text + + def __repr__(self): + return f"<{self.__class__.__name__}(text='{self._text}') at {hex(id(self))}>" + + +# Old way +def main(): + """Here is my main function""" + parser = log21.ColorizingArgumentParser() + parser.add_argument('--positional-arg', '-p', action='store', type=int, + required=True, help="This argument is positional!") + parser.add_argument('--optional-arg', '-o', action='store', type=ReversedText, + help="Whatever you pass here will be REVERSED!") + parser.add_argument('--arg-with-default', '-a', action='store', default=21, + help="The default value is 21") + parser.add_argument('--additional-arg', '-A', action='store', + help="This one is extra.") + parser.add_argument('--verbose', '-v', action='store_true', + help="Increase verbosity") + args = parser.parse_args() + + if args.verbose: + log21.basic_config(level='DEBUG') + + log21.info(f"positional_arg = {args.positional_arg}") + log21.info(f"optional_arg = {args.optional_arg}") + log21.debug(f"arg_with_default = {args.arg_with_default}") + log21.debug(f"additional_arg = {args.additional_arg}") + + +if __name__ == '__main__': + main() + + +# New way +def main(positional_arg: int, /, optional_arg: ReversedText, arg_with_default: int = 21, + additional_arg=None, verbose: bool = False): + """Some description + + :param positional_arg: This argument is positional! + :param optional_arg: Whatever you pass here will be REVERSED! + :param arg_with_default: The default value is 21 + :param additional_arg: This one is extra. + :param verbose: Increase verbosity + """ + if verbose: + log21.basic_config(level='DEBUG') + + log21.info(f"{positional_arg = }") + log21.info(f"{optional_arg = !s}") + log21.debug(f"{arg_with_default = }") + log21.debug(f"{additional_arg = !s}") + + +if __name__ == '__main__': + log21.argumentify(main) +``` + +![Old-Way](https://github.com/MPCodeWriter21/log21/raw/master/screen-shots/example-6.1.png) +![New-Way](https://github.com/MPCodeWriter21/log21/raw/master/screen-shots/example-6.2.png) + +Example with multiple functions as entry-point: + +```python +import ast +import operator +from functools import reduce + +import log21 + +# `safe_eval` Based on https://stackoverflow.com/a/9558001/1113207 +# Supported Operators +operators = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.FloorDiv: operator.floordiv, + ast.Pow: operator.pow, + ast.BitXor: operator.xor, + ast.USub: operator.neg +} + + +def safe_eval(expr: str): + """Safely evaluate a mathematical expression. + + >>> eval_expr('2^6') + 4 + >>> eval_expr('2**6') + 64 + >>> eval_expr('1 + 2*3**(4^5) / (6 + -7)') + -5.0 + + :param expr: expression to evaluate + :raises SyntaxError: on invalid expression + :return: result of the evaluation + """ + try: + return _eval(ast.parse(expr, mode='eval').body) + except (TypeError, KeyError, SyntaxError): + log21.error(f'Invalid expression: {expr}') + raise + + +def _eval(node: ast.AST): + """Internal implementation of `safe_eval`. + + :param node: AST node to evaluate + :raises TypeError: on invalid node + :raises KeyError: on invalid operator + :raises ZeroDivisionError: on division by zero + :raises ValueError: on invalid literal + :raises SyntaxError: on invalid syntax + :return: result of the evaluation + """ + if isinstance(node, ast.Num): # + return node.n + if isinstance(node, ast.BinOp): # + return operators[type(node.op)](_eval(node.left), _eval(node.right)) + if isinstance(node, ast.UnaryOp): # e.g., -1 + return operators[type(node.op)](_eval(node.operand)) + raise TypeError(node) + + +# Example code +def addition(*numbers: float): + """Addition of numbers. + + Args: + numbers (float): numbers to add + """ + if len(numbers) < 2: + log21.error('At least two numbers are required! Use `-n`.') + return + log21.info(f'Result: {sum(numbers)}') + + +def multiplication(*numbers: float): + """Multiplication of numbers. + + Args: + numbers (float): numbers to multiply + """ + if len(numbers) < 2: + log21.error('At least two numbers are required! Use `-n`.') + return + log21.info(f'Result: {reduce(lambda x, y: x * y, numbers)}') + + +def calc(*inputs: str, verbose: bool = False): + """Calculate numbers. + + :param inputs: numbers and operators + """ + expression = ' '.join(inputs) + + if len(expression) < 3: + log21.error('At least two numbers and one operator are required! Use `-i`.') + return + + if verbose: + log21.basic_config(level='DEBUG') + + log21.debug(f'Expression: {expression}') + try: + log21.info(f'Result: {safe_eval(expression)}') + except (TypeError, KeyError, SyntaxError): + pass + + +if __name__ == "__main__": + log21.argumentify({'add': addition, 'mul': multiplication, 'calc': calc}) +``` + +![multi-entry](https://github.com/MPCodeWriter21/log21/raw/master/screen-shots/example-6.3.png) + + About ----- Author: CodeWriter21 (Mehrad Pooryoussof) diff --git a/pyproject.toml b/pyproject.toml index f2349b7..bb24af2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,10 @@ classifiers = [ "Operating System :: MacOS :: MacOS X" ] dependencies = [ - "webcolors" + "webcolors", + "docstring-parser" ] -version = "2.5.5" +version = "2.6.0" [tool.setuptools.packages.find] where = ["src"] diff --git a/screen-shots/example-6.1.png b/screen-shots/example-6.1.png new file mode 100644 index 0000000..671191a Binary files /dev/null and b/screen-shots/example-6.1.png differ diff --git a/screen-shots/example-6.2.png b/screen-shots/example-6.2.png new file mode 100644 index 0000000..a764fe5 Binary files /dev/null and b/screen-shots/example-6.2.png differ diff --git a/screen-shots/example-6.3.png b/screen-shots/example-6.3.png new file mode 100644 index 0000000..f9f7349 Binary files /dev/null and b/screen-shots/example-6.3.png differ diff --git a/src/log21/Argumentify.py b/src/log21/Argumentify.py new file mode 100644 index 0000000..1ba42b0 --- /dev/null +++ b/src/log21/Argumentify.py @@ -0,0 +1,416 @@ +# log21.Argparse.py +# CodeWriter21 + +import re as _re +import string as _string +import inspect as _inspect +import argparse as _argparse +from typing import (Any as _Any, Set as _Set, Dict as _Dict, List as _List, + Tuple as _Tuple, Union as _Union, Callable as _Callable, + Optional as _Optional, Coroutine as _Coroutine, + OrderedDict as _OrderedDict) +from dataclasses import field as _field, dataclass as _dataclass + +from docstring_parser import Docstring as _Docstring, parse as _parse + +from log21.Argparse import ColorizingArgumentParser as _ColorizingArgumentParser + +__all__ = [ + 'argumentify', 'ArgumentifyError', 'ArgumentTypeError', 'FlagGenerationError', + 'RESERVED_FLAGS', 'Callable', 'Argument', 'FunctionInfo', 'generate_flag', + 'normalize_name', 'normalize_name_to_snake_case' +] + +Callable = _Union[_Callable[..., _Any], _Callable[..., _Coroutine[_Any, _Any, _Any]]] +RESERVED_FLAGS = {'--help', '-h'} + + +class ArgumentifyError(Exception): + """Base class for exceptions in this module.""" + + +class ArgumentTypeError(ArgumentifyError, TypeError): + """Exception raised when a function has an unsupported type of argument. + + e.g: a function has a VAR_KEYWORD argument. + """ + + def __init__( + self, message: _Optional[str] = None, unsupported_arg: _Optional[str] = None + ): + """Initialize the exception. + + :param message: The message to display. + :param unsupported_arg: The name of the unsupported argument. + """ + if message is None: + if unsupported_arg is None: + message = 'Unsupported argument type.' + else: + message = f'Unsupported argument type: {unsupported_arg}' + self.message = message + self.unsupported_arg = unsupported_arg + + +class FlagGenerationError(ArgumentifyError, RuntimeError): + """Exception raised when an error occurs while generating a flag. + + Most likely raised when there are arguments with the same name. + """ + + def __init__(self, message: _Optional[str] = None, arg_name: _Optional[str] = None): + """Initialize the exception. + + :param message: The message to display. + :param arg_name: The name of the argument that caused the error. + """ + if message is None: + if arg_name is None: + message = 'An error occurred while generating a flag.' + else: + message = ( + 'An error occurred while generating a flag for argument: ' + f'{arg_name}' + ) + self.message = message + self.arg_name = arg_name + + +def normalize_name_to_snake_case(name: str, sep_char: str = '_') -> str: + """Returns the normalized name a class. + + >>> normalize_name_to_snake_case('main') + 'main' + >>> normalize_name_to_snake_case('MyClassName') + 'my_class_name' + >>> normalize_name_to_snake_case('HelloWorld') + 'hello_world' + >>> normalize_name_to_snake_case('myVar') + 'my_var' + >>> normalize_name_to_snake_case("It's cool") + 'it_s_cool' + >>> normalize_name_to_snake_case("test-name") + 'test_name' + + :param name: The name to normalize. + :param sep_char: The character that will replace space and separate words + :return: The normalized name. + """ + for char in _string.punctuation: + name = name.replace(char, sep_char) + name = _re.sub(rf'([\s{sep_char}]+)|(([a-zA-z])([A-Z]))', rf'\3{sep_char}\4', + name).lower() + return name + + +def normalize_name(name: str, sep_char: str = '_') -> str: + """Returns the normalized name a class. + + >>> normalize_name('main') + 'main' + >>> normalize_name('MyFunction') + 'MyFunction' + >>> normalize_name('HelloWorld') + 'HelloWorld' + >>> normalize_name('myVar') + 'myVar' + >>> normalize_name("It's cool") + 'It_s_cool' + >>> normalize_name("test-name") + 'test_name' + + :param name: The name to normalize. + :param sep_char: The character that will replace space and separate words + :return: The normalized name. + """ + for char in _string.punctuation: + name = name.replace(char, sep_char) + name = _re.sub(rf'([\s{sep_char}]+)', sep_char, name) + return name + + +@_dataclass +class Argument: + """Represents a function argument.""" + name: str + kind: _inspect._ParameterKind + annotation: _Any = _inspect._empty + default: _Any = _inspect._empty + help: _Optional[str] = None + + def __post_init__(self): + """Sets the some values to None if they are empty.""" + if self.annotation == _inspect._empty: + self.annotation = None + if self.default == _inspect._empty: + self.default = None + + +@_dataclass +class FunctionInfo: + """Represents a function.""" + function: Callable + name: str = _field(init=False) + arguments: _Dict[str, Argument] = _field(init=False) + docstring: _Docstring = _field(init=False) + parser: _ColorizingArgumentParser = _field(init=False) + + def __post_init__(self): + self.name = normalize_name_to_snake_case( + self.function.__init__.__name__ + ) if isinstance(self.function, + type) else normalize_name(self.function.__name__) + self.function = self.function.__init__ if isinstance( + self.function, type + ) else self.function + + self.arguments: _OrderedDict[str, Argument] = _OrderedDict() + for parameter in _inspect.signature(self.function).parameters.values(): + self.arguments[parameter.name] = Argument( + name=parameter.name, + kind=parameter.kind, + default=parameter.default, + annotation=parameter.annotation, + ) + + self.docstring = _parse(self.function.__doc__ or '') + for parameter in self.docstring.params: + if parameter.arg_name in self.arguments: + self.arguments[parameter.arg_name].help = parameter.description + + +def generate_flag( + argument: Argument, + no_dash: bool = False, + reserved_flags: _Optional[_Set[str]] = None +) -> _List[str]: + """Generates one or more flags for an argument based on its attributes. + + :param argument: The argument to generate flags for. + :param no_dash: Whether to generate flags without dashes as + prefixes. + :param reserved_flags: A set of flags that are reserved. (Default: `RESERVED_FLAGS`) + :raises FlagGenerationError: If all the suitable flags are reserved. + :return: A list of flags for the argument. + """ + if reserved_flags is None: + reserved_flags = RESERVED_FLAGS + flags: _List[str] = [] + flag1_base = ('' if no_dash else '--') + flag1 = flag1_base + normalize_name_to_snake_case(argument.name, '-') + if flag1 in reserved_flags: + flag1 = flag1_base + normalize_name(argument.name, sep_char='-') + if flag1 in reserved_flags: + flag1 = flag1_base + argument.name + if flag1 in reserved_flags: + flag1 = flag1_base + normalize_name( + ' '.join(normalize_name_to_snake_case(argument.name, '-').split('-') + ).capitalize(), + sep_char='-' + ) + if flag1 in reserved_flags: + flag1 = flag1_base + normalize_name(argument.name, sep_char='-').upper() + if flag1 in reserved_flags and no_dash: + raise FlagGenerationError(f"Failed to generate a flag for argument: {argument}") + if flag1 not in reserved_flags: + flags.append(flag1) + + if not no_dash: + flag2 = '-' + argument.name[:1].lower() + if flag2 in reserved_flags: + flag2 = flag2.upper() + if flag2 in reserved_flags: + flag2 = '-' + ''.join( + part[:1] + for part in normalize_name_to_snake_case(argument.name).split('_') + ) + if flag2 in reserved_flags: + flag2 = flag2.capitalize() + if flag2 in reserved_flags: + flag2 = flag2.upper() + if flag2 not in reserved_flags: + flags.append(flag2) + + if not flags: + raise FlagGenerationError(f"Failed to generate a flag for argument: {argument}") + + reserved_flags.update(flags) + + return flags + + +def _add_arguments( + parser: _Union[_ColorizingArgumentParser, _argparse._ArgumentGroup], + info: FunctionInfo, + reserved_flags: _Optional[_Set[str]] = None +) -> None: + """Add the arguments to the parser. + + :param parser: The parser to add the arguments to. + :param info: The function info. + :param reserved_flags: The reserved flags. + """ + if reserved_flags is None: + reserved_flags = RESERVED_FLAGS.copy() + # Add the arguments + for argument in info.arguments.values(): + config: _Dict[str, _Any] = { + 'action': 'store', + 'dest': argument.name, + 'help': argument.help + } + if isinstance(argument.annotation, type): + config['type'] = argument.annotation + if argument.annotation == bool: + config['action'] = 'store_true' + config.pop('type') + if argument.kind == _inspect._ParameterKind.POSITIONAL_ONLY: + config['required'] = True + if argument.kind == _inspect._ParameterKind.VAR_POSITIONAL: + config['nargs'] = '*' + if argument.default: + config['default'] = argument.default + parser.add_argument( + *generate_flag(argument, reserved_flags=reserved_flags), **config + ) + + +def _argumentify_one(func: Callable): + """This function argumentifies one function as the entry point of the + script. + + :param function: The function to argumentify. + """ + info = FunctionInfo(func) + + # Check if the function has a VAR_KEYWORD argument + # Raises a ArgumentTypeError if it does + for argument in info.arguments.values(): + if argument.kind == _inspect._ParameterKind.VAR_KEYWORD: + raise ArgumentTypeError( + f"The function has a `**{argument.name}` argument, " + "which is not supported.", + unsupported_arg=argument.name + ) + + # Create the parser + parser = _ColorizingArgumentParser(description=info.docstring.short_description) + # Add the arguments + _add_arguments(parser, info) + cli_args = parser.parse_args() + args = [] + kwargs = {} + for argument in info.arguments.values(): + if argument.kind in (_inspect._ParameterKind.POSITIONAL_ONLY, + _inspect._ParameterKind.POSITIONAL_OR_KEYWORD): + args.append(getattr(cli_args, argument.name)) + elif argument.kind == _inspect._ParameterKind.VAR_POSITIONAL: + args.extend(getattr(cli_args, argument.name) or []) + else: + kwargs[argument.name] = getattr(cli_args, argument.name) + func(*args, **kwargs) + + +def _argumentify(functions: _Dict[str, Callable]): + """This function argumentifies one or more functions as the entry point of + the script. + + :param functions: A dictionary of functions to argumentify. + :raises RuntimeError: + """ + functions_info: _Dict[str, _Tuple[Callable, FunctionInfo]] = {} + for name, function in functions.items(): + functions_info[name] = (function, FunctionInfo(function)) + + # Check if the function has a VAR_KEYWORD argument + # Raises a ArgumentTypeError if it does + for argument in functions_info[name][1].arguments.values(): + if argument.kind == _inspect._ParameterKind.VAR_KEYWORD: + raise ArgumentTypeError( + f"Function {name} has `**{argument.name}` argument, " + "which is not supported.", + unsupported_arg=argument.name + ) + parser = _ColorizingArgumentParser() + subparsers = parser.add_subparsers(required=True) + for name, (_, info) in functions_info.items(): + subparser = subparsers.add_parser(name, help=info.docstring.short_description) + _add_arguments(subparser, info) + subparser.set_defaults(func=info.function) + cli_args = parser.parse_args() + args = [] + kwargs = {} + info = None + for name, (function, info) in functions_info.items(): + if function == cli_args.func: + break + else: + raise RuntimeError('No function found for the given arguments.') + for argument in info.arguments.values(): + if argument.kind in (_inspect._ParameterKind.POSITIONAL_ONLY, + _inspect._ParameterKind.POSITIONAL_OR_KEYWORD): + args.append(getattr(cli_args, argument.name)) + elif argument.kind == _inspect._ParameterKind.VAR_POSITIONAL: + args.extend(getattr(cli_args, argument.name) or []) + else: + kwargs[argument.name] = getattr(cli_args, argument.name) + function(*args, **kwargs) + + +def argumentify(entry_point: _Union[Callable, _List[Callable], _Dict[str, Callable]]): + """This function argumentifies one or more functions as the entry point of + the script. + + 1 #!/usr/bin/env python + 2 # argumentified.py + 3 from log21 import argumentify + 4 + 5 + 6 def main(first_name: str, last_name: str, /, *, age: int = None) -> None: + 7 if age is not None: + 8 print(f'{first_name} {last_name} is {age} years old.') + 9 else: + 10 print(f'{first_name} {last_name} is not yet born.') + 11 + 12 if __name__ == '__main__': + 13 argumentify(main) + + $ python argumentified.py Ahmad Ahmadi --age 20 + Ahmad Ahmadi is 20 years old. + $ python argumentified.py Mehrad Pooryoussof + Mehrad Pooryoussof is not yet born. + + :param entry_point: The function(s) to argumentify. + :raises TypeError: A function must be a function or a list of functions or a + dictionary of functions. + """ + + functions = {} + # Check the types + if callable(entry_point): + _argumentify_one(entry_point) + return entry_point + if isinstance(entry_point, _List): + for func in entry_point: + if not callable(func): + raise TypeError( + "argumentify: func must be a function or a list of functions or a " + "dictionary of functions." + ) + functions[func.__name__] = func + elif isinstance(entry_point, _Dict): + for func in entry_point.values(): + if not callable(func): + raise TypeError( + "argumentify: func must be a function or a list of functions or a " + "dictionary of functions." + ) + functions = entry_point + else: + raise TypeError( + "argumentify: func must be a function or a list of functions or a " + "dictionary of functions." + ) + + _argumentify(functions) + return entry_point diff --git a/src/log21/__init__.py b/src/log21/__init__.py index cd431ee..753539d 100644 --- a/src/log21/__init__.py +++ b/src/log21/__init__.py @@ -4,25 +4,27 @@ import io as _io import os as _os import logging as _logging - -from typing import Union as _Union, Mapping as _Mapping, Type as _Type, Tuple as _Tuple, Optional as _Optional - -import log21.CrashReporter as CrashReporter - -from log21.Levels import INPUT, CRITICAL, FATAL, ERROR, WARNING, WARN, INFO, DEBUG, NOTSET +from typing import (Type as _Type, Tuple as _Tuple, Union as _Union, + Mapping as _Mapping, Optional as _Optional) + +from log21 import CrashReporter +from log21.Colors import (Colors, get_color, get_colors, ansi_escape, closest_color, + get_color_name) +from log21.Levels import (INFO, WARN, DEBUG, ERROR, FATAL, INPUT, NOTSET, WARNING, + CRITICAL) from log21.Logger import Logger -from log21.Manager import Manager -from log21.ProgressBar import ProgressBar from log21.PPrint import PrettyPrinter, pformat -from log21.TreePrint import TreePrint, tree_format +from log21.Manager import Manager from log21.Argparse import ColorizingArgumentParser +from log21.TreePrint import TreePrint, tree_format +from log21.Formatters import ColorizingFormatter, DecolorizingFormatter +from log21.Argumentify import argumentify from log21.FileHandler import DecolorizingFileHandler +from log21.ProgressBar import ProgressBar from log21.LoggingWindow import LoggingWindow, LoggingWindowHandler -from log21.StreamHandler import ColorizingStreamHandler, StreamHandler -from log21.Formatters import ColorizingFormatter, DecolorizingFormatter -from log21.Colors import Colors, get_color, get_colors, ansi_escape, get_color_name, closest_color +from log21.StreamHandler import StreamHandler, ColorizingStreamHandler -__version__ = "2.5.5" +__version__ = "2.6.0" __author__ = "CodeWriter21 (Mehrad Pooryoussof)" __github__ = "Https://GitHub.com/MPCodeWriter21/log21" __all__ = [ @@ -35,7 +37,7 @@ '__github__', 'debug', 'info', 'warning', 'warn', 'error', 'critical', 'fatal', 'exception', 'log', 'basic_config', 'basicConfig', 'ProgressBar', 'progress_bar', 'LoggingWindow', 'LoggingWindowHandler', 'get_logging_window', 'CrashReporter', - 'console_reporter', 'file_reporter' + 'console_reporter', 'file_reporter', 'argumentify' ] _manager = Manager() @@ -223,8 +225,7 @@ def get_logging_window( height: int = 20, allow_shell: bool = False ) -> LoggingWindow: - """ - Returns a logging window. + """Returns a logging window. >>> # Let's see how it works >>> # Imports log21 and time modules @@ -387,8 +388,7 @@ def basic_config( format_: str = None, level: _Union[int, str] = None ): - """ - Do basic configuration for the logging system. + """Do basic configuration for the logging system. This function does nothing if the root logger already has handlers configured, unless the keyword argument *force* is set to ``True``. @@ -488,9 +488,10 @@ def basic_config( def critical(*msg, args=(), **kwargs): - """ - Log a message with severity 'CRITICAL' on the root logger. If the logger has no handlers, call basicConfig() to add - a console handler with a pre-defined format. + """Log a message with severity 'CRITICAL' on the root logger. + + If the logger has no handlers, call basicConfig() to add a console + handler with a pre-defined format. """ if len(root.handlers) == 0: basic_config() @@ -498,16 +499,15 @@ def critical(*msg, args=(), **kwargs): def fatal(*msg, args=(), **kwargs): - """ - Don't use this function, use critical() instead. - """ + """Don't use this function, use critical() instead.""" critical(*msg, args=args, **kwargs) def error(*msg, args=(), **kwargs): - """ - Log a message with severity 'ERROR' on the root logger. If the logger has no handlers, call basicConfig() to add a - console handler with a pre-defined format. + """Log a message with severity 'ERROR' on the root logger. + + If the logger has no handlers, call basicConfig() to add a console + handler with a pre-defined format. """ if len(root.handlers) == 0: basic_config() @@ -515,17 +515,20 @@ def error(*msg, args=(), **kwargs): def exception(*msg, args=(), exc_info=True, **kwargs): - """ - Log a message with severity 'ERROR' on the root logger, with exception information. If the logger has no handlers, - basicConfig() is called to add a console handler with a pre-defined format. + """Log a message with severity 'ERROR' on the root logger, with exception + information. + + If the logger has no handlers, basicConfig() is called to add a + console handler with a pre-defined format. """ error(*msg, args=args, exc_info=exc_info, **kwargs) def warning(*msg, args=(), **kwargs): - """ - Log a message with severity 'WARNING' on the root logger. If the logger has no handlers, call basicConfig() to add - a console handler with a pre-defined format. + """Log a message with severity 'WARNING' on the root logger. + + If the logger has no handlers, call basicConfig() to add a console + handler with a pre-defined format. """ if len(root.handlers) == 0: basic_config() @@ -537,9 +540,10 @@ def warn(*msg, args=(), **kwargs): def info(*msg, args=(), **kwargs): - """ - Log a message with severity 'INFO' on the root logger. If the logger has no handlers, call basicConfig() to add a - console handler with a pre-defined format. + """Log a message with severity 'INFO' on the root logger. + + If the logger has no handlers, call basicConfig() to add a console + handler with a pre-defined format. """ if len(root.handlers) == 0: basic_config() @@ -547,9 +551,10 @@ def info(*msg, args=(), **kwargs): def debug(*msg, args=(), **kwargs): - """ - Log a message with severity 'DEBUG' on the root logger. If the logger has no handlers, call basicConfig() to add a - console handler with a pre-defined format. + """Log a message with severity 'DEBUG' on the root logger. + + If the logger has no handlers, call basicConfig() to add a console + handler with a pre-defined format. """ if len(root.handlers) == 0: basic_config() @@ -557,9 +562,10 @@ def debug(*msg, args=(), **kwargs): def log(level, *msg, args=(), **kwargs): - """ - Log 'msg % args' with the integer severity 'level' on the root logger. If the logger has no handlers, call - basicConfig() to add a console handler with a pre-defined format. + """Log 'msg % args' with the integer severity 'level' on the root logger. + + If the logger has no handlers, call basicConfig() to add a console + handler with a pre-defined format. """ if len(root.handlers) == 0: basic_config() @@ -574,9 +580,7 @@ def progress_bar( suffix: str = '|', show_percentage: bool = True ): - """ - Print a progress bar to the console. - """ + """Print a progress bar to the console.""" bar = ProgressBar( width=width, prefix=prefix, suffix=suffix, show_percentage=show_percentage