diff --git a/CHANGELOG.md b/CHANGELOG.md index d301e5c..1b5b8f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Help this project by [Donation](DONATE.md) Changes ------- +### 2.10.0 + ++ Added some exception classes to raise in the "argumentified" functions to show + *parser error* to the user: `ArgumentError`, `IncompatibleArguments`, + `RequiredArgument`, `TooFewArguments` + ### 2.9.2 + Added `Sequence[T]` as a supported type to the ColorizingArgumentParser. diff --git a/EXAMPLES.md b/EXAMPLES.md index a7ac261..45c88fa 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -376,3 +376,56 @@ if __name__ == "__main__": ![multi-entry](https://github.com/MPCodeWriter21/log21/raw/master/screen-shots/example-6.3.png) +Example with parser errors: + +```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))}>" + + +# New way +def main(positional_arg: int, /, optional_arg: ReversedText, arg_with_default: int = 21, + additional_arg=None, verbose: bool = False, quiet: 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 + :param quiet: Make the script quiet + """ + if verbose and quiet: + raise log21.IncompatibleArguments( + '--verbose', + '--quiet', + message="You can not make the script quiet and except it to be more verbose!" + ) + if verbose: + log21.basic_config(level='DEBUG') + if quiet: + log21.basic_config(level='WARN') + + log21.info(f"{positional_arg = }") + log21.info(f"{optional_arg = !s}") + log21.warn("THIS IS IMPORTANT!!!") + log21.debug(f"{arg_with_default = }") + log21.debug(f"{additional_arg = !s}") + + +if __name__ == '__main__': + log21.argumentify(main) +``` + +![parser-error](https://github.com/MPCodeWriter21/log21/raw/master/screen-shots/example-6.4.png) diff --git a/README.md b/README.md index 5c2ca5a..b086a16 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,11 @@ pip install git+https://github.com/MPCodeWriter21/log21 Changes ------- -### 2.9.2 +### 2.10.0 -+ Added `Sequence[T]` as a supported type to the ColorizingArgumentParser. -+ Bug fixes. ++ Added some exception classes to raise in the "argumentified" functions to show + *parser error* to the user: `ArgumentError`, `IncompatibleArguments`, + `RequiredArgument`, `TooFewArguments` [Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md) diff --git a/pyproject.toml b/pyproject.toml index a0ca303..572ee32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "log21" authors = [ - {name = "CodeWriter21(Mehrad Pooryoussof)", email = "CodeWriter21@gmail.com"} + {name = "CodeWriter21(Mehrad Pooryoussof)", email = "CodeWriter21@gmail.com"} ] description = "A simple logging package that helps you log colorized messages in Windows console." readme = {file = "README.md", content-type = "text/markdown"} @@ -13,20 +13,20 @@ requires-python = ">=3.8" keywords = ['python', 'log', 'colorize', 'color', 'logging', 'Python3', 'CodeWriter21'] license = {text = "Apache License 2.0"} classifiers = [ - "Intended Audience :: Developers", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Operating System :: Unix", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X" + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X" ] dependencies = [ - "webcolors", - "docstring-parser" + "webcolors", + "docstring-parser" ] -version = "2.9.2" +version = "2.10.0" [tool.setuptools.packages.find] where = ["src"] @@ -43,11 +43,11 @@ dev = ["yapf", "isort", "docformatter", "pylint", "json5", "pytest"] max-line-length = 88 disable = [ - "too-few-public-methods", - "too-many-arguments", - "protected-access", - "too-many-locals", - "fixme", + "too-few-public-methods", + "too-many-arguments", + "protected-access", + "too-many-locals", + "fixme", ] [tool.pylint.design] diff --git a/screen-shots/example-6.4.png b/screen-shots/example-6.4.png new file mode 100644 index 0000000..e0c3c05 Binary files /dev/null and b/screen-shots/example-6.4.png differ diff --git a/src/log21/Argumentify.py b/src/log21/Argumentify.py index a7e19ba..eccdd9c 100644 --- a/src/log21/Argumentify.py +++ b/src/log21/Argumentify.py @@ -18,7 +18,8 @@ __all__ = [ 'argumentify', 'ArgumentifyError', 'ArgumentTypeError', 'FlagGenerationError', 'RESERVED_FLAGS', 'Callable', 'Argument', 'FunctionInfo', 'generate_flag', - 'normalize_name', 'normalize_name_to_snake_case' + 'normalize_name', 'normalize_name_to_snake_case', 'ArgumentError', + 'IncompatibleArguments', 'RequiredArgument', 'TooFewArguments' ] Callable = _Union[_Callable[..., _Any], _Callable[..., _Coroutine[_Any, _Any, _Any]]] @@ -76,6 +77,90 @@ def __init__(self, message: _Optional[str] = None, arg_name: _Optional[str] = No self.arg_name = arg_name +class ArgumentError(ArgumentifyError): + """Base of errors to raise in the argumentified functions to raise parser errors.""" + + def __init__(self, *args, message: _Optional[str] = None): + """Initialize the exception. + + :param args: The arguments that have a problem. + :param message: The error message to show. + """ + if message is None: + if args: + if len(args) > 1: + message = "There is a problem with the arguments: " + ', '.join( + f"`{arg}`" for arg in args + ) + else: + message = "The argument `" + args[0] + "` is invalid." + self.message = message + self.arguments = args + + +class IncompatibleArguments(ArgumentError): + """Raise when the user has used arguments that are incompatible with each other.""" + + def __init__(self, *args, message: _Optional[str] = None): + """Initialize the exception. + + :param args: The arguments that are incompatible. + :param message: The error message to show. + """ + super().__init__(*args, message) + if message is None: + if args: + if len(args) > 1: + message = "You cannot use all these together: " + ', '.join( + f"`{arg}`" for arg in args + ) + else: + message = "The argument `" + args[0] + "` is not compatible." + self.message = message + + +class RequiredArgument(ArgumentError): + """Raise this when there is a required argument missing.""" + + def __init__(self, *args, message: _Optional[str] = None): + """Initialize the exception. + + :param args: The arguments that are required. + :param message: The error message to show. + """ + super().__init__(*args, message) + if message is None: + if args: + if len(args) > 1: + message = "These arguments are required: " + ', '.join( + f"`{arg}`" for arg in args + ) + else: + message = "The argument `" + args[0] + "` is required." + self.message = message + + +class TooFewArguments(ArgumentError): + """Raise this when there were not enough arguments passed.""" + + def __init__(self, *args, message: _Optional[str] = None): + """Initialize the exception. + + :param args: The arguments that should be passed. + :param message: The error message to show. + """ + super().__init__(*args, message) + if message is None: + if args: + if len(args) > 1: + message = "You should use these arguments: " + ', '.join( + f"`{arg}`" for arg in args + ) + else: + message = "The argument `" + args[0] + "` should be used." + self.message = message + + def normalize_name_to_snake_case(name: str, sep_char: str = '_') -> str: """Returns the normalized name a class. @@ -278,8 +363,7 @@ def _add_arguments( def _argumentify_one(func: Callable): - """This function argumentifies one function as the entry point of the - script. + """This function argumentifies one function as the entry point of the script. :param function: The function to argumentify. """ @@ -312,15 +396,18 @@ def _argumentify_one(func: Callable): args.extend(getattr(cli_args, argument.name) or []) else: kwargs[argument.name] = getattr(cli_args, argument.name) - result = func(*args, **kwargs) - # Check if the result is a coroutine - if isinstance(result, (_Coroutine, _Awaitable)): - _asyncio.run(result) + try: + result = func(*args, **kwargs) + # Check if the result is a coroutine + if isinstance(result, (_Coroutine, _Awaitable)): + _asyncio.run(result) + except ArgumentError as error: + parser.error(error.message) def _argumentify(functions: _Dict[str, Callable]): - """This function argumentifies one or more functions as the entry point of - the script. + """This function argumentifies one or more functions as the entry point of the + script. :param functions: A dictionary of functions to argumentify. :raises RuntimeError: @@ -361,15 +448,18 @@ def _argumentify(functions: _Dict[str, Callable]): args.extend(getattr(cli_args, argument.name) or []) else: kwargs[argument.name] = getattr(cli_args, argument.name) - result = function(*args, **kwargs) - # Check if the result is a coroutine - if isinstance(result, (_Coroutine, _Awaitable)): - _asyncio.run(result) + try: + result = function(*args, **kwargs) + # Check if the result is a coroutine + if isinstance(result, (_Coroutine, _Awaitable)): + _asyncio.run(result) + except ArgumentError as error: + parser.error(error.message) 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. + """This function argumentifies one or more functions as the entry point of the + script. 1 #!/usr/bin/env python 2 # argumentified.py diff --git a/src/log21/__init__.py b/src/log21/__init__.py index 6c4a446..52438fd 100644 --- a/src/log21/__init__.py +++ b/src/log21/__init__.py @@ -16,14 +16,15 @@ from .Argparse import ColorizingArgumentParser from .TreePrint import TreePrint, tree_format from .Formatters import ColorizingFormatter, DecolorizingFormatter, _Formatter -from .Argumentify import argumentify +from .Argumentify import (ArgumentError, TooFewArguments, RequiredArgument, + IncompatibleArguments, argumentify) from .FileHandler import FileHandler, DecolorizingFileHandler from .ProgressBar import ProgressBar from .LoggingWindow import LoggingWindow, LoggingWindowHandler from .StreamHandler import StreamHandler, ColorizingStreamHandler __author__ = 'CodeWriter21 (Mehrad Pooryoussof)' -__version__ = '2.9.2' +__version__ = '2.10.0' __github__ = 'Https://GitHub.com/MPCodeWriter21/log21' __all__ = [ 'ColorizingStreamHandler', 'DecolorizingFileHandler', 'ColorizingFormatter', @@ -35,7 +36,8 @@ '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', 'argumentify' + 'console_reporter', 'file_reporter', 'argumentify', 'ArgumentError', + 'IncompatibleArguments', 'RequiredArgument', 'TooFewArguments' ] _manager = Manager()