log21
Advanced tools
Sorry, the diff of this file is too big to display
| # log21._module_helper.py | ||
| # CodeWriter21 | ||
| from types import ModuleType | ||
| from typing import Any, Callable | ||
| ModuleAttribute = Any | ||
| class FakeModule(ModuleType): | ||
| def __init__(self, real_module: ModuleType, on_call: Callable) -> None: | ||
| super().__init__(real_module.__name__) | ||
| self.__dict__.update(real_module.__dict__) | ||
| self.__real_module = real_module | ||
| self.__on_call = on_call | ||
| def __getattr__(self, name: str) -> ModuleAttribute: | ||
| return getattr(self.__real_module, name) | ||
| def __call__(self, *args, **kwargs): # noqa | ||
| return self.__on_call(*args, **kwargs) |
| # log21.argparse.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| from __future__ import annotations | ||
| import re as _re | ||
| import sys as _sys | ||
| import types as _types | ||
| import typing as _typing | ||
| import contextlib | ||
| import collections.abc | ||
| from enum import Enum as _Enum | ||
| from typing import (Any as _Any, Tuple as _Tuple, Mapping as _Mapping, | ||
| Callable as _Callable, NoReturn, Optional as _Optional, | ||
| Sequence as _Sequence) | ||
| from gettext import gettext as _gettext | ||
| from textwrap import TextWrapper as _TextWrapper | ||
| from collections import OrderedDict as _OrderedDict | ||
| import log21 as _log21 | ||
| from log21.colors import get_colors as _gc | ||
| from log21.formatters import DecolorizingFormatter as _Formatter | ||
| from . import _argparse | ||
| # yapf: enable | ||
| __all__ = [ | ||
| 'ColorizingArgumentParser', 'ColorizingHelpFormatter', 'ColorizingTextWrapper', | ||
| 'Literal' | ||
| ] | ||
| # ruff: noqa: ANN001 | ||
| class Literal: | ||
| """A class for representing literals in argparse arguments.""" | ||
| def __init__(self, literal: _typing._LiteralGenericAlias) -> None: | ||
| self.literal = literal | ||
| # Only str arguments are allowed | ||
| if not all(isinstance(x, str) for x in self.literal.__args__): | ||
| raise TypeError('Only str arguments are allowed for Literal.') | ||
| def __repr__(self) -> str: | ||
| return f'Literal[{", ".join(map(str, self.literal.__args__))}]' | ||
| def __str__(self) -> str: | ||
| return self.__repr__() | ||
| def __call__(self, value: _Any) -> _Any: | ||
| if value not in self.literal.__args__: | ||
| raise ValueError( | ||
| f'Value must be one of [{", ".join(map(str, self.literal.__args__))}]' | ||
| ) | ||
| return value | ||
| class ColorizingHelpFormatter(_argparse.HelpFormatter): | ||
| """A help formatter that supports colorizing help messages.""" | ||
| def __init__( | ||
| self, | ||
| prog: str, | ||
| indent_increment: int = 2, | ||
| max_help_position: int = 24, | ||
| width: _Optional[int] = None, | ||
| colors: _Optional[_Mapping[str, str]] = None | ||
| ) -> None: | ||
| super().__init__(prog, indent_increment, max_help_position, width) | ||
| self.colors = { | ||
| 'usage': 'Cyan', | ||
| 'brackets': 'LightRed', | ||
| 'switches': 'LightCyan', | ||
| 'values': 'Green', | ||
| 'colons': 'LightRed', | ||
| 'commas': 'LightRed', | ||
| 'section headers': 'LightGreen', | ||
| 'help': 'LightWhite', | ||
| 'choices': 'LightGreen' | ||
| } | ||
| if colors: | ||
| for key, value in colors.items(): | ||
| if key in self.colors: | ||
| self.colors[key] = value | ||
| class _Section: | ||
| def __init__(self, formatter, parent, heading=None) -> None: | ||
| self.formatter = formatter | ||
| self.parent = parent | ||
| self.heading = heading | ||
| self.items = [] | ||
| def format_help(self) -> str: | ||
| # format the indented section | ||
| if self.parent is not None: | ||
| self.formatter._indent() | ||
| join = self.formatter._join_parts | ||
| item_help = join([func(*args) for func, args in self.items]) | ||
| if self.parent is not None: | ||
| self.formatter._dedent() | ||
| # return nothing if the section was empty | ||
| if not item_help: | ||
| return '' | ||
| # add the heading if the section was non-empty | ||
| if self.heading is not _argparse.SUPPRESS and self.heading is not None: | ||
| current_indent = self.formatter._current_indent | ||
| heading = ( | ||
| '%*s%s' % (current_indent, '', self.heading) + | ||
| _gc(self.formatter.colors['colons']) + ':\033[0m\n' | ||
| ) | ||
| else: | ||
| heading = '' | ||
| # join the section-initial newline, the heading and the help | ||
| return join( | ||
| ['\n', heading, | ||
| _gc(self.formatter.colors['help']), item_help, '\n'] | ||
| ) | ||
| def _add_item(self, func, args) -> None: | ||
| self._current_section.items.append((func, args)) | ||
| def _fill_text(self, text, width, indent) -> ColorizingTextWrapper: | ||
| text = self._whitespace_matcher.sub(' ', text).strip() | ||
| return ColorizingTextWrapper( | ||
| width=width, initial_indent=indent, subsequent_indent=indent | ||
| ).fill(text) | ||
| def _split_lines(self, text, width) -> ColorizingTextWrapper: | ||
| text = self._whitespace_matcher.sub(' ', text).strip() | ||
| return ColorizingTextWrapper(width=width).wrap(text) | ||
| def start_section(self, heading) -> None: | ||
| self._indent() | ||
| section = self._Section( | ||
| self, self._current_section, | ||
| _gc(self.colors['section headers']) + str(heading) + '\033[0m' | ||
| ) | ||
| self._add_item(section.format_help, []) | ||
| self._current_section = section | ||
| def _format_action(self, action) -> str: | ||
| # determine the required width and the entry label | ||
| help_position = min(self._action_max_length + 2, self._max_help_position) | ||
| help_width = max(self._width - help_position, 11) | ||
| action_width = help_position - self._current_indent - 2 | ||
| action_header = _gc('rst') + self._format_action_invocation(action) | ||
| indent_first = 0 | ||
| # no help; start on same line and add a final newline | ||
| if not action.help: | ||
| action_header = self._current_indent * ' ' + action_header + '\n' | ||
| # short action name; start on the same line and pad two spaces | ||
| elif len(action_header) <= action_width: | ||
| action_header = '%*s%-*s ' % ( | ||
| self._current_indent, '', action_width, action_header | ||
| ) | ||
| # long action name; start on the next line | ||
| else: | ||
| action_header = self._current_indent * ' ' + action_header + '\n' | ||
| indent_first = help_position | ||
| # collect the pieces of the action help | ||
| parts = [action_header] | ||
| # if there was help for the action, add lines of help text | ||
| if action.help: | ||
| help_text = _gc(self.colors['help']) + self._expand_help(action) | ||
| help_lines = self._split_lines(help_text, help_width) | ||
| parts.append('%*s%s\n' % (indent_first, '', help_lines[0])) | ||
| for line in help_lines[1:]: | ||
| parts.append('%*s%s\n' % (help_position, '', line)) | ||
| # or add a newline if the description doesn't end with one | ||
| elif not action_header.endswith('\n'): | ||
| parts.append('\n') | ||
| # if there are any sub-actions, add their help as well | ||
| for subaction in self._iter_indented_subactions(action): | ||
| parts.append(self._format_action(subaction)) | ||
| # return a single string | ||
| return self._join_parts(parts) | ||
| # modified upstream code, not going to refactor for complexity. | ||
| def _format_usage( # noqa: C901, PLR0915 | ||
| self, usage, actions, groups, prefix | ||
| ) -> str: | ||
| if prefix is None: | ||
| prefix = _gettext('usage: ') | ||
| # if usage is specified, use that | ||
| if usage is not None: | ||
| usage = usage % {'prog': self._prog} | ||
| # if no optionals or positionals are available, usage is just prog | ||
| elif usage is None and not actions: | ||
| usage = '%(prog)s' % {'prog': self._prog} | ||
| # if optionals and positionals are available, calculate usage | ||
| elif usage is None: | ||
| prog = '%(prog)s' % {'prog': self._prog} | ||
| # split optionals from positionals | ||
| optionals = [] | ||
| positionals = [] | ||
| for action in actions: | ||
| if action.option_strings: | ||
| optionals.append(action) | ||
| else: | ||
| positionals.append(action) | ||
| # build full usage string | ||
| action_usage = self._format_actions_usage(optionals + positionals, groups) | ||
| usage = ' '.join([s for s in [prog, action_usage] if s]) | ||
| # wrap the usage parts if it's too long | ||
| text_width = self._width - self._current_indent | ||
| if len(prefix) + len(_Formatter.decolorize(usage)) > text_width: | ||
| # break usage into wrappable parts | ||
| part_regexp = r'\(.*?\)+|\[.*?\]+|\S+' | ||
| opt_usage = self._format_actions_usage(optionals, groups) | ||
| pos_usage = self._format_actions_usage(positionals, groups) | ||
| opt_parts = _re.findall(part_regexp, opt_usage) | ||
| pos_parts = _re.findall(part_regexp, pos_usage) | ||
| assert ' '.join(opt_parts) == opt_usage | ||
| assert ' '.join(pos_parts) == pos_usage | ||
| # helper for wrapping lines | ||
| def get_lines(parts, indent, prefix=None) -> list: | ||
| lines = [] | ||
| line = [] | ||
| if prefix is not None: | ||
| line_len = len(prefix) - 1 | ||
| else: | ||
| line_len = len(indent) - 1 | ||
| for part in parts: | ||
| if line_len + 1 + len(_Formatter.decolorize(part) | ||
| ) > text_width and line: | ||
| lines.append(indent + ' '.join(line)) | ||
| line = [] | ||
| line_len = len(indent) - 1 | ||
| line.append(part) | ||
| line_len += len(_Formatter.decolorize(part)) + 1 | ||
| if line: | ||
| lines.append(indent + ' '.join(line)) | ||
| if prefix is not None: | ||
| lines[0] = lines[0][len(indent):] | ||
| return lines | ||
| # if prog is short, follow it with optionals or positionals | ||
| len_prog = len(_Formatter.decolorize(prog)) | ||
| if len(prefix) + len_prog <= 0.75 * text_width: | ||
| indent = ' ' * (len(prefix) + len_prog + 1) | ||
| if opt_parts: | ||
| lines = get_lines([prog] + opt_parts, indent, prefix) | ||
| lines.extend(get_lines(pos_parts, indent)) | ||
| elif pos_parts: | ||
| lines = get_lines([prog] + pos_parts, indent, prefix) | ||
| else: | ||
| lines = [prog] | ||
| # if prog is long, put it on its own line | ||
| else: | ||
| indent = ' ' * len(prefix) | ||
| parts = opt_parts + pos_parts | ||
| lines = get_lines(parts, indent) | ||
| if len(lines) > 1: | ||
| lines = [] | ||
| lines.extend(get_lines(opt_parts, indent)) | ||
| lines.extend(get_lines(pos_parts, indent)) | ||
| lines = [prog] + lines | ||
| # join lines into usage | ||
| usage = '\n'.join(lines) | ||
| # prefix with 'usage:' | ||
| return prefix + usage + '\n\n' | ||
| def _format_actions_usage(self, actions: list, groups) -> None: # noqa: PLR0915 | ||
| # find group indices and identify actions in groups | ||
| group_actions = set() | ||
| inserts = {} | ||
| for group in groups: | ||
| if not group._group_actions: | ||
| raise ValueError(f'empty group {group}') | ||
| try: | ||
| start = actions.index(group._group_actions[0]) | ||
| except ValueError: | ||
| continue | ||
| else: | ||
| group_action_count = len(group._group_actions) | ||
| end = start + group_action_count | ||
| if actions[start:end] == group._group_actions: | ||
| suppressed_actions_count = 0 | ||
| for action in group._group_actions: | ||
| group_actions.add(action) | ||
| if action.help is _argparse.SUPPRESS: | ||
| suppressed_actions_count += 1 | ||
| exposed_actions_count = group_action_count - suppressed_actions_count | ||
| if not exposed_actions_count: | ||
| continue | ||
| if not group.required: | ||
| if start in inserts: | ||
| inserts[start] += ' [' | ||
| else: | ||
| inserts[start] = '[' | ||
| if end in inserts: | ||
| inserts[end] += ']' | ||
| else: | ||
| inserts[end] = ']' | ||
| elif exposed_actions_count > 1: | ||
| if start in inserts: | ||
| inserts[start] += ' (' | ||
| else: | ||
| inserts[start] = '(' | ||
| if end in inserts: | ||
| inserts[end] += ')' | ||
| else: | ||
| inserts[end] = ')' | ||
| for i in range(start + 1, end): | ||
| inserts[i] = '|' | ||
| # collect all actions format strings | ||
| parts = [] | ||
| for i, action in enumerate(actions): | ||
| # suppressed arguments are marked with None | ||
| # remove | separators for suppressed arguments | ||
| if action.help is _argparse.SUPPRESS: | ||
| parts.append(None) | ||
| if inserts.get(i) == '|': | ||
| inserts.pop(i) | ||
| elif inserts.get(i + 1) == '|': | ||
| inserts.pop(i + 1) | ||
| # produce all arg strings | ||
| elif not action.option_strings: | ||
| default = self._get_default_metavar_for_positional(action) | ||
| part = self._format_args(action, default) | ||
| # if it's in a group, strip the outer [] | ||
| if action in group_actions and part[0] == '[' and part[-1] == ']': | ||
| part = part[1:-1] | ||
| # add the action string to the list | ||
| parts.append(part) | ||
| # produce the first way to invoke the option in brackets | ||
| else: | ||
| option_string = action.option_strings[0] | ||
| # if the Optional doesn't take a value, format is: | ||
| # -s or --long | ||
| if action.nargs == 0: | ||
| part = _gc(self.colors['switches']) + action.format_usage() | ||
| # if the Optional takes a value, format is: | ||
| # -s ARGS or --long ARGS | ||
| else: | ||
| default = self._get_default_metavar_for_optional(action) | ||
| args_string = self._format_args(action, default) | ||
| part = _gc(self.colors['switches']) + '%s %s%s' % ( | ||
| option_string, _gc(self.colors['values']), args_string | ||
| ) | ||
| # make it look optional if it's not required or in a group | ||
| if not action.required and action not in group_actions: | ||
| part = _gc(self.colors['brackets']) + '[' + part + _gc( | ||
| self.colors['brackets'] | ||
| ) + ']\033[0m' | ||
| # add the action string to the list | ||
| parts.append(part) | ||
| # insert things at the necessary indices | ||
| for i in sorted(inserts, reverse=True): | ||
| parts[i:i] = [inserts[i]] | ||
| # join all the action items with spaces | ||
| text = ' '.join([item for item in parts if item is not None]) | ||
| # clean up separators for mutually exclusive groups | ||
| open = r'[\[(]' | ||
| close = r'[\])]' | ||
| text = _re.sub(r'(%s) ' % open, r'\1', text) | ||
| text = _re.sub(r' (%s)' % close, r'\1', text) | ||
| text = _re.sub(r'%s *%s' % (open, close), r'', text) | ||
| text = _re.sub(r'\(([^|]*)\)', r'\1', text) | ||
| text = text.strip() | ||
| # return the text | ||
| return text | ||
| def _format_action_invocation(self, action) -> str: | ||
| if not action.option_strings: | ||
| default = self._get_default_metavar_for_positional(action) | ||
| metavar, = self._metavar_formatter(action, default)(1) | ||
| return metavar | ||
| else: | ||
| parts = [] | ||
| # if the Optional doesn't take a value, format is: | ||
| # -s, --long | ||
| if action.nargs == 0: | ||
| for option_string in action.option_strings: | ||
| parts.append(_gc(self.colors['switches']) + option_string) | ||
| # if the Optional takes a value, format is: | ||
| # -s ARGS, --long ARGS | ||
| else: | ||
| default = self._get_default_metavar_for_optional(action) | ||
| args_string = self._format_args(action, default) | ||
| for option_string in action.option_strings: | ||
| parts.append( | ||
| _gc(self.colors['switches']) + '%s %s%s' % | ||
| (option_string, _gc(self.colors['values']), args_string) | ||
| ) | ||
| return _gc(self.colors['commas']) + ', '.join(parts) | ||
| def _metavar_formatter(self, action, default_metavar) -> _Callable: | ||
| if action.metavar is not None: | ||
| result = action.metavar | ||
| elif action.choices is not None: | ||
| choice_strs = [str(choice) for choice in action.choices] | ||
| result = ( | ||
| _gc(self.colors['brackets']) + '{ ' + | ||
| (_gc(self.colors['commas']) + ', ').join( | ||
| _gc(self.colors['choices']) + choice_str | ||
| for choice_str in choice_strs | ||
| ) + _gc(self.colors['brackets']) + ' }' | ||
| ) | ||
| else: | ||
| result = default_metavar | ||
| def format(tuple_size) -> tuple: | ||
| if isinstance(result, tuple): | ||
| return result | ||
| else: | ||
| return (result, ) * tuple_size | ||
| return format | ||
| class ColorizingTextWrapper(_TextWrapper): | ||
| # modified upstream code, not going to refactor for complexity. | ||
| def _wrap_chunks(self, chunks) -> list: # noqa: C901, PLR0915 | ||
| """_wrap_chunks(chunks : [string]) -> [string] | ||
| Wrap a sequence of text chunks and return a list of lines of | ||
| length 'self.width' or less. (If 'break_long_words' is false, | ||
| some lines may be longer than this.) Chunks correspond roughly | ||
| to words and the whitespace between them: each chunk is | ||
| indivisible (modulo 'break_long_words'), but a line break can | ||
| come between any two chunks. Chunks should not have internal | ||
| whitespace; i.e. a chunk is either all whitespace or a "word". | ||
| Whitespace chunks will be removed from the beginning and end of | ||
| lines, but apart from that whitespace is preserved. | ||
| """ | ||
| lines = [] | ||
| if self.width <= 0: | ||
| raise ValueError("invalid width %r (must be > 0)" % self.width) | ||
| if self.max_lines is not None: | ||
| if self.max_lines > 1: | ||
| indent = self.subsequent_indent | ||
| else: | ||
| indent = self.initial_indent | ||
| if len(indent) + len(self.placeholder.lstrip()) > self.width: | ||
| raise ValueError("placeholder too large for max width") | ||
| # Arrange in reverse order so items can be efficiently popped | ||
| # from a stack of chucks. | ||
| chunks.reverse() | ||
| while chunks: | ||
| # Start the list of chunks that will make up the current line. | ||
| # current_len is just the length of all the chunks in current_line. | ||
| current_line = [] | ||
| current_len = 0 | ||
| # Figure out which static string will prefix this line. | ||
| indent = self.subsequent_indent if lines else self.initial_indent | ||
| # Maximum width for this line. | ||
| width = self.width - len(indent) | ||
| # First chunk on the line is whitespace -- drop it, unless this | ||
| # is the very beginning of the text (i.e. no lines started yet). | ||
| if self.drop_whitespace and _Formatter.decolorize(chunks[-1] | ||
| ).strip() == '' and lines: | ||
| del chunks[-1] | ||
| while chunks: | ||
| # modified upstream code, not going to refactor for ambiguous variable | ||
| # name. | ||
| length = len(_Formatter.decolorize(chunks[-1])) # noqa: E741 | ||
| # Can at least squeeze this chunk onto the current line. | ||
| # Modified upstream code, not going to refactor for ambiguous variable | ||
| # name. | ||
| if current_len + length <= width: # noqa: E741 | ||
| current_line.append(chunks.pop()) | ||
| current_len += length | ||
| # Nope, this line is full. | ||
| else: | ||
| break | ||
| # The current line is full, and the next chunk is too big to | ||
| # fit on *any* line (not just this one). | ||
| if chunks and len(_Formatter.decolorize(chunks[-1])) > width: | ||
| self._handle_long_word(chunks, current_line, current_len, width) | ||
| current_len = sum(map(len, current_line)) | ||
| # If the last chunk on this line is all whitespace, drop it. | ||
| if self.drop_whitespace and current_line and _Formatter.decolorize( | ||
| current_line[-1]).strip() == '': | ||
| current_len -= len(_Formatter.decolorize(current_line[-1])) | ||
| del current_line[-1] | ||
| if current_line: | ||
| if (self.max_lines is None or len(lines) + 1 < self.max_lines | ||
| or (not chunks or self.drop_whitespace and len(chunks) == 1 | ||
| and not chunks[0].strip()) and current_len <= width): | ||
| # Convert current line back to a string and store it in | ||
| # list of all lines (return value). | ||
| lines.append(indent + ''.join(current_line)) | ||
| else: | ||
| while current_line: | ||
| if _Formatter.decolorize( | ||
| current_line[-1] | ||
| ).strip() and current_len + len(self.placeholder) <= width: | ||
| current_line.append(self.placeholder) | ||
| lines.append(indent + ''.join(current_line)) | ||
| break | ||
| current_len -= len(_Formatter.decolorize(current_line[-1])) | ||
| del current_line[-1] | ||
| else: | ||
| if lines: | ||
| prev_line = lines[-1].rstrip() | ||
| if len(_Formatter.decolorize(prev_line)) + len( | ||
| self.placeholder) <= self.width: | ||
| lines[-1] = prev_line + self.placeholder | ||
| break | ||
| lines.append(indent + self.placeholder.lstrip()) | ||
| break | ||
| return lines | ||
| class _ActionsContainer(_argparse._ActionsContainer): # novm | ||
| """Container for the actions for a single command line option.""" | ||
| # pylint: disable=too-many-branches | ||
| def _validate_func_type(self, action, func_type, kwargs, level: int = 0) -> _Tuple: | ||
| # raise an error if the action type is not callable | ||
| if (hasattr(_types, 'UnionType') and not callable(func_type) | ||
| and not isinstance(func_type, (_types.UnionType, tuple))): | ||
| raise ValueError(f'{func_type} is not callable; level={level}') | ||
| # Handle `UnionType` as a type (e.g. `int|str`) | ||
| if hasattr(_types, 'UnionType') and isinstance(func_type, _types.UnionType): | ||
| func_type = func_type.__args__ # type: ignore | ||
| # Handle `Literal` as a type (e.g. `Literal[1, 2, 3]`) | ||
| elif (hasattr(_typing, '_LiteralGenericAlias') | ||
| and isinstance(func_type, _typing._LiteralGenericAlias)): # type: ignore | ||
| func_type = Literal(func_type) | ||
| # Handle `Union` and `Optional` as a type (e.g. `Union[int, str]` and | ||
| # `Optional[int]`) | ||
| elif (hasattr(_typing, '_UnionGenericAlias') | ||
| and isinstance(func_type, _typing._UnionGenericAlias)): # type: ignore | ||
| # Optional[T] is just Union[T, NoneType] | ||
| # Optional | ||
| if (hasattr(_types, 'NoneType') and len(func_type.__args__) == 2 | ||
| and func_type.__args__[1] is _types.NoneType): | ||
| action.required = False | ||
| func_type = func_type.__args__[0] | ||
| # Union | ||
| else: | ||
| func_type = func_type.__args__ # type: ignore | ||
| # Handle `List` as a type (e.g. `List[int]`) | ||
| elif (hasattr(_typing, '_GenericAlias') | ||
| and isinstance(func_type, _typing._GenericAlias) # type: ignore | ||
| and func_type.__origin__ is list) or ( | ||
| hasattr(_typing, '_GenericAlias') | ||
| and isinstance(func_type, _typing._GenericAlias) # type: ignore | ||
| and func_type.__origin__ is collections.abc.Sequence): | ||
| func_type = func_type.__args__[0] | ||
| if kwargs.get('nargs') is None: | ||
| action.nargs = '+' | ||
| # Handle `Required` as a type (e.g. `Required[int]`) | ||
| elif (hasattr(_typing, 'Required') and hasattr(_typing, '_GenericAlias') | ||
| and isinstance(func_type, _typing._GenericAlias) # type: ignore | ||
| and func_type.__origin__ is _typing.Required): | ||
| func_type = func_type.__args__[0] | ||
| action.required = True | ||
| # Handle Enum as a type | ||
| elif callable(func_type) and isinstance(func_type, type) and issubclass( | ||
| func_type, _Enum) and action.choices is None and level == 0: | ||
| action.choices = tuple((x.value for x in func_type.__members__.values())) | ||
| # Handle SpecialForms | ||
| elif isinstance(func_type, _typing._SpecialForm): | ||
| if func_type is _typing.Any or func_type is _typing.ClassVar or func_type is _typing.Union: | ||
| func_type = None | ||
| elif func_type is _typing.Optional: | ||
| func_type = None | ||
| action.required = False | ||
| elif func_type is _typing.Type or func_type is _typing.TypeVar: | ||
| func_type = None | ||
| else: | ||
| raise ValueError(f'Unknown special form {func_type}') | ||
| elif func_type is _argparse.FileType: | ||
| raise ValueError( | ||
| f'{func_type} is a FileType class object, instance of it must be passed' | ||
| ) | ||
| if isinstance(func_type, _Sequence): | ||
| temp = [] | ||
| for type_ in _OrderedDict(zip(func_type, [0] * len(func_type))): | ||
| temp.extend(self._validate_func_type(action, type_, kwargs, level + 1)) | ||
| func_type = tuple(temp) | ||
| elif (hasattr(_types, 'UnionType') and hasattr(_typing, '_GenericAlias') | ||
| and hasattr(_typing, '_UnionGenericAlias') | ||
| and hasattr(_typing, '_LiteralGenericAlias') and isinstance( | ||
| func_type, | ||
| ( | ||
| _typing._GenericAlias, # type: ignore | ||
| _typing._UnionGenericAlias, # type: ignore | ||
| _typing._LiteralGenericAlias, # type: ignore | ||
| _types.UnionType, | ||
| ))): | ||
| func_type = self._validate_func_type(action, func_type, kwargs, level + 1) | ||
| else: | ||
| func_type = (func_type, ) | ||
| return func_type | ||
| # Override the default add_argument method defined in argparse._ActionsContainer | ||
| # to add the support for different type annotations | ||
| def add_argument(self, *args, **kwargs): # noqa: ANN202 | ||
| """Add an argument to the parser. | ||
| Signature: | ||
| add_argument(dest, ..., name=value, ...) | ||
| add_argument(option_string, option_string, ..., name=value, ...) | ||
| """ | ||
| # if no positional args are supplied or only one is supplied and | ||
| # it doesn't look like an option string, parse a positional | ||
| # argument | ||
| chars = self.prefix_chars | ||
| if not args or len(args) == 1 and args[0][0] not in chars: | ||
| if args and 'dest' in kwargs: | ||
| raise ValueError('dest supplied twice for positional argument') | ||
| kwargs = self._get_positional_kwargs(*args, **kwargs) | ||
| # otherwise, we're adding an optional argument | ||
| else: | ||
| kwargs = self._get_optional_kwargs(*args, **kwargs) | ||
| # if no default was supplied, use the parser-level default | ||
| if 'default' not in kwargs: | ||
| dest = kwargs['dest'] | ||
| if dest in self._defaults: | ||
| kwargs['default'] = self._defaults[dest] | ||
| elif self.argument_default is not None: | ||
| kwargs['default'] = self.argument_default | ||
| # create the action object, and add it to the parser | ||
| action_class = self._pop_action_class(kwargs) | ||
| if not callable(action_class): | ||
| raise ValueError(f'unknown action "{action_class}"') | ||
| action = action_class(**kwargs) | ||
| func_type = self._registry_get('type', action.type, action.type) | ||
| action.type = self._validate_func_type( | ||
| action, func_type, kwargs | ||
| ) # type: ignore | ||
| if len(action.type) == 1: | ||
| action.type = action.type[0] | ||
| elif len(action.type) == 0: | ||
| action.type = None | ||
| # raise an error if the metavar does not match the type | ||
| if hasattr(self, "_get_formatter"): | ||
| try: | ||
| self._get_formatter()._format_args(action, None) | ||
| except TypeError: | ||
| raise ValueError( | ||
| "length of metavar tuple does not match nargs" | ||
| ) from None | ||
| return self._add_action(action) | ||
| def add_argument_group(self, *args, **kwargs) -> _ArgumentGroup: | ||
| group = _ArgumentGroup(self, *args, **kwargs) | ||
| self._action_groups.append(group) | ||
| return group | ||
| def add_mutually_exclusive_group(self, **kwargs) -> _MutuallyExclusiveGroup: | ||
| group = _MutuallyExclusiveGroup(self, **kwargs) | ||
| self._mutually_exclusive_groups.append(group) | ||
| return group | ||
| class ColorizingArgumentParser(_argparse.ArgumentParser, _ActionsContainer): | ||
| """An ArgumentParser that colorizes its output and more.""" | ||
| def __init__( | ||
| self, | ||
| formatter_class=ColorizingHelpFormatter, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| **kwargs | ||
| ) -> None: | ||
| self.logger = _log21.Logger('ArgumentParser') | ||
| self.colors = colors | ||
| super().__init__(formatter_class=formatter_class, **kwargs) | ||
| def _print_message(self, message, file=None) -> None: | ||
| if message: | ||
| self.logger.handlers.clear() | ||
| handler = _log21.ColorizingStreamHandler(stream=file) | ||
| self.logger.addHandler(handler) | ||
| self.logger.info(message + _gc('rst')) | ||
| def exit(self, status=0, message=None) -> None: | ||
| if message: | ||
| self._print_message(_gc('lr') + message + _gc('rst'), _sys.stderr) | ||
| _sys.exit(status) | ||
| def error(self, message) -> NoReturn: | ||
| self.print_usage(_sys.stderr) | ||
| args = {'prog': self.prog, 'message': message} | ||
| self.exit( | ||
| 2, | ||
| _gettext( | ||
| f'%(prog)s: {_gc("r")}error{_gc("lr")}:{_gc("rst")} %(message)s\n' | ||
| ) % args | ||
| ) | ||
| return NoReturn | ||
| def _get_formatter(self): # noqa: ANN202 | ||
| if hasattr(self.formatter_class, 'colors'): | ||
| return self.formatter_class(prog=self.prog, colors=self.colors) | ||
| return self.formatter_class(prog=self.prog) | ||
| def _get_value(self, action, arg_string): # noqa: ANN202 | ||
| """Override _get_value to add support for types such as Union and Literal.""" | ||
| func_type = self._registry_get('type', action.type, action.type) | ||
| if not callable(func_type) and not isinstance(func_type, tuple): | ||
| raise _argparse.ArgumentError( | ||
| action, _gettext(f'{func_type!r} is not callable') | ||
| ) | ||
| name = getattr(action.type, '__name__', repr(action.type)) | ||
| # convert the value to the appropriate type | ||
| try: | ||
| if callable(func_type): | ||
| result = func_type(arg_string) | ||
| else: | ||
| exception = ValueError() | ||
| for type_ in func_type: | ||
| name = getattr(type_, '__name__', repr(type_)) | ||
| try: | ||
| result = type_(arg_string) | ||
| break | ||
| except (ValueError, TypeError) as ex: | ||
| exception = ex | ||
| else: | ||
| raise exception | ||
| # ArgumentTypeErrors indicate errors | ||
| except _argparse.ArgumentTypeError as ex: | ||
| msg = str(ex) | ||
| raise _argparse.ArgumentError(action, msg) from None | ||
| # TypeErrors or ValueErrors also indicate errors | ||
| except (TypeError, ValueError): | ||
| raise _argparse.ArgumentError( | ||
| action, _gettext(f'invalid {name!s} value: {arg_string!r}') | ||
| ) from None | ||
| # return the converted value | ||
| return result | ||
| def __convert_type(self, func_type, arg_string): # noqa: ANN202 | ||
| result = None | ||
| if callable(func_type): | ||
| with contextlib.suppress(Exception): | ||
| result = func_type(arg_string) | ||
| else: | ||
| for type_ in func_type: | ||
| try: | ||
| result = type_(arg_string) | ||
| break | ||
| except Exception: | ||
| pass | ||
| return result | ||
| def _check_value(self, action, choice) -> None: | ||
| # converted value must be one of the choices (if specified) | ||
| if action.choices is not None: | ||
| choices = set(action.choices) | ||
| func_type = self._registry_get('type', action.type, action.type) | ||
| if not callable(func_type) and not isinstance(func_type, tuple): | ||
| raise _argparse.ArgumentError( | ||
| action, _gettext(f'{func_type!r} is not callable') | ||
| ) | ||
| for value in action.choices: | ||
| choices.add(self.__convert_type(func_type, value)) | ||
| if choice not in choices: | ||
| raise _argparse.ArgumentError( | ||
| action, | ||
| _gettext( | ||
| f'invalid choice: {choice!r} ' | ||
| f'(choose from {", ".join(map(repr, action.choices))})' | ||
| ) | ||
| ) | ||
| def _read_args_from_files(self, arg_strings) -> list: | ||
| # expand arguments referencing files | ||
| new_arg_strings = [] | ||
| for arg_string in arg_strings: | ||
| # for regular arguments, just add them back into the list | ||
| if not arg_string or arg_string[0] not in (self.fromfile_prefix_chars | ||
| or ''): | ||
| new_arg_strings.append(arg_string) | ||
| # replace arguments referencing files with the file content | ||
| else: | ||
| try: | ||
| with open(arg_string[1:], encoding='utf-8') as args_file: | ||
| arg_strings = [] | ||
| for arg_line in args_file.read().splitlines(): | ||
| for arg in self.convert_arg_line_to_args(arg_line): | ||
| arg_strings.append(arg) | ||
| arg_strings = self._read_args_from_files(arg_strings) | ||
| new_arg_strings.extend(arg_strings) | ||
| except OSError as err: | ||
| self.error(str(err)) | ||
| # return the modified argument list | ||
| return new_arg_strings | ||
| def _parse_known_args( # noqa: PLR0915 | ||
| self, arg_strings, namespace, intermixed | ||
| ) -> tuple: | ||
| # replace arg strings that are file references | ||
| if self.fromfile_prefix_chars is not None: | ||
| arg_strings = self._read_args_from_files(arg_strings) | ||
| # map all mutually exclusive arguments to the other arguments | ||
| # they can't occur with | ||
| action_conflicts = {} | ||
| for mutex_group in self._mutually_exclusive_groups: | ||
| group_actions = mutex_group._group_actions | ||
| for i, mutex_action in enumerate(mutex_group._group_actions): | ||
| conflicts = action_conflicts.setdefault(mutex_action, []) | ||
| conflicts.extend(group_actions[:i]) | ||
| conflicts.extend(group_actions[i + 1:]) | ||
| # find all option indices, and determine the arg_string_pattern | ||
| # which has an 'O' if there is an option at an index, | ||
| # an 'A' if there is an argument, or a '-' if there is a '--' | ||
| option_string_indices = {} | ||
| arg_string_pattern_parts = [] | ||
| arg_strings_iter = iter(arg_strings) | ||
| for i, arg_string in enumerate(arg_strings_iter): | ||
| # all args after -- are non-options | ||
| if arg_string == '--': | ||
| arg_string_pattern_parts.append('-') | ||
| for _ in arg_strings_iter: | ||
| arg_string_pattern_parts.append('A') | ||
| # otherwise, add the arg to the arg strings | ||
| # and note the index if it was an option | ||
| else: | ||
| option_tuples = self._parse_optional(arg_string) | ||
| if option_tuples is None: | ||
| pattern = 'A' | ||
| else: | ||
| option_string_indices[i] = option_tuples | ||
| pattern = 'O' | ||
| arg_string_pattern_parts.append(pattern) | ||
| # join the pieces together to form the pattern | ||
| arg_strings_pattern = ''.join(arg_string_pattern_parts) | ||
| # converts arg strings to the appropriate and then takes the action | ||
| seen_actions = set() | ||
| seen_non_default_actions = set() | ||
| warned = set() | ||
| def take_action(action, argument_strings, option_string=None) -> None: | ||
| seen_actions.add(action) | ||
| argument_values = self._get_values(action, argument_strings) | ||
| # error if this argument is not allowed with other previously | ||
| # seen arguments | ||
| if action.option_strings or argument_strings: | ||
| seen_non_default_actions.add(action) | ||
| for conflict_action in action_conflicts.get(action, []): | ||
| if conflict_action in seen_non_default_actions: | ||
| action_name = _argparse._get_action_name(conflict_action) | ||
| msg = _gettext(f'not allowed with argument {action_name}') | ||
| raise _argparse.ArgumentError(action, msg) | ||
| # take the action if we didn't receive a SUPPRESS value | ||
| # (e.g. from a default) | ||
| if argument_values is not _argparse.SUPPRESS: | ||
| action(self, namespace, argument_values, option_string) | ||
| # function to convert arg_strings into an optional action | ||
| def consume_optional(start_index: int) -> int: # noqa: PLR0915 | ||
| # get the optional identified at this index | ||
| option_tuples = option_string_indices[start_index] | ||
| # if multiple actions match, the option string was ambiguous | ||
| if len(option_tuples) > 1: | ||
| options = ', '.join( | ||
| [ | ||
| option_string | ||
| for action, option_string, sep, explicit_arg in option_tuples | ||
| ] | ||
| ) | ||
| args = {'option': arg_strings[start_index], 'matches': options} | ||
| msg = _gettext('ambiguous option: %(option)s could match %(matches)s') | ||
| raise _argparse.ArgumentError(None, msg % args) | ||
| action, option_string, sep, explicit_arg = option_tuples[0] | ||
| # identify additional optionals in the same arg string | ||
| # (e.g. -xyz is the same as -x -y -z if no args are required) | ||
| match_argument = self._match_argument | ||
| action_tuples = [] | ||
| while True: | ||
| # if we found no optional action, skip it | ||
| if action is None: | ||
| extras.append(arg_strings[start_index]) | ||
| extras_pattern.append('O') | ||
| return start_index + 1 | ||
| # if there is an explicit argument, try to match the | ||
| # optional's string arguments to only this | ||
| if explicit_arg is not None: | ||
| arg_count = match_argument(action, 'A') | ||
| # if the action is a single-dash option and takes no | ||
| # arguments, try to parse more single-dash options out | ||
| # of the tail of the option string | ||
| chars = self.prefix_chars | ||
| if (arg_count == 0 and option_string[1] not in chars | ||
| and explicit_arg != ''): | ||
| if sep or explicit_arg[0] in chars: | ||
| msg = _gettext('ignored explicit argument %r') | ||
| raise _argparse.ArgumentError(action, msg % explicit_arg) | ||
| action_tuples.append((action, [], option_string)) | ||
| char = option_string[0] | ||
| option_string = char + explicit_arg[0] | ||
| optionals_map = self._option_string_actions | ||
| if option_string in optionals_map: | ||
| action = optionals_map[option_string] | ||
| explicit_arg = explicit_arg[1:] | ||
| if not explicit_arg: | ||
| sep = explicit_arg = None | ||
| elif explicit_arg[0] == '=': | ||
| sep = '=' | ||
| explicit_arg = explicit_arg[1:] | ||
| else: | ||
| sep = '' | ||
| else: | ||
| extras.append(char + explicit_arg) | ||
| extras_pattern.append('O') | ||
| stop = start_index + 1 | ||
| break | ||
| # if the action expect exactly one argument, we've | ||
| # successfully matched the option; exit the loop | ||
| elif arg_count == 1: | ||
| stop = start_index + 1 | ||
| args = [explicit_arg] | ||
| action_tuples.append((action, args, option_string)) | ||
| break | ||
| # error if a double-dash option did not use the | ||
| # explicit argument | ||
| else: | ||
| msg = _gettext('ignored explicit argument %r') | ||
| raise _argparse.ArgumentError(action, msg % explicit_arg) | ||
| # if there is no explicit argument, try to match the | ||
| # optional's string arguments with the following strings | ||
| # if successful, exit the loop | ||
| else: | ||
| start = start_index + 1 | ||
| selected_patterns = arg_strings_pattern[start:] | ||
| arg_count = match_argument(action, selected_patterns) | ||
| stop = start + arg_count | ||
| args = arg_strings[start:stop] | ||
| action_tuples.append((action, args, option_string)) | ||
| break | ||
| # add the Optional to the list and return the index at which | ||
| # the Optional's string args stopped | ||
| assert action_tuples | ||
| for action, args, option_string in action_tuples: | ||
| if action.deprecated and option_string not in warned: | ||
| self._warning( | ||
| _gettext("option '%(option)s' is deprecated") % | ||
| {'option': option_string} | ||
| ) | ||
| warned.add(option_string) | ||
| take_action(action, args, option_string) | ||
| return stop | ||
| # the list of Positionals left to be parsed; this is modified | ||
| # by consume_positionals() | ||
| positionals = self._get_positional_actions() | ||
| # function to convert arg_strings into positional actions | ||
| def consume_positionals(start_index: int) -> int: | ||
| # match as many Positionals as possible | ||
| match_partial = self._match_arguments_partial | ||
| selected_pattern = arg_strings_pattern[start_index:] | ||
| arg_counts = match_partial(positionals, selected_pattern) | ||
| # slice off the appropriate arg strings for each Positional | ||
| # and add the Positional and its args to the list | ||
| for action, arg_count in zip(positionals, arg_counts): | ||
| args = arg_strings[start_index:start_index + arg_count] | ||
| start_index += arg_count | ||
| if args and action.deprecated and action.dest not in warned: | ||
| self._warning( | ||
| _gettext("argument '%(argument_name)s' is deprecated") % | ||
| {'argument_name': action.dest} | ||
| ) | ||
| warned.add(action.dest) | ||
| take_action(action, args) | ||
| # slice off the Positionals that we just parsed and return the | ||
| # index at which the Positionals' string args stopped | ||
| positionals[:] = positionals[len(arg_counts):] | ||
| return start_index | ||
| # consume Positionals and Optionals alternately, until we have | ||
| # passed the last option string | ||
| extras = [] | ||
| extras_pattern = [] | ||
| start_index = 0 | ||
| if option_string_indices: | ||
| max_option_string_index = max(option_string_indices) | ||
| else: | ||
| max_option_string_index = -1 | ||
| while start_index <= max_option_string_index: | ||
| # consume any Positionals preceding the next option | ||
| next_option_string_index = start_index | ||
| while next_option_string_index <= max_option_string_index: | ||
| if next_option_string_index in option_string_indices: | ||
| break | ||
| next_option_string_index += 1 | ||
| if not intermixed and start_index != next_option_string_index: | ||
| positionals_end_index = consume_positionals(start_index) | ||
| # only try to parse the next optional if we didn't consume | ||
| # the option string during the positionals parsing | ||
| if positionals_end_index > start_index: | ||
| start_index = positionals_end_index | ||
| continue | ||
| else: | ||
| start_index = positionals_end_index | ||
| # if we consumed all the positionals we could and we're not | ||
| # at the index of an option string, there were extra arguments | ||
| if start_index not in option_string_indices: | ||
| strings = arg_strings[start_index:next_option_string_index] | ||
| extras.extend(strings) | ||
| extras_pattern.extend( | ||
| arg_strings_pattern[start_index:next_option_string_index] | ||
| ) | ||
| start_index = next_option_string_index | ||
| # consume the next optional and any arguments for it | ||
| start_index = consume_optional(start_index) | ||
| if not intermixed: | ||
| # consume any positionals following the last Optional | ||
| stop_index = consume_positionals(start_index) | ||
| # if we didn't consume all the argument strings, there were extras | ||
| extras.extend(arg_strings[stop_index:]) | ||
| else: | ||
| extras.extend(arg_strings[start_index:]) | ||
| extras_pattern.extend(arg_strings_pattern[start_index:]) | ||
| extras_pattern = ''.join(extras_pattern) | ||
| assert len(extras_pattern) == len(extras) | ||
| # consume all positionals | ||
| arg_strings = [s for s, c in zip(extras, extras_pattern) if c != 'O'] | ||
| arg_strings_pattern = extras_pattern.replace('O', '') | ||
| stop_index = consume_positionals(0) | ||
| # leave unknown optionals and non-consumed positionals in extras | ||
| for i, c in enumerate(extras_pattern): | ||
| if not stop_index: | ||
| break | ||
| if c != 'O': | ||
| stop_index -= 1 | ||
| extras[i] = None | ||
| extras = [s for s in extras if s is not None] | ||
| # make sure all required actions were present and also convert | ||
| # action defaults which were not given as arguments | ||
| required_actions = [] | ||
| for action in self._actions: | ||
| if action not in seen_actions: | ||
| if action.required: | ||
| required_actions.append(_argparse._get_action_name(action)) | ||
| # Convert action default now instead of doing it before | ||
| # parsing arguments to avoid calling convert functions | ||
| # twice (which may fail) if the argument was given, but | ||
| # only if it was defined already in the namespace | ||
| elif (action.default is not None and isinstance(action.default, str) | ||
| and hasattr(namespace, action.dest) | ||
| and action.default is getattr(namespace, action.dest)): | ||
| setattr( | ||
| namespace, action.dest, self._get_value(action, action.default) | ||
| ) | ||
| if required_actions: | ||
| self.error( | ||
| _gettext( | ||
| 'the following arguments are required: ' + | ||
| ", ".join(required_actions) | ||
| ) | ||
| ) | ||
| # make sure all required groups had one option present | ||
| for group in self._mutually_exclusive_groups: | ||
| if group.required: | ||
| for action in group._group_actions: | ||
| if action in seen_non_default_actions: | ||
| break | ||
| # if no actions were used, report the error | ||
| else: | ||
| names = [ | ||
| _argparse._get_action_name(action) | ||
| for action in group._group_actions | ||
| if action.help is not _argparse.SUPPRESS | ||
| ] | ||
| self.error( | ||
| _gettext( | ||
| 'one of the arguments ' + | ||
| ' '.join(name for name in names if name is not None) + | ||
| ' is required' | ||
| ) | ||
| ) | ||
| for group in self._action_groups: | ||
| if isinstance(group, _ArgumentGroup) and group.required: | ||
| for action in group._group_actions: | ||
| if action in seen_non_default_actions: | ||
| break | ||
| # if no actions were used, report the error | ||
| else: | ||
| names = [ | ||
| _argparse._get_action_name(action) | ||
| for action in group._group_actions | ||
| if action.help is not _argparse.SUPPRESS | ||
| ] | ||
| self.error( | ||
| _gettext( | ||
| 'one of the arguments ' + | ||
| ' '.join(name for name in names if name is not None) + | ||
| ' is required' | ||
| ) | ||
| ) | ||
| # return the updated namespace and the extra arguments | ||
| return namespace, extras | ||
| class _ArgumentGroup(_argparse._ArgumentGroup, _ActionsContainer): | ||
| def __init__( | ||
| self, | ||
| container, | ||
| title=None, | ||
| description=None, | ||
| required: bool = False, | ||
| **kwargs | ||
| ) -> None: | ||
| super().__init__(container, title=title, description=description, **kwargs) | ||
| self.required = required | ||
| class _MutuallyExclusiveGroup(_argparse._MutuallyExclusiveGroup, _ArgumentGroup): | ||
| pass |
| # log21.argumentify.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| import re as _re | ||
| import string as _string | ||
| import asyncio as _asyncio | ||
| import inspect as _inspect | ||
| 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, Awaitable as _Awaitable, | ||
| 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 | ||
| import log21.argparse as _argparse | ||
| # yapf: enable | ||
| __all__ = [ | ||
| 'argumentify', 'ArgumentifyError', 'ArgumentTypeError', 'FlagGenerationError', | ||
| 'RESERVED_FLAGS', 'Callable', 'Argument', 'FunctionInfo', 'generate_flag', | ||
| 'normalize_name', 'normalize_name_to_snake_case', 'ArgumentError', | ||
| 'IncompatibleArgumentsError', 'RequiredArgumentError', 'TooFewArgumentsError' | ||
| ] | ||
| 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 | ||
| ) -> 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 | ||
| ) -> 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 | ||
| class ArgumentError(ArgumentifyError): | ||
| """Base of errors to raise in the argumentified functions to raise parser errors.""" | ||
| def __init__(self, *args, message: _Optional[str] = None) -> None: | ||
| """Initialize the exception. | ||
| :param args: The arguments that have a problem. | ||
| :param message: The error message to show. | ||
| """ | ||
| if message is None and 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 IncompatibleArgumentsError(ArgumentError): | ||
| """Raise when the user has used arguments that are incompatible with each other.""" | ||
| def __init__(self, *args, message: _Optional[str] = None) -> 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 and 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 RequiredArgumentError(ArgumentError): | ||
| """Raise this when there is a required argument missing.""" | ||
| def __init__(self, *args, message: _Optional[str] = None) -> 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 and 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 TooFewArgumentsError(ArgumentError): | ||
| """Raise this when there were not enough arguments passed.""" | ||
| def __init__(self, *args, message: _Optional[str] = None) -> 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 and 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. | ||
| >>> 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) -> None: | ||
| """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: _OrderedDict[str, Argument] = _field(init=False) | ||
| docstring: _Docstring = _field(init=False) | ||
| parser: _argparse.ColorizingArgumentParser = _field(init=False) | ||
| def __post_init__(self) -> None: | ||
| 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( # pylint: disable=too-many-branches | ||
| 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: | ||
| if no_dash: | ||
| raise FlagGenerationError( | ||
| f"Failed to generate a flag for argument: {argument}" | ||
| ) | ||
| else: | ||
| 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[_argparse.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 argument.annotation is bool: | ||
| config['action'] = 'store_true' | ||
| elif argument.annotation: | ||
| config['type'] = argument.annotation | ||
| 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) -> None: | ||
| """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 = _argparse.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) | ||
| 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]) -> None: | ||
| """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 = _argparse.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(): # noqa: B007 | ||
| 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) | ||
| 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]] | ||
| ) -> _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 |
| # log21.colors.py | ||
| # CodeWriter21 | ||
| import re as _re | ||
| from typing import Union as _Union, Sequence as _Sequence | ||
| import webcolors as _webcolors | ||
| __all__ = [ | ||
| 'Colors', 'get_color', 'get_colors', 'ansi_escape', 'get_color_name', | ||
| 'closest_color', 'hex_escape', 'RESET', 'BLACK', 'RED', 'GREEN', 'YELLOW', 'BLUE', | ||
| 'MAGENTA', 'CYAN', 'WHITE', 'BACK_BLACK', 'BACK_RED', 'BACK_GREEN', 'BACK_YELLOW', | ||
| 'BACK_BLUE', 'BACK_MAGENTA', 'BACK_CYAN', 'BACK_WHITE', 'GREY', 'LIGHT_RED', | ||
| 'LIGHT_GREEN', 'LIGHT_YELLOW', 'LIGHT_BLUE', 'LIGHT_MAGENTA', 'LIGHT_CYAN', | ||
| 'LIGHT_WHITE', 'BACK_GREY', 'BACK_LIGHT_RED', 'BACK_LIGHT_GREEN', | ||
| 'BACK_LIGHT_YELLOW', 'BACK_LIGHT_BLUE', 'BACK_LIGHT_MAGENTA', 'BACK_LIGHT_CYAN', | ||
| 'BACK_LIGHT_WHITE' | ||
| ] | ||
| # Regex pattern to find ansi colors in message | ||
| ansi_escape = _re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m') | ||
| hex_escape = _re.compile(r'\x1b(#[0-9a-fA-F]{6})h([f|b])') | ||
| RESET = '\033[0m' | ||
| BLACK = '\033[30m' | ||
| RED = '\033[31m' | ||
| GREEN = '\033[32m' | ||
| YELLOW = '\033[33m' | ||
| BLUE = '\033[34m' | ||
| MAGENTA = '\033[35m' | ||
| CYAN = '\033[36m' | ||
| WHITE = '\033[37m' | ||
| BACK_BLACK = '\033[40m' | ||
| BACK_RED = '\033[41m' | ||
| BACK_GREEN = '\033[42m' | ||
| BACK_YELLOW = '\033[43m' | ||
| BACK_BLUE = '\033[44m' | ||
| BACK_MAGENTA = '\033[45m' | ||
| BACK_CYAN = '\033[46m' | ||
| BACK_WHITE = '\033[47m' | ||
| GREY = '\033[90m' | ||
| LIGHT_RED = '\033[91m' | ||
| LIGHT_GREEN = '\033[92m' | ||
| LIGHT_YELLOW = '\033[93m' | ||
| LIGHT_BLUE = '\033[94m' | ||
| LIGHT_MAGENTA = '\033[95m' | ||
| LIGHT_CYAN = '\033[96m' | ||
| LIGHT_WHITE = '\033[97m' | ||
| BACK_GREY = '\033[100m' | ||
| BACK_LIGHT_RED = '\033[101m' | ||
| BACK_LIGHT_GREEN = '\033[102m' | ||
| BACK_LIGHT_YELLOW = '\033[103m' | ||
| BACK_LIGHT_BLUE = '\033[104m' | ||
| BACK_LIGHT_MAGENTA = '\033[105m' | ||
| BACK_LIGHT_CYAN = '\033[106m' | ||
| BACK_LIGHT_WHITE = '\033[107m' | ||
| class Colors: | ||
| """A class containing color-maps.""" | ||
| color_map = { | ||
| 'Reset': RESET, | ||
| 'Black': BLACK, | ||
| 'Red': RED, | ||
| 'Green': GREEN, | ||
| 'Yellow': YELLOW, | ||
| 'Blue': BLUE, | ||
| 'Magenta': MAGENTA, | ||
| 'Cyan': CYAN, | ||
| 'White': WHITE, | ||
| 'BackBlack': BACK_BLACK, | ||
| 'BackRed': BACK_RED, | ||
| 'BackGreen': BACK_GREEN, | ||
| 'BackYellow': BACK_YELLOW, | ||
| 'BackBlue': BACK_BLUE, | ||
| 'BackMagenta': BACK_MAGENTA, | ||
| 'BackCyan': BACK_CYAN, | ||
| 'BackWhite': BACK_WHITE, | ||
| 'Grey': GREY, | ||
| 'LightRed': LIGHT_RED, | ||
| 'LightGreen': LIGHT_GREEN, | ||
| 'LightYellow': LIGHT_YELLOW, | ||
| 'LightBlue': LIGHT_BLUE, | ||
| 'LightMagenta': LIGHT_MAGENTA, | ||
| 'LightCyan': LIGHT_CYAN, | ||
| 'LightWhite': LIGHT_WHITE, | ||
| 'BackGrey': BACK_GREY, | ||
| 'BackLightRed': BACK_LIGHT_RED, | ||
| 'BackLightGreen': BACK_LIGHT_GREEN, | ||
| 'BackLightYellow': BACK_LIGHT_YELLOW, | ||
| 'BackLightBlue': BACK_LIGHT_BLUE, | ||
| 'BackLightMagenta': BACK_LIGHT_MAGENTA, | ||
| 'BackLightCyan': BACK_LIGHT_CYAN, | ||
| 'BackLightWhite': BACK_LIGHT_WHITE, | ||
| } | ||
| color_map_ = { | ||
| 'reset': RESET, | ||
| 'black': BLACK, | ||
| 'red': RED, | ||
| 'green': GREEN, | ||
| 'yellow': YELLOW, | ||
| 'blue': BLUE, | ||
| 'magenta': MAGENTA, | ||
| 'cyan': CYAN, | ||
| 'white': WHITE, | ||
| 'backblack': BACK_BLACK, | ||
| 'backred': BACK_RED, | ||
| 'backgreen': BACK_GREEN, | ||
| 'backyellow': BACK_YELLOW, | ||
| 'backblue': BACK_BLUE, | ||
| 'backmagenta': BACK_MAGENTA, | ||
| 'backcyan': BACK_CYAN, | ||
| 'backwhite': BACK_WHITE, | ||
| 'grey': GREY, | ||
| 'gray': GREY, | ||
| 'lightred': LIGHT_RED, | ||
| 'lightgreen': LIGHT_GREEN, | ||
| 'lightyellow': LIGHT_YELLOW, | ||
| 'lightblue': LIGHT_BLUE, | ||
| 'lightmagenta': LIGHT_MAGENTA, | ||
| 'lightcyan': LIGHT_CYAN, | ||
| 'lightwhite': LIGHT_WHITE, | ||
| 'backgrey': BACK_GREY, | ||
| 'backlightred': BACK_LIGHT_RED, | ||
| 'backlightgreen': BACK_LIGHT_GREEN, | ||
| 'backlightyellow': BACK_LIGHT_YELLOW, | ||
| 'backlightblue': BACK_LIGHT_BLUE, | ||
| 'backlightmagenta': BACK_LIGHT_MAGENTA, | ||
| 'backlightcyan': BACK_LIGHT_CYAN, | ||
| 'backlightwhite': BACK_LIGHT_WHITE, | ||
| 'brightblack': GREY, | ||
| 'brightred': LIGHT_RED, | ||
| 'brightgreen': LIGHT_GREEN, | ||
| 'brightyellow': LIGHT_YELLOW, | ||
| 'brightblue': LIGHT_BLUE, | ||
| 'brightmagenta': LIGHT_MAGENTA, | ||
| 'brightcyan': LIGHT_CYAN, | ||
| 'brightwhite': LIGHT_WHITE, | ||
| 'backbrightblack': BACK_GREY, | ||
| 'backbrightred': BACK_LIGHT_RED, | ||
| 'backbrightgreen': BACK_LIGHT_GREEN, | ||
| 'backbrightyellow': BACK_LIGHT_YELLOW, | ||
| 'backbrightblue': BACK_LIGHT_BLUE, | ||
| 'backbrightmagenta': BACK_LIGHT_MAGENTA, | ||
| 'backbrightcyan': BACK_LIGHT_CYAN, | ||
| 'backbrightwhite': BACK_LIGHT_WHITE, | ||
| 'rst': RESET, | ||
| 'bk': BLACK, | ||
| 'r': RED, | ||
| 'g': GREEN, | ||
| 'y': YELLOW, | ||
| 'b': BLUE, | ||
| 'm': MAGENTA, | ||
| 'c': CYAN, | ||
| 'w': WHITE, | ||
| 'bbk': BACK_BLACK, | ||
| 'br': BACK_RED, | ||
| 'bg': BACK_GREEN, | ||
| 'by': BACK_YELLOW, | ||
| 'bb': BACK_BLUE, | ||
| 'bm': BACK_MAGENTA, | ||
| 'bc': BACK_CYAN, | ||
| 'bw': BACK_WHITE, | ||
| 'gr': GREY, | ||
| 'lr': LIGHT_RED, | ||
| 'lg': LIGHT_GREEN, | ||
| 'ly': LIGHT_YELLOW, | ||
| 'lb': LIGHT_BLUE, | ||
| 'lm': LIGHT_MAGENTA, | ||
| 'lc': LIGHT_CYAN, | ||
| 'lw': LIGHT_WHITE, | ||
| 'bgr': BACK_GREY, | ||
| 'blr': BACK_LIGHT_RED, | ||
| 'blg': BACK_LIGHT_GREEN, | ||
| 'bly': BACK_LIGHT_YELLOW, | ||
| 'blb': BACK_LIGHT_BLUE, | ||
| 'blm': BACK_LIGHT_MAGENTA, | ||
| 'blc': BACK_LIGHT_CYAN, | ||
| 'blw': BACK_LIGHT_WHITE, | ||
| } | ||
| change_map = { | ||
| 'aqua': 'LightCyan', | ||
| 'blue': 'LightBlue', | ||
| 'fuchsia': 'LightMagenta', | ||
| 'lime': 'LightGreen', | ||
| 'maroon': 'Red', | ||
| 'navy': 'Blue', | ||
| 'olive': 'Yellow', | ||
| 'purple': 'Magenta', | ||
| 'red': 'LightRed', | ||
| 'silver': 'Grey', | ||
| 'teal': 'Cyan', | ||
| 'white': 'BrightWhite', | ||
| 'yellow': 'LightYellow', | ||
| } | ||
| def closest_color(requested_color: _Sequence[int]) -> str: | ||
| """Takes a color in RGB and returns the name of the closest color to the value. Uses | ||
| the `webcolors.CSS2_HEX_TO_NAMES` dictionary to find the closest color. | ||
| :param requested_color: Sequence[int, int, int]: The input color in RGB. | ||
| :return: str: The name of the closest color. | ||
| """ | ||
| min_colors = {} | ||
| for key, name in _webcolors.CSS2_HEX_TO_NAMES.items(): | ||
| r_c, g_c, b_c = _webcolors.hex_to_rgb(key) | ||
| r_d = (r_c - requested_color[0])**2 | ||
| g_d = (g_c - requested_color[1])**2 | ||
| b_d = (b_c - requested_color[2])**2 | ||
| min_colors[(r_d + g_d + b_d)] = name | ||
| return min_colors[min(min_colors.keys())] | ||
| def get_color_name( | ||
| color: _Union[str, _Sequence[int], _webcolors.IntegerRGB, | ||
| _webcolors.HTML5SimpleColor], | ||
| raise_exceptions: bool = False | ||
| ) -> str: | ||
| """ | ||
| Takes a color in RGB format and returns a color name close to the RGB value. | ||
| >>> | ||
| >>> get_color_name('#00FF00') | ||
| 'LightGreen' | ||
| >>> | ||
| >>> get_color_name((128, 0, 128)) | ||
| 'Magenta' | ||
| >>> | ||
| :param color: Union[str, Sequence[int]: The input color. Example: '#00FF00', | ||
| (128, 0, 128) | ||
| :param raise_exceptions: bool = False: Returns empty string when raise_exceptions is | ||
| False and an error occurs. | ||
| :raises TypeError | ||
| :return: str: The color name. | ||
| """ | ||
| # Makes sure that the input parameters has valid values. | ||
| if not isinstance(color, | ||
| (str, tuple, _webcolors.IntegerRGB, _webcolors.HTML5SimpleColor)): | ||
| if raise_exceptions: | ||
| raise TypeError( | ||
| 'Input color must be a str or Tuple[int, int, int] or ' | ||
| 'webcolors.IntegerRGB or webcolors.HTML5SimpleColor' | ||
| ) | ||
| return '' | ||
| if isinstance(color, str): | ||
| if color.startswith('#') and len(color) == 7: | ||
| color = _webcolors.hex_to_rgb(color) | ||
| elif color.isdigit() and len(color) == 9: | ||
| color = (int(color[:3]), int(color[3:6]), int(color[6:9])) | ||
| else: | ||
| if raise_exceptions: | ||
| raise TypeError('String color format must be `#0021ff` or `000033255`!') | ||
| return '' | ||
| if isinstance(color, _Sequence): | ||
| if len(color) == 3: | ||
| if not (isinstance(color[0], int) and isinstance(color[1], int) | ||
| and isinstance(color[2], int)): | ||
| if raise_exceptions: | ||
| raise TypeError('Color sequence format must be (int, int, int)!') | ||
| return '' | ||
| else: | ||
| if raise_exceptions: | ||
| raise TypeError('Color sequence format must be (int, int, int)!') | ||
| return '' | ||
| # Looks for the name of the input RGB color. | ||
| try: | ||
| closest_name = _webcolors.rgb_to_name(tuple(color)) | ||
| except ValueError: | ||
| closest_name = closest_color(color) | ||
| if closest_name in Colors.change_map: | ||
| closest_name = Colors.change_map[closest_name] | ||
| return closest_name | ||
| def get_color(color: _Union[str, _Sequence], raise_exceptions: bool = False) -> str: | ||
| """Gets a color name and returns it in ansi format | ||
| >>> | ||
| >>> get_color('LightRed') | ||
| '\x1b[91m' | ||
| >>> | ||
| >>> import log21 | ||
| >>> log21.get_logger().info(log21.get_color('Blue') + 'Hello World!') | ||
| [21:21:21] [INFO] Hello World! | ||
| >>> # Note that you must run it yourself to see the colorful result ;D | ||
| >>> | ||
| :param color: color name(Example: Blue) | ||
| :param raise_exceptions: bool = False: | ||
| False: It will return '' instead of raising exceptions when an error occurs. | ||
| True: It may raise TypeError or KeyError | ||
| :raises TypeError: `color` must be str | ||
| :raises KeyError: `color` not found! | ||
| :return: str: an ansi color | ||
| """ | ||
| if not isinstance(color, (str, _Sequence)): | ||
| if raise_exceptions: | ||
| raise TypeError('`color` must be str or Sequence!') | ||
| return '' | ||
| if isinstance(color, _Sequence) and not isinstance(color, str): | ||
| color = get_color_name(color) | ||
| return get_color(color) | ||
| color = color.lower() | ||
| color = color.replace(' ', '').replace('_', '').replace('-', '') | ||
| color = color.replace('foreground', '').replace('fore', '').replace('ground', '') | ||
| if (color.startswith('#') and len(color) == 7) or (color.isdigit() | ||
| and len(color) == 9): | ||
| color = get_color_name(color) | ||
| return get_color(color) | ||
| if color in Colors.color_map_: | ||
| return Colors.color_map_[color] | ||
| if ansi_escape.match(color): | ||
| return ansi_escape.match(color).group() | ||
| if color in Colors.change_map: | ||
| return get_color(Colors.change_map[color]) | ||
| if raise_exceptions: | ||
| raise KeyError(f'`{color}` not found!') | ||
| return '' | ||
| def get_colors(*colors: str, raise_exceptions: bool = False) -> str: | ||
| """Gets a list of colors and combines them into one. | ||
| >>> | ||
| >>> get_colors('LightCyan') | ||
| '\x1b[96m' | ||
| >>> | ||
| >>> import log21 | ||
| >>> log21.get_logger().info(log21.get_colors('Green', 'Background White') + | ||
| ... 'Hello World!') | ||
| [21:21:21] [INFO] Hello World! | ||
| >>> # Note that you must run it yourself to see the colorful result ;D | ||
| >>> | ||
| :param colors: Input colors | ||
| :param raise_exceptions: bool = False: | ||
| False: It will return '' instead of raising exceptions when an error occurs. | ||
| True: It may raise TypeError or KeyError | ||
| :raises TypeError: `color` must be str | ||
| :raises KeyError: `color` not found! | ||
| :return: str: a combined color | ||
| """ | ||
| output = '' | ||
| for color in colors: | ||
| output += get_color(str(color), raise_exceptions=raise_exceptions) | ||
| parts = ansi_escape.split(output) | ||
| output = '\033[' | ||
| for part in parts: | ||
| if part: | ||
| output += part + ';' | ||
| if output.endswith(';'): | ||
| output = output[:-1] + 'm' | ||
| return output | ||
| return '' |
| # log21.crash_reporter.__init__.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| from . import reporters, formatters | ||
| from .reporters import Reporter, FileReporter, EmailReporter, ConsoleReporter | ||
| from .formatters import (FILE_REPORTER_FORMAT, EMAIL_REPORTER_FORMAT, | ||
| CONSOLE_REPORTER_FORMAT, Formatter) | ||
| # yapf: enable | ||
| __all__ = [ | ||
| 'reporters', 'formatters', 'Reporter', 'FileReporter', 'EmailReporter', | ||
| 'ConsoleReporter', 'FILE_REPORTER_FORMAT', 'EMAIL_REPORTER_FORMAT', | ||
| 'CONSOLE_REPORTER_FORMAT', 'Formatter' | ||
| ] |
| # log21.crash_reporter.formatters.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| import traceback | ||
| from typing import (Any as _Any, Union as _Union, Mapping as _Mapping, | ||
| Callable as _Callable, Optional as _Optional) | ||
| from datetime import datetime as _datetime | ||
| # yapf: enable | ||
| __all__ = [ | ||
| 'Formatter', 'CONSOLE_REPORTER_FORMAT', 'FILE_REPORTER_FORMAT', | ||
| 'EMAIL_REPORTER_FORMAT' | ||
| ] | ||
| RESERVED_KEYS = ( | ||
| '__name__', 'type', 'message', 'traceback', 'name', 'file', 'lineno', 'function', | ||
| 'asctime' | ||
| ) | ||
| class Formatter: | ||
| """The base class for all CrashReporter formatters.""" | ||
| def __init__( | ||
| self, | ||
| format_: str, | ||
| style: str = '%', | ||
| datefmt: str = '%Y-%m-%d %H:%M:%S', | ||
| extra_values: _Optional[_Mapping[str, _Union[str, _Callable, _Any]]] = None | ||
| ) -> None: | ||
| """Initialize the formatter. | ||
| :param format_: The format string. | ||
| :param style: The style of the format string. Valid styles: %, { | ||
| :param datefmt: The date format string. | ||
| :param extra_values: A mapping of extra values to be added to the log record. | ||
| """ | ||
| self._format = format_ | ||
| if style in ['%', '{']: | ||
| self.__style = style | ||
| else: | ||
| raise ValueError('Invalid style: "' + str(style) + '" Valid styles: %, {') | ||
| self.datefmt = datefmt | ||
| self.extra_values = {} | ||
| if extra_values: | ||
| for key in extra_values: | ||
| if key in RESERVED_KEYS: | ||
| raise ValueError( | ||
| f'`{key}` is a reserved-key and cannot be used in ' | ||
| '`extra_values`.' | ||
| ) | ||
| self.extra_values[key] = extra_values[key] | ||
| def format(self, exception: BaseException) -> str: | ||
| """Format the exception. | ||
| :param exception: The exception to format. | ||
| :raises ValueError: If the style is not either '%' or '{'. | ||
| :return: The formatted exception. | ||
| """ | ||
| exception_dict = { | ||
| '__name__': __name__, | ||
| 'type': type(exception), | ||
| 'message': exception.args[0], | ||
| 'traceback': traceback.format_tb(exception.__traceback__.tb_next), | ||
| 'name': exception.__class__.__name__, | ||
| 'file': exception.__traceback__.tb_next.tb_frame.f_code.co_filename, | ||
| 'lineno': exception.__traceback__.tb_next.tb_lineno, | ||
| 'function': exception.__traceback__.tb_next.tb_frame.f_code.co_name, | ||
| 'asctime': _datetime.now().strftime(self.datefmt), | ||
| } | ||
| for key, value in self.extra_values.items(): | ||
| if callable(value): | ||
| exception_dict[key] = value() | ||
| else: | ||
| exception_dict[key] = value | ||
| if self.__style == '%': | ||
| return self._format % exception_dict | ||
| if self.__style == '{': | ||
| return self._format.format(**exception_dict) | ||
| raise ValueError( | ||
| 'Invalid style: "' + str(self.__style) + '" Valid styles: %, {' | ||
| ) | ||
| CONSOLE_REPORTER_FORMAT = { | ||
| 'format_': | ||
| '\033[91m%(name)s: %(message)s\033[0m\n' # Name and message of the exception. | ||
| '\tFile\033[91m:\033[0m "%(file)s"\n' # The file that exception was raised in. | ||
| '\tLine\033[91m:\033[0m %(lineno)d', # The line that exception was raised on. | ||
| 'style': | ||
| '%' | ||
| } | ||
| FILE_REPORTER_FORMAT = { | ||
| 'format_': | ||
| '[%(asctime)s] %(name)s: %(message)s' # Name and message of the exception. | ||
| '; File: "%(file)s"' # The file that exception was raised in. | ||
| '; Line: %(lineno)d\n', # The line that exception was raised on. | ||
| 'style': | ||
| '%' | ||
| } | ||
| EMAIL_REPORTER_FORMAT = { | ||
| 'format_': """ | ||
| <html> | ||
| <body> | ||
| <h1>Crash Report: %(__name__)s</h1> | ||
| <h2>%(name)s: %(message)s</h2> | ||
| <p> | ||
| <span style="bold">File:</span> "%(file)s"<br> | ||
| <span style="bold">Line:</span> %(lineno)d<br> | ||
| <span style="center">%(asctime)s</span><br> | ||
| </p> | ||
| <body> | ||
| </html> | ||
| """, | ||
| 'style': '%' | ||
| } |
| # log21.crash_reporter.reporters.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| from __future__ import annotations | ||
| import ssl as _ssl | ||
| import smtplib as _smtplib # This module is used to send emails. | ||
| from os import PathLike as _PathLike | ||
| from typing import (IO as _IO, Any as _Any, Set as _Set, Type as _Type, Union as _Union, | ||
| Callable as _Callable, Iterable as _Iterable, Optional as _Optional) | ||
| from functools import wraps as _wraps | ||
| from email.mime.text import MIMEText as _MIMEText | ||
| from email.mime.multipart import MIMEMultipart as _MIMEMultipart | ||
| import log21 as _log21 | ||
| from .formatters import (FILE_REPORTER_FORMAT as _FILE_REPORTER_FORMAT, | ||
| EMAIL_REPORTER_FORMAT as _EMAIL_REPORTER_FORMAT, | ||
| CONSOLE_REPORTER_FORMAT as _CONSOLE_REPORTER_FORMAT) | ||
| # yapf: enable | ||
| __all__ = ['Reporter', 'ConsoleReporter', 'FileReporter', 'EmailReporter'] | ||
| # pylint: disable=redefined-builtin | ||
| def print(*msg, args: tuple = (), end: str = '\033[0m\n', **kwargs) -> None: | ||
| """Prints a message to the console using the log21.Logger.""" | ||
| logger = _log21.get_logger( | ||
| 'log21.print', level='DEBUG', show_time=False, show_level=False | ||
| ) | ||
| logger.print(*msg, args=args, end=end, **kwargs) | ||
| class Reporter: | ||
| """Reporter is a decorator that wraps a function and calls a function when an | ||
| exception is raised. | ||
| Usage Example: | ||
| >>> | ||
| >>> # Define a function that gets an exception and somehow reports it to you | ||
| >>> def report_function(exception): | ||
| ... print(exception) | ||
| ... | ||
| >>> | ||
| >>> # Create a Reporter object and pass the reporter function you defined to it | ||
| >>> reporter_object = Reporter(report_function, False) | ||
| >>> | ||
| >>> # Define the function you want to wrap | ||
| >>> # This function might raise an exception | ||
| >>> # You can wrap your main function, so that you get notified whenever your | ||
| >>> # app crashes | ||
| >>> @reporter_object.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| division by zero | ||
| >>> | ||
| >>> # You also can wrap a function like this | ||
| >>> import math | ||
| >>> wrapped_sqrt = reporter_object.reporter(math.sqrt) | ||
| >>> wrapped_sqrt(121) | ||
| 11.0 | ||
| >>> wrapped_sqrt(-1) | ||
| math domain error | ||
| >>> | ||
| """ | ||
| _reporter_function: _Callable[[ | ||
| BaseException | ||
| ], _Any] # A function that will be called when an exception is raised. | ||
| _exceptions_to_catch: _Set = None | ||
| _exceptions_to_ignore: _Set = None | ||
| raise_after_report: bool | ||
| def __init__( | ||
| self, | ||
| report_function: _Optional[_Callable[[BaseException], _Any]], | ||
| raise_after_report: bool = False, | ||
| formatter: _Optional[_log21.crash_reporter.Formatter] = None, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ) -> None: | ||
| """ | ||
| :param report_function: Function to call when an exception is raised. | ||
| :param raise_after_report: If True, the exception will be raised after the | ||
| report_function is called. | ||
| """ | ||
| self._reporter_function = report_function | ||
| self.raise_after_report = raise_after_report | ||
| self.formatter = formatter | ||
| self._exceptions_to_catch = set( | ||
| exceptions_to_catch | ||
| ) if exceptions_to_catch else None | ||
| self._exceptions_to_ignore = set( | ||
| exceptions_to_ignore | ||
| ) if exceptions_to_ignore else None | ||
| def reporter(self, func: _Callable) -> _Callable: | ||
| """It will wrap the function and call the report_function when an exception is | ||
| raised. | ||
| :param func: Function to wrap. | ||
| :return: Wrapped function. | ||
| """ | ||
| exceptions_to_catch = tuple( | ||
| self._exceptions_to_catch | ||
| ) if self._exceptions_to_catch else BaseException | ||
| exceptions_to_ignore = tuple(self._exceptions_to_ignore | ||
| ) if self._exceptions_to_ignore else () | ||
| @_wraps(func) | ||
| def wrap(*args, **kwargs) -> _Any: | ||
| try: | ||
| return func(*args, **kwargs) | ||
| except BaseException as e: | ||
| if isinstance(e, exceptions_to_catch) and not isinstance( | ||
| e, exceptions_to_ignore): | ||
| self._reporter_function(e) | ||
| if self.raise_after_report: | ||
| raise e | ||
| else: | ||
| raise e | ||
| return wrap | ||
| def catch(self, exception: _Type[BaseException]) -> None: | ||
| """Add an exception to the list of exceptions to catch. | ||
| :param exception: Exception to catch. | ||
| """ | ||
| if not issubclass(exception, BaseException): | ||
| raise TypeError('`exception` must be a subclass of BaseException.') | ||
| if self._exceptions_to_catch is None: | ||
| self._exceptions_to_catch = set() | ||
| if exception not in self._exceptions_to_catch: | ||
| self._exceptions_to_catch.add(exception) | ||
| else: | ||
| raise ValueError('exception is already in the list of exceptions to catch') | ||
| def ignore(self, exception: _Type[BaseException]) -> None: | ||
| """Add an exception to the list of exceptions to ignore. | ||
| :param exception: Exception to ignore. | ||
| """ | ||
| if not issubclass(exception, BaseException): | ||
| raise TypeError('`exception` must be a subclass of BaseException.') | ||
| if self._exceptions_to_ignore is None: | ||
| self._exceptions_to_ignore = set() | ||
| if exception not in self._exceptions_to_ignore: | ||
| self._exceptions_to_ignore.add(exception) | ||
| else: | ||
| raise ValueError('exception is already in the list of exceptions to ignore') | ||
| def __call__(self, func: _Callable) -> _Callable: | ||
| return self.reporter(func) | ||
| class ConsoleReporter(Reporter): | ||
| """ConsoleReporter is a Reporter that prints the exception to the console. | ||
| Usage Example: | ||
| >>> | ||
| >>> # Define a ConsoleReporter object | ||
| >>> console_reporter = ConsoleReporter() | ||
| >>> | ||
| >>> # Define a function that raises an exception | ||
| >>> @console_reporter.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| ZeroDivisionError: division by zero | ||
| File: "<stdin>" | ||
| Line: 3 | ||
| >>> | ||
| >>> # You can also use costume formatters | ||
| >>> import log21 | ||
| >>> BLUE = log21.get_color('Light Blue') | ||
| >>> RED = log21.get_color('Light Red') | ||
| >>> YELLOW = log21.get_color('LIGHT YELLOW') | ||
| >>> RESET = log21.get_color('reset') | ||
| >>> formatter = log21.crash_reporter.Formatter( | ||
| ... format_='[' + BLUE + '%(asctime)s' + RESET + '] ' + | ||
| ... YELLOW + '%(function)s' + RED + ': ' + | ||
| ... RESET + 'Line ' + RED + '%(lineno)d: %(name)s:' + | ||
| ... RESET + ' %(message)s' | ||
| ... ) | ||
| >>> console_reporter = log21.crash_reporter.ConsoleReporter(formatter=formatter) | ||
| >>> | ||
| >>> @console_reporter.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| [2121-12-21 21:21:21] divide: Line 3: ZeroDivisionError: division by zero | ||
| >>> | ||
| """ | ||
| def __init__( | ||
| self, | ||
| raise_after_report: bool = False, | ||
| formatter: _Optional[_log21.crash_reporter.Formatter] = None, | ||
| print_function: _Optional[_Callable] = print, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ) -> None: | ||
| """ | ||
| :param raise_after_report: If True, the exception will be raised after the | ||
| report_function is called. | ||
| :param print_function: Function to use to print the message. | ||
| """ | ||
| super().__init__( | ||
| self._report, raise_after_report, formatter, exceptions_to_catch, | ||
| exceptions_to_ignore | ||
| ) | ||
| if formatter: | ||
| if isinstance(formatter, _log21.crash_reporter.Formatter): | ||
| self.formatter = formatter | ||
| else: | ||
| raise ValueError('formatter must be a log21.crash_reporter.Formatter') | ||
| else: | ||
| self.formatter = _log21.crash_reporter.formatters.Formatter( | ||
| **_CONSOLE_REPORTER_FORMAT | ||
| ) | ||
| self.print = print_function | ||
| def _report(self, exception: BaseException) -> None: | ||
| """Prints the exception to the console. | ||
| :param exception: Exception to print. | ||
| :return: | ||
| """ | ||
| self.print(self.formatter.format(exception)) | ||
| class FileReporter(Reporter): | ||
| """FileReporter is a Reporter that writes the exception to a file.""" | ||
| def __init__( | ||
| self, | ||
| *, | ||
| file: _Union[str, _PathLike, _IO], | ||
| encoding: str = 'utf-8', | ||
| raise_after_report: bool = True, | ||
| formatter: _Optional[_log21.crash_reporter.Formatter] = None, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ) -> None: | ||
| super().__init__( | ||
| self._report, raise_after_report, formatter, exceptions_to_catch, | ||
| exceptions_to_ignore | ||
| ) | ||
| # pylint: disable=consider-using-with | ||
| if isinstance(file, (str, _PathLike)): | ||
| self.file = open(file, 'a', encoding=encoding) # noqa: SIM115 | ||
| elif isinstance(file, _IO): | ||
| if file.writable(): | ||
| self.file = file | ||
| else: | ||
| raise ValueError('file must be writable') | ||
| else: | ||
| raise ValueError('file must be a string, PathLike, or IO object') | ||
| if formatter: | ||
| if isinstance(formatter, _log21.crash_reporter.Formatter): | ||
| self.formatter = formatter | ||
| else: | ||
| raise ValueError('formatter must be a log21.crash_reporter.Formatter') | ||
| else: | ||
| self.formatter = _log21.crash_reporter.formatters.Formatter( | ||
| **_FILE_REPORTER_FORMAT | ||
| ) | ||
| def _report(self, exception: BaseException) -> None: | ||
| """Writes the exception to the file. | ||
| :param exception: Exception to write. | ||
| :return: | ||
| """ | ||
| self.file.write(self.formatter.format(exception)) | ||
| self.file.flush() | ||
| class EmailReporter(Reporter): # pylint: disable=too-many-instance-attributes | ||
| """EmailReporter is a Reporter that sends an email with the exception. | ||
| Usage Example: | ||
| >>> | ||
| >>> # Define a EmailReporter object | ||
| >>> email_reporter = EmailReporter( | ||
| ... mail_host='smtp.yandex.ru', | ||
| ... port=465, | ||
| ... from_address='MyEmail@yandex.ru', | ||
| ... to_address='CodeWriter21@gmail.com', | ||
| ... password='My$up3rStr0ngP@assw0rd XD' | ||
| ... ) | ||
| ... | ||
| >>> # Define the function you want to wrap | ||
| >>> @email_reporter.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| Traceback (most recent call last): | ||
| File "<stdin>", line 1, in <module> | ||
| File ".../site-packages/log21/crash_reporter/reporters.py", | ||
| line 81, in wrap | ||
| raise e | ||
| File ".../site-packages/log21/crash_reporter/reporters.py", | ||
| line 77, in wrap | ||
| return func(*args, **kwargs) | ||
| File "<stdin>", line 3, in divide | ||
| ZeroDivisionError: division by zero | ||
| >>> # At this point a Crash Report is sent to my email: CodeWriter21@gmail.com | ||
| >>> | ||
| """ | ||
| def __init__( | ||
| self, | ||
| mail_host: str, | ||
| port: int, | ||
| from_address: str, | ||
| to_address: str, | ||
| password: str, | ||
| username: str = '', | ||
| tls: bool = True, | ||
| raise_after_report: bool = True, | ||
| formatter: _Optional[_log21.crash_reporter.Formatter] = None, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ) -> None: | ||
| super().__init__( | ||
| self._report, raise_after_report, formatter, exceptions_to_catch, | ||
| exceptions_to_ignore | ||
| ) | ||
| self.mail_host = mail_host | ||
| self.port = port | ||
| self.from_address = from_address | ||
| self.to_address = to_address | ||
| self.password = password | ||
| if username: | ||
| self.username = username | ||
| else: | ||
| self.username = self.from_address | ||
| self.tls = tls | ||
| # Checks if the sender email is accessible | ||
| try: | ||
| if self.tls: | ||
| context = _ssl.create_default_context() | ||
| with _smtplib.SMTP_SSL(self.mail_host, port, context=context) as server: | ||
| server.ehlo() | ||
| server.login(self.username, self.password) | ||
| else: | ||
| with _smtplib.SMTP(self.mail_host, port) as server: | ||
| server.ehlo() | ||
| server.login(self.username, self.password) | ||
| server.ehlo() | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| raise ex | ||
| if formatter: | ||
| if isinstance(formatter, _log21.crash_reporter.Formatter): | ||
| self.formatter = formatter | ||
| else: | ||
| raise ValueError('formatter must be a log21.crash_reporter.Formatter') | ||
| else: | ||
| self.formatter = _log21.crash_reporter.Formatters.Formatter( | ||
| **_EMAIL_REPORTER_FORMAT | ||
| ) | ||
| def _report(self, exception: BaseException) -> None: | ||
| """Sends an email with the exception. | ||
| :param exception: Exception to send. | ||
| :return: | ||
| """ | ||
| message = _MIMEMultipart() | ||
| message['From'] = self.from_address # Sender | ||
| message['To'] = self.to_address # Receiver | ||
| message['Subject'] = f'Crash Report: {exception.__class__.__name__}' # Subject | ||
| message.attach(_MIMEText(self.formatter.format(exception), 'html')) | ||
| if self.tls: | ||
| context = _ssl.create_default_context() | ||
| with _smtplib.SMTP_SSL(self.mail_host, port=self.port, | ||
| context=context) as server: | ||
| server.login(self.username, self.password) | ||
| server.sendmail(self.from_address, self.to_address, message.as_string()) | ||
| else: | ||
| with _smtplib.SMTP(self.username, port=self.port) as server: | ||
| server.login(self.from_address, self.password) | ||
| server.sendmail(self.from_address, self.to_address, message.as_string()) |
| # log21.file_handler.py | ||
| # CodeWriter21 | ||
| from typing import Optional as _Optional | ||
| from logging import FileHandler as _FileHandler | ||
| from log21.formatters import DecolorizingFormatter as _DecolorizingFormatter | ||
| # ruff: noqa: ANN001 | ||
| class FileHandler(_FileHandler): | ||
| """A subclass of logging.FileHandler that allows you to specify a formatter and a | ||
| level when you initialize it.""" | ||
| def __init__( | ||
| self, | ||
| filename, | ||
| mode: str = 'a', | ||
| encoding: _Optional[str] = None, | ||
| delay: bool = False, | ||
| errors=None, | ||
| formatter=None, | ||
| level=None | ||
| ) -> None: | ||
| """Initialize the handler. | ||
| :param filename: The filename of the log file. | ||
| :param mode: The mode to open the file in. | ||
| :param encoding: The encoding to use when opening the file. | ||
| :param delay: Whether to delay opening the file. | ||
| :param errors: The error handling scheme to use. | ||
| :param formatter: The formatter to use. | ||
| :param level: The level to use. | ||
| """ | ||
| super().__init__(filename, mode, encoding, delay, errors) | ||
| if formatter is not None: | ||
| self.setFormatter(formatter) | ||
| if level is not None: | ||
| self.setLevel(level) | ||
| class DecolorizingFileHandler(FileHandler): | ||
| """A subclass of FileHandler that removes ANSI colors from the log messages before | ||
| writing them to the file.""" | ||
| terminator = '' | ||
| def emit(self, record) -> None: | ||
| """Emit a record.""" | ||
| if self.stream is None: | ||
| self.stream = self._open() | ||
| try: | ||
| msg = self.format(record) | ||
| msg = _DecolorizingFormatter.decolorize(msg) | ||
| stream = self.stream | ||
| stream.write(msg + self.terminator) | ||
| self.flush() | ||
| except Exception: # pylint: disable=broad-except | ||
| self.handleError(record) |
| # log21.formatters.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| import time as _time | ||
| from typing import (Dict as _Dict, Tuple as _Tuple, Literal as _Literal, | ||
| Mapping as _Mapping, Optional as _Optional) | ||
| from logging import Formatter as __Formatter | ||
| from log21.colors import get_colors as _gc, ansi_escape | ||
| from log21.levels import INFO, DEBUG, ERROR, INPUT, PRINT, WARNING, CRITICAL | ||
| # yapf: enable | ||
| __all__ = ['ColorizingFormatter', 'DecolorizingFormatter'] | ||
| class _Formatter(__Formatter): | ||
| def __init__( | ||
| self, | ||
| fmt: _Optional[str] = None, | ||
| datefmt: _Optional[str] = None, | ||
| style: _Literal["%", "{", "$"] = '%', | ||
| level_names: _Optional[_Mapping[int, str]] = None | ||
| ) -> None: | ||
| """`level_names` usage: | ||
| >>> import log21 | ||
| >>> logger = log21.Logger('MyLogger', log21.DEBUG) | ||
| >>> stream_handler = log21.ColorizingStreamHandler() | ||
| >>> formatter = log21.ColorizingFormatter(fmt='[%(levelname)s] %(message)s', | ||
| ... level_names={log21.DEBUG: ' ', log21.INFO: '+', | ||
| ... log21.WARNING: '-', log21.ERROR: '!', | ||
| ... log21.CRITICAL: 'X'}) | ||
| >>> stream_handler.setFormatter(formatter) | ||
| >>> logger.addHandler(stream_handler) | ||
| >>> | ||
| >>> logger.debug('Just wanna see if this works...') | ||
| [ ] Just wanna see if this works... | ||
| >>> logger.info("FYI: I'm glad somebody read this 8)") | ||
| [+] FYI: I'm glad somebody read this 8) | ||
| >>> logger.warning("Oh no! Something's gonna happen!") | ||
| [-] Oh no! something's gonna happen! | ||
| >>> logger.error('AN ERROR OCCURRED! (told ya ;))') | ||
| [!] AN ERROR OCCURRED! (told ya ;)) | ||
| >>> logger.critical('Crashed....') | ||
| [X] Crashed.... | ||
| >>> | ||
| >>> # Hope you've enjoyed | ||
| >>> | ||
| :param fmt: The format string to use. | ||
| :param datefmt: The date format string to use. | ||
| :param style: The style to use. | ||
| :param level_names: A dictionary mapping logging levels to their names. | ||
| """ | ||
| super().__init__(fmt=fmt, datefmt=datefmt, style=style) | ||
| self._level_names: _Dict[int, str] = { | ||
| DEBUG: 'DEBUG', | ||
| INFO: 'INFO', | ||
| WARNING: 'WARNING', | ||
| ERROR: 'ERROR', | ||
| CRITICAL: 'CRITICAL', | ||
| PRINT: 'PRINT', | ||
| INPUT: 'INPUT' | ||
| } | ||
| if level_names: | ||
| for level, name in level_names.items(): | ||
| self.level_names[level] = name | ||
| @property | ||
| def level_names(self) -> _Dict[int, str]: | ||
| """Get the level names mapping.""" | ||
| return self._level_names | ||
| @level_names.setter | ||
| def level_names(self, level_names: _Mapping[int, str]) -> None: | ||
| if level_names: | ||
| if not isinstance(level_names, _Mapping): | ||
| raise TypeError( | ||
| '`level_names` must be a Mapping, a dictionary like object!' | ||
| ) | ||
| self._level_names = level_names | ||
| else: | ||
| self._level_names = {} | ||
| def format(self, record) -> str: # noqa: ANN001 | ||
| record.message = record.getMessage() | ||
| if self.usesTime(): | ||
| record.asctime = self.formatTime(record, self.datefmt) | ||
| record.levelname = self.level_names.get(record.levelno, 'NOTSET') | ||
| s = self.formatMessage(record) # pylint: disable=invalid-name | ||
| if record.exc_info and not record.exc_text: | ||
| record.exc_text = self.formatException(record.exc_info) | ||
| if record.exc_text: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + record.exc_text | ||
| if record.stack_info: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + self.formatStack(record.stack_info) | ||
| return s | ||
| class ColorizingFormatter(_Formatter): # pylint: disable=too-many-instance-attributes | ||
| """A formatter that helps adding colors to the log records.""" | ||
| time_color: _Tuple[str, ...] = ('lightblue', ) | ||
| name_color = pathname_color = filename_color = module_color = func_name_color = \ | ||
| thread_name_color = message_color = () | ||
| def __init__( | ||
| self, | ||
| fmt: _Optional[str] = None, | ||
| datefmt: _Optional[str] = None, | ||
| style: str = '%', | ||
| level_names: _Optional[_Mapping[int, str]] = None, | ||
| level_colors: _Optional[_Mapping[int, _Tuple[str]]] = None, | ||
| time_color: _Optional[_Tuple[str, ...]] = None, | ||
| name_color: _Optional[_Tuple[str, ...]] = None, | ||
| pathname_color: _Optional[_Tuple[str, ...]] = None, | ||
| filename_color: _Optional[_Tuple[str, ...]] = None, | ||
| module_color: _Optional[_Tuple[str, ...]] = None, | ||
| func_name_color: _Optional[_Tuple[str, ...]] = None, | ||
| thread_name_color: _Optional[_Tuple[str, ...]] = None, | ||
| message_color: _Optional[_Tuple[str, ...]] = None | ||
| ) -> None: # pylint: disable=too-many-branches | ||
| """Initialize the formatter. | ||
| :param fmt: The format string to use for the message. | ||
| :param datefmt: The format string to use for the date/time portion of the | ||
| message. | ||
| :param style: The format style to use. | ||
| :param level_names: A mapping of level numbers to level names. | ||
| :param level_colors: A mapping of level numbers to level colors. | ||
| :param time_color: The color to use for the time portion of the message. | ||
| :param name_color: The color to use for the logger name portion of the message. | ||
| :param pathname_color: The color to use for the pathname portion of the message. | ||
| :param filename_color: The color to use for the filename portion of the message. | ||
| :param module_color: The color to use for the module portion of the message. | ||
| :param func_name_color: The color to use for the function name portion of the | ||
| message. | ||
| :param thread_name_color: The color to use for the thread name portion of the | ||
| message. | ||
| :param message_color: The color to use for the message portion of the message. | ||
| """ | ||
| super().__init__(fmt=fmt, datefmt=datefmt, style=style, level_names=level_names) | ||
| self.level_colors: _Dict[int, _Tuple[str, ...]] = { | ||
| DEBUG: ('lightblue', ), | ||
| INFO: ('green', ), | ||
| WARNING: ('lightyellow', ), | ||
| ERROR: ('light red', ), | ||
| CRITICAL: ('background red', 'white'), | ||
| PRINT: ('Cyan', ), | ||
| INPUT: ('Magenta', ) | ||
| } | ||
| # Checks and sets colors | ||
| if level_colors: | ||
| if not isinstance(level_colors, _Mapping): | ||
| raise TypeError('`level_colors` must be a dictionary like object!') | ||
| for level, color in level_colors.items(): | ||
| self.level_colors[level] = (_gc(*color), ) | ||
| if time_color: | ||
| if not isinstance(time_color, tuple): | ||
| raise TypeError('`time_color` must be a tuple!') | ||
| self.time_color = time_color | ||
| if name_color: | ||
| if not isinstance(name_color, tuple): | ||
| raise TypeError('`name_color` must be a tuple!') | ||
| self.name_color = name_color | ||
| if pathname_color: | ||
| if not isinstance(pathname_color, tuple): | ||
| raise TypeError('`pathname_color` must be a tuple!') | ||
| self.pathname_color = pathname_color | ||
| if filename_color: | ||
| if not isinstance(filename_color, tuple): | ||
| raise TypeError('`filename_color` must be a tuple!') | ||
| self.filename_color = filename_color | ||
| if module_color: | ||
| if not isinstance(module_color, tuple): | ||
| raise TypeError('`module_color` must be a tuple!') | ||
| self.module_color = module_color | ||
| if func_name_color: | ||
| if not isinstance(func_name_color, tuple): | ||
| raise TypeError('`func_name_color` must be a tuple!') | ||
| self.func_name_color = func_name_color | ||
| if thread_name_color: | ||
| if not isinstance(thread_name_color, tuple): | ||
| raise TypeError('`thread_name_color` must be a tuple!') | ||
| self.thread_name_color = thread_name_color | ||
| if message_color: | ||
| if not isinstance(message_color, tuple): | ||
| raise TypeError('`message_color` must be a tuple!') | ||
| self.message_color = message_color | ||
| def format(self, record) -> str: # noqa: ANN001 | ||
| """Colorizes the record and returns the formatted message.""" | ||
| record.message = record.getMessage() | ||
| if self.usesTime(): | ||
| record.asctime = self.formatTime(record, self.datefmt) | ||
| record.levelname = self.level_names.get(record.levelno, 'NOTSET') | ||
| record = self.colorize(record) | ||
| s = self.formatMessage(record) # pylint: disable=invalid-name | ||
| # Cache the traceback text to avoid converting it multiple times | ||
| # (it's constant anyway) | ||
| if record.exc_info and not record.exc_text: | ||
| record.exc_text = self.formatException(record.exc_info) | ||
| if record.exc_text: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + record.exc_text | ||
| if record.stack_info: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + self.formatStack(record.stack_info) | ||
| return s | ||
| def colorize(self, record): # noqa: ANN001, ANN201 | ||
| """Colorizes the record attributes. | ||
| :param record: | ||
| :return: colorized record | ||
| """ | ||
| reset = '\033[0m' | ||
| if hasattr(record, 'asctime'): | ||
| record.asctime = _gc(*self.time_color) + record.asctime + reset | ||
| if hasattr(record, 'levelno'): | ||
| record.levelname = _gc( | ||
| *self.level_colors.get(int(record.levelno), ('lw', )) | ||
| ) + getattr(record, 'levelname', 'NOTSET') + reset | ||
| if hasattr(record, 'name'): | ||
| record.name = _gc(*self.name_color) + str(record.name) + reset | ||
| if hasattr(record, 'pathname'): | ||
| record.pathname = _gc(*self.pathname_color) + record.pathname + reset | ||
| if hasattr(record, 'filename'): | ||
| record.filename = _gc(*self.filename_color) + record.filename + reset | ||
| if hasattr(record, 'module'): | ||
| record.module = _gc(*self.module_color) + record.module + reset | ||
| if hasattr(record, 'funcName'): | ||
| record.funcName = _gc(*self.func_name_color) + record.funcName + reset | ||
| if hasattr(record, 'threadName'): | ||
| record.threadName = _gc(*self.thread_name_color) + record.threadName + reset | ||
| if hasattr(record, 'message'): | ||
| record.message = _gc(*self.message_color) + record.message | ||
| return record | ||
| class DecolorizingFormatter(_Formatter): | ||
| """Formatter that removes color codes from the log records.""" | ||
| def formatTime(self, record, datefmt=None) -> str: # noqa: ANN001 | ||
| """Returns the creation time of the specified LogRecord as formatted text.""" | ||
| ct = self.converter(int(record.created)) | ||
| if datefmt: | ||
| s = _time.strftime(datefmt, ct) | ||
| else: | ||
| t = _time.strftime(self.default_time_format, ct) | ||
| s = self.default_msec_format % (t, record.msecs) | ||
| return s | ||
| def format(self, record) -> str: # noqa: ANN001 | ||
| """Decolorizes the record and returns the formatted message. | ||
| :param record: | ||
| :return: str | ||
| """ | ||
| return self.decolorize(super().format(record)) | ||
| @staticmethod | ||
| def decolorize(text: str) -> str: | ||
| """Removes all ansi colors in the text. | ||
| :param text: str: Input text | ||
| :return: str: decolorized text | ||
| """ | ||
| return ansi_escape.sub('', text) |
| # log21.levels.py | ||
| # CodeWriter21 | ||
| import logging as _logging | ||
| __all__ = [ | ||
| 'CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET', 'INPUT', | ||
| 'PRINT' | ||
| ] | ||
| INPUT = 70 | ||
| PRINT = 60 | ||
| CRITICAL = _logging.CRITICAL | ||
| FATAL = CRITICAL | ||
| ERROR = _logging.ERROR | ||
| WARNING = _logging.WARNING | ||
| WARN = WARNING | ||
| INFO = _logging.INFO | ||
| DEBUG = _logging.DEBUG | ||
| NOTSET = _logging.NOTSET |
| # log21.logger.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| import re as _re | ||
| import sys as _sys | ||
| import logging as _logging | ||
| from types import MethodType as _MethodType | ||
| from typing import (TYPE_CHECKING as _TYPE_CHECKING, Any as _Any, List, Union as _Union, | ||
| Literal as _Literal, Mapping, Callable as _Callable, | ||
| Optional as _Optional, Sequence as _Sequence) | ||
| from getpass import getpass as _getpass | ||
| from logging import raiseExceptions as _raiseExceptions | ||
| from log21.levels import INFO, DEBUG, ERROR, INPUT, PRINT, NOTSET, WARNING, CRITICAL | ||
| if _TYPE_CHECKING: | ||
| import log21 as _log21 | ||
| # yapf: enable | ||
| __all__ = ['Logger'] | ||
| class Logger(_logging.Logger): | ||
| """A Logger that can print to the console and log to a file.""" | ||
| def __init__( | ||
| self, | ||
| name: str, | ||
| level: _Union[int, str] = NOTSET, | ||
| handlers: _Optional[_Union[_Sequence[_logging.Handler], | ||
| _logging.Handler]] = None | ||
| ) -> None: | ||
| """Initialize a Logger object. | ||
| :param name: The name of the logger. | ||
| :param level: The level of the logger. | ||
| :param handlers: The handlers to add to the logger. | ||
| """ | ||
| super().__init__(name, level) | ||
| self.setLevel(level) | ||
| self._progress_bar = None | ||
| if handlers: | ||
| if not isinstance(handlers, _Sequence): | ||
| if isinstance(handlers, _logging.Handler): | ||
| handlers = [handlers] | ||
| else: | ||
| raise TypeError( | ||
| 'handlers must be a list of logging.Handler objects' | ||
| ) | ||
| for handler in handlers: | ||
| if not isinstance(handler, _logging.Handler): | ||
| raise TypeError( | ||
| 'handlers must be a list of logging.Handler objects' | ||
| ) | ||
| self.addHandler(handler) | ||
| def isEnabledFor(self, level: int) -> bool: | ||
| """Is this logger enabled for level 'level'?""" | ||
| return (self.level <= level) or (level in (PRINT, INPUT)) | ||
| def log( | ||
| self, level: int, *msg, args: tuple = (), end: str = '\n', **kwargs | ||
| ) -> None: | ||
| """Log 'msg % args' with the integer severity 'level'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.log(level, "We have a %s", args=("mysterious problem",), exc_info=1) | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| if not isinstance(level, int): | ||
| if _raiseExceptions: | ||
| raise TypeError('level must be an integer') | ||
| return | ||
| if self.isEnabledFor(level): | ||
| self._log(level, msg, args, **kwargs) | ||
| def debug(self, *msg, args: tuple = (), end: str = '\n', **kwargs) -> None: | ||
| """Log 'msg % args' with severity 'DEBUG'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.debug("Houston, we have a %s", args=("thorny problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(DEBUG): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(DEBUG, msg, args, **kwargs) | ||
| def info(self, *msg, args: tuple = (), end: str = '\n', **kwargs) -> None: | ||
| """Log 'msg % args' with severity 'INFO'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.info("Houston, we have an %s", args=("interesting problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(INFO): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(INFO, msg, args, **kwargs) | ||
| def warning(self, *msg, args: tuple = (), end: str = '\n', **kwargs) -> None: | ||
| """Log 'msg % args' with severity 'WARNING'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.warning("Houston, we have a %s", args=("bit of a problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(WARNING): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(WARNING, msg, args, **kwargs) | ||
| warn = warning | ||
| def write(self, *msg, args: tuple = (), end: str = '', **kwargs) -> None: | ||
| """Log 'msg % args' with severity 'WARNING'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.write("Houston, we have a %s", args=("bit of a problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(WARNING): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(WARNING, msg, args, **kwargs) | ||
| def error(self, *msg, args: tuple = (), end: str = '\n', **kwargs) -> None: | ||
| """Log 'msg % args' with severity 'ERROR'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.error("Houston, we have a %s", args=("major problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(ERROR): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(ERROR, msg, args, **kwargs) | ||
| def exception( # ty: ignore[invalid-method-override] | ||
| self, *msg, args: tuple = (), exc_info: bool = True, **kwargs | ||
| ) -> None: | ||
| """Convenience method for logging an ERROR with exception information.""" | ||
| self.error(*msg, args=args, exc_info=exc_info, **kwargs) | ||
| def critical(self, *msg, args: tuple = (), end: str = '\n', **kwargs) -> None: | ||
| """Log 'msg % args' with severity 'CRITICAL'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.critical("Houston, we have a %s", args=("major disaster",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(CRITICAL): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(CRITICAL, msg, args, **kwargs) | ||
| fatal = critical | ||
| def print(self, *msg, args: tuple = (), end: str = '\n', **kwargs) -> None: | ||
| """Log 'msg % args'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.print("Houston, we have a %s", args=("major disaster",), exc_info=1) | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(PRINT, msg, args, **kwargs) | ||
| def input(self, *msg, args: tuple = (), end: str = '', **kwargs) -> str: | ||
| """Log 'msg % args'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value. | ||
| Usage example: | ||
| age = logger.input("Enter your age: ") | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(INPUT, msg, args, **kwargs) | ||
| return input() | ||
| def getpass(self, *msg, args: tuple = (), end: str = '', **kwargs) -> str: | ||
| """Takes a password input from the user. | ||
| :param msg: The message to display to the user. | ||
| :param args: The arguments to pass to the message. | ||
| :param end: The ending character to append to the message. | ||
| :return: The password. | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(self.level if self.level >= NOTSET else NOTSET, msg, args, **kwargs) | ||
| return _getpass('') | ||
| def print_progress(self, progress: float, total: float, **kwargs) -> None: | ||
| """Log progress.""" | ||
| self.progress_bar(progress, total, **kwargs) | ||
| @property | ||
| def progress_bar(self) -> '_log21.ProgressBar': | ||
| """Return a progress bar instance. | ||
| If not exists, create a new one. | ||
| """ | ||
| if not self._progress_bar: | ||
| # avoid circular import; pylint: disable=import-outside-toplevel | ||
| from log21.progress_bar import ProgressBar # noqa: PLC0415 | ||
| self._progress_bar = ProgressBar(logger=self) | ||
| return self._progress_bar | ||
| @progress_bar.setter | ||
| def progress_bar(self, value: '_log21.ProgressBar') -> None: | ||
| self._progress_bar = value | ||
| def clear_line(self, length: _Optional[int] = None) -> None: | ||
| """Clear the current line. | ||
| :param length: The length of the line to clear. | ||
| :return: | ||
| """ | ||
| for handler in self.handlers: | ||
| if isinstance(getattr(handler, 'clear_line', None), _Callable): | ||
| handler.clear_line(length) # type: ignore | ||
| def add_level( | ||
| self, | ||
| level: int, | ||
| name: str, | ||
| errors: _Literal['raise', 'ignore', 'handle', 'force'] = 'raise' | ||
| ) -> str: | ||
| """Adds a new method to the logger with a specific level and name. | ||
| :param level: The level of the new method. | ||
| :param name: The name of the new method. | ||
| :param errors: The action to take if the level already exists. | ||
| + ``raise`` (default): Raise an exception if anything goes wrong. | ||
| + ``ignore``: Do nothing. | ||
| + ``handle``: Handle the situation if a method with the same ``name`` | ||
| already exists. Adds a number to the name to avoid the conflict. | ||
| + ``force``: Add the new level with the specified level even if a | ||
| method with the same ``name`` already exists. | ||
| :raises TypeError: If ``level`` is not an integer. | ||
| :raises TypeError: If ``name`` is not a string. | ||
| :raises ValueError: If ``errors`` is not one of "raise", "ignore", "handle", | ||
| or "force". | ||
| :raises ValueError: If ``name`` starts with a number. | ||
| :raises ValueError: If ``name`` is not a valid identifier. | ||
| :raises AttributeError: If ``errors`` is "raise" and a method with the | ||
| same ``name`` already exists. | ||
| :return: The name of the new method. | ||
| """ | ||
| def raise_(error: BaseException) -> _Optional[BaseException]: | ||
| if errors == 'ignore': | ||
| return | ||
| raise error | ||
| if not isinstance(level, int): | ||
| raise_(TypeError('level must be an integer')) | ||
| if not isinstance(name, str): | ||
| raise_(TypeError('name must be a string')) | ||
| if errors not in ('raise', 'ignore', 'handle', 'force'): | ||
| raise_( | ||
| ValueError( | ||
| 'errors must be one of "raise", "ignore", "handle", "force"' | ||
| ) | ||
| ) | ||
| name = _re.sub(r'\s', '_', name) | ||
| if _re.match(r'[0-9].*', name): | ||
| raise_(ValueError(f'level name cannot start with a number: "{name}"')) | ||
| if not _re.fullmatch(r'[a-zA-Z_][a-zA-Z0-9_]*', name): | ||
| raise_(ValueError(f'level name must be a valid identifier: "{name}"')) | ||
| if hasattr(self, name): | ||
| if errors == 'raise': | ||
| raise AttributeError(f'level "{name}" already exists') | ||
| if errors == 'ignore': | ||
| return name | ||
| if errors == 'handle': | ||
| return self.add_level(level, _add_one(name), errors) | ||
| def log_for_level( | ||
| self: Logger, *msg, args: tuple = (), end: str = '\n', **kwargs | ||
| ) -> None: | ||
| self.log(level, *msg, args=args, end=end, **kwargs) | ||
| setattr(self, name, _MethodType(log_for_level, self)) | ||
| return name | ||
| def add_levels( | ||
| self, | ||
| level_names: Mapping[int, str], | ||
| errors: _Literal['raise', 'ignore', 'handle', 'force'] = 'raise' | ||
| ) -> None: | ||
| """Adds new methods to the logger with specific levels and names. | ||
| :param level_names: A mapping of levels to names. | ||
| :param errors: The action to take if the level already exists | ||
| :return: | ||
| """ | ||
| for level, name in level_names.items(): | ||
| self.add_level(level, name, errors) | ||
| def __lshift__(self, obj: _Any) -> "Logger": | ||
| """Prints the object to the output stream. | ||
| This operator is meant to make the Logger object be usable in a | ||
| std::cout-like way. | ||
| :param obj: The object to print. | ||
| :return: The Logger object. | ||
| """ | ||
| logger = self | ||
| found = 0 | ||
| while logger: | ||
| for handler in logger.handlers: | ||
| if (isinstance(handler, _logging.StreamHandler) | ||
| and hasattr(handler.stream, 'write') | ||
| and hasattr(handler.stream, 'flush')): | ||
| found = found + 1 | ||
| handler.stream.write(str(obj)) # ty: ignore[call-non-callable] | ||
| handler.stream.flush() # ty: ignore[call-non-callable] | ||
| if not logger.propagate: | ||
| break | ||
| logger = logger.parent | ||
| if found == 0: | ||
| _sys.stderr.write( | ||
| f"No handlers could be found for logger \"{self.name}\"\n" | ||
| ) | ||
| return self | ||
| def __rshift__(self, obj: List[type]) -> 'Logger': | ||
| """A way of receiving input from the stdin. | ||
| This operator is meant to make a std::cin-like operation possible in Python. | ||
| Usage examples: | ||
| >>> import log21 | ||
| >>> cout = cin = log21.get_logger() | ||
| >>> | ||
| >>> # Example 1 | ||
| >>> # Get three inputs of type: str, str or None, and float | ||
| >>> data = [str, None, float] # first name, last name and age | ||
| >>> cout << "Please enter a first name, last name and age(separated by space): " | ||
| Please enter a first name, last name and age(separated by space): | ||
| >>> cin >> data; | ||
| M 21 | ||
| >>> name = data[0] + (data[1] if data[1] is not None else '') | ||
| >>> age = data[2] | ||
| >>> cout << name << " is " << age << " years old." << log21.endl; | ||
| M is 21.0 years old. | ||
| >>> | ||
| >>> # Example 2 | ||
| >>> # Get any number of inputs | ||
| >>> data = [] | ||
| >>> cout << "Enter something: "; | ||
| Enter something: | ||
| >>> cin >> data; | ||
| What ever man 1 2 3 ! | ||
| >>> cout << "Here are the items you chose: " << data << log21.endl; | ||
| Here are the items you chose: ['What', 'ever', 'man', '1', '2', '3', '!'] | ||
| >>> | ||
| >>> # Example 3 | ||
| >>> # Get two inputs of type int with defaults: 1280 and 720 | ||
| >>> data = [1280, 720] | ||
| >>> cout << "Enter the width and the height: "; | ||
| Enter the width and the height: | ||
| >>> cin >> data; | ||
| 500 | ||
| >>> cout << "Width: " << data[0] << " Height: " << data[1] << log21.endl; | ||
| Width: 500 Height: 720 | ||
| >>> | ||
| :param obj: The object to redirect the output to. | ||
| :return: The Logger object. | ||
| """ | ||
| n = len(obj) - 1 | ||
| if n >= 0: | ||
| data = [] | ||
| while n >= 0: | ||
| tmp = _sys.stdin.readline()[:-1].split(' ', maxsplit=n) | ||
| if tmp: | ||
| data.extend(tmp) | ||
| else: | ||
| data.append('') | ||
| n -= len(tmp) | ||
| tmp = [] | ||
| for i, item in enumerate(data): | ||
| if obj[i] is None: | ||
| tmp.append(item or None) | ||
| elif isinstance(obj[i], type): | ||
| try: | ||
| tmp.append(obj[i](item)) | ||
| except ValueError: | ||
| tmp.append(obj[i]()) | ||
| else: | ||
| try: | ||
| tmp.append(obj[i].__class__(item)) | ||
| except ValueError: | ||
| tmp.append(obj[i]) | ||
| obj[:] = tmp | ||
| else: | ||
| obj[:] = _sys.stdin.readline()[:-1].split( | ||
| ) # ty: ignore[invalid-assignment] | ||
| return self | ||
| def _add_one(name: str) -> str: | ||
| """Add one to the end of a string. | ||
| :param name: The string to add one to. | ||
| :return: The string with one added to the end. | ||
| """ | ||
| match = _re.match(r'([\S]+)_([0-9]+)', name) | ||
| if not match: | ||
| return name + '_1' | ||
| return f'{match.group(1)}_{int(match.group(2)) + 1}' |
| # log21.logging_window.py | ||
| # CodeWriter2 | ||
| # yapf: disable | ||
| from __future__ import annotations | ||
| import re as _re | ||
| import threading as _threading | ||
| import subprocess as _subprocess | ||
| from enum import Enum as _Enum | ||
| from time import sleep as _sleep | ||
| from uuid import uuid4 as _uuid4 | ||
| from string import printable as _printable | ||
| from typing import (TYPE_CHECKING as _TYPE_CHECKING, Union as _Union, | ||
| Optional as _Optional) | ||
| from logging import FileHandler as _FileHandler | ||
| from argparse import Namespace as _Namespace | ||
| from log21.colors import hex_escape as _hex_escape, ansi_escape as _ansi_escape | ||
| from log21.levels import NOTSET as _NOTSET | ||
| from log21.logger import Logger as _Logger | ||
| from log21.stream_handler import StreamHandler as _StreamHandler | ||
| if _TYPE_CHECKING: | ||
| import log21 as _log21 | ||
| # yapf: enable | ||
| __all__ = ['LoggingWindow', 'LoggingWindowHandler'] | ||
| try: | ||
| import tkinter as _tkinter | ||
| except ImportError: | ||
| _tkinter = None | ||
| ansi_to_hex_color_map = { | ||
| # https://chrisyeh96.github.io/2020/03/28/terminal-colors.html | ||
| '30': ('#000000', 'foreground'), # Black foreground | ||
| '31': ('#cc0000', 'foreground'), # Red foreground | ||
| '32': ('#4e9a06', 'foreground'), # Green foreground | ||
| '33': ('#c4a000', 'foreground'), # Yellow foreground | ||
| '34': ('#729fcf', 'foreground'), # Blue foreground | ||
| '35': ('#75507b', 'foreground'), # Magenta foreground | ||
| '36': ('#06989a', 'foreground'), # Cyan foreground | ||
| '37': ('#d3d7cf', 'foreground'), # White foreground | ||
| '90': ('#555753', 'foreground'), # Bright black foreground | ||
| '91': ('#ef2929', 'foreground'), # Bright red foreground | ||
| '92': ('#8ae234', 'foreground'), # Bright green foreground | ||
| '93': ('#fce94f', 'foreground'), # Bright yellow foreground | ||
| '94': ('#32afff', 'foreground'), # Bright blue foreground | ||
| '95': ('#ad7fa8', 'foreground'), # Bright magenta foreground | ||
| '96': ('#34e2e2', 'foreground'), # Bright cyan foreground | ||
| '97': ('#ffffff', 'foreground'), # Bright white foreground | ||
| '40': ('#000000', 'background'), # Black background | ||
| '41': ('#cc0000', 'background'), # Red background | ||
| '42': ('#4e9a06', 'background'), # Green background | ||
| '43': ('#c4a000', 'background'), # Yellow background | ||
| '44': ('#729fcf', 'background'), # Blue background | ||
| '45': ('#75507b', 'background'), # Magenta background | ||
| '46': ('#06989a', 'background'), # Cyan background | ||
| '47': ('#d3d7cf', 'background'), # White background | ||
| '100': ('#555753', 'background'), # Bright black background | ||
| '101': ('#ef2929', 'background'), # Bright red background | ||
| '102': ('#8ae234', 'background'), # Bright green background | ||
| '103': ('#fce94f', 'background'), # Bright yellow background | ||
| '104': ('#32afff', 'background'), # Bright blue background | ||
| '105': ('#ad7fa8', 'background'), # Bright magenta background | ||
| '106': ('#34e2e2', 'background'), # Bright cyan background | ||
| '107': ('#ffffff', 'background'), # Bright white background | ||
| } | ||
| _lock = _threading.RLock() | ||
| class GettingInputStatus(_Enum): | ||
| """An enum for the status of getting input.""" | ||
| NOT_GETTING_INPUT = 0 | ||
| GETTING_INPUT = 1 | ||
| CANCELLED = 2 | ||
| class CancelledInputError(InterruptedError, Exception): | ||
| """An exception raised when the input is cancelled.""" | ||
| class LoggingWindowHandler(_StreamHandler): | ||
| """A handler for logging to a LoggingWindow.""" | ||
| def __init__( | ||
| self, | ||
| logging_window: LoggingWindow, | ||
| handle_carriage_return: bool = True, | ||
| handle_new_line: bool = True | ||
| ) -> None: | ||
| """Initialize the LoggingWindowHandler. | ||
| :param logging_window: The LoggingWindow to log to. | ||
| :param handle_carriage_return: Whether to handle carriage returns. | ||
| :param handle_new_line: Whether to handle new lines. | ||
| """ | ||
| self.HandleCR = handle_carriage_return | ||
| self.HandleNL = handle_new_line | ||
| self.__carriage_return: bool = False | ||
| self.LoggingWindow = logging_window # pylint: disable=invalid-name | ||
| super().__init__(stream=None) | ||
| def emit(self, record) -> None: # noqa: ANN001 | ||
| try: | ||
| if self.HandleCR: | ||
| self.check_cr(record) | ||
| if self.HandleNL: | ||
| self.check_nl(record) | ||
| msg = self.format(record) | ||
| self.write(msg) | ||
| self.write(self.terminator) | ||
| except Exception: # pylint: disable=broad-except | ||
| self.handleError(record) | ||
| def write(self, message: str) -> None: # pylint: disable=too-many-branches | ||
| """Write a message to the LoggingWindow. | ||
| :param message: The message to write. | ||
| """ | ||
| if self.LoggingWindow is not None: # pylint: disable=too-many-nested-blocks | ||
| # Sets the element's state to normal so that it can be modified. | ||
| self.LoggingWindow.logs.config(state=_tkinter.NORMAL) | ||
| # Handles carriage return | ||
| parts = _re.split(r'(\r)', message) | ||
| while parts: | ||
| part = parts.pop(0) | ||
| if self.__carriage_return and any( | ||
| (char in _printable[:-6]) | ||
| for char in _hex_escape.sub('', _ansi_escape.sub('', part))): | ||
| # Removes the last line | ||
| self.LoggingWindow.logs.delete('end - 1 lines', _tkinter.END) | ||
| if self.LoggingWindow.logs.count('0.0', 'end')[0] != 1: | ||
| self.LoggingWindow.logs.insert('end', '\n') | ||
| self.__carriage_return = False | ||
| tags = [] | ||
| # Handles ANSI color codes | ||
| ansi_parts = _ansi_escape.split(part) | ||
| while ansi_parts: | ||
| ansi_text = ansi_parts.pop(0) | ||
| if ansi_text: | ||
| # Handles HEX color codes | ||
| hex_parts = _hex_escape.split(ansi_text) | ||
| while hex_parts: | ||
| hex_text = hex_parts.pop(0) | ||
| if hex_text: | ||
| self.LoggingWindow.logs.insert(_tkinter.END, hex_text) | ||
| if hex_parts: | ||
| hex_color = hex_parts.pop(0) | ||
| tag = str(_uuid4()) | ||
| # Foreground color | ||
| if hex_parts.pop(0) == 'f': | ||
| tags.append( | ||
| { | ||
| 'name': tag, | ||
| 'start': | ||
| self.LoggingWindow.logs.index('end-1c'), | ||
| 'config': { | ||
| 'foreground': hex_color | ||
| } | ||
| } | ||
| ) | ||
| # Background color | ||
| else: | ||
| tags.append( | ||
| { | ||
| 'name': tag, | ||
| 'start': | ||
| self.LoggingWindow.logs.index('end-1c'), | ||
| 'config': { | ||
| 'background': hex_color | ||
| } | ||
| } | ||
| ) | ||
| if ansi_parts: | ||
| ansi_params = ansi_parts.pop(0).split(';') | ||
| ansi_color = {'foreground': None, 'background': None} | ||
| for part in ansi_params: | ||
| if part in ansi_to_hex_color_map: | ||
| color_ = ansi_to_hex_color_map[part] | ||
| ansi_color[color_[1]] = color_[0] | ||
| elif part == '0': | ||
| ansi_color[ | ||
| 'foreground' | ||
| ] = self.LoggingWindow.default_foreground_color | ||
| ansi_color[ | ||
| 'background' | ||
| ] = self.LoggingWindow.default_background_color | ||
| else: | ||
| pass # error condition ignored | ||
| if ansi_color['foreground'] or ansi_color['background']: | ||
| tags.append( | ||
| { | ||
| 'name': str(_uuid4()), | ||
| 'start': self.LoggingWindow.logs.index('end-1c'), | ||
| 'config': ansi_color | ||
| } | ||
| ) | ||
| # Applies the color tags | ||
| for tag in tags: | ||
| self.LoggingWindow.logs.tag_add(tag['name'], tag['start'], 'end') | ||
| self.LoggingWindow.logs.tag_config(tag['name'], **tag['config']) | ||
| if parts: | ||
| parts.pop(0) | ||
| self.__carriage_return = True | ||
| self.LoggingWindow.logs.config(state=_tkinter.DISABLED) | ||
| self.LoggingWindow.logs.see(_tkinter.END) | ||
| class LoggingWindow(_Logger): # pylint: disable=too-many-instance-attributes | ||
| """ | ||
| Usage Example: | ||
| >>> # Manual creation | ||
| >>> # Imports the LoggingWindow and LoggingWindowHandler classes | ||
| >>> from log21 import LoggingWindow, LoggingWindowHandler | ||
| >>> # Creates a new LoggingWindow object | ||
| >>> window = LoggingWindow('Test Window', level='DEBUG') | ||
| >>> # Creates a new LoggingWindowHandler object and adds it to the LoggingWindow | ||
| >>> # we created earlier | ||
| >>> window.addHandler(LoggingWindowHandler(window)) | ||
| >>> window.debug('A debug message') | ||
| >>> window.info('An info message') | ||
| >>> # Run these lines to see the messages in the window | ||
| >>> | ||
| >>> # Automatic creation | ||
| >>> # Imports log21 and time modules | ||
| >>> import log21, time | ||
| >>> # Creates a new LoggingWindow object | ||
| >>> window = log21.get_logging_window('Test Window') | ||
| >>> # Use it without any additional steps to add handlers and formatters | ||
| >>> window.info('This works properly!') | ||
| >>> # ANSI colors usage: | ||
| >>> window.info('This is a \033[91mred\033[0m message.') | ||
| >>> window.info('\033[102mThis is a message with green background.') | ||
| >>> # HEX colors usage: | ||
| >>> window.info('\033#00FFFFhfThis is a message with cyan foreground.') | ||
| >>> window.info('\033#0000FFhbThis is a message with blue background.') | ||
| >>> # Progressbar usage: | ||
| >>> for i in range(100): | ||
| ... window.print_progress(i + 1, 100) | ||
| ... time.sleep(0.1) | ||
| ... | ||
| >>> # Gettig input from the user: | ||
| >>> name: str = window.input('Enter your name: ') | ||
| >>> window.print('Hello, ' + name + '!') | ||
| >>> # Run these lines to see the messages in the window | ||
| >>> | ||
| """ | ||
| def __init__( # noqa: PLR0915 | ||
| self, | ||
| name: str, | ||
| level: _Union[int, str] = _NOTSET, | ||
| width: int = 80, | ||
| height: int = 20, | ||
| default_foreground_color: str = 'white', | ||
| default_background_color: str = 'black', | ||
| font: tuple = ('Courier', 10), | ||
| allow_python: bool = False, | ||
| allow_shell: bool = False, | ||
| command_history_buffer_size: int = 100 | ||
| ) -> None: # pylint: disable=too-many-statements | ||
| """Creates a new LoggingWindow object. | ||
| :param name: The name of the logger. | ||
| :param level: The level of the logger. | ||
| :param width: The width of the LoggingWindow. | ||
| :param height: The height of the LoggingWindow. | ||
| :param default_foreground_color: The default foreground color of the | ||
| LoggingWindow. | ||
| :param default_background_color: The default background color of the | ||
| LoggingWindow. | ||
| :param font: The font of the LoggingWindow. | ||
| """ | ||
| super().__init__(name, level) | ||
| self.window = _tkinter.Tk() | ||
| self.window.title(name) | ||
| # Hides window instead of closing it | ||
| self.window.protocol("WM_DELETE_WINDOW", self.hide) | ||
| self.window.resizable(False, False) | ||
| self.logs = _tkinter.Text(self.window) | ||
| self.logs.grid(row=0, column=0, sticky='nsew') | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.logs.config(wrap=_tkinter.NONE) | ||
| # Commands entry | ||
| self.command_entry = _tkinter.Entry(self.window) | ||
| self.command_entry.grid(row=1, column=0, sticky='nsew') | ||
| self.command_entry.bind('<Return>', self.execute_command) | ||
| self.command_entry.bind('<Up>', self.history_up) | ||
| self.command_entry.bind('<Down>', self.history_down) | ||
| self.command_history = [] | ||
| self.command_history_index = 0 | ||
| if not isinstance(command_history_buffer_size, (int, float)): | ||
| raise TypeError('command_history_buffer_size must be a number') | ||
| self.command_history_buffer_size = ( | ||
| command_history_buffer_size if command_history_buffer_size > 0 else 0 | ||
| ) | ||
| # Hides the command entry if allow_python and allow_shell are False | ||
| if not allow_python and not allow_shell: | ||
| self.command_entry.grid_remove() | ||
| if allow_python: | ||
| raise NotImplementedError('Python commands are not supported yet!') | ||
| self.__allow_python = False | ||
| self.__allow_shell = allow_shell | ||
| # Scroll bars | ||
| self.logs.config( | ||
| xscrollcommand=_tkinter.Scrollbar(self.window, orient=_tkinter.HORIZONTAL | ||
| ).set | ||
| ) | ||
| self.logs.config(yscrollcommand=_tkinter.Scrollbar(self.window).set) | ||
| # Input related lines | ||
| self.getting_input_status: GettingInputStatus = ( | ||
| GettingInputStatus.NOT_GETTING_INPUT | ||
| ) | ||
| self.getting_pass = False | ||
| self.input_text = '' | ||
| # cursor counter is used for making a nice blinking cursor | ||
| self.__cursor_counter = 1 | ||
| self._cursor_position = None | ||
| self.cursor_position = 0 | ||
| # KeyPress event for self.logs | ||
| self.logs.bind('<KeyPress>', self.key_press) | ||
| self.font = font | ||
| self.width = width | ||
| self.height = height | ||
| self.default_foreground_color = default_foreground_color | ||
| self.default_background_color = default_background_color | ||
| # Events for multi-threading support | ||
| self.window.bind('<<hide>>', self.__hide) | ||
| self.window.bind('<<show>>', self.__show) | ||
| self.window.bind('<<clear>>', self.__clear) | ||
| self.window.bind('<<log>>', self.__log) | ||
| self.window.bind('<<input>>', self.__input) | ||
| self.window.bind('<<type input>>', self.__type_input) | ||
| self.window.bind('<<getpass>>', self.__getpass) | ||
| self.window.bind('<<SetAllowPython>>', self.__set_allow_python) | ||
| self.window.bind('<<SetAllowShell>>', self.__set_allow_shell) | ||
| self.window.bind('<<SetCursorPosition>>', self.__set_cursor_position) | ||
| self.window.bind( | ||
| '<<SetDefaultForegroundColor>>', self.__set_default_foreground_color | ||
| ) | ||
| self.window.bind( | ||
| '<<SetDefaultBackgroundColor>>', self.__set_default_background_color | ||
| ) | ||
| self.window.bind('<<SetFont>>', self.__set_font) | ||
| self.window.bind('<<SetWidth>>', self.__set_width) | ||
| self.window.bind('<<SetHeight>>', self.__set_height) | ||
| def addHandler(self, hdlr: _Union[_FileHandler, LoggingWindowHandler]) -> None: | ||
| if not isinstance(hdlr, (LoggingWindowHandler, _FileHandler)): | ||
| raise TypeError("Handler must be a FileHandler or LoggingWindowHandler") | ||
| super().addHandler(hdlr) | ||
| def __hide(self, _) -> None: # noqa: ANN001 | ||
| self.window.withdraw() | ||
| def __show(self, _) -> None: # noqa: ANN001 | ||
| self.window.deiconify() | ||
| def __clear(self, _) -> None: # noqa: ANN001 | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete('1.0', _tkinter.END) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| def __log(self, event) -> None: # noqa: ANN001 | ||
| data = event.data | ||
| if self.getting_input_status == GettingInputStatus.GETTING_INPUT: | ||
| raise RuntimeError( | ||
| 'Cannot log while getting input from the user! ' | ||
| 'Please cancel the input first.' | ||
| ) | ||
| super()._log( | ||
| data.level, data.msg, data.args, data.exc_info, data.extra, data.stack_info, | ||
| data.stacklevel | ||
| ) | ||
| def __input(self, event) -> None: # noqa: ANN001 | ||
| data = event.data | ||
| msg = ' '.join([str(m) for m in data.msg]) + data.end | ||
| self._log( | ||
| self.level if self.level >= _NOTSET else _NOTSET, msg, data.args, | ||
| **data.kwargs | ||
| ) | ||
| self.input_text = '' | ||
| self.getting_input_status = GettingInputStatus.GETTING_INPUT | ||
| self.cursor_position = 0 | ||
| self.logs.focus() | ||
| try: | ||
| while self.getting_input_status == GettingInputStatus.GETTING_INPUT: | ||
| self.cursor_position = self.cursor_position | ||
| self.window.update() | ||
| self.window.after(10) | ||
| except KeyboardInterrupt: | ||
| self.input_text = '' | ||
| self.getting_input_status = GettingInputStatus.NOT_GETTING_INPUT | ||
| if self.getting_input_status == GettingInputStatus.NOT_GETTING_INPUT: | ||
| data.output = self.input_text | ||
| elif self.getting_input_status == GettingInputStatus.CANCELLED: | ||
| if data.raise_error: | ||
| raise CancelledInputError('Input cancelled!') | ||
| data.output = '' | ||
| def __type_input(self, event) -> None: # noqa: ANN001 | ||
| data = event.data | ||
| if (self.getting_input_status | ||
| != GettingInputStatus.GETTING_INPUT) and data.wait <= 0: | ||
| raise RuntimeError( | ||
| 'The logger must be getting input for this method to work! ' | ||
| 'Use `input` method or set the `wait` argument to ' | ||
| 'a value greater than 0.' | ||
| ) | ||
| while self.getting_input_status != GettingInputStatus.GETTING_INPUT: | ||
| _sleep(data.wait) | ||
| self.input_text += data.text | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete(f'end-{len(self.input_text) + 1}c', 'end-1c') | ||
| self.logs.insert(_tkinter.END, self.input_text) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.cursor_position = len(self.input_text) - 1 | ||
| def __getpass(self, event) -> None: # noqa: ANN001 | ||
| data = event.data | ||
| msg = ' '.join([str(m) for m in data.msg]) + data.end | ||
| self._log( | ||
| self.level if self.level >= _NOTSET else _NOTSET, msg, data.args, | ||
| **data.kwargs | ||
| ) | ||
| self.input_text = '' | ||
| self.getting_pass = True | ||
| self.cursor_position = 0 | ||
| self.logs.focus() | ||
| try: | ||
| while self.getting_pass: | ||
| self.cursor_position = self.cursor_position | ||
| self.window.update() | ||
| self.window.after(10) | ||
| except KeyboardInterrupt: | ||
| self.input_text = '' | ||
| self.getting_pass = False | ||
| data.output = self.input_text | ||
| def hide(self) -> None: | ||
| """Hides the LoggingWindow. | ||
| :return: | ||
| """ | ||
| self.window.event_generate('<<hide>>') | ||
| def show(self) -> None: | ||
| """Shows the LoggingWindow. | ||
| :return: | ||
| """ | ||
| self.window.event_generate('<<show>>') | ||
| def clear(self) -> None: | ||
| """Clears the LoggingWindow. | ||
| :return: | ||
| """ | ||
| self.window.event_generate('<<clear>>') | ||
| def _log( | ||
| self, | ||
| level: int, | ||
| msg: str, | ||
| args: tuple, | ||
| exc_info: _Optional[bool] = None, | ||
| extra=None, # noqa: ANN001 | ||
| stack_info: bool = False, | ||
| stacklevel: int = 1 | ||
| ) -> None: | ||
| _lock.acquire() | ||
| self.window.event_generate( | ||
| '<<log>>', | ||
| when='tail', | ||
| data=_Namespace( | ||
| level=level, | ||
| msg=msg, | ||
| args=args, | ||
| exc_info=exc_info, | ||
| extra=extra, | ||
| stack_info=stack_info, | ||
| stacklevel=stacklevel | ||
| ) | ||
| ) | ||
| _lock.release() | ||
| def input( | ||
| self, | ||
| *msg, | ||
| args: tuple = (), | ||
| end: str = '', | ||
| raise_error: str = False, | ||
| **kwargs | ||
| ) -> str: | ||
| """Prints a message and waits for input. | ||
| :param msg: The message to print. | ||
| :param args: The arguments to pass to the message. | ||
| :param end: The end of the message. | ||
| :param raise_error: If True, raises an error instead of returning an empty | ||
| string. | ||
| :param kwargs: | ||
| :return: The input. | ||
| """ | ||
| _lock.acquire() | ||
| data = _Namespace( | ||
| msg=msg, args=args, end=end, raise_error=raise_error, kwargs=kwargs | ||
| ) | ||
| self.window.event_generate('<<input>>', when='tail', data=data) | ||
| _lock.release() | ||
| return data.output | ||
| def cancel_input(self) -> str: | ||
| """Cancels the input. | ||
| :return: Part of the input that the user has typed | ||
| """ | ||
| if self.getting_input_status != GettingInputStatus.GETTING_INPUT: | ||
| raise RuntimeError( | ||
| 'The logger must be getting input for this method to work! ' | ||
| 'Use `input` method.' | ||
| ) | ||
| self.getting_input_status = GettingInputStatus.CANCELLED | ||
| return self.input_text | ||
| def type_input(self, text: str, wait: _Union[int, float, bool] = False) -> None: | ||
| """Types some text as a part of the input that the user can edit and enter. | ||
| :param text: The text to type for the user | ||
| :param wait: Wait until the input function is called and then type the text | ||
| :return: | ||
| """ | ||
| self.window.event_generate( | ||
| '<<type input>>', when='tail', data=_Namespace(text=text, wait=wait) | ||
| ) | ||
| def getpass(self, *msg, args: tuple = (), end: str = '', **kwargs) -> str: | ||
| """Prints a message and waits for input. | ||
| :param msg: The message to print. | ||
| :param args: The arguments to pass to the message. | ||
| :param end: The end of the message. | ||
| :param kwargs: | ||
| :return: The input. | ||
| """ | ||
| _lock.acquire() | ||
| data = _Namespace(msg=msg, args=args, end=end, kwargs=kwargs) | ||
| self.window.event_generate('<<getpass>>', when='tail', data=data) | ||
| _lock.release() | ||
| return data.output | ||
| def key_press(self, event) -> None: # pylint: disable=too-many-branches # noqa: ANN001 | ||
| """KeyPress event callback for self.logs.""" | ||
| if (self.getting_input_status == GettingInputStatus.GETTING_INPUT | ||
| or self.getting_pass): | ||
| # Handles Enter key | ||
| if event.keysym == 'Return': | ||
| self.getting_input_status = GettingInputStatus.NOT_GETTING_INPUT | ||
| self.getting_pass = False | ||
| self.cursor_position = 0 | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.insert(_tkinter.END, '\n') | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.logs.see(_tkinter.END) | ||
| # Handles Backspace key | ||
| elif event.keysym == 'BackSpace': | ||
| if self.input_text: | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete(f'end-{len(self.input_text) + 1}c', 'end-1c') | ||
| self.input_text = self.input_text[:self.cursor_position - 1] + \ | ||
| self.input_text[self.cursor_position:] | ||
| if self.getting_pass: | ||
| self.logs.insert(_tkinter.END, '*' * len(self.input_text)) | ||
| else: | ||
| self.logs.insert(_tkinter.END, self.input_text) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.cursor_position -= 1 | ||
| # Handles Right Arrow | ||
| elif event.keysym == 'Right': | ||
| if self.cursor_position < len(self.input_text | ||
| ) and not self.getting_pass: | ||
| self.cursor_position += 1 | ||
| # Handles Left Arrow | ||
| elif event.keysym == 'Left': | ||
| if self.cursor_position > 0 and not self.getting_pass: | ||
| self.cursor_position -= 1 | ||
| # Handles other keys | ||
| elif event.char: | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete(f'end-{len(self.input_text) + 1}c', 'end-1c') | ||
| self.input_text = ( | ||
| self.input_text[:self.cursor_position] + event.char + | ||
| self.input_text[self.cursor_position:] | ||
| ) | ||
| if self.getting_pass: | ||
| self.logs.insert(_tkinter.END, '*' * len(self.input_text)) | ||
| else: | ||
| self.logs.insert(_tkinter.END, self.input_text) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.cursor_position += 1 | ||
| def execute_command(self, _) -> None: # noqa: ANN001 | ||
| """Executes the command in self.command_entry.""" | ||
| command = self.command_entry.get() | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| self.command_history.append(command) | ||
| self.command_history = self.command_history[-self.command_history_buffer_size:] | ||
| # FIXME: It doesn't support multiline commands yet | ||
| # Shell commands: | ||
| if command.startswith('!'): | ||
| if self.allow_shell: | ||
| try: | ||
| # TODO: Add the support of interactive programmes such as python | ||
| # shell and bash | ||
| output = _subprocess.check_output(command[1:].strip(), shell=False) | ||
| self.print(output.decode('utf-8').strip('\r\n')) | ||
| except _subprocess.CalledProcessError as ex: | ||
| self.error( | ||
| 'Error code:', ex.returncode, | ||
| ex.output.decode('utf-8').strip('\r\n') | ||
| ) | ||
| except FileNotFoundError: | ||
| self.error('File not found: Unrecognized command.') | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| self.error(ex) | ||
| else: | ||
| self.error('Shell commands are not allowed!') | ||
| # Python commands: | ||
| elif self.allow_python: | ||
| try: | ||
| # TODO: Add the support of python commands | ||
| raise NotImplementedError | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| self.error(ex) | ||
| else: | ||
| try: | ||
| output = _subprocess.check_output(command.strip(), shell=False) | ||
| self.print(output.decode('utf-8').strip('\r\n')) | ||
| except _subprocess.CalledProcessError as ex: | ||
| self.error( | ||
| 'Error code:', ex.returncode, | ||
| ex.output.decode('utf-8').strip('\r\n') | ||
| ) | ||
| except FileNotFoundError: | ||
| self.error('File not found: Unrecognized command.') | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| self.error(ex) | ||
| self.command_history_index = len(self.command_history) | ||
| def history_up(self, _) -> None: # noqa: ANN001 | ||
| """Moves up the command history.""" | ||
| _lock.acquire() | ||
| if self.command_history_index > 0: | ||
| self.command_history_index -= 1 | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| self.command_entry.insert( | ||
| 0, self.command_history[self.command_history_index] | ||
| ) | ||
| _lock.release() | ||
| def history_down(self, _) -> None: # noqa: ANN001 | ||
| """Moves down the command history.""" | ||
| _lock.acquire() | ||
| if self.command_history_index < len(self.command_history) - 1: | ||
| self.command_history_index += 1 | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| self.command_entry.insert( | ||
| 0, self.command_history[self.command_history_index] | ||
| ) | ||
| else: | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| _lock.release() | ||
| def __set_allow_python(self, event) -> None: # noqa: ANN001 | ||
| """Sets the allow_python attribute.""" | ||
| self.__allow_python = event.data | ||
| # Hides the command entry if allow_python and allow_shell are False | ||
| if not self.__allow_python and not self.__allow_shell: | ||
| self.command_entry.grid_remove() | ||
| # Shows the command entry if allow_python or allow_shell are True | ||
| else: | ||
| self.command_entry.grid(row=1, column=0, sticky='nsew') | ||
| def __set_allow_shell(self, event) -> None: # noqa: ANN001 | ||
| """Sets the allow_shell attribute.""" | ||
| self.__allow_shell = event.data | ||
| # Hides the command entry if allow_python and allow_shell are False | ||
| if not self.__allow_python and not self.__allow_shell: | ||
| self.command_entry.grid_remove() | ||
| # Shows the command entry if allow_python or allow_shell are True | ||
| else: | ||
| self.command_entry.grid(row=1, column=0, sticky='nsew') | ||
| def __set_cursor_position(self, event) -> None: # noqa: ANN001 | ||
| """Sets the cursor_position attribute.""" | ||
| new_value = self.cursor_position != event.data | ||
| # Removes the cursor from the last position | ||
| if self.cursor_position is not None and (self.__cursor_counter % 50 == 0 | ||
| or new_value): | ||
| index = self.logs.index( | ||
| f'end-{len(self.input_text) - self.cursor_position + 2}c' | ||
| ) | ||
| self.logs.tag_add( | ||
| index, index, f'end-{len(self.input_text) - self.cursor_position + 1}c' | ||
| ) | ||
| self.logs.tag_config( | ||
| index, | ||
| background=self.default_background_color, | ||
| foreground=self.default_foreground_color | ||
| ) | ||
| self._cursor_position = event.data | ||
| self.__cursor_counter += 1 | ||
| # Places the new cursor | ||
| if self.getting_input_status == GettingInputStatus.GETTING_INPUT and ( | ||
| self.__cursor_counter % 100 == 0 or new_value): | ||
| self.__cursor_counter = 1 | ||
| index = self.logs.index( | ||
| f'end-{len(self.input_text) - self.cursor_position + 2}c' | ||
| ) | ||
| self.logs.tag_add( | ||
| index, index, f'end-{len(self.input_text) - self.cursor_position + 1}c' | ||
| ) | ||
| self.logs.tag_config( | ||
| index, | ||
| background=self.default_foreground_color, | ||
| foreground=self.default_background_color | ||
| ) | ||
| def __set_default_foreground_color(self, event) -> None: # noqa: ANN001 | ||
| """Sets the default_foreground_color attribute.""" | ||
| self._default_foreground_color = event.data | ||
| self.logs.config(foreground=event.data) | ||
| def __set_default_background_color(self, event) -> None: # noqa: ANN001 | ||
| """Sets the default_background_color attribute.""" | ||
| self._default_background_color = event.data | ||
| self.logs.config(background=event.data) | ||
| def __set_font(self, event) -> None: # noqa: ANN001 | ||
| """Sets the font attribute.""" | ||
| self.logs.config(font=event.data) | ||
| def __set_width(self, event) -> None: # noqa: ANN001 | ||
| """Sets the width attribute.""" | ||
| self.logs.config(width=event.data) | ||
| def __set_height(self, event) -> None: # noqa: ANN001 | ||
| """Sets the height attribute.""" | ||
| self.logs.config(height=event.data) | ||
| @property | ||
| def allow_python(self) -> bool: | ||
| return self.__allow_python | ||
| @allow_python.setter | ||
| def allow_python(self, value: bool) -> None: | ||
| raise NotImplementedError('Python commands are not supported yet!') | ||
| self.window.event_generate('<<SetAllowPython>>', when='tail', data=value) | ||
| @property | ||
| def allow_shell(self) -> bool: | ||
| return self.__allow_shell | ||
| @allow_shell.setter | ||
| def allow_shell(self, value: bool) -> None: | ||
| self.window.event_generate('<<SetAllowShell>>', when='tail', data=value) | ||
| @property | ||
| def cursor_position(self) -> _Optional[int]: | ||
| return self._cursor_position | ||
| @cursor_position.setter | ||
| def cursor_position(self, value: int) -> None: | ||
| self.window.event_generate('<<SetCursorPosition>>', when='tail', data=value) | ||
| @property | ||
| def default_foreground_color(self): # noqa: ANN201 | ||
| return self._default_foreground_color | ||
| @default_foreground_color.setter | ||
| def default_foreground_color(self, value) -> None: # noqa: ANN001 | ||
| self.window.event_generate( | ||
| '<<SetDefaultForegroundColor>>', when='tail', data=value | ||
| ) | ||
| @property | ||
| def default_background_color(self): # noqa: ANN201 | ||
| return self._default_background_color | ||
| @default_background_color.setter | ||
| def default_background_color(self, value) -> None: # noqa: ANN001 | ||
| self.window.event_generate( | ||
| '<<SetDefaultBackgroundColor>>', when='tail', data=value | ||
| ) | ||
| @property | ||
| def font(self): # noqa: ANN201 | ||
| return self.logs.config()['font'] | ||
| @font.setter | ||
| def font(self, value) -> None: # noqa: ANN001 | ||
| self.window.event_generate('<<SetFont>>', when='tail', data=value) | ||
| @property | ||
| def width(self) -> int: | ||
| return self.logs.config()['width'][-1] | ||
| @width.setter | ||
| def width(self, value: int) -> None: | ||
| self.window.event_generate('<<SetWidth>>', when='tail', data=value) | ||
| @property | ||
| def height(self) -> int: | ||
| return self.logs.config()['height'][-1] | ||
| @height.setter | ||
| def height(self, value: int) -> None: | ||
| self.window.event_generate('<<SetHeight>>', when='tail', data=value) | ||
| @property | ||
| def progress_bar(self) -> '_log21.ProgressBar': | ||
| if not self._progress_bar: | ||
| # Import here to avoid circular import | ||
| # pylint: disable=import-outside-toplevel | ||
| from log21.progressbar import ProgressBar # noqa: PLC0415 | ||
| self._progress_bar = ProgressBar(logger=self, width=self.width) | ||
| self.window.update() | ||
| return self._progress_bar | ||
| def __del__(self) -> None: | ||
| self.window.withdraw() | ||
| self.window.destroy() | ||
| del self.window | ||
| if not _tkinter: | ||
| class LoggingWindow: # pylint: disable=function-redefined | ||
| """LoggingWindow requires tkinter to be installed.""" | ||
| def __init__(self, *args, **kwargs) -> None: | ||
| raise ImportError('LoggingWindow requires tkinter to be installed.') | ||
| class LoggingWindowHandler: # pylint: disable=function-redefined | ||
| """LoggingWindow requires tkinter to be installed.""" | ||
| def __init__(self, *args, **kwargs) -> None: | ||
| raise ImportError('LoggingWindow requires tkinter to be installed.') |
| # log21.manager.py | ||
| # CodeWriter21 | ||
| import logging as _logging | ||
| from typing import Union as _Union | ||
| from log21.levels import INFO as _INFO | ||
| from log21.logger import Logger as _loggerClass | ||
| root = _logging.RootLogger(_INFO) | ||
| LoggingType = _Union[_loggerClass, _logging.Logger] | ||
| class Manager(_logging.Manager): | ||
| """The Manager class is a subclass of the logging.Manager class. | ||
| It overrides the getLogger method to make it more compatible with the log21.Logger | ||
| class. It also overrides the constructor. | ||
| """ | ||
| def __init__(self) -> None: | ||
| self.root = root | ||
| self.disable = 0 | ||
| self.emittedNoHandlerWarning = False | ||
| self.loggerDict = {} | ||
| self.loggerClass = None | ||
| self.logRecordFactory = None | ||
| def getLogger( # ty: ignore[invalid-method-override] | ||
| self, name: str | ||
| ) -> _Union[LoggingType, None]: | ||
| """Takes the name of a logger and if there was a logger with that name in the | ||
| loggerDict it will return the logger otherwise it'll return None. | ||
| :param name: The name of the logger. | ||
| :raises TypeError: A logger name must be a string | ||
| :return: | ||
| """ | ||
| if not isinstance(name, str): | ||
| raise TypeError('A logger name must be a string') | ||
| try: | ||
| if name in self.loggerDict: | ||
| rv = self.loggerDict[name] | ||
| if isinstance(rv, _logging.PlaceHolder): | ||
| rv = (self.loggerClass or _loggerClass)(name) | ||
| rv.manager = self | ||
| self.loggerDict[name] = rv | ||
| else: | ||
| return None | ||
| except Exception: # pylint: disable=broad-except | ||
| return None | ||
| return rv | ||
| def addLogger(self, name: str, logger: LoggingType) -> None: | ||
| """Adds a logger to the loggerDict dictionary. | ||
| :param name: str: The name of the logger. | ||
| :param logger: The logger to save. | ||
| :raises TypeError: A logger name must be a string | ||
| :return: None | ||
| """ | ||
| if not isinstance(name, str): | ||
| raise TypeError('A logger name must be a string') | ||
| self.loggerDict[name] = logger | ||
| get_logger = getLogger | ||
| add_logger = addLogger |
| # log21.pprint.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| import re as _re | ||
| import sys as _sys | ||
| import types as _types | ||
| import collections as _collections | ||
| import dataclasses as _dataclasses | ||
| from pprint import PrettyPrinter as _PrettyPrinter | ||
| from typing import (Any as _Any, Dict as _Dict, Mapping as _Mapping, | ||
| Optional as _Optional, Sequence as _Sequence, | ||
| Generator as _Generator) | ||
| from log21.colors import get_colors as _gc | ||
| # yapf: enable | ||
| _builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)}) | ||
| def _recursion(obj: _Any) -> str: | ||
| return f"<Recursion on {type(obj).__name__} with id={id(obj)}>" | ||
| def _safe_tuple(t: tuple) -> tuple["_SafeKey", "_SafeKey"]: | ||
| """Helper function for comparing 2-tuples.""" | ||
| return _SafeKey(t[0]), _SafeKey(t[1]) | ||
| def _wrap_bytes_repr(obj: _Any, width: int, | ||
| allowance: int) -> _Generator[str, None, None]: | ||
| current = b'' | ||
| last = len(obj) // 4 * 4 | ||
| for i in range(0, len(obj), 4): | ||
| part = obj[i:i + 4] | ||
| candidate = current + part | ||
| if i == last: | ||
| width -= allowance | ||
| if len(repr(candidate)) > width: | ||
| if current: | ||
| yield repr(current) | ||
| current = part | ||
| else: | ||
| current = candidate | ||
| if current: | ||
| yield repr(current) | ||
| class _SafeKey: | ||
| """Helper function for key functions when sorting unorderable objects. | ||
| The wrapped-object will fallback to a Py2.x style comparison for unorderable types | ||
| (sorting first comparing the type name and then by the obj ids). Does not work | ||
| recursively, so dict.items() must have _safe_key applied to both the key and the | ||
| value. | ||
| """ | ||
| __slots__ = ['obj'] | ||
| def __init__(self, obj: _Any) -> None: | ||
| self.obj = obj | ||
| def __lt__(self, other: "_SafeKey") -> bool: | ||
| try: | ||
| return self.obj < other.obj | ||
| except TypeError: | ||
| return (str(type(self.obj)), id(self.obj | ||
| )) < (str(type(other.obj)), id(other.obj)) | ||
| class PrettyPrinter(_PrettyPrinter): | ||
| def __init__( | ||
| self, | ||
| indent: int = 1, | ||
| width: int = 80, | ||
| depth: _Optional[int] = None, | ||
| stream=None, # noqa: ANN001 | ||
| sign_colors: _Optional[_Mapping[str, str]] = None, | ||
| *, | ||
| compact: bool = False, | ||
| sort_dicts: bool = True, | ||
| underscore_numbers: bool = False, | ||
| **kwargs | ||
| ) -> None: | ||
| super().__init__( | ||
| indent=indent, | ||
| width=width, | ||
| depth=depth, | ||
| stream=stream, | ||
| compact=compact, | ||
| **kwargs | ||
| ) | ||
| self._depth = depth | ||
| self._indent_per_level = indent | ||
| self._width = width | ||
| if stream is not None: | ||
| self._stream = stream | ||
| else: | ||
| self._stream = _sys.stdout | ||
| self._compact = bool(compact) | ||
| self._sort_dicts = sort_dicts | ||
| self._underscore_numbers = underscore_numbers | ||
| self.sign_colors: _Dict[str, str] = { | ||
| 'square-brackets': _gc('LightCyan'), | ||
| 'curly-braces': _gc('LightBlue'), | ||
| 'parenthesis': _gc('LightGreen'), | ||
| 'comma': _gc('LightRed'), | ||
| 'colon': _gc('LightRed'), | ||
| '...': _gc('LightMagenta'), | ||
| 'data': _gc('Green') | ||
| } | ||
| if sign_colors: | ||
| for sign, color in sign_colors.items(): | ||
| self.sign_colors[sign.lower()] = _gc(color) | ||
| def _format( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: # ty: ignore[invalid-method-override] | ||
| objid = id(obj) | ||
| if objid in context: | ||
| stream.write(_recursion(obj)) | ||
| self._recursive = True | ||
| self._readable = False | ||
| return | ||
| rep = self._repr(obj, context, level) | ||
| max_width = self._width - indent - allowance | ||
| if len(rep) > max_width: | ||
| p = self._dispatch.get(type(obj).__repr__, None) | ||
| if p is not None: | ||
| context[objid] = 1 | ||
| p(self, obj, stream, indent, allowance, context, level + 1) | ||
| del context[objid] | ||
| return | ||
| elif (_dataclasses.is_dataclass(obj) and not isinstance(obj, type) | ||
| and obj.__dataclass_params__.repr | ||
| and # Check dataclass has generated repr method. | ||
| hasattr(obj.__repr__, "__wrapped__") and "__create_fn__" | ||
| in obj.__repr__.__wrapped__.__qualname__): | ||
| context[objid] = 1 | ||
| self._pprint_dataclass( | ||
| obj, stream, indent, allowance, context, level + 1 | ||
| ) | ||
| del context[objid] | ||
| return | ||
| stream.write(rep) | ||
| def _pprint_dataclass( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| cls_name = obj.__class__.__name__ | ||
| indent += len(cls_name) + 1 | ||
| items = [ | ||
| (f.name, getattr(obj, f.name)) for f in _dataclasses.fields(obj) if f.repr | ||
| ] | ||
| stream.write(cls_name + '(') | ||
| self._format_namespace_items(items, stream, indent, allowance, context, level) | ||
| stream.write(')') | ||
| def _repr( | ||
| self, | ||
| obj: _Any, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> str: # ty: ignore[invalid-method-override] | ||
| repr, readable, recursive = self.format( | ||
| obj, | ||
| context.copy(), | ||
| self._depth, # ty: ignore[invalid-argument-type] | ||
| level | ||
| ) | ||
| if not readable: | ||
| self._readable = False | ||
| if recursive: | ||
| self._recursive = True | ||
| return repr | ||
| def _safe_repr( # noqa: PLR0915 | ||
| self, | ||
| object_: _Any, | ||
| context, # noqa: ANN001 | ||
| max_levels, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> tuple[str, bool, bool]: | ||
| # Return triple (repr_string, isreadable, isrecursive). | ||
| type_ = type(object_) | ||
| if type_ in _builtin_scalars: | ||
| return repr(object_), True, False | ||
| representation = getattr(type_, "__repr__", None) | ||
| if issubclass(type_, int) and representation is int.__repr__: | ||
| if self._underscore_numbers: | ||
| return f"{object_:_d}", True, False | ||
| else: | ||
| return repr(object_), True, False | ||
| if issubclass(type_, dict) and representation is dict.__repr__: | ||
| if not object_: | ||
| return self.sign_colors.get( | ||
| 'curly-braces', '' | ||
| ) + "{}" + self.sign_colors.get('data', ''), True, False | ||
| object_id = id(object_) | ||
| if max_levels and level >= max_levels: | ||
| return ( | ||
| self.sign_colors.get('curly-braces', '') + "{" + | ||
| self.sign_colors.get('...', '') + "..." + | ||
| self.sign_colors.get('curly-braces', '') + "}" + | ||
| self.sign_colors.get('data', ''), False, object_id in context | ||
| ) | ||
| if object_id in context: | ||
| return _recursion(object_), False, True | ||
| context[object_id] = 1 | ||
| readable = True | ||
| recursive = False | ||
| components = [] | ||
| append = components.append | ||
| level += 1 | ||
| if self._sort_dicts: | ||
| items = sorted(object_.items(), key=_safe_tuple) | ||
| else: | ||
| items = object_.items() | ||
| for k, v in items: | ||
| krepr, kreadable, krecur = self.format(k, context, max_levels, level) | ||
| vrepr, vreadable, vrecur = self.format(v, context, max_levels, level) | ||
| append( | ||
| f"{krepr}{self.sign_colors.get('colon')}:" | ||
| f"{self.sign_colors.get('data')} {vrepr}" | ||
| ) | ||
| readable = readable and kreadable and vreadable | ||
| if krecur or vrecur: | ||
| recursive = True | ||
| del context[object_id] | ||
| return ( | ||
| self.sign_colors.get('curly-braces', '') + "{" + | ||
| self.sign_colors.get('data', '') + ( | ||
| self.sign_colors.get('comma', '') + ", " + | ||
| self.sign_colors.get('data', '') | ||
| ).join(components) + self.sign_colors.get('curly-braces', '') + "}", | ||
| readable, recursive | ||
| ) | ||
| if (issubclass(type_, list) and representation is list.__repr__) or \ | ||
| (issubclass(type_, tuple) and representation is tuple.__repr__): | ||
| if issubclass(type_, list): | ||
| if not object_: | ||
| return self.sign_colors.get( | ||
| 'square-brackets', '' | ||
| ) + "[]" + self.sign_colors.get('data', ''), True, False | ||
| format_ = ( | ||
| self.sign_colors.get('square-brackets', '') + "[" + | ||
| self.sign_colors.get('data', '') + "%s" + | ||
| self.sign_colors.get('square-brackets', '') + "]" + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| elif len(object_) == 1: | ||
| format_ = ( | ||
| self.sign_colors.get('parenthesis', '') + "(" + | ||
| self.sign_colors.get('data', '') + "%s" + | ||
| self.sign_colors.get('comma', '') + "," + | ||
| self.sign_colors.get('parenthesis', '') + ")" + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| if not object_: | ||
| return self.sign_colors.get( | ||
| 'parenthesis', '' | ||
| ) + "()" + self.sign_colors.get('data', ''), True, False | ||
| format_ = ( | ||
| self.sign_colors.get('parenthesis', '') + "(" + | ||
| self.sign_colors.get('data', '') + "%s" + | ||
| self.sign_colors.get('parenthesis', '') + ")" + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| object_id = id(object_) | ||
| if max_levels and level >= max_levels: | ||
| return format_ % self.sign_colors.get( | ||
| '...' | ||
| ) + "...", False, object_id in context | ||
| if object_id in context: | ||
| return _recursion(object_), False, True | ||
| context[object_id] = 1 | ||
| readable = True | ||
| recursive = False | ||
| components = [] | ||
| append = components.append | ||
| level += 1 | ||
| for o in object_: | ||
| orepr, oreadable, orecur = self.format(o, context, max_levels, level) | ||
| append(orepr) | ||
| if not oreadable: | ||
| readable = False | ||
| if orecur: | ||
| recursive = True | ||
| del context[object_id] | ||
| return ( | ||
| format_ % ( | ||
| self.sign_colors.get('comma', '') + ", " + | ||
| self.sign_colors.get('data', '') | ||
| ).join(components), readable, recursive | ||
| ) | ||
| rep = repr(object_) | ||
| return rep, bool(rep and not rep.startswith('<')), False | ||
| _dispatch = {} | ||
| def _pprint_dict( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: # ty: ignore[invalid-method-override] | ||
| write = stream.write | ||
| write( | ||
| self.sign_colors.get('curly-braces', '') + '{' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| if self._indent_per_level > 1: | ||
| write((self._indent_per_level - 1) * ' ') | ||
| length = len(obj) | ||
| if length: | ||
| if self._sort_dicts: | ||
| items = sorted(obj.items(), key=_safe_tuple) | ||
| else: | ||
| items = obj.items() | ||
| self._format_dict_items( | ||
| items, stream, indent, allowance + 1, context, level | ||
| ) | ||
| write( | ||
| self.sign_colors.get('curly-braces', '') + '}' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[dict.__repr__] = _pprint_dict | ||
| def _pprint_ordered_dict( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis') + '(' + | ||
| self.sign_colors.get('data') | ||
| ) | ||
| self._format( | ||
| list(obj.items()), stream, indent + len(cls.__name__) + 1, allowance + 1, | ||
| context, level | ||
| ) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict | ||
| def _pprint_list( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: # ty: ignore[invalid-method-override] | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + '[' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format_items(obj, stream, indent, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + ']' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[list.__repr__] = _pprint_list | ||
| def _pprint_tuple( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: # ty: ignore[invalid-method-override] | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| endchar = ( | ||
| self.sign_colors.get('comma', '') + ',' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| if len(obj) == 1 else self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format_items( | ||
| obj, stream, indent, allowance + len(endchar), context, level | ||
| ) | ||
| stream.write(endchar) | ||
| _dispatch[tuple.__repr__] = _pprint_tuple | ||
| def _pprint_set( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: # ty: ignore[invalid-method-override] | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| typ = obj.__class__ | ||
| if typ is set: | ||
| stream.write( | ||
| self.sign_colors.get('curly-braces', '') + '{' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| endchar = self.sign_colors.get('curly-braces', | ||
| '') + '}' + self.sign_colors.get('data', '') | ||
| else: | ||
| stream.write( | ||
| typ.__name__ + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('curly-braces', '') + '{' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| endchar = ( | ||
| self.sign_colors.get('curly-braces', '') + '}' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| indent += len(typ.__name__) + 1 | ||
| obj = sorted(obj, key=_SafeKey) | ||
| self._format_items( | ||
| obj, stream, indent, allowance + len(endchar), context, level | ||
| ) | ||
| stream.write(endchar) | ||
| _dispatch[set.__repr__] = _pprint_set | ||
| _dispatch[frozenset.__repr__] = _pprint_set | ||
| def _pprint_str( | ||
| self, | ||
| object_: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| write = stream.write | ||
| if not len(object_): | ||
| write(repr(object_)) | ||
| return | ||
| chunks = [] | ||
| lines = object_.splitlines(True) | ||
| if level == 1: | ||
| indent += 1 | ||
| allowance += 1 | ||
| max_width1 = max_width = self._width - indent | ||
| representation = '' | ||
| for i, line in enumerate(lines): | ||
| representation = repr(line) | ||
| if i == len(lines) - 1: | ||
| max_width1 -= allowance | ||
| if len(representation) <= max_width1: | ||
| chunks.append(representation) | ||
| else: | ||
| # A list of alternating (non-space, space) strings | ||
| parts = _re.findall(r'\S*\s*', line) | ||
| assert parts | ||
| assert not parts[-1] | ||
| parts.pop() # drop empty last part | ||
| max_width2 = max_width | ||
| current = '' | ||
| for j, part in enumerate(parts): | ||
| candidate = current + part | ||
| if j == len(parts) - 1 and i == len(lines) - 1: | ||
| max_width2 -= allowance | ||
| if len(repr(candidate)) > max_width2: | ||
| if current: | ||
| chunks.append(repr(current)) | ||
| current = part | ||
| else: | ||
| current = candidate | ||
| if current: | ||
| chunks.append(repr(current)) | ||
| if len(chunks) == 1: | ||
| write(representation) | ||
| return | ||
| if level == 1: | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| for i, representation in enumerate(chunks): | ||
| if i > 0: | ||
| write('\n' + ' ' * indent) | ||
| write(representation) | ||
| if level == 1: | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[str.__repr__] = _pprint_str | ||
| def _pprint_bytes( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| write = stream.write | ||
| if len(obj) <= 4: | ||
| write(repr(obj)) | ||
| return | ||
| parens = level == 1 | ||
| if parens: | ||
| indent += 1 | ||
| allowance += 1 | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| delim = '' | ||
| for rep in _wrap_bytes_repr(obj, self._width - indent, allowance): | ||
| write(delim) | ||
| write(rep) | ||
| if not delim: | ||
| delim = '\n' + ' ' * indent | ||
| if parens: | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[bytes.__repr__] = _pprint_bytes | ||
| def _pprint_bytearray( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| write = stream.write | ||
| write( | ||
| 'bytearray' + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._pprint_bytes( | ||
| bytes(obj), stream, indent + 10, allowance + 1, context, level + 1 | ||
| ) | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[bytearray.__repr__] = _pprint_bytearray | ||
| def _pprint_mappingproxy( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| stream.write( | ||
| 'mappingproxy' + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format(obj.copy(), stream, indent + 13, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy | ||
| def _pprint_simplenamespace( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| if isinstance(obj, _types.SimpleNamespace): | ||
| # The SimpleNamespace repr is "namespace" instead of the class | ||
| # name, so we do the same here. For subclasses; use the class name. | ||
| cls_name = 'namespace' | ||
| else: | ||
| cls_name = obj.__class__.__name__ | ||
| indent += len(cls_name) + 1 | ||
| items = obj.__dict__.items() | ||
| stream.write( | ||
| cls_name + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format_namespace_items(items, stream, indent, allowance, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace | ||
| def _format_dict_items( | ||
| self, | ||
| items: _Sequence[tuple[_Any, _Any]], | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| write = stream.write | ||
| indent += self._indent_per_level | ||
| delimnl = self.sign_colors.get('comma', '') + ',\n' + self.sign_colors.get( | ||
| 'data', '' | ||
| ) + ' ' * indent | ||
| last_index = len(items) - 1 | ||
| for i, (key, ent) in enumerate(items): | ||
| last = i == last_index | ||
| rep = self._repr(key, context, level) | ||
| write(rep) | ||
| write( | ||
| self.sign_colors.get('colon', '') + ': ' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format( | ||
| ent, stream, indent + len(rep) + 2, allowance if last else 1, context, | ||
| level | ||
| ) | ||
| if not last: | ||
| write(delimnl) | ||
| def _format_namespace_items( | ||
| self, | ||
| items: _Sequence[tuple[_Any, _Any]], | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| write = stream.write | ||
| delimnl = self.sign_colors.get('comma', '') + ',\n' + self.sign_colors.get( | ||
| 'data', '' | ||
| ) + ' ' * indent | ||
| last_index = len(items) - 1 | ||
| for i, (key, ent) in enumerate(items): | ||
| last = i == last_index | ||
| write(key) | ||
| write('=') | ||
| if id(ent) in context: | ||
| # Special-case representation of recursion to match standard | ||
| # recursive dataclass repr. | ||
| write( | ||
| self.sign_colors.get('...', '') + "..." + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| self._format( | ||
| ent, stream, indent + len(key) + 1, allowance if last else 1, | ||
| context, level | ||
| ) | ||
| if not last: | ||
| write(delimnl) | ||
| def _format_items( | ||
| self, | ||
| items: _Sequence[tuple[_Any, _Any]], | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: # ty: ignore[invalid-method-override] | ||
| write = stream.write | ||
| indent += self._indent_per_level | ||
| if self._indent_per_level > 1: | ||
| write((self._indent_per_level - 1) * ' ') | ||
| delimnl = self.sign_colors.get('comma', '') + ',\n' + self.sign_colors.get( | ||
| 'data', '' | ||
| ) + ' ' * indent | ||
| delim = '' | ||
| width = max_width = self._width - indent + 1 | ||
| it = iter(items) | ||
| try: | ||
| next_ent = next(it) | ||
| except StopIteration: | ||
| return | ||
| last = False | ||
| while not last: | ||
| ent = next_ent | ||
| try: | ||
| next_ent = next(it) | ||
| except StopIteration: | ||
| last = True | ||
| max_width -= allowance | ||
| width -= allowance | ||
| if self._compact: | ||
| rep = self._repr(ent, context, level) | ||
| w = len(rep) + 2 | ||
| if width < w: | ||
| width = max_width | ||
| if delim: | ||
| delim = delimnl | ||
| if width >= w: | ||
| width -= w | ||
| write(delim) | ||
| delim = self.sign_colors.get( | ||
| 'comma', '' | ||
| ) + ', ' + self.sign_colors.get('data', '') | ||
| write(rep) | ||
| continue | ||
| write(delim) | ||
| delim = delimnl | ||
| self._format(ent, stream, indent, allowance if last else 1, context, level) | ||
| def _pprint_default_dict( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| rdf = self._repr(obj.default_factory, context, level) | ||
| cls = obj.__class__ | ||
| indent += len(cls.__name__) + 1 | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis') + '(' + | ||
| self.sign_colors.get('data') + rdf + self.sign_colors.get('comma') + ',\n' + | ||
| self.sign_colors.get('data') + (' ' * indent) | ||
| ) | ||
| self._pprint_dict(obj, stream, indent, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict | ||
| def _pprint_counter( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis') + '(' + | ||
| self.sign_colors.get('curly-braces') + '{' + self.sign_colors.get('data') | ||
| ) | ||
| if self._indent_per_level > 1: | ||
| stream.write((self._indent_per_level - 1) * ' ') | ||
| items = obj.most_common() | ||
| self._format_dict_items( | ||
| items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level | ||
| ) | ||
| stream.write( | ||
| self.sign_colors.get('curly-braces', '') + '}' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_collections.Counter.__repr__] = _pprint_counter | ||
| def _pprint_chain_map( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| if not len(obj.maps): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| indent += len(cls.__name__) + 1 | ||
| for i, m in enumerate(obj.maps): | ||
| if i == len(obj.maps) - 1: | ||
| self._format(m, stream, indent, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| self._format(m, stream, indent, 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('comma', '') + ',\n' + | ||
| self.sign_colors.get('data', '') + ' ' * indent | ||
| ) | ||
| _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map | ||
| def _pprint_deque( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: # ty: ignore[invalid-method-override] | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| indent += len(cls.__name__) + 1 | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + '[' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| if obj.maxlen is None: | ||
| self._format_items(obj, stream, indent, allowance + 2, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + ']' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| self._format_items(obj, stream, indent, 2, context, level) | ||
| rml = self._repr(obj.maxlen, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + ']' + | ||
| self.sign_colors.get('comma', '') + ',' + | ||
| self.sign_colors.get('data', '') + '\n' + (' ' * indent) + 'maxlen=' + | ||
| rml + self.sign_colors.get('parenthesis', '') + ')' | ||
| ) | ||
| _dispatch[_collections.deque.__repr__] = _pprint_deque | ||
| def _pprint_user_dict( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| self._format(obj.data, stream, indent, allowance, context, level - 1) | ||
| _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict | ||
| def _pprint_user_list( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| self._format(obj.data, stream, indent, allowance, context, level - 1) | ||
| _dispatch[_collections.UserList.__repr__] = _pprint_user_list | ||
| def _pprint_user_string( | ||
| self, | ||
| obj: _Any, | ||
| stream, # noqa: ANN001 | ||
| indent: int, | ||
| allowance: int, | ||
| context, # noqa: ANN001 | ||
| level # noqa: ANN001 | ||
| ) -> None: | ||
| self._format(obj.data, stream, indent, allowance, context, level - 1) | ||
| _dispatch[_collections.UserString.__repr__] = _pprint_user_string | ||
| # novm | ||
| def pformat( | ||
| obj: _Any, | ||
| indent: int = 1, | ||
| width: int = 80, | ||
| depth: _Optional[int] = None, | ||
| signs_colors: _Optional[_Mapping[str, str]] = None, | ||
| *, | ||
| compact: bool = False, | ||
| sort_dicts: bool = True, | ||
| underscore_numbers: bool = False, | ||
| **kwargs | ||
| ) -> str: | ||
| """Format a Python object into a pretty-printed representation. | ||
| :param obj: the object to format. | ||
| :param indent: the number of spaces to indent for each level of nesting. | ||
| :param width: the maximum width of the formatted representation. | ||
| :param depth: the maximum depth to print out nested structures. | ||
| :param signs_colors: a mapping of signs and colors. | ||
| :param compact: if `True`, several items will be combined in one line. | ||
| :param sort_dicts: if `True`, dictionaries will be sorted by key. (py38+) | ||
| :param underscore_numbers: if `True`, numbers will be represented with an | ||
| underscore between every digit. (py310+) | ||
| :param kwargs: additional keyword arguments to pass to the underlying pretty | ||
| printer. | ||
| :return: the formatted representation. | ||
| """ | ||
| return PrettyPrinter( | ||
| indent=indent, | ||
| width=width, | ||
| depth=depth, | ||
| compact=compact, | ||
| sign_colors=signs_colors, | ||
| sort_dicts=sort_dicts, | ||
| underscore_numbers=underscore_numbers, | ||
| **kwargs | ||
| ).pformat(obj) |
| # log21.progress_bar.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| from __future__ import annotations | ||
| import sys as _sys | ||
| import shutil as _shutil | ||
| from typing import (TYPE_CHECKING as _TYPE_CHECKING, Any as _Any, Mapping as _Mapping, | ||
| Optional as _Optional) | ||
| from log21.colors import get_colors as _gc | ||
| from log21.logger import Logger as _Logger | ||
| from log21.stream_handler import ColorizingStreamHandler as _ColorizingStreamHandler | ||
| from ._module_helper import FakeModule as _FakeModule | ||
| if _TYPE_CHECKING: | ||
| from types import ModuleType as _ModuleType | ||
| import log21 as _log21 | ||
| # yapf: enable | ||
| _logger = _Logger('ProgressBar') | ||
| _logger.addHandler(_ColorizingStreamHandler()) | ||
| __all__ = ['ProgressBar'] | ||
| class ProgressBar: # pylint: disable=too-many-instance-attributes, line-too-long | ||
| """ | ||
| Usage Example: | ||
| >>> pb = ProgressBar(width=20, show_percentage=False, prefix='[', suffix=']', | ||
| ... fill='=', empty='-') | ||
| >>> pb(0, 10) | ||
| [/-----------------] | ||
| >>> pb(1, 10) | ||
| [==----------------] | ||
| >>> pb(2, 10) | ||
| [====\\-------------] | ||
| >>> | ||
| >>> # A better example | ||
| >>> import time | ||
| >>> pb = ProgressBar() | ||
| >>> for i in range(500): | ||
| ... pb(i + 1, 500) | ||
| ... time.sleep(0.01) | ||
| ... | ||
| |███████████████████████████████████████████████████████████████████| 100% | ||
| >>> # Of course, You should try it yourself to see the progress! XD | ||
| >>> | ||
| """ | ||
| def __init__( # noqa: PLR0915 | ||
| self, | ||
| *, | ||
| width: _Optional[int] = None, | ||
| show_percentage: bool = True, | ||
| prefix: str = '|', | ||
| suffix: str = '|', | ||
| fill: str = '█', | ||
| empty: str = ' ', | ||
| format_: _Optional[str] = None, | ||
| style: str = '%', | ||
| new_line_when_complete: bool = True, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| no_color: bool = False, | ||
| logger: _log21.Logger = _logger, | ||
| additional_variables: _Optional[_Mapping[str, _Any]] = None | ||
| ) -> None: # pylint: disable=too-many-branches, too-many-statements | ||
| """ | ||
| :param args: Prevents the use of positional arguments | ||
| :param width: The width of the progress bar | ||
| :param show_percentage: Whether to show the percentage of the progress | ||
| :param prefix: The prefix of the progress bar | ||
| :param suffix: The suffix of the progress bar | ||
| :param fill: The fill character of the progress bar | ||
| :param empty: The empty character of the progress bar | ||
| :param format_: The format of the progress bar | ||
| :param style: The style that is used to format the progress bar | ||
| :param new_line_when_complete: Whether to print a new line when the progress is | ||
| complete or failed | ||
| :param colors: The colors of the progress bar | ||
| :param no_color: If True, removes the colors of the progress bar | ||
| :param logger: The logger to use | ||
| :param additional_variables: Additional variables to use in the format and their | ||
| default values | ||
| """ | ||
| # Sets a default value for the width | ||
| if width is None: | ||
| try: | ||
| width = _shutil.get_terminal_size().columns - 1 | ||
| except OSError: | ||
| width = 50 | ||
| if width < 1: | ||
| width = 50 | ||
| self.width = width | ||
| if self.width < 3: | ||
| raise ValueError('`width` must be greater than 1') | ||
| if not isinstance(fill, str): | ||
| raise TypeError('`fill` must be a string') | ||
| if not isinstance(empty, str): | ||
| raise TypeError('`empty` must be a string') | ||
| if not isinstance(prefix, str): | ||
| raise TypeError('`prefix` must be a string') | ||
| if not isinstance(suffix, str): | ||
| raise TypeError('`suffix` must be a string') | ||
| if len(fill) != 1: | ||
| raise ValueError('`fill` must be a single character') | ||
| if len(empty) != 1: | ||
| raise ValueError('`empty` must be a single character') | ||
| if style not in ['%', '{']: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| if colors and no_color: | ||
| raise PermissionError( | ||
| 'You cannot use `no_color` and `colors` parameters together!' | ||
| ) | ||
| if additional_variables: | ||
| if not isinstance(additional_variables, _Mapping): | ||
| raise TypeError( | ||
| '`additional_variables` must be a dictionary like object.' | ||
| ) | ||
| for key, value in additional_variables.items(): | ||
| if not isinstance(key, str): | ||
| raise TypeError('`additional_variables` keys must be strings') | ||
| if not isinstance(value, str): | ||
| additional_variables[key] = str(value) | ||
| else: | ||
| additional_variables = {} | ||
| self.colors = { | ||
| 'progress in-progress': _gc('LightYellow'), | ||
| 'progress complete': _gc('LightGreen'), | ||
| 'progress failed': _gc('LightRed'), | ||
| 'percentage in-progress': _gc('LightBlue'), | ||
| 'percentage complete': _gc('LightCyan'), | ||
| 'percentage failed': _gc('LightRed'), | ||
| 'prefix-color in-progress': _gc('Yellow'), | ||
| 'prefix-color complete': _gc('Green'), | ||
| 'prefix-color failed': _gc('Red'), | ||
| 'suffix-color in-progress': _gc('Yellow'), | ||
| 'suffix-color complete': _gc('Green'), | ||
| 'suffix-color failed': _gc('Red'), | ||
| 'reset-color': _gc('Reset'), | ||
| } | ||
| self.spinner = ['|', '/', '-', '\\'] | ||
| self.fill = fill | ||
| self.empty = empty | ||
| self.prefix = prefix | ||
| self.suffix = suffix | ||
| if format_: | ||
| self.format = format_ | ||
| else: | ||
| self.format = ( | ||
| '%(prefix)s%(bar)s%(suffix)s %(percentage)s%%' | ||
| if show_percentage else '%(prefix)s%(bar)s%(suffix)s' | ||
| ) | ||
| style = '%' | ||
| self.style = style | ||
| self.new_line_when_complete = new_line_when_complete | ||
| if colors: | ||
| for key, value in colors.items(): | ||
| self.colors[key] = value | ||
| if no_color: | ||
| self.colors = dict.fromkeys(self.colors, '') | ||
| self.logger = logger | ||
| self.additional_variables = additional_variables | ||
| self.i = 0 | ||
| def get_bar(self, progress: float, total: float, **kwargs) -> str: | ||
| """Return the progress bar as a string. | ||
| :param progress: The current progress. (e.g. 21) | ||
| :param total: The total progress. (e.g. 100) | ||
| :param kwargs: Additional variables to be used in the format | ||
| string. | ||
| :raises ValueError: If the style is not supported. | ||
| Set the style to one of the following: | ||
| + '%' | ||
| + '{' | ||
| e.g. bar = ProgressBar(style='{') | ||
| :return: The progress bar as a string. | ||
| """ | ||
| if progress == total: | ||
| return self.progress_complete(**kwargs) | ||
| if progress > total or progress < 0: | ||
| return self.progress_failed(progress, total, **kwargs) | ||
| return self.progress_in_progress(progress, total, **kwargs) | ||
| def progress_in_progress(self, progress: float, total: float, **kwargs) -> str: | ||
| """Return the progress bar as a string when the progress is in progress. | ||
| :param progress: The current progress. (e.g. 21) | ||
| :param total: The total progress. (e.g. 100) | ||
| :param kwargs: Additional variables to be used in the format | ||
| string. | ||
| :raises ValueError: If the style is not supported. (supported | ||
| styles: '%', '{') | ||
| :return: The progress bar as a string. | ||
| """ | ||
| percentage = str(round(progress / total * 100, 2)) | ||
| progress_dict = { | ||
| 'prefix': self.prefix, | ||
| 'bar': '', | ||
| 'suffix': self.suffix, | ||
| 'percentage': percentage, | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| if key in ['prefix', 'bar', 'suffix', 'percentage']: | ||
| raise ValueError(f'`{key}` is a reserved keyword') | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| used_characters = len(self.format % progress_dict) | ||
| elif self.style == '{': | ||
| used_characters = len(self.format.format(**progress_dict)) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| fill_length = round(progress / total * (self.width - used_characters)) | ||
| empty_length = (self.width - (fill_length + used_characters)) - 1 | ||
| if self.i >= 3: | ||
| self.i = 0 | ||
| else: | ||
| self.i += 1 | ||
| spinner_char = self.spinner[self.i] if empty_length > 0 else '' | ||
| progress_dict = { | ||
| 'prefix': | ||
| self.colors['prefix-color in-progress'] + self.prefix + | ||
| self.colors['reset-color'], | ||
| 'bar': | ||
| self.colors['progress in-progress'] + | ||
| (self.fill * fill_length + spinner_char + self.empty * empty_length) + | ||
| self.colors['reset-color'], | ||
| 'suffix': | ||
| self.colors['suffix-color in-progress'] + self.suffix + | ||
| self.colors['reset-color'], | ||
| 'percentage': | ||
| self.colors["percentage in-progress"] + str(percentage) + | ||
| self.colors['reset-color'], | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| return '\r' + self.format % progress_dict + self.colors['reset-color'] | ||
| if self.style == '{': | ||
| return '\r' + self.format.format(**progress_dict | ||
| ) + self.colors['reset-color'] | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| def progress_complete(self, **kwargs) -> str: | ||
| """Prints the progress bar as complete. | ||
| :param kwargs: Additional variables to be passed to the format string. | ||
| :raises ValueError: If the style is not either `%` or `{`. | ||
| :return: The formatted progress bar. | ||
| """ | ||
| progress_dict = { | ||
| 'prefix': self.prefix, | ||
| 'bar': '', | ||
| 'suffix': self.suffix, | ||
| 'percentage': '100', | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| if key in ['prefix', 'bar', 'suffix', 'percentage']: | ||
| raise ValueError(f'`{key}` is a reserved keyword') | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| bar_length = self.width - len(self.format % progress_dict) | ||
| elif self.style == '{': | ||
| bar_length = self.width - len(self.format.format(**progress_dict)) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| progress_dict = { | ||
| 'prefix': | ||
| self.colors['prefix-color complete'] + self.prefix + | ||
| self.colors['reset-color'], | ||
| 'bar': | ||
| self.colors['progress complete'] + (self.fill * bar_length) + | ||
| self.colors['reset-color'], | ||
| 'suffix': | ||
| self.colors['suffix-color complete'] + self.suffix + | ||
| self.colors['reset-color'], | ||
| 'percentage': | ||
| self.colors["percentage complete"] + '100' + self.colors['reset-color'], | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| return ( | ||
| '\r' + self.format % progress_dict + self.colors['reset-color'] + | ||
| ('\n' if self.new_line_when_complete else '') | ||
| ) | ||
| if self.style == '{': | ||
| return ( | ||
| '\r' + self.format.format(**progress_dict) + | ||
| self.colors['reset-color'] + | ||
| ('\n' if self.new_line_when_complete else '') | ||
| ) | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| def progress_failed(self, progress: float, total: float, **kwargs) -> str: | ||
| """Returns a progress bar with a failed state. | ||
| :param progress: The current progress. | ||
| :param total: The total progress. | ||
| :param kwargs: Additional variables to be passed to the format string. | ||
| :raises ValueError: If the style is not `%` or `{`. | ||
| :return: A progress bar with a failed state. | ||
| """ | ||
| progress_dict = { | ||
| 'prefix': self.prefix, | ||
| 'bar': '', | ||
| 'suffix': self.suffix, | ||
| 'percentage': str(round(progress / total * 100, 2)), | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| if key in ['prefix', 'bar', 'suffix', 'percentage']: | ||
| raise ValueError(f'`{key}` is a reserved keyword') | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| bar_length = self.width - len(self.format % progress_dict) | ||
| elif self.style == '{': | ||
| bar_length = self.width - len(self.format.format(**progress_dict)) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| bar_char = self.fill if progress > total else self.empty | ||
| progress_dict = { | ||
| 'prefix': | ||
| self.colors['prefix-color failed'] + self.prefix + | ||
| self.colors['reset-color'], | ||
| 'bar': | ||
| self.colors['progress failed'] + (bar_char * bar_length) + | ||
| self.colors['reset-color'], | ||
| 'suffix': | ||
| self.colors['suffix-color failed'] + self.suffix + | ||
| self.colors['reset-color'], | ||
| 'percentage': | ||
| self.colors["percentage failed"] + progress_dict['percentage'] + | ||
| self.colors['reset-color'], | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| progress_bar = self.format % progress_dict | ||
| elif self.style == '{': | ||
| progress_bar = self.format.format(**progress_dict) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| return '\r' + progress_bar + self.colors['reset-color'] + ( | ||
| '\n' if self.new_line_when_complete else '' | ||
| ) | ||
| def __call__( | ||
| self, | ||
| progress: float, | ||
| total: float, | ||
| logger: _Optional[_log21.Logger] = None, | ||
| **kwargs | ||
| ) -> None: | ||
| if not logger: | ||
| logger = self.logger | ||
| logger.print(self.get_bar(progress, total, **kwargs), end='') | ||
| def update( | ||
| self, | ||
| progress: float, | ||
| total: float, | ||
| logger: _Optional[_log21.Logger] = None, | ||
| **kwargs | ||
| ) -> None: | ||
| """Update the progress bar. | ||
| :param progress: The current progress. | ||
| :param total: The total progress. | ||
| :param logger: The logger to use. If not specified, the logger specified in the | ||
| constructor will be used. | ||
| :param kwargs: Additional variables to be used in the format string. | ||
| :raises ValueError: If the style is not `%` or `{`. | ||
| """ | ||
| self(progress, total, logger, **kwargs) | ||
| class _Module(_FakeModule): | ||
| def __init__(self, real_module: _ModuleType) -> None: | ||
| super().__init__(real_module, lambda: None) | ||
| self.__progress_bar: _Optional[ProgressBar] = None | ||
| def __call__( | ||
| self, | ||
| progress: float, | ||
| total: float, | ||
| width: _Optional[int] = None, | ||
| prefix: str = '|', | ||
| suffix: str = '|', | ||
| show_percentage: bool = True | ||
| ) -> None: | ||
| """Print a progress bar to the console.""" | ||
| if (not self.__progress_bar | ||
| or (self.__progress_bar.width != width and width is not None) | ||
| or self.__progress_bar.prefix != prefix | ||
| or self.__progress_bar.suffix != suffix | ||
| or self.__progress_bar.style != ('%' if show_percentage else '')): | ||
| self.__progress_bar = ProgressBar( | ||
| width=width, | ||
| prefix=prefix, | ||
| suffix=suffix, | ||
| show_percentage=show_percentage | ||
| ) | ||
| self.__progress_bar(progress, total) | ||
| _sys.modules[__name__] = _Module(_sys.modules[__name__]) |
| # log21.stream_handler.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| import os as _os | ||
| import re as _re | ||
| import shutil as _shutil | ||
| from typing import Optional as _Optional | ||
| from logging import StreamHandler as _StreamHandler | ||
| from log21.colors import (get_colors as _gc, hex_escape as _hex_escape, | ||
| ansi_escape as _ansi_escape) | ||
| # yapf: enable | ||
| __all__ = ['IS_WINDOWS', 'ColorizingStreamHandler', 'StreamHandler'] | ||
| IS_WINDOWS = _os.name == 'nt' | ||
| if IS_WINDOWS: | ||
| import ctypes | ||
| # ruff: noqa: ANN001 | ||
| class StreamHandler(_StreamHandler): | ||
| """A StreamHandler that can handle carriage returns and new lines.""" | ||
| terminator = '' | ||
| def __init__( | ||
| self, | ||
| handle_carriage_return: bool = True, | ||
| handle_new_line: bool = True, | ||
| stream=None, | ||
| formatter=None, | ||
| level=None | ||
| ) -> None: | ||
| """Initialize the StreamHandler. | ||
| :param handle_carriage_return: Whether to handle carriage returns. | ||
| :param handle_new_line: Whether to handle new lines. | ||
| :param stream: The stream to write to. | ||
| :param formatter: The formatter to use. | ||
| :param level: The level to log at. | ||
| """ | ||
| self.HandleCR = handle_carriage_return | ||
| self.HandleNL = handle_new_line | ||
| super().__init__(stream=stream) | ||
| if formatter is not None: | ||
| self.setFormatter(formatter) | ||
| if level is not None: | ||
| self.setLevel(level) | ||
| def check_cr(self, record) -> None: | ||
| """Check if the record contains a carriage return and handle it.""" | ||
| if record.msg: | ||
| msg = _hex_escape.sub( | ||
| '', _ansi_escape.sub('', record.msg.strip(' \t\n\x0b\x0c')) | ||
| ) | ||
| if msg[:1] == '\r': | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): # stdout or stderr | ||
| self.stream.write( | ||
| '\r' + ( | ||
| ' ' * ( | ||
| _shutil.get_terminal_size(file_descriptor).columns - | ||
| 1 | ||
| ) | ||
| ) + '\r' | ||
| ) | ||
| index = record.msg.rfind('\r') | ||
| find = _re.compile(r'(\x1b\[(?:\d+(?:;(?:\d+))*)m)') | ||
| record.msg = _gc(*find.split(record.msg[:index]) | ||
| ) + record.msg[index + 1:] | ||
| def check_nl(self, record) -> None: | ||
| """Check if the record contains a newline and handle it.""" | ||
| while record.msg and record.msg[0] == '\n': | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): # stdout or stderr | ||
| self.stream.write('\n') | ||
| record.msg = record.msg[1:] | ||
| def emit(self, record) -> None: | ||
| if self.HandleCR: | ||
| self.check_cr(record) | ||
| if self.HandleNL: | ||
| self.check_nl(record) | ||
| super().emit(record) | ||
| def clear_line(self, length: _Optional[int] = None) -> None: | ||
| """Clear the current line. | ||
| :param length: The length of the line to clear. | ||
| :return: | ||
| """ | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): | ||
| if length is None: | ||
| length = _shutil.get_terminal_size(file_descriptor).columns | ||
| self.stream.write('\r' + (' ' * (length - 1)) + '\r') | ||
| # A stream handler that supports colorizing. | ||
| class ColorizingStreamHandler(StreamHandler): | ||
| """A stream handler that supports colorizing even in Windows.""" | ||
| def emit(self, record) -> None: | ||
| try: | ||
| if self.HandleCR: | ||
| self.check_cr(record) | ||
| if self.HandleNL: | ||
| self.check_nl(record) | ||
| msg = self.format(record) | ||
| if IS_WINDOWS: | ||
| self.convert_and_write(msg) | ||
| self.convert_and_write(self.terminator) | ||
| else: | ||
| self.write(msg) | ||
| self.write(self.terminator) | ||
| self.flush() | ||
| except Exception: # pylint: disable=broad-except | ||
| self.handleError(record) | ||
| # Writes colorized text to the Windows console. | ||
| def convert_and_write(self, message) -> None: | ||
| """Convert the message to a Windows console colorized message and write it to | ||
| the stream.""" | ||
| nt_color_map = { | ||
| 30: 0, # foreground: black - 0b00000000 | ||
| 31: 4, # foreground: red - 0b00000100 | ||
| 32: 2, # foreground: green - 0b00000010 | ||
| 33: 6, # foreground: yellow - 0b00000110 | ||
| 34: 1, # foreground: blue - 0b00000001 | ||
| 35: 5, # foreground: magenta - 0b00000101 | ||
| 36: 3, # foreground: cyan - 0b00000011 | ||
| 37: 7, # foreground: white - 0b00000111 | ||
| 40: 0, # background: black - 0b00000000 = 0 << 4 | ||
| 41: 64, # background: red - 0b01000000 = 4 << 4 | ||
| 42: 32, # background: green - 0b00100000 = 2 << 4 | ||
| 43: 96, # background: yellow - 0b01100000 = 6 << 4 | ||
| 44: 16, # background: blue - 0b00010000 = 1 << 4 | ||
| 45: 80, # background: magenta - 0b01010000 = 5 << 4 | ||
| 46: 48, # background: cyan - 0b00110000 = 3 << 4 | ||
| 47: 112, # background: white - 0b01110000 = 7 << 4 | ||
| 90: 8, # foreground: gray - 0b00001000 | ||
| 91: 12, # foreground: light red - 0b00001100 | ||
| 92: 10, # foreground: light green - 0b00001010 | ||
| 93: 14, # foreground: light yellow - 0b00001110 | ||
| 94: 9, # foreground: light blue - 0b00001001 | ||
| 95: 13, # foreground: light magenta - 0b00001101 | ||
| 96: 11, # foreground: light cyan - 0b00001011 | ||
| 97: 15, # foreground: light white - 0b00001111 | ||
| 100: 128, # background: gray - 0b10000000 = 8 << 4 | ||
| 101: 192, # background: light red - 0b11000000 = 12 << 4 | ||
| 102: 160, # background: light green - 0b10100000 = 10 << 4 | ||
| 103: 224, # background: light yellow - 0b11100000 = 14 << 4 | ||
| 104: 144, # background: light blue - 0b10010000 = 9 << 4 | ||
| 105: 208, # background: light magenta - 0b11010000 = 13 << 4 | ||
| 106: 176, # background: light cyan - 0b10110000 = 11 << 4 | ||
| 107: 240, # background: light white - 0b11110000 = 15 << 4 | ||
| 2: 8, | ||
| 0: 7 | ||
| } | ||
| parts = _ansi_escape.split(message) | ||
| win_handle = None | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): # stdout or stderr | ||
| win_handle = ctypes.windll.kernel32.GetStdHandle(-10 - file_descriptor) | ||
| while parts: | ||
| text = parts.pop(0) | ||
| if text: | ||
| self.write(text) | ||
| if parts: | ||
| params = parts.pop(0) | ||
| if win_handle is not None: | ||
| params = [int(p) for p in params.split(';')] | ||
| color = 0 | ||
| for param in params: | ||
| if param in nt_color_map: | ||
| color |= nt_color_map[param] | ||
| else: | ||
| pass # error condition ignored | ||
| ctypes.windll.kernel32.SetConsoleTextAttribute(win_handle, color) | ||
| # Writes the message to the console. | ||
| def write(self, message) -> None: | ||
| """Write the message to the stream.""" | ||
| self.stream.write(message) | ||
| self.flush() |
| # log21.tree_print.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| from __future__ import annotations | ||
| from typing import (List as _List, Union as _Union, Mapping as _Mapping, | ||
| Optional as _Optional, Sequence as _Sequence) | ||
| from log21.colors import get_colors as _gc | ||
| # yapf: enable | ||
| DataType = _Union[_Mapping[_Union[int, str], "DataType"], _Sequence["DataType"], str, | ||
| int] | ||
| class TreePrint: | ||
| """A class to help you print objects in a tree-like format.""" | ||
| class Node: | ||
| """A class to represent a node in a tree.""" | ||
| def __init__( | ||
| self, | ||
| value: _Union[str, int], | ||
| children: _Optional[_List[TreePrint.Node]] = None, | ||
| indent: int = 4, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| mode: str = '-' | ||
| ) -> None: | ||
| """Initialize a node. | ||
| :param value: The value of the node. | ||
| :param children: The children of the node. | ||
| :param indent: Number of spaces to indent the node. | ||
| :param colors: Colors to use for the node. | ||
| :param mode: Choose between '-' and '='. | ||
| """ | ||
| self.value = str(value) | ||
| if children: | ||
| self._children = children | ||
| else: | ||
| self._children = [] | ||
| self.indent = indent | ||
| self.colors = { | ||
| 'branches': _gc('Green'), | ||
| 'fruit': _gc('LightMagenta'), | ||
| } | ||
| if colors: | ||
| for key, value_ in colors.items(): | ||
| if key in self.colors: | ||
| self.colors[key] = _gc(value_) | ||
| if not mode: | ||
| self.mode = 1 | ||
| else: | ||
| self.mode = self._get_mode(mode) | ||
| if self.mode == -1: | ||
| raise ValueError('`mode` must be - or =') | ||
| def _get_mode(self, mode: _Optional[_Union[str, int]] = None) -> int: | ||
| if not mode: | ||
| mode = self.mode | ||
| if isinstance(mode, int): | ||
| if mode in [1, 2]: | ||
| return mode | ||
| elif isinstance(mode, str): | ||
| if mode in '-_─┌│|└┬├└': | ||
| return 1 | ||
| if mode in '=═╔║╠╚╦╚': | ||
| return 2 | ||
| return -1 | ||
| def __str__( | ||
| self, | ||
| level: int = 0, | ||
| prefix: str = '', | ||
| mode: _Optional[_Union[str, int]] = None | ||
| ) -> str: | ||
| mode = self._get_mode(mode) | ||
| if mode == -1: | ||
| raise ValueError('`mode` must be - or =') | ||
| chars = '─┌│└┬├└' | ||
| if mode == 2: | ||
| chars = '═╔║╚╦╠╚' | ||
| text = _gc(self.colors['branches']) + prefix | ||
| if level == 0: | ||
| text += chars[0] # ─ OR ═ | ||
| prefix += ' ' | ||
| if self.has_child(): | ||
| text += chars[4] # ┬ OR ╦ | ||
| else: | ||
| text += chars[0] # ─ OR ═ | ||
| text += ' ' + _gc(self.colors['fruit']) + str(self.value) + '\n' | ||
| for i, child in enumerate(self._children): | ||
| prefix_ = '' | ||
| for part in prefix: | ||
| if part in '┌│├┬╔║╠╦': | ||
| prefix_ += chars[2] # │ OR ║ | ||
| elif part in chars: | ||
| prefix_ += ' ' | ||
| else: | ||
| prefix_ += part | ||
| if i + 1 == len(self._children): | ||
| prefix_ += chars[6] # └ OR ╚ | ||
| else: | ||
| prefix_ += chars[5] # ├ OR ╠ | ||
| prefix_ += chars[0] * (self.indent - 1) # ─ OR ═ | ||
| prefix_ = prefix_[:len(prefix)] + _gc(self.colors['branches'] | ||
| ) + prefix_[len(prefix):] | ||
| text += child.__str__(level=level + 1, prefix=prefix_, mode=mode) | ||
| return text | ||
| def has_child(self) -> bool: | ||
| """Return True if node has children, False otherwise.""" | ||
| return len(self._children) > 0 | ||
| def add_child(self, child: TreePrint.Node) -> None: | ||
| """Add a child to the node.""" | ||
| if not isinstance(child, TreePrint.Node): | ||
| raise TypeError('`child` must be TreePrint.Node') | ||
| self._children.append(child) | ||
| def get_child( # noqa: ANN201 | ||
| self, | ||
| value: _Optional[str] = None, | ||
| index: _Optional[int] = None | ||
| ): | ||
| """Get a child by value or index.""" | ||
| if value and index: | ||
| raise ValueError('`value` and `index` can not be both set') | ||
| if not value and not index: | ||
| raise ValueError('`value` or `index` must be set') | ||
| if value: | ||
| for child in self._children: | ||
| if child.value == value: | ||
| return child | ||
| if index: | ||
| return self._children[index] | ||
| raise ValueError(f'Failed to find child: {value = }, {index = }') | ||
| def add_to( | ||
| self: TreePrint.Node, | ||
| data: DataType, | ||
| indent: int = 4, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| mode: str = '-' | ||
| ) -> None: # pylint: disable=too-many-branches | ||
| """Add data to the node.""" | ||
| if isinstance(data, _Mapping): | ||
| if len(data) == 1: | ||
| child = TreePrint.Node( | ||
| list(data.keys())[0], # ty: ignore[invalid-argument-type] | ||
| indent=indent, | ||
| colors=colors, | ||
| mode=mode | ||
| ) | ||
| child.add_to( | ||
| list(data.values())[0], # ty: ignore[invalid-argument-type] | ||
| indent=indent, | ||
| colors=colors, | ||
| mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| else: | ||
| for key, value in data.items(): | ||
| child = TreePrint.Node( | ||
| key, # ty: ignore[invalid-argument-type] | ||
| indent=indent, | ||
| colors=colors, | ||
| mode=mode | ||
| ) | ||
| child.add_to( | ||
| value, # ty: ignore[invalid-argument-type] | ||
| indent=indent, | ||
| colors=colors, | ||
| mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| elif isinstance(data, _Sequence) and not isinstance(data, str): | ||
| if len(data) == 1: | ||
| self.add_child( | ||
| TreePrint.Node( | ||
| data[0], indent=indent, colors=colors, mode=mode | ||
| ) | ||
| ) | ||
| else: | ||
| for value in data: | ||
| if isinstance(value, _Mapping): | ||
| for key, dict_value in value.items(): | ||
| child = TreePrint.Node( | ||
| key, # ty: ignore[invalid-argument-type] | ||
| indent=indent, | ||
| colors=colors, | ||
| mode=mode | ||
| ) | ||
| child.add_to( | ||
| dict_value, # ty: ignore[invalid-argument-type] | ||
| indent=indent, | ||
| colors=colors, | ||
| mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| elif isinstance(value, _Sequence): | ||
| child = TreePrint.Node( | ||
| '>', indent=indent, colors=colors, mode=mode | ||
| ) | ||
| child.add_to(value, indent=indent, colors=colors, mode=mode) | ||
| self.add_child(child) | ||
| else: | ||
| child = TreePrint.Node( | ||
| str(value), indent=indent, colors=colors, mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| else: | ||
| child = TreePrint.Node( | ||
| str(data), indent=indent, colors=colors, mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| def __init__( | ||
| self, | ||
| data: DataType, | ||
| indent: int = 4, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| mode: str = '-' | ||
| ) -> None: | ||
| self.indent = indent | ||
| self.mode = mode | ||
| if isinstance(data, _Mapping): | ||
| if len(data) == 1: | ||
| self.root = self.Node( | ||
| list(data.keys())[0], # ty: ignore[invalid-argument-type] | ||
| indent=indent, | ||
| colors=colors | ||
| ) | ||
| self.add_to_root(list(data.values()), colors=colors) | ||
| else: | ||
| self.root = self.Node('root', indent=indent, colors=colors) | ||
| self.add_to_root(data, colors=colors) | ||
| elif isinstance(data, _Sequence): | ||
| self.root = self.Node('root', indent=indent, colors=colors) | ||
| self.add_to_root(data, colors=colors) | ||
| else: | ||
| self.root = self.Node(str(data), indent=indent, colors=colors) | ||
| def add_to_root( | ||
| self, | ||
| data: _Union[_Mapping, _Sequence, str, int], | ||
| colors: _Optional[_Mapping[str, str]] = None | ||
| ) -> None: | ||
| """Add data to root node.""" | ||
| self.root.add_to(data, indent=self.indent, colors=colors, mode=self.mode) | ||
| def __str__(self, mode: _Optional[_Union[str, int]] = None) -> str: | ||
| if not mode: | ||
| mode = self.mode | ||
| return self.root.__str__(mode=mode) | ||
| def tree_format( | ||
| data: _Union[_Mapping, _Sequence, str, int], | ||
| indent: int = 4, | ||
| mode: str = '-', | ||
| colors: _Optional[_Mapping[str, str]] = None | ||
| ) -> str: | ||
| """Return a tree representation of data. | ||
| :param data: data to be represented as a tree (dict, list, str, int) | ||
| :param indent: number of spaces to indent each level of the tree | ||
| :param mode: mode of tree representation ('-', '=') | ||
| :param colors: colors to use for each level of the tree | ||
| :return: tree representation of data | ||
| """ | ||
| return str(TreePrint(data, indent=indent, colors=colors, mode=mode)) |
+88
-27
@@ -1,31 +0,25 @@ | ||
| Metadata-Version: 2.1 | ||
| Metadata-Version: 2.3 | ||
| Name: log21 | ||
| Version: 2.10.2 | ||
| Summary: A simple logging package that helps you log colorized messages in Windows console. | ||
| Author-email: "CodeWriter21(Mehrad Pooryoussof)" <CodeWriter21@gmail.com> | ||
| Version: 3.0.0 | ||
| Summary: A simple logging package | ||
| Keywords: python,log,colorize,color,logging,Python3,CodeWriter21 | ||
| Author: CodeWriter21(Mehrad Pooryoussof) | ||
| Author-email: CodeWriter21(Mehrad Pooryoussof) <CodeWriter21@gmail.com> | ||
| License: Apache License 2.0 | ||
| Project-URL: Homepage, https://github.com/MPCodeWriter21/log21 | ||
| Project-URL: Donations, https://github.com/MPCodeWriter21/log21/blob/master/DONATE.md | ||
| Project-URL: Source, https://github.com/MPCodeWriter21/log21 | ||
| Keywords: python,log,colorize,color,logging,Python3,CodeWriter21 | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Operating System :: Unix | ||
| Classifier: Operating System :: Microsoft :: Windows | ||
| Classifier: Operating System :: MacOS :: MacOS X | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE.txt | ||
| Requires-Dist: webcolors | ||
| Requires-Dist: docstring-parser | ||
| Provides-Extra: dev | ||
| Requires-Dist: yapf; extra == "dev" | ||
| Requires-Dist: isort; extra == "dev" | ||
| Requires-Dist: docformatter; extra == "dev" | ||
| Requires-Dist: pylint; extra == "dev" | ||
| Requires-Dist: json5; extra == "dev" | ||
| Requires-Dist: pytest; extra == "dev" | ||
| Requires-Python: >=3.9 | ||
| Project-URL: Donations, https://github.com/MPCodeWriter21/log21/blob/master/DONATE.md | ||
| Project-URL: Homepage, https://github.com/MPCodeWriter21/log21 | ||
| Project-URL: Source, https://github.com/MPCodeWriter21/log21 | ||
| Description-Content-Type: text/markdown | ||
@@ -49,3 +43,3 @@ log21 | ||
| the support of ANSI colors. | ||
| + Argument parsing : log21's argument parser can be used like python's argparse but it | ||
| + Argument parsing : log21's argument parser can be used like python's argparse, but it | ||
| also colorizes the output. | ||
@@ -57,4 +51,4 @@ + Logging : A similar logger to logging. Logger but with colorized output and other | ||
| log21's pretty printer can do that. | ||
| + Tree printing : You can pass a dict or list to `log21.tree_print` function and it will | ||
| print it in a tree-like structure. It's also colorized XD. | ||
| + Tree printing : You can pass a dict or list to `log21.tree_print` function, and it | ||
| will print it in a tree-like structure. It's also colorized XD. | ||
| + ProgressBar : log21's progress bar can be used to show progress of a process in a | ||
@@ -102,9 +96,75 @@ beautiful way. | ||
| Changes | ||
| ------- | ||
| Changelog | ||
| --------- | ||
| ### 2.10.1 | ||
| ### v3.0.0 | ||
| + Updated the Argparse module to be usable with python 3.12.3. | ||
| 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**. | ||
| #### Breaking Changes | ||
| + **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 `log21` remain supported. | ||
| + **Argumentify exception renames** | ||
| + Several exceptions were renamed to follow a consistent `*Error` naming convention: | ||
| + `TooFewArguments` → `TooFewArgumentsError` | ||
| + `RequiredArgument` → `RequiredArgumentError` | ||
| + `IncompatibleArguments` → `IncompatibleArgumentsError` | ||
| + Code that explicitly raises or catches these exceptions must be updated. | ||
| #### Changes | ||
| + **Crash reporter behavior improvement** | ||
| + Prevented the default file crash reporter from creating `.crash_report` files when it | ||
| is not actually used. | ||
| + Implemented using an internal `FakeModule` helper. | ||
| + **Argparse compatibility update** | ||
| + Bundled and used the Python 3.13 `argparse` implementation to ensure consistent | ||
| behavior across supported Python versions. | ||
| + **Progress bar module rename** | ||
| + Renamed the internal progress bar module to `progress_bar` for 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 the `FakeModule` helper. | ||
| + **Examples added and updated** | ||
| + Added new example code files. | ||
| + Updated existing examples to match the v3 API and conventions. | ||
| #### Fixes | ||
| + 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. | ||
| #### Internal and Maintenance Changes | ||
| + Migrated the build system configuration to `uv`. | ||
| + Updated Python version classifiers and set the supported Python version to 3.9+. | ||
| + Added `vermin` to 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. | ||
| #### Notes | ||
| + 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](https://github.com/MPCodeWriter21/log21/blob/master/MIGRATION-V2-V3.md) | ||
| for detailed upgrade instructions. | ||
| [Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md) | ||
@@ -138,3 +198,4 @@ | ||
| Or if you can't, give [this project](https://github.com/MPCodeWriter21/log21) a star on GitHub :) | ||
| Or if you can't, give [this project](https://github.com/MPCodeWriter21/log21) a star on | ||
| GitHub :) | ||
@@ -141,0 +202,0 @@ References |
+22
-30
@@ -1,5 +0,1 @@ | ||
| [build-system] | ||
| requires = ["setuptools>=61.0.0", "wheel"] | ||
| build-backend = "setuptools.build_meta" | ||
| [project] | ||
@@ -10,5 +6,5 @@ name = "log21" | ||
| ] | ||
| description = "A simple logging package that helps you log colorized messages in Windows console." | ||
| description = "A simple logging package" | ||
| readme = {file = "README.md", content-type = "text/markdown"} | ||
| requires-python = ">=3.8" | ||
| requires-python = ">=3.9" | ||
| keywords = ['python', 'log', 'colorize', 'color', 'logging', 'Python3', 'CodeWriter21'] | ||
@@ -19,5 +15,6 @@ license = {text = "Apache License 2.0"} | ||
| "Programming Language :: Python :: 3", | ||
| "Programming Language :: Python :: 3.7", | ||
| "Programming Language :: Python :: 3.9", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Operating System :: Unix", | ||
@@ -31,6 +28,7 @@ "Operating System :: Microsoft :: Windows", | ||
| ] | ||
| version = "2.10.2" | ||
| version = "3.0.0" | ||
| [tool.setuptools.packages.find] | ||
| where = ["src"] | ||
| [build-system] | ||
| requires = ["uv_build>=0.8.15,<0.9.0"] | ||
| build-backend = "uv_build" | ||
@@ -42,30 +40,13 @@ [project.urls] | ||
| [project.optional-dependencies] | ||
| dev = ["yapf", "isort", "docformatter", "pylint", "json5", "pytest"] | ||
| [tool.pylint.messages_control] | ||
| max-line-length = 88 | ||
| disable = [ | ||
| "too-few-public-methods", | ||
| "too-many-arguments", | ||
| "protected-access", | ||
| "too-many-locals", | ||
| "fixme", | ||
| ] | ||
| [tool.pylint.design] | ||
| max-returns = 8 | ||
| [tool.yapf] | ||
| column_limit = 88 | ||
| split_before_dot = true | ||
| dedent_closing_brackets = true | ||
| split_before_first_argument = true | ||
| dedent_closing_brackets = true | ||
| [tool.isort] | ||
| line_length = 88 | ||
| order_by_type = true | ||
| length_sort = true | ||
| combine_as_imports = true | ||
| length_sort = true | ||
| order_by_type = true | ||
@@ -76,1 +57,12 @@ [tool.docformatter] | ||
| wrap-descriptions = 88 | ||
| [tool.ruff] | ||
| show-fixes = true | ||
| exclude = ["migrations"] | ||
| target-version = "py39" | ||
| line-length = 88 | ||
| [tool.ruff.lint] | ||
| extend-select = ["C4", "SIM", "TCH", "PL", "ANN", "N", "B"] | ||
| ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0914", "ANN101", "B008", "N816", | ||
| "ANN002", "ANN003", "ANN401", "N802", "PLR2004"] |
+75
-8
@@ -18,3 +18,3 @@ log21 | ||
| the support of ANSI colors. | ||
| + Argument parsing : log21's argument parser can be used like python's argparse but it | ||
| + Argument parsing : log21's argument parser can be used like python's argparse, but it | ||
| also colorizes the output. | ||
@@ -26,4 +26,4 @@ + Logging : A similar logger to logging. Logger but with colorized output and other | ||
| log21's pretty printer can do that. | ||
| + Tree printing : You can pass a dict or list to `log21.tree_print` function and it will | ||
| print it in a tree-like structure. It's also colorized XD. | ||
| + Tree printing : You can pass a dict or list to `log21.tree_print` function, and it | ||
| will print it in a tree-like structure. It's also colorized XD. | ||
| + ProgressBar : log21's progress bar can be used to show progress of a process in a | ||
@@ -71,9 +71,75 @@ beautiful way. | ||
| Changes | ||
| ------- | ||
| Changelog | ||
| --------- | ||
| ### 2.10.1 | ||
| ### v3.0.0 | ||
| + Updated the Argparse module to be usable with python 3.12.3. | ||
| 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**. | ||
| #### Breaking Changes | ||
| + **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 `log21` remain supported. | ||
| + **Argumentify exception renames** | ||
| + Several exceptions were renamed to follow a consistent `*Error` naming convention: | ||
| + `TooFewArguments` → `TooFewArgumentsError` | ||
| + `RequiredArgument` → `RequiredArgumentError` | ||
| + `IncompatibleArguments` → `IncompatibleArgumentsError` | ||
| + Code that explicitly raises or catches these exceptions must be updated. | ||
| #### Changes | ||
| + **Crash reporter behavior improvement** | ||
| + Prevented the default file crash reporter from creating `.crash_report` files when it | ||
| is not actually used. | ||
| + Implemented using an internal `FakeModule` helper. | ||
| + **Argparse compatibility update** | ||
| + Bundled and used the Python 3.13 `argparse` implementation to ensure consistent | ||
| behavior across supported Python versions. | ||
| + **Progress bar module rename** | ||
| + Renamed the internal progress bar module to `progress_bar` for 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 the `FakeModule` helper. | ||
| + **Examples added and updated** | ||
| + Added new example code files. | ||
| + Updated existing examples to match the v3 API and conventions. | ||
| #### Fixes | ||
| + 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. | ||
| #### Internal and Maintenance Changes | ||
| + Migrated the build system configuration to `uv`. | ||
| + Updated Python version classifiers and set the supported Python version to 3.9+. | ||
| + Added `vermin` to 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. | ||
| #### Notes | ||
| + 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](https://github.com/MPCodeWriter21/log21/blob/master/MIGRATION-V2-V3.md) | ||
| for detailed upgrade instructions. | ||
| [Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md) | ||
@@ -107,3 +173,4 @@ | ||
| Or if you can't, give [this project](https://github.com/MPCodeWriter21/log21) a star on GitHub :) | ||
| Or if you can't, give [this project](https://github.com/MPCodeWriter21/log21) a star on | ||
| GitHub :) | ||
@@ -110,0 +177,0 @@ References |
+121
-102
| # log21.__init__.py | ||
| # CodeWriter21 | ||
| # yapf: disable | ||
| import os as _os | ||
| import sys as _sys | ||
| import logging as _logging | ||
| from typing import (Type as _Type, Tuple as _Tuple, Union as _Union, | ||
| Mapping as _Mapping, Optional as _Optional) | ||
| from types import ModuleType as _ModuleType | ||
| from typing import (Type as _Type, Union as _Union, Literal as _Literal, | ||
| Mapping as _Mapping, Iterable as _Iterable, Optional as _Optional) | ||
| from . import CrashReporter | ||
| from .Colors import (Colors, get_color, get_colors, ansi_escape, closest_color, | ||
| from . import crash_reporter | ||
| from .colors import (Colors, get_color, get_colors, ansi_escape, closest_color, | ||
| get_color_name) | ||
| from .Levels import INFO, WARN, DEBUG, ERROR, FATAL, INPUT, NOTSET, WARNING, CRITICAL | ||
| from .Logger import Logger | ||
| from .PPrint import PrettyPrinter, pformat | ||
| from .Manager import Manager | ||
| from .Argparse import ColorizingArgumentParser | ||
| from .TreePrint import TreePrint, tree_format | ||
| from .Formatters import ColorizingFormatter, DecolorizingFormatter, _Formatter | ||
| 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 | ||
| from .levels import INFO, WARN, DEBUG, ERROR, FATAL, INPUT, NOTSET, WARNING, CRITICAL | ||
| from .logger import Logger | ||
| from .pprint import PrettyPrinter, pformat | ||
| from .manager import Manager | ||
| from .argparse import ColorizingArgumentParser | ||
| from .formatters import ColorizingFormatter, DecolorizingFormatter, _Formatter | ||
| from .tree_print import TreePrint, tree_format | ||
| from .argumentify import (ArgumentError, TooFewArgumentsError, RequiredArgumentError, | ||
| IncompatibleArgumentsError, argumentify) | ||
| from .file_handler import FileHandler, DecolorizingFileHandler | ||
| from .progress_bar import ProgressBar | ||
| from ._module_helper import FakeModule as _FakeModule | ||
| from .logging_window import LoggingWindow, LoggingWindowHandler | ||
| from .stream_handler import StreamHandler, ColorizingStreamHandler | ||
| # yapf: enable | ||
| __author__ = 'CodeWriter21 (Mehrad Pooryoussof)' | ||
| __version__ = '2.10.2' | ||
| __github__ = 'Https://GitHub.com/MPCodeWriter21/log21' | ||
| __version__ = '3.0.0' | ||
| __github__ = 'https://GitHub.com/MPCodeWriter21/log21' | ||
| __all__ = [ | ||
@@ -37,6 +44,6 @@ 'ColorizingStreamHandler', 'DecolorizingFileHandler', 'ColorizingFormatter', | ||
| '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', 'ArgumentError', | ||
| 'IncompatibleArguments', 'RequiredArgument', 'TooFewArguments' | ||
| 'log', 'basic_config', 'basicConfig', 'ProgressBar', 'LoggingWindow', | ||
| 'LoggingWindowHandler', 'get_logging_window', 'crash_reporter', 'console_reporter', | ||
| 'file_reporter', 'argumentify', 'ArgumentError', 'IncompatibleArgumentsError', | ||
| 'RequiredArgumentError', 'TooFewArgumentsError' | ||
| ] | ||
@@ -50,3 +57,3 @@ | ||
| fmt: _Optional[str] = None, | ||
| style: str = '%', | ||
| style: _Literal["%", "{", "$"] = "%", | ||
| datefmt: str = '%H:%M:%S', | ||
@@ -57,5 +64,5 @@ show_level: bool = True, | ||
| level_names: _Optional[_Mapping[int, str]] = None, | ||
| level_colors: _Optional[_Mapping[int, _Tuple[str, ...]]] = None, | ||
| level_colors: _Optional[_Mapping[int, tuple[str, ...]]] = None, | ||
| formatter_class: _Type[_logging.Formatter] = ColorizingFormatter | ||
| ): | ||
| ) -> _logging.Formatter: | ||
| # Prepares a formatting if the fmt was None | ||
@@ -95,4 +102,4 @@ if not fmt: | ||
| for key in formatter.level_colors: | ||
| formatter.level_colors[key] = tuple() | ||
| formatter.time_color = tuple() | ||
| formatter.level_colors[key] = () | ||
| formatter.time_color = () | ||
@@ -110,26 +117,42 @@ return formatter | ||
| datefmt: str = '%H:%M:%S', | ||
| style: str = '%', | ||
| style: _Literal["%", "{", "$"] = '%', | ||
| handle_carriage_return: bool = True, | ||
| handle_new_line: bool = True, | ||
| override=False, | ||
| override: bool = False, | ||
| level_names: _Optional[_Mapping[int, str]] = None, | ||
| level_colors: _Optional[_Mapping[int, _Tuple[str, ...]]] = None, | ||
| level_colors: _Optional[_Mapping[int, tuple[str, ...]]] = None, | ||
| file: _Optional[_Union[_os.PathLike, str]] = None | ||
| ) -> Logger: | ||
| """Returns a logging.Logger with colorizing support. >>> >>> import log21 >>> >>> l | ||
| = log21.get_logger() >>> l.warning('Pretty basic, huh?') [14:49:41] [WARNING] Pretty | ||
| basic, huh? >>> l.critical('CONTINUE READING!! please...') [14:50:08] [CRITICAL] | ||
| CONTINUE READING!! please... >>> >>> my_logger = | ||
| log21.get_logger(name='CodeWriter21', level=log21.INFO, ... fmt='{asctime} -> | ||
| [{levelname}]: {message}', style='{', override=True) >>> >>> my_logger.info('FYI: My | ||
| name is Mehrad.') 14:56:12 -> [INFO]: FYI: My name is Mehrad. >>> | ||
| my_logger.error(log21.get_color('LightRed') + 'Oh no! Something went wrong D:') | ||
| 14:56:29 -> [ERROR]: Oh no! Something went wrong D: >>> >>> my_logger.debug(1 ,2 ,3) | ||
| """Returns a logging.Logger with colorizing support. | ||
| >>> | ||
| >>> import log21 | ||
| >>> | ||
| >>> l = log21.get_logger() | ||
| >>> l.warning('Pretty basic, huh?') | ||
| [14:49:41] [WARNING] Pretty basic, huh? | ||
| >>> l.critical('CONTINUE READING!! please...') | ||
| [14:50:08] [CRITICAL] CONTINUE READING!! please... | ||
| >>> | ||
| >>> my_logger = log21.get_logger(name='CodeWriter21', level=log21.INFO, ... fmt='{asctime} -> [{levelname}]: {message}', style='{', override=True) | ||
| >>> | ||
| >>> my_logger.info('FYI: My name is Mehrad.') | ||
| 14:56:12 -> [INFO]: FYI: My name is Mehrad. | ||
| >>> my_logger.error(log21.get_color('LightRed') + 'Oh no! Something went wrong D:') | ||
| 14:56:29 -> [ERROR]: Oh no! Something went wrong D: | ||
| >>> | ||
| >>> my_logger.debug(1 ,2 ,3) | ||
| >>> # It prints Nothing because our logger level is INFO and DEBUG level is lower | ||
| >>> # than INFO. >>> # So let's modify the my_logger's level >>> | ||
| my_logger.setLevel(log21.DEBUG) >>> # Now we try again... >>> my_logger.debug(1, 2, | ||
| 3) 14:57:34 -> [DEBUG]: 1 2 3 >>> # Well Done. Right? >>> # Let's see more >>> | ||
| my_logger.debug('I like %s number!', args=('21', ), end='\033[0m\n\n\n') 15:01:43 -> | ||
| [DEBUG]: I like 21 number! | ||
| >>> # than INFO. | ||
| >>> # So let's modify the my_logger's level | ||
| >>> my_logger.setLevel(log21.DEBUG) | ||
| >>> # Now we try again... | ||
| >>> my_logger.debug(1, 2, 3) | ||
| 14:57:34 -> [DEBUG]: 1 2 3 | ||
| >>> # Well Done. Right? | ||
| >>> # Let's see more | ||
| >>> my_logger.debug('I like %s number!', args=('21', ), end='\033[0m\n\n\n') | ||
| 15:01:43 -> [DEBUG]: I like 21 number! | ||
| >>> # Well, I've got a question... | ||
@@ -208,3 +231,3 @@ >>> # Do you know the name of this color? | ||
| return logger | ||
| return logger # ty: ignore[invalid-return-type] | ||
@@ -220,6 +243,6 @@ | ||
| datefmt: str = '%H:%M:%S', | ||
| style: str = '%', | ||
| style: _Literal["%", "{", "$"] = '%', | ||
| handle_carriage_return: bool = True, | ||
| handle_new_line: bool = True, | ||
| override=False, | ||
| override: bool = False, | ||
| level_names: _Optional[_Mapping[int, str]] = None, | ||
@@ -310,3 +333,3 @@ width: int = 80, | ||
| _manager.addLogger(name, logging_window) | ||
| return logging_window | ||
| return logging_window # ty: ignore[invalid-return-type] | ||
@@ -320,5 +343,5 @@ | ||
| args: tuple = (), | ||
| end='\033[0m\n', | ||
| end: str='\033[0m\n', | ||
| **kwargs | ||
| ): | ||
| ) -> None: | ||
| """Works like the print function but ANSI colors are supported (even on Windows) and | ||
@@ -333,5 +356,5 @@ it ends with a new line and a reset color by default.""" | ||
| args: tuple = (), | ||
| end='', | ||
| end: str='', | ||
| **kwargs | ||
| ): | ||
| ) -> str: | ||
| """Works like the input function but ANSI colors are supported (even on Windows).""" | ||
@@ -342,3 +365,3 @@ logger = get_logger('log21.input', level=DEBUG, show_time=False, show_level=False) | ||
| def getpass(*msg, args: tuple = (), end='', **kwargs): | ||
| def getpass(*msg, args: tuple = (), end: str = '', **kwargs) -> str: | ||
| """Works like the getpass.getpass function but ANSI colors are supported (even on | ||
@@ -351,14 +374,14 @@ Windows).""" | ||
| def pprint( | ||
| obj, | ||
| indent=1, | ||
| width=80, | ||
| depth=None, | ||
| obj, # noqa: ANN001 | ||
| indent: int = 1, | ||
| width: int = 80, | ||
| depth: _Optional[int] = None, | ||
| signs_colors: _Optional[_Mapping[str, str]] = None, | ||
| *, | ||
| sort_dicts=True, | ||
| underscore_numbers=False, | ||
| compact=False, | ||
| end='\033[0m\n', | ||
| sort_dicts: bool = True, | ||
| underscore_numbers: bool = False, | ||
| compact: bool = False, | ||
| end: str = '\033[0m\n', | ||
| **kwargs | ||
| ): | ||
| ) -> None: | ||
| """A colorful version of the pprint.pprint function. | ||
@@ -400,9 +423,9 @@ | ||
| def tree_print( | ||
| obj, | ||
| obj, # noqa: ANN001 | ||
| indent: int = 4, | ||
| mode='-', | ||
| mode: _Literal['-', '='] = '-', | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| end='\033[0m\n', | ||
| end: str = '\033[0m\n', | ||
| **kwargs | ||
| ): | ||
| ) -> None: | ||
| """Prints a tree representation of the given object. (e.g. a dictionary) | ||
@@ -434,5 +457,5 @@ | ||
| errors: _Optional[str] = 'backslashreplace', | ||
| handlers=None, | ||
| stream=None, | ||
| filename=None, | ||
| handlers: _Optional[_Iterable[_logging.Handler]] = None, | ||
| stream=None, # noqa: ANN001 | ||
| filename: _Optional[_Union[str, _os.PathLike]] = None, | ||
| filemode: str = 'a', | ||
@@ -443,3 +466,3 @@ date_format: str = '%H:%M:%S', | ||
| level: _Optional[_Union[int, str]] = None | ||
| ): # pylint: disable=too-many-branches | ||
| ) -> None: # pylint: disable=too-many-branches | ||
| """Do basic configuration for the logging system. | ||
@@ -505,8 +528,7 @@ | ||
| ) | ||
| else: | ||
| if stream or filename: | ||
| raise ValueError( | ||
| "'stream' or 'filename' should not be specified together with " | ||
| "'handlers'" | ||
| ) | ||
| elif stream or filename: | ||
| raise ValueError( | ||
| "'stream' or 'filename' should not be specified together with " | ||
| "'handlers'" | ||
| ) | ||
| if handlers is None: | ||
@@ -544,3 +566,3 @@ if filename: | ||
| def critical(*msg, args=(), **kwargs): | ||
| def critical(*msg, args: tuple = (), **kwargs) -> None: | ||
| """Log a message with severity 'CRITICAL' on the root logger. | ||
@@ -556,3 +578,3 @@ | ||
| def fatal(*msg, args=(), **kwargs): | ||
| def fatal(*msg, args: tuple = (), **kwargs) -> None: | ||
| """Don't use this function, use critical() instead.""" | ||
@@ -562,3 +584,3 @@ critical(*msg, args=args, **kwargs) | ||
| def error(*msg, args=(), **kwargs): | ||
| def error(*msg, args: tuple = (), **kwargs) -> None: | ||
| """Log a message with severity 'ERROR' on the root logger. | ||
@@ -574,3 +596,3 @@ | ||
| def exception(*msg, args=(), exc_info=True, **kwargs): | ||
| def exception(*msg, args: tuple = (), exc_info: bool = True, **kwargs) -> None: | ||
| """Log a message with severity 'ERROR' on the root logger, with exception | ||
@@ -585,3 +607,3 @@ information. | ||
| def warning(*msg, args=(), **kwargs): | ||
| def warning(*msg, args: tuple = (), **kwargs) -> None: | ||
| """Log a message with severity 'WARNING' on the root logger. | ||
@@ -597,3 +619,3 @@ | ||
| def warn(*msg, args=(), **kwargs): | ||
| def warn(*msg, args: tuple = (), **kwargs) -> None: | ||
| """An alias of warning()""" | ||
@@ -603,3 +625,3 @@ warning(*msg, args=args, **kwargs) | ||
| def info(*msg, args=(), **kwargs): | ||
| def info(*msg, args: tuple = (), **kwargs) -> None: | ||
| """Log a message with severity 'INFO' on the root logger. | ||
@@ -615,3 +637,3 @@ | ||
| def debug(*msg, args=(), **kwargs): | ||
| def debug(*msg, args: tuple = (), **kwargs) -> None: | ||
| """Log a message with severity 'DEBUG' on the root logger. | ||
@@ -627,3 +649,3 @@ | ||
| def log(level, *msg, args=(), **kwargs): | ||
| def log(level: int, *msg, args: tuple = (), **kwargs) -> None: | ||
| """Log 'msg % args' with the integer severity 'level' on the root logger. | ||
@@ -639,23 +661,20 @@ | ||
| def progress_bar( | ||
| progress: float, | ||
| total: float, | ||
| width: _Optional[int] = None, | ||
| prefix: str = '|', | ||
| suffix: str = '|', | ||
| show_percentage: bool = True | ||
| ): | ||
| """Print a progress bar to the console.""" | ||
| endl = '\n' | ||
| progress_bar_ = ProgressBar( | ||
| width=width, prefix=prefix, suffix=suffix, show_percentage=show_percentage | ||
| ) | ||
| console_reporter = crash_reporter.ConsoleReporter() | ||
| print(progress_bar_.get_bar(progress, total)) | ||
| class _Module(_FakeModule): | ||
| endl = '\n' | ||
| def __init__(self, real_module: _ModuleType) -> None: | ||
| super().__init__(real_module, lambda: None) | ||
| self.__file_reporter: _Optional[crash_reporter.FileReporter] = None | ||
| console_reporter = CrashReporter.ConsoleReporter() | ||
| @property | ||
| def file_reporter(self) -> crash_reporter.FileReporter: | ||
| if self.__file_reporter is None: | ||
| self.__file_reporter = crash_reporter.FileReporter(file='.crash_report.log') | ||
| return self.__file_reporter | ||
| file_reporter = CrashReporter.FileReporter(file='.crash_report.log') | ||
| _sys.modules[__name__] = _Module(_sys.modules[__name__]) |
-201
| Apache License | ||
| Version 2.0, January 2004 | ||
| http://www.apache.org/licenses/ | ||
| TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||
| 1. Definitions. | ||
| "License" shall mean the terms and conditions for use, reproduction, | ||
| and distribution as defined by Sections 1 through 9 of this document. | ||
| "Licensor" shall mean the copyright owner or entity authorized by | ||
| the copyright owner that is granting the License. | ||
| "Legal Entity" shall mean the union of the acting entity and all | ||
| other entities that control, are controlled by, or are under common | ||
| control with that entity. For the purposes of this definition, | ||
| "control" means (i) the power, direct or indirect, to cause the | ||
| direction or management of such entity, whether by contract or | ||
| otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||
| outstanding shares, or (iii) beneficial ownership of such entity. | ||
| "You" (or "Your") shall mean an individual or Legal Entity | ||
| exercising permissions granted by this License. | ||
| "Source" form shall mean the preferred form for making modifications, | ||
| including but not limited to software source code, documentation | ||
| source, and configuration files. | ||
| "Object" form shall mean any form resulting from mechanical | ||
| transformation or translation of a Source form, including but | ||
| not limited to compiled object code, generated documentation, | ||
| and conversions to other media types. | ||
| "Work" shall mean the work of authorship, whether in Source or | ||
| Object form, made available under the License, as indicated by a | ||
| copyright notice that is included in or attached to the work | ||
| (an example is provided in the Appendix below). | ||
| "Derivative Works" shall mean any work, whether in Source or Object | ||
| form, that is based on (or derived from) the Work and for which the | ||
| editorial revisions, annotations, elaborations, or other modifications | ||
| represent, as a whole, an original work of authorship. For the purposes | ||
| of this License, Derivative Works shall not include works that remain | ||
| separable from, or merely link (or bind by name) to the interfaces of, | ||
| the Work and Derivative Works thereof. | ||
| "Contribution" shall mean any work of authorship, including | ||
| the original version of the Work and any modifications or additions | ||
| to that Work or Derivative Works thereof, that is intentionally | ||
| submitted to Licensor for inclusion in the Work by the copyright owner | ||
| or by an individual or Legal Entity authorized to submit on behalf of | ||
| the copyright owner. For the purposes of this definition, "submitted" | ||
| means any form of electronic, verbal, or written communication sent | ||
| to the Licensor or its representatives, including but not limited to | ||
| communication on electronic mailing lists, source code control systems, | ||
| and issue tracking systems that are managed by, or on behalf of, the | ||
| Licensor for the purpose of discussing and improving the Work, but | ||
| excluding communication that is conspicuously marked or otherwise | ||
| designated in writing by the copyright owner as "Not a Contribution." | ||
| "Contributor" shall mean Licensor and any individual or Legal Entity | ||
| on behalf of whom a Contribution has been received by Licensor and | ||
| subsequently incorporated within the Work. | ||
| 2. Grant of Copyright License. Subject to the terms and conditions of | ||
| this License, each Contributor hereby grants to You a perpetual, | ||
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||
| copyright license to reproduce, prepare Derivative Works of, | ||
| publicly display, publicly perform, sublicense, and distribute the | ||
| Work and such Derivative Works in Source or Object form. | ||
| 3. Grant of Patent License. Subject to the terms and conditions of | ||
| this License, each Contributor hereby grants to You a perpetual, | ||
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||
| (except as stated in this section) patent license to make, have made, | ||
| use, offer to sell, sell, import, and otherwise transfer the Work, | ||
| where such license applies only to those patent claims licensable | ||
| by such Contributor that are necessarily infringed by their | ||
| Contribution(s) alone or by combination of their Contribution(s) | ||
| with the Work to which such Contribution(s) was submitted. If You | ||
| institute patent litigation against any entity (including a | ||
| cross-claim or counterclaim in a lawsuit) alleging that the Work | ||
| or a Contribution incorporated within the Work constitutes direct | ||
| or contributory patent infringement, then any patent licenses | ||
| granted to You under this License for that Work shall terminate | ||
| as of the date such litigation is filed. | ||
| 4. Redistribution. You may reproduce and distribute copies of the | ||
| Work or Derivative Works thereof in any medium, with or without | ||
| modifications, and in Source or Object form, provided that You | ||
| meet the following conditions: | ||
| (a) You must give any other recipients of the Work or | ||
| Derivative Works a copy of this License; and | ||
| (b) You must cause any modified files to carry prominent notices | ||
| stating that You changed the files; and | ||
| (c) You must retain, in the Source form of any Derivative Works | ||
| that You distribute, all copyright, patent, trademark, and | ||
| attribution notices from the Source form of the Work, | ||
| excluding those notices that do not pertain to any part of | ||
| the Derivative Works; and | ||
| (d) If the Work includes a "NOTICE" text file as part of its | ||
| distribution, then any Derivative Works that You distribute must | ||
| include a readable copy of the attribution notices contained | ||
| within such NOTICE file, excluding those notices that do not | ||
| pertain to any part of the Derivative Works, in at least one | ||
| of the following places: within a NOTICE text file distributed | ||
| as part of the Derivative Works; within the Source form or | ||
| documentation, if provided along with the Derivative Works; or, | ||
| within a display generated by the Derivative Works, if and | ||
| wherever such third-party notices normally appear. The contents | ||
| of the NOTICE file are for informational purposes only and | ||
| do not modify the License. You may add Your own attribution | ||
| notices within Derivative Works that You distribute, alongside | ||
| or as an addendum to the NOTICE text from the Work, provided | ||
| that such additional attribution notices cannot be construed | ||
| as modifying the License. | ||
| You may add Your own copyright statement to Your modifications and | ||
| may provide additional or different license terms and conditions | ||
| for use, reproduction, or distribution of Your modifications, or | ||
| for any such Derivative Works as a whole, provided Your use, | ||
| reproduction, and distribution of the Work otherwise complies with | ||
| the conditions stated in this License. | ||
| 5. Submission of Contributions. Unless You explicitly state otherwise, | ||
| any Contribution intentionally submitted for inclusion in the Work | ||
| by You to the Licensor shall be under the terms and conditions of | ||
| this License, without any additional terms or conditions. | ||
| Notwithstanding the above, nothing herein shall supersede or modify | ||
| the terms of any separate license agreement you may have executed | ||
| with Licensor regarding such Contributions. | ||
| 6. Trademarks. This License does not grant permission to use the trade | ||
| names, trademarks, service marks, or product names of the Licensor, | ||
| except as required for reasonable and customary use in describing the | ||
| origin of the Work and reproducing the content of the NOTICE file. | ||
| 7. Disclaimer of Warranty. Unless required by applicable law or | ||
| agreed to in writing, Licensor provides the Work (and each | ||
| Contributor provides its Contributions) on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||
| implied, including, without limitation, any warranties or conditions | ||
| of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||
| PARTICULAR PURPOSE. You are solely responsible for determining the | ||
| appropriateness of using or redistributing the Work and assume any | ||
| risks associated with Your exercise of permissions under this License. | ||
| 8. Limitation of Liability. In no event and under no legal theory, | ||
| whether in tort (including negligence), contract, or otherwise, | ||
| unless required by applicable law (such as deliberate and grossly | ||
| negligent acts) or agreed to in writing, shall any Contributor be | ||
| liable to You for damages, including any direct, indirect, special, | ||
| incidental, or consequential damages of any character arising as a | ||
| result of this License or out of the use or inability to use the | ||
| Work (including but not limited to damages for loss of goodwill, | ||
| work stoppage, computer failure or malfunction, or any and all | ||
| other commercial damages or losses), even if such Contributor | ||
| has been advised of the possibility of such damages. | ||
| 9. Accepting Warranty or Additional Liability. While redistributing | ||
| the Work or Derivative Works thereof, You may choose to offer, | ||
| and charge a fee for, acceptance of support, warranty, indemnity, | ||
| or other liability obligations and/or rights consistent with this | ||
| License. However, in accepting such obligations, You may act only | ||
| on Your own behalf and on Your sole responsibility, not on behalf | ||
| of any other Contributor, and only if You agree to indemnify, | ||
| defend, and hold each Contributor harmless for any liability | ||
| incurred by, or claims asserted against, such Contributor by reason | ||
| of your accepting any such warranty or additional liability. | ||
| END OF TERMS AND CONDITIONS | ||
| APPENDIX: How to apply the Apache License to your work. | ||
| To apply the Apache License to your work, attach the following | ||
| boilerplate notice, with the fields enclosed by brackets "[]" | ||
| replaced with your own identifying information. (Don't include | ||
| the brackets!) The text should be enclosed in the appropriate | ||
| comment syntax for the file format. We also recommend that a | ||
| file or class name and description of purpose be included on the | ||
| same "printed page" as the copyright notice for easier | ||
| identification within third-party archives. | ||
| Copyright [2021-2024] [CodeWriter21] | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. |
| [egg_info] | ||
| tag_build = | ||
| tag_date = 0 | ||
| Metadata-Version: 2.1 | ||
| Name: log21 | ||
| Version: 2.10.2 | ||
| Summary: A simple logging package that helps you log colorized messages in Windows console. | ||
| Author-email: "CodeWriter21(Mehrad Pooryoussof)" <CodeWriter21@gmail.com> | ||
| License: Apache License 2.0 | ||
| Project-URL: Homepage, https://github.com/MPCodeWriter21/log21 | ||
| Project-URL: Donations, https://github.com/MPCodeWriter21/log21/blob/master/DONATE.md | ||
| Project-URL: Source, https://github.com/MPCodeWriter21/log21 | ||
| Keywords: python,log,colorize,color,logging,Python3,CodeWriter21 | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Operating System :: Unix | ||
| Classifier: Operating System :: Microsoft :: Windows | ||
| Classifier: Operating System :: MacOS :: MacOS X | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE.txt | ||
| Requires-Dist: webcolors | ||
| Requires-Dist: docstring-parser | ||
| Provides-Extra: dev | ||
| Requires-Dist: yapf; extra == "dev" | ||
| Requires-Dist: isort; extra == "dev" | ||
| Requires-Dist: docformatter; extra == "dev" | ||
| Requires-Dist: pylint; extra == "dev" | ||
| Requires-Dist: json5; extra == "dev" | ||
| Requires-Dist: pytest; extra == "dev" | ||
| log21 | ||
| ===== | ||
|  | ||
|  | ||
|  | ||
|  | ||
| [](https://www.codefactor.io/repository/github/mpcodewriter21/log21) | ||
| A simple logging package that helps you log colorized messages in Windows console and | ||
| other operating systems. | ||
| Features | ||
| -------- | ||
| + Colors : The main reason for this package was to log text in the Windows console with | ||
| the support of ANSI colors. | ||
| + Argument parsing : log21's argument parser can be used like python's argparse but it | ||
| also colorizes the output. | ||
| + Logging : A similar logger to logging. Logger but with colorized output and other | ||
| options such as levelname modifications. It can also decolorize the output if you want | ||
| to log into a file. | ||
| + Pretty printing : Have you ever wanted to colorize the output of the pprint module? | ||
| log21's pretty printer can do that. | ||
| + Tree printing : You can pass a dict or list to `log21.tree_print` function and it will | ||
| print it in a tree-like 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! | ||
| + 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. | ||
|  | ||
|  | ||
| Installation | ||
| ------------ | ||
| Well, this is a python package so the first thing you need is python. | ||
| If you don't have python installed, please visit [Python.org](https://python.org) and | ||
| install the latest version of python. | ||
| Then you can install log21 using pip module: | ||
| ```bash | ||
| python -m pip install log21 -U | ||
| ``` | ||
| Or you can clone [the repository](https://github.com/MPCodeWriter21/log21) and run: | ||
| ```bash | ||
| pip install . | ||
| ``` | ||
| Or let the pip get it using git: | ||
| ```bash | ||
| pip install git+https://github.com/MPCodeWriter21/log21 | ||
| ``` | ||
| Changes | ||
| ------- | ||
| ### 2.10.1 | ||
| + Updated the Argparse module to be usable with python 3.12.3. | ||
| [Full CHANGELOG](https://github.com/MPCodeWriter21/log21/blob/master/CHANGELOG.md) | ||
| Usage Examples | ||
| --------------- | ||
| See [EXAMPLES.md](https://github.com/MPCodeWriter21/log21/blob/master/EXAMPLES.md) | ||
| About | ||
| ----- | ||
| Author: CodeWriter21 (Mehrad Pooryoussof) | ||
| GitHub: [MPCodeWriter21](https://github.com/MPCodeWriter21) | ||
| Telegram Channel: [@CodeWriter21](https://t.me/CodeWriter21) | ||
| ### License | ||
|  | ||
| [apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | ||
| ### Donate | ||
| In order to support this project you can donate some crypto of your choice 8D | ||
| [Donate Addresses](https://github.com/MPCodeWriter21/log21/blob/master/DONATE.md) | ||
| Or if you can't, give [this project](https://github.com/MPCodeWriter21/log21) a star on GitHub :) | ||
| References | ||
| ---------- | ||
| + ANSI Color Codes (Wikipedia): | ||
| [https://en.wikipedia.org/wiki/ANSI_escape_code](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) |
| webcolors | ||
| docstring-parser | ||
| [dev] | ||
| yapf | ||
| isort | ||
| docformatter | ||
| pylint | ||
| json5 | ||
| pytest |
| LICENSE.txt | ||
| README.md | ||
| pyproject.toml | ||
| src/log21/Argparse.py | ||
| src/log21/Argumentify.py | ||
| src/log21/Colors.py | ||
| src/log21/FileHandler.py | ||
| src/log21/Formatters.py | ||
| src/log21/Levels.py | ||
| src/log21/Logger.py | ||
| src/log21/LoggingWindow.py | ||
| src/log21/Manager.py | ||
| src/log21/PPrint.py | ||
| src/log21/ProgressBar.py | ||
| src/log21/StreamHandler.py | ||
| src/log21/TreePrint.py | ||
| src/log21/__init__.py | ||
| src/log21.egg-info/PKG-INFO | ||
| src/log21.egg-info/SOURCES.txt | ||
| src/log21.egg-info/dependency_links.txt | ||
| src/log21.egg-info/requires.txt | ||
| src/log21.egg-info/top_level.txt | ||
| src/log21/CrashReporter/Formatters.py | ||
| src/log21/CrashReporter/Reporters.py | ||
| src/log21/CrashReporter/__init__.py |
| # log21.Argparse.py | ||
| # CodeWriter21 | ||
| from __future__ import annotations | ||
| import re as _re | ||
| import sys as _sys | ||
| import types as _types | ||
| import typing as _typing | ||
| import argparse as _argparse | ||
| import collections.abc | ||
| from enum import Enum as _Enum | ||
| from typing import (Tuple as _Tuple, Mapping as _Mapping, NoReturn, | ||
| Optional as _Optional, Sequence as _Sequence) | ||
| from gettext import gettext as _gettext | ||
| from textwrap import TextWrapper as _TextWrapper | ||
| from collections import OrderedDict as _OrderedDict | ||
| import log21 as _log21 | ||
| from log21.Colors import get_colors as _gc | ||
| from log21.Formatters import DecolorizingFormatter as _Formatter | ||
| __all__ = [ | ||
| 'ColorizingArgumentParser', 'ColorizingHelpFormatter', 'ColorizingTextWrapper', | ||
| 'Literal' | ||
| ] | ||
| class Literal: | ||
| """A class for representing literals in argparse arguments.""" | ||
| def __init__(self, literal: _typing._LiteralGenericAlias): | ||
| self.literal = literal | ||
| # Only str arguments are allowed | ||
| if not all(map(lambda x: isinstance(x, str), self.literal.__args__)): | ||
| raise TypeError('Only str arguments are allowed for Literal.') | ||
| def __repr__(self): | ||
| return f'Literal[{", ".join(map(str, self.literal.__args__))}]' | ||
| def __str__(self): | ||
| return self.__repr__() | ||
| def __call__(self, value): | ||
| if value not in self.literal.__args__: | ||
| raise ValueError( | ||
| f'Value must be one of [{", ".join(map(str, self.literal.__args__))}]' | ||
| ) | ||
| return value | ||
| class ColorizingHelpFormatter(_argparse.HelpFormatter): | ||
| """A help formatter that supports colorizing help messages.""" | ||
| def __init__( | ||
| self, | ||
| prog, | ||
| indent_increment=2, | ||
| max_help_position=24, | ||
| width=None, | ||
| colors: _Optional[_Mapping[str, str]] = None | ||
| ): | ||
| super().__init__(prog, indent_increment, max_help_position, width) | ||
| self.colors = { | ||
| 'usage': 'Cyan', | ||
| 'brackets': 'LightRed', | ||
| 'switches': 'LightCyan', | ||
| 'values': 'Green', | ||
| 'colons': 'LightRed', | ||
| 'commas': 'LightRed', | ||
| 'section headers': 'LightGreen', | ||
| 'help': 'LightWhite', | ||
| 'choices': 'LightGreen' | ||
| } | ||
| if colors: | ||
| for key, value in colors.items(): | ||
| if key in self.colors: | ||
| self.colors[key] = value | ||
| class _Section: | ||
| def __init__(self, formatter, parent, heading=None): | ||
| self.formatter = formatter | ||
| self.parent = parent | ||
| self.heading = heading | ||
| self.items = [] | ||
| def format_help(self): | ||
| # format the indented section | ||
| if self.parent is not None: | ||
| self.formatter._indent() | ||
| join = self.formatter._join_parts | ||
| item_help = join([func(*args) for func, args in self.items]) | ||
| if self.parent is not None: | ||
| self.formatter._dedent() | ||
| # return nothing if the section was empty | ||
| if not item_help: | ||
| return '' | ||
| # add the heading if the section was non-empty | ||
| if self.heading is not _argparse.SUPPRESS and self.heading is not None: | ||
| current_indent = self.formatter._current_indent | ||
| heading = ( | ||
| '%*s%s' % (current_indent, '', self.heading) + | ||
| _gc(self.formatter.colors['colons']) + ':\033[0m\n' | ||
| ) | ||
| else: | ||
| heading = '' | ||
| # join the section-initial newline, the heading and the help | ||
| return join( | ||
| ['\n', heading, | ||
| _gc(self.formatter.colors['help']), item_help, '\n'] | ||
| ) | ||
| def _add_item(self, func, args): | ||
| self._current_section.items.append((func, args)) | ||
| def _fill_text(self, text, width, indent): | ||
| text = self._whitespace_matcher.sub(' ', text).strip() | ||
| return ColorizingTextWrapper( | ||
| width=width, initial_indent=indent, subsequent_indent=indent | ||
| ).fill(text) | ||
| def _split_lines(self, text, width): | ||
| text = self._whitespace_matcher.sub(' ', text).strip() | ||
| return ColorizingTextWrapper(width=width).wrap(text) | ||
| def start_section(self, heading): | ||
| self._indent() | ||
| section = self._Section( | ||
| self, self._current_section, | ||
| _gc(self.colors['section headers']) + str(heading) + '\033[0m' | ||
| ) | ||
| self._add_item(section.format_help, []) | ||
| self._current_section = section | ||
| def _format_action(self, action): | ||
| # determine the required width and the entry label | ||
| help_position = min(self._action_max_length + 2, self._max_help_position) | ||
| help_width = max(self._width - help_position, 11) | ||
| action_width = help_position - self._current_indent - 2 | ||
| action_header = _gc('rst') + self._format_action_invocation(action) | ||
| indent_first = 0 | ||
| # no help; start on same line and add a final newline | ||
| if not action.help: | ||
| action_header = self._current_indent * ' ' + action_header + '\n' | ||
| # short action name; start on the same line and pad two spaces | ||
| elif len(action_header) <= action_width: | ||
| action_header = '%*s%-*s ' % ( | ||
| self._current_indent, '', action_width, action_header | ||
| ) | ||
| # long action name; start on the next line | ||
| else: | ||
| action_header = self._current_indent * ' ' + action_header + '\n' | ||
| indent_first = help_position | ||
| # collect the pieces of the action help | ||
| parts = [action_header] | ||
| # if there was help for the action, add lines of help text | ||
| if action.help: | ||
| help_text = _gc(self.colors['help']) + self._expand_help(action) | ||
| help_lines = self._split_lines(help_text, help_width) | ||
| parts.append('%*s%s\n' % (indent_first, '', help_lines[0])) | ||
| for line in help_lines[1:]: | ||
| parts.append('%*s%s\n' % (help_position, '', line)) | ||
| # or add a newline if the description doesn't end with one | ||
| elif not action_header.endswith('\n'): | ||
| parts.append('\n') | ||
| # if there are any sub-actions, add their help as well | ||
| for subaction in self._iter_indented_subactions(action): | ||
| parts.append(self._format_action(subaction)) | ||
| # return a single string | ||
| return self._join_parts(parts) | ||
| # modified upstream code, not going to refactor for complexity. | ||
| def _format_usage(self, usage, actions, groups, prefix): # noqa: C901 | ||
| if prefix is None: | ||
| prefix = _gettext('usage: ') | ||
| # if usage is specified, use that | ||
| if usage is not None: | ||
| usage = usage % dict(prog=self._prog) | ||
| # if no optionals or positionals are available, usage is just prog | ||
| elif usage is None and not actions: | ||
| usage = '%(prog)s' % dict(prog=self._prog) | ||
| # if optionals and positionals are available, calculate usage | ||
| elif usage is None: | ||
| prog = '%(prog)s' % dict(prog=self._prog) | ||
| # split optionals from positionals | ||
| optionals = [] | ||
| positionals = [] | ||
| for action in actions: | ||
| if action.option_strings: | ||
| optionals.append(action) | ||
| else: | ||
| positionals.append(action) | ||
| # build full usage string | ||
| action_usage = self._format_actions_usage(optionals + positionals, groups) | ||
| usage = ' '.join([s for s in [prog, action_usage] if s]) | ||
| # wrap the usage parts if it's too long | ||
| text_width = self._width - self._current_indent | ||
| if len(prefix) + len(_Formatter.decolorize(usage)) > text_width: | ||
| # break usage into wrappable parts | ||
| part_regexp = r'\(.*?\)+|\[.*?\]+|\S+' | ||
| opt_usage = self._format_actions_usage(optionals, groups) | ||
| pos_usage = self._format_actions_usage(positionals, groups) | ||
| opt_parts = _re.findall(part_regexp, opt_usage) | ||
| pos_parts = _re.findall(part_regexp, pos_usage) | ||
| assert ' '.join(opt_parts) == opt_usage | ||
| assert ' '.join(pos_parts) == pos_usage | ||
| # helper for wrapping lines | ||
| def get_lines(parts, indent, prefix=None): | ||
| lines = [] | ||
| line = [] | ||
| if prefix is not None: | ||
| line_len = len(prefix) - 1 | ||
| else: | ||
| line_len = len(indent) - 1 | ||
| for part in parts: | ||
| if line_len + 1 + len(_Formatter.decolorize(part) | ||
| ) > text_width and line: | ||
| lines.append(indent + ' '.join(line)) | ||
| line = [] | ||
| line_len = len(indent) - 1 | ||
| line.append(part) | ||
| line_len += len(_Formatter.decolorize(part)) + 1 | ||
| if line: | ||
| lines.append(indent + ' '.join(line)) | ||
| if prefix is not None: | ||
| lines[0] = lines[0][len(indent):] | ||
| return lines | ||
| # if prog is short, follow it with optionals or positionals | ||
| len_prog = len(_Formatter.decolorize(prog)) | ||
| if len(prefix) + len_prog <= 0.75 * text_width: | ||
| indent = ' ' * (len(prefix) + len_prog + 1) | ||
| if opt_parts: | ||
| lines = get_lines([prog] + opt_parts, indent, prefix) | ||
| lines.extend(get_lines(pos_parts, indent)) | ||
| elif pos_parts: | ||
| lines = get_lines([prog] + pos_parts, indent, prefix) | ||
| else: | ||
| lines = [prog] | ||
| # if prog is long, put it on its own line | ||
| else: | ||
| indent = ' ' * len(prefix) | ||
| parts = opt_parts + pos_parts | ||
| lines = get_lines(parts, indent) | ||
| if len(lines) > 1: | ||
| lines = [] | ||
| lines.extend(get_lines(opt_parts, indent)) | ||
| lines.extend(get_lines(pos_parts, indent)) | ||
| lines = [prog] + lines | ||
| # join lines into usage | ||
| usage = '\n'.join(lines) | ||
| # prefix with 'usage:' | ||
| return prefix + usage + '\n\n' | ||
| def _format_actions_usage(self, actions: list, groups): | ||
| # find group indices and identify actions in groups | ||
| group_actions = set() | ||
| inserts = {} | ||
| for group in groups: | ||
| if not group._group_actions: | ||
| raise ValueError(f'empty group {group}') | ||
| try: | ||
| start = actions.index(group._group_actions[0]) | ||
| except ValueError: | ||
| continue | ||
| else: | ||
| group_action_count = len(group._group_actions) | ||
| end = start + group_action_count | ||
| if actions[start:end] == group._group_actions: | ||
| suppressed_actions_count = 0 | ||
| for action in group._group_actions: | ||
| group_actions.add(action) | ||
| if action.help is _argparse.SUPPRESS: | ||
| suppressed_actions_count += 1 | ||
| exposed_actions_count = group_action_count - suppressed_actions_count | ||
| if not exposed_actions_count: | ||
| continue | ||
| if not group.required: | ||
| if start in inserts: | ||
| inserts[start] += ' [' | ||
| else: | ||
| inserts[start] = '[' | ||
| if end in inserts: | ||
| inserts[end] += ']' | ||
| else: | ||
| inserts[end] = ']' | ||
| elif exposed_actions_count > 1: | ||
| if start in inserts: | ||
| inserts[start] += ' (' | ||
| else: | ||
| inserts[start] = '(' | ||
| if end in inserts: | ||
| inserts[end] += ')' | ||
| else: | ||
| inserts[end] = ')' | ||
| for i in range(start + 1, end): | ||
| inserts[i] = '|' | ||
| # collect all actions format strings | ||
| parts = [] | ||
| for i, action in enumerate(actions): | ||
| # suppressed arguments are marked with None | ||
| # remove | separators for suppressed arguments | ||
| if action.help is _argparse.SUPPRESS: | ||
| parts.append(None) | ||
| if inserts.get(i) == '|': | ||
| inserts.pop(i) | ||
| elif inserts.get(i + 1) == '|': | ||
| inserts.pop(i + 1) | ||
| # produce all arg strings | ||
| elif not action.option_strings: | ||
| default = self._get_default_metavar_for_positional(action) | ||
| part = self._format_args(action, default) | ||
| # if it's in a group, strip the outer [] | ||
| if action in group_actions: | ||
| if part[0] == '[' and part[-1] == ']': | ||
| part = part[1:-1] | ||
| # add the action string to the list | ||
| parts.append(part) | ||
| # produce the first way to invoke the option in brackets | ||
| else: | ||
| option_string = action.option_strings[0] | ||
| # if the Optional doesn't take a value, format is: | ||
| # -s or --long | ||
| if action.nargs == 0: | ||
| part = _gc(self.colors['switches']) + action.format_usage() | ||
| # if the Optional takes a value, format is: | ||
| # -s ARGS or --long ARGS | ||
| else: | ||
| default = self._get_default_metavar_for_optional(action) | ||
| args_string = self._format_args(action, default) | ||
| part = _gc(self.colors['switches']) + '%s %s%s' % ( | ||
| option_string, _gc(self.colors['values']), args_string | ||
| ) | ||
| # make it look optional if it's not required or in a group | ||
| if not action.required and action not in group_actions: | ||
| part = _gc(self.colors['brackets']) + '[' + part + _gc( | ||
| self.colors['brackets'] | ||
| ) + ']\033[0m' | ||
| # add the action string to the list | ||
| parts.append(part) | ||
| # insert things at the necessary indices | ||
| for i in sorted(inserts, reverse=True): | ||
| parts[i:i] = [inserts[i]] | ||
| # join all the action items with spaces | ||
| text = ' '.join([item for item in parts if item is not None]) | ||
| # clean up separators for mutually exclusive groups | ||
| open = r'[\[(]' | ||
| close = r'[\])]' | ||
| text = _re.sub(r'(%s) ' % open, r'\1', text) | ||
| text = _re.sub(r' (%s)' % close, r'\1', text) | ||
| text = _re.sub(r'%s *%s' % (open, close), r'', text) | ||
| text = _re.sub(r'\(([^|]*)\)', r'\1', text) | ||
| text = text.strip() | ||
| # return the text | ||
| return text | ||
| def _format_action_invocation(self, action): | ||
| if not action.option_strings: | ||
| default = self._get_default_metavar_for_positional(action) | ||
| metavar, = self._metavar_formatter(action, default)(1) | ||
| return metavar | ||
| else: | ||
| parts = [] | ||
| # if the Optional doesn't take a value, format is: | ||
| # -s, --long | ||
| if action.nargs == 0: | ||
| for option_string in action.option_strings: | ||
| parts.append(_gc(self.colors['switches']) + option_string) | ||
| # if the Optional takes a value, format is: | ||
| # -s ARGS, --long ARGS | ||
| else: | ||
| default = self._get_default_metavar_for_optional(action) | ||
| args_string = self._format_args(action, default) | ||
| for option_string in action.option_strings: | ||
| parts.append( | ||
| _gc(self.colors['switches']) + '%s %s%s' % | ||
| (option_string, _gc(self.colors['values']), args_string) | ||
| ) | ||
| return _gc(self.colors['commas']) + ', '.join(parts) | ||
| def _metavar_formatter(self, action, default_metavar): | ||
| if action.metavar is not None: | ||
| result = action.metavar | ||
| elif action.choices is not None: | ||
| choice_strs = [str(choice) for choice in action.choices] | ||
| result = ( | ||
| _gc(self.colors['brackets']) + '{ ' + | ||
| (_gc(self.colors['commas']) + ', ').join( | ||
| _gc(self.colors['choices']) + choice_str | ||
| for choice_str in choice_strs | ||
| ) + _gc(self.colors['brackets']) + ' }' | ||
| ) | ||
| else: | ||
| result = default_metavar | ||
| def format(tuple_size): | ||
| if isinstance(result, tuple): | ||
| return result | ||
| else: | ||
| return (result, ) * tuple_size | ||
| return format | ||
| class ColorizingTextWrapper(_TextWrapper): | ||
| # modified upstream code, not going to refactor for complexity. | ||
| def _wrap_chunks(self, chunks): # noqa: C901 | ||
| """_wrap_chunks(chunks : [string]) -> [string] | ||
| Wrap a sequence of text chunks and return a list of lines of | ||
| length 'self.width' or less. (If 'break_long_words' is false, | ||
| some lines may be longer than this.) Chunks correspond roughly | ||
| to words and the whitespace between them: each chunk is | ||
| indivisible (modulo 'break_long_words'), but a line break can | ||
| come between any two chunks. Chunks should not have internal | ||
| whitespace; i.e. a chunk is either all whitespace or a "word". | ||
| Whitespace chunks will be removed from the beginning and end of | ||
| lines, but apart from that whitespace is preserved. | ||
| """ | ||
| lines = [] | ||
| if self.width <= 0: | ||
| raise ValueError("invalid width %r (must be > 0)" % self.width) | ||
| if self.max_lines is not None: | ||
| if self.max_lines > 1: | ||
| indent = self.subsequent_indent | ||
| else: | ||
| indent = self.initial_indent | ||
| if len(indent) + len(self.placeholder.lstrip()) > self.width: | ||
| raise ValueError("placeholder too large for max width") | ||
| # Arrange in reverse order so items can be efficiently popped | ||
| # from a stack of chucks. | ||
| chunks.reverse() | ||
| while chunks: | ||
| # Start the list of chunks that will make up the current line. | ||
| # current_len is just the length of all the chunks in current_line. | ||
| current_line = [] | ||
| current_len = 0 | ||
| # Figure out which static string will prefix this line. | ||
| if lines: | ||
| indent = self.subsequent_indent | ||
| else: | ||
| indent = self.initial_indent | ||
| # Maximum width for this line. | ||
| width = self.width - len(indent) | ||
| # First chunk on the line is whitespace -- drop it, unless this | ||
| # is the very beginning of the text (i.e. no lines started yet). | ||
| if self.drop_whitespace and _Formatter.decolorize(chunks[-1] | ||
| ).strip() == '' and lines: | ||
| del chunks[-1] | ||
| while chunks: | ||
| # modified upstream code, not going to refactor for ambiguous variable | ||
| # name. | ||
| length = len(_Formatter.decolorize(chunks[-1])) # noqa: E741 | ||
| # Can at least squeeze this chunk onto the current line. | ||
| # Modified upstream code, not going to refactor for ambiguous variable | ||
| # name. | ||
| if current_len + length <= width: # noqa: E741 | ||
| current_line.append(chunks.pop()) | ||
| current_len += length | ||
| # Nope, this line is full. | ||
| else: | ||
| break | ||
| # The current line is full, and the next chunk is too big to | ||
| # fit on *any* line (not just this one). | ||
| if chunks and len(_Formatter.decolorize(chunks[-1])) > width: | ||
| self._handle_long_word(chunks, current_line, current_len, width) | ||
| current_len = sum(map(len, current_line)) | ||
| # If the last chunk on this line is all whitespace, drop it. | ||
| if self.drop_whitespace and current_line and _Formatter.decolorize( | ||
| current_line[-1]).strip() == '': | ||
| current_len -= len(_Formatter.decolorize(current_line[-1])) | ||
| del current_line[-1] | ||
| if current_line: | ||
| if (self.max_lines is None or len(lines) + 1 < self.max_lines | ||
| or (not chunks or self.drop_whitespace and len(chunks) == 1 | ||
| and not chunks[0].strip()) and current_len <= width): | ||
| # Convert current line back to a string and store it in | ||
| # list of all lines (return value). | ||
| lines.append(indent + ''.join(current_line)) | ||
| else: | ||
| while current_line: | ||
| if _Formatter.decolorize( | ||
| current_line[-1] | ||
| ).strip() and current_len + len(self.placeholder) <= width: | ||
| current_line.append(self.placeholder) | ||
| lines.append(indent + ''.join(current_line)) | ||
| break | ||
| current_len -= len(_Formatter.decolorize(current_line[-1])) | ||
| del current_line[-1] | ||
| else: | ||
| if lines: | ||
| prev_line = lines[-1].rstrip() | ||
| if len(_Formatter.decolorize(prev_line)) + len( | ||
| self.placeholder) <= self.width: | ||
| lines[-1] = prev_line + self.placeholder | ||
| break | ||
| lines.append(indent + self.placeholder.lstrip()) | ||
| break | ||
| return lines | ||
| class _ActionsContainer(_argparse._ActionsContainer): | ||
| """Container for the actions for a single command line option.""" | ||
| # pylint: disable=too-many-branches | ||
| def _validate_func_type(self, action, func_type, kwargs, level: int = 0) -> _Tuple: | ||
| # raise an error if the action type is not callable | ||
| if (hasattr(_types, 'UnionType') and not callable(func_type) | ||
| and not isinstance(func_type, (_types.UnionType, tuple))): | ||
| raise ValueError(f'{func_type} is not callable; level={level}') | ||
| # Handle `UnionType` as a type (e.g. `int|str`) | ||
| if hasattr(_types, 'UnionType') and isinstance(func_type, _types.UnionType): | ||
| func_type = func_type.__args__ # type: ignore | ||
| # Handle `Literal` as a type (e.g. `Literal[1, 2, 3]`) | ||
| elif (hasattr(_typing, '_LiteralGenericAlias') | ||
| and isinstance(func_type, _typing._LiteralGenericAlias)): # type: ignore | ||
| func_type = Literal(func_type) | ||
| # Handle `Union` and `Optional` as a type (e.g. `Union[int, str]` and | ||
| # `Optional[int]`) | ||
| elif (hasattr(_typing, '_UnionGenericAlias') | ||
| and isinstance(func_type, _typing._UnionGenericAlias)): # type: ignore | ||
| # Optional[T] is just Union[T, NoneType] | ||
| # Optional | ||
| if (hasattr(_types, 'NoneType') and len(func_type.__args__) == 2 | ||
| and func_type.__args__[1] is _types.NoneType): | ||
| action.required = False | ||
| func_type = func_type.__args__[0] | ||
| # Union | ||
| else: | ||
| func_type = func_type.__args__ # type: ignore | ||
| # Handle `List` as a type (e.g. `List[int]`) | ||
| elif (hasattr(_typing, '_GenericAlias') | ||
| and isinstance(func_type, _typing._GenericAlias) # type: ignore | ||
| and getattr(func_type, '__origin__') is list): | ||
| func_type = func_type.__args__[0] | ||
| if kwargs.get('nargs') is None: | ||
| action.nargs = '+' | ||
| # Handle `Sequence` as a type (e.g. `Sequence[int]`) | ||
| elif (hasattr(_typing, '_GenericAlias') | ||
| and isinstance(func_type, _typing._GenericAlias) # type: ignore | ||
| and getattr(func_type, '__origin__') is collections.abc.Sequence): | ||
| func_type = func_type.__args__[0] | ||
| if kwargs.get('nargs') is None: | ||
| action.nargs = '+' | ||
| # Handle `Required` as a type (e.g. `Required[int]`) | ||
| elif (hasattr(_typing, 'Required') and hasattr(_typing, '_GenericAlias') | ||
| and isinstance(func_type, _typing._GenericAlias) # type: ignore | ||
| and getattr(func_type, '__origin__') is _typing.Required): | ||
| func_type = func_type.__args__[0] | ||
| action.required = True | ||
| # Handle Enum as a type | ||
| elif callable(func_type) and isinstance(func_type, type) and issubclass( | ||
| func_type, _Enum) and action.choices is None and level == 0: | ||
| action.choices = tuple( | ||
| map(lambda x: x.value, func_type.__members__.values()) | ||
| ) | ||
| # Handle SpecialForms | ||
| elif isinstance(func_type, _typing._SpecialForm): | ||
| if func_type is _typing.Any: | ||
| func_type = None | ||
| elif func_type is _typing.ClassVar: | ||
| func_type = None | ||
| elif func_type is _typing.Union: | ||
| func_type = None | ||
| elif func_type is _typing.Optional: | ||
| func_type = None | ||
| action.required = False | ||
| elif func_type is _typing.Type: | ||
| func_type = None | ||
| elif func_type is _typing.TypeVar: | ||
| func_type = None | ||
| else: | ||
| raise ValueError(f'Unknown special form {func_type}') | ||
| elif func_type is _argparse.FileType: | ||
| raise ValueError( | ||
| f'{func_type} is a FileType class object, instance of it must be passed' | ||
| ) | ||
| if isinstance(func_type, _Sequence): | ||
| temp = [] | ||
| for type_ in _OrderedDict(zip(func_type, [0] * len(func_type))): | ||
| temp.extend(self._validate_func_type(action, type_, kwargs, level + 1)) | ||
| func_type = tuple(temp) | ||
| else: | ||
| if (hasattr(_types, 'UnionType') and hasattr(_typing, '_GenericAlias') | ||
| and hasattr(_typing, '_UnionGenericAlias') | ||
| and hasattr(_typing, '_LiteralGenericAlias') and isinstance( | ||
| func_type, | ||
| ( | ||
| _typing._GenericAlias, # type: ignore | ||
| _typing._UnionGenericAlias, # type: ignore | ||
| _typing._LiteralGenericAlias, # type: ignore | ||
| _types.UnionType, | ||
| ))): | ||
| func_type = self._validate_func_type( | ||
| action, func_type, kwargs, level + 1 | ||
| ) | ||
| else: | ||
| func_type = (func_type, ) | ||
| return func_type | ||
| # Override the default add_argument method defined in argparse._ActionsContainer | ||
| # to add the support for different type annotations | ||
| def add_argument(self, *args, **kwargs): | ||
| """Add an argument to the parser. | ||
| Signature: | ||
| add_argument(dest, ..., name=value, ...) | ||
| add_argument(option_string, option_string, ..., name=value, ...) | ||
| """ | ||
| # if no positional args are supplied or only one is supplied and | ||
| # it doesn't look like an option string, parse a positional | ||
| # argument | ||
| chars = self.prefix_chars | ||
| if not args or len(args) == 1 and args[0][0] not in chars: | ||
| if args and 'dest' in kwargs: | ||
| raise ValueError('dest supplied twice for positional argument') | ||
| kwargs = self._get_positional_kwargs(*args, **kwargs) | ||
| # otherwise, we're adding an optional argument | ||
| else: | ||
| kwargs = self._get_optional_kwargs(*args, **kwargs) | ||
| # if no default was supplied, use the parser-level default | ||
| if 'default' not in kwargs: | ||
| dest = kwargs['dest'] | ||
| if dest in self._defaults: | ||
| kwargs['default'] = self._defaults[dest] | ||
| elif self.argument_default is not None: | ||
| kwargs['default'] = self.argument_default | ||
| # create the action object, and add it to the parser | ||
| action_class = self._pop_action_class(kwargs) | ||
| if not callable(action_class): | ||
| raise ValueError(f'unknown action "{action_class}"') | ||
| action = action_class(**kwargs) | ||
| func_type = self._registry_get('type', action.type, action.type) | ||
| action.type = self._validate_func_type( | ||
| action, func_type, kwargs | ||
| ) # type: ignore | ||
| if len(action.type) == 1: | ||
| action.type = action.type[0] | ||
| elif len(action.type) == 0: | ||
| action.type = None | ||
| # raise an error if the metavar does not match the type | ||
| if hasattr(self, "_get_formatter"): | ||
| try: | ||
| self._get_formatter()._format_args(action, None) | ||
| except TypeError: | ||
| raise ValueError( | ||
| "length of metavar tuple does not match nargs" | ||
| ) from None | ||
| return self._add_action(action) | ||
| def add_argument_group(self, *args, **kwargs): | ||
| group = _ArgumentGroup(self, *args, **kwargs) | ||
| self._action_groups.append(group) | ||
| return group | ||
| def add_mutually_exclusive_group(self, **kwargs): | ||
| group = _MutuallyExclusiveGroup(self, **kwargs) | ||
| self._mutually_exclusive_groups.append(group) | ||
| return group | ||
| class ColorizingArgumentParser(_argparse.ArgumentParser, _ActionsContainer): | ||
| """An ArgumentParser that colorizes its output and more.""" | ||
| def __init__( | ||
| self, | ||
| formatter_class=ColorizingHelpFormatter, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| **kwargs | ||
| ): | ||
| self.logger = _log21.Logger('ArgumentParser') | ||
| self.colors = colors | ||
| super().__init__(formatter_class=formatter_class, **kwargs) | ||
| def _print_message(self, message, file=None): | ||
| if message: | ||
| self.logger.handlers.clear() | ||
| handler = _log21.ColorizingStreamHandler(stream=file) | ||
| self.logger.addHandler(handler) | ||
| self.logger.info(message + _gc('rst')) | ||
| def exit(self, status=0, message=None): | ||
| if message: | ||
| self._print_message(_gc('lr') + message + _gc('rst'), _sys.stderr) | ||
| _sys.exit(status) | ||
| def error(self, message): | ||
| self.print_usage(_sys.stderr) | ||
| args = {'prog': self.prog, 'message': message} | ||
| self.exit( | ||
| 2, | ||
| _gettext( | ||
| f'%(prog)s: {_gc("r")}error{_gc("lr")}:{_gc("rst")} %(message)s\n' | ||
| ) % args | ||
| ) | ||
| return NoReturn | ||
| def _get_formatter(self): | ||
| if hasattr(self.formatter_class, 'colors'): | ||
| return self.formatter_class(prog=self.prog, colors=self.colors) | ||
| return self.formatter_class(prog=self.prog) | ||
| def _get_value(self, action, arg_string): | ||
| """Override _get_value to add support for types such as Union and Literal.""" | ||
| func_type = self._registry_get('type', action.type, action.type) | ||
| if not callable(func_type) and not isinstance(func_type, tuple): | ||
| raise _argparse.ArgumentError( | ||
| action, _gettext(f'{func_type!r} is not callable') | ||
| ) | ||
| name = getattr(action.type, '__name__', repr(action.type)) | ||
| # convert the value to the appropriate type | ||
| try: | ||
| if callable(func_type): | ||
| result = func_type(arg_string) | ||
| else: | ||
| exception = ValueError() | ||
| for type_ in func_type: | ||
| name = getattr(type_, '__name__', repr(type_)) | ||
| try: | ||
| result = type_(arg_string) | ||
| break | ||
| except (ValueError, TypeError) as ex: | ||
| exception = ex | ||
| else: | ||
| raise exception | ||
| # ArgumentTypeErrors indicate errors | ||
| except _argparse.ArgumentTypeError as ex: | ||
| msg = str(ex) | ||
| raise _argparse.ArgumentError(action, msg) | ||
| # TypeErrors or ValueErrors also indicate errors | ||
| except (TypeError, ValueError): | ||
| raise _argparse.ArgumentError( | ||
| action, _gettext(f'invalid {name!s} value: {arg_string!r}') | ||
| ) from None | ||
| # return the converted value | ||
| return result | ||
| def __convert_type(self, func_type, arg_string): | ||
| result = None | ||
| if callable(func_type): | ||
| try: | ||
| result = func_type(arg_string) | ||
| except Exception: | ||
| pass | ||
| else: | ||
| for type_ in func_type: | ||
| try: | ||
| result = type_(arg_string) | ||
| break | ||
| except Exception: | ||
| pass | ||
| return result | ||
| def _check_value(self, action, choice): | ||
| # converted value must be one of the choices (if specified) | ||
| if action.choices is not None: | ||
| choices = set(action.choices) | ||
| func_type = self._registry_get('type', action.type, action.type) | ||
| if not callable(func_type) and not isinstance(func_type, tuple): | ||
| raise _argparse.ArgumentError( | ||
| action, _gettext(f'{func_type!r} is not callable') | ||
| ) | ||
| for value in action.choices: | ||
| choices.add(self.__convert_type(func_type, value)) | ||
| if choice not in choices: | ||
| raise _argparse.ArgumentError( | ||
| action, | ||
| _gettext( | ||
| f'invalid choice: {choice!r} ' | ||
| f'(choose from {", ".join(map(repr, action.choices))})' | ||
| ) | ||
| ) | ||
| def _read_args_from_files(self, arg_strings): | ||
| # expand arguments referencing files | ||
| new_arg_strings = [] | ||
| for arg_string in arg_strings: | ||
| # for regular arguments, just add them back into the list | ||
| if not arg_string or arg_string[0] not in (self.fromfile_prefix_chars | ||
| or ''): | ||
| new_arg_strings.append(arg_string) | ||
| # replace arguments referencing files with the file content | ||
| else: | ||
| try: | ||
| with open(arg_string[1:], encoding='utf-8') as args_file: | ||
| arg_strings = [] | ||
| for arg_line in args_file.read().splitlines(): | ||
| for arg in self.convert_arg_line_to_args(arg_line): | ||
| arg_strings.append(arg) | ||
| arg_strings = self._read_args_from_files(arg_strings) | ||
| new_arg_strings.extend(arg_strings) | ||
| except OSError as err: | ||
| self.error(str(err)) | ||
| # return the modified argument list | ||
| return new_arg_strings | ||
| def _parse_known_args(self, arg_strings, namespace): | ||
| # replace arg strings that are file references | ||
| if self.fromfile_prefix_chars is not None: | ||
| arg_strings = self._read_args_from_files(arg_strings) | ||
| # map all mutually exclusive arguments to the other arguments | ||
| # they can't occur with | ||
| action_conflicts = {} | ||
| for mutex_group in self._mutually_exclusive_groups: | ||
| group_actions = mutex_group._group_actions | ||
| for i, mutex_action in enumerate(mutex_group._group_actions): | ||
| conflicts = action_conflicts.setdefault(mutex_action, []) | ||
| conflicts.extend(group_actions[:i]) | ||
| conflicts.extend(group_actions[i + 1:]) | ||
| # find all option indices, and determine the arg_string_pattern | ||
| # which has an 'O' if there is an option at an index, | ||
| # an 'A' if there is an argument, or a '-' if there is a '--' | ||
| option_string_indices = {} | ||
| arg_string_pattern_parts = [] | ||
| arg_strings_iter = iter(arg_strings) | ||
| for i, arg_string in enumerate(arg_strings_iter): | ||
| # all args after -- are non-options | ||
| if arg_string == '--': | ||
| arg_string_pattern_parts.append('-') | ||
| for arg_string in arg_strings_iter: | ||
| arg_string_pattern_parts.append('A') | ||
| # otherwise, add the arg to the arg strings | ||
| # and note the index if it was an option | ||
| else: | ||
| option_tuple = self._parse_optional(arg_string) | ||
| if option_tuple is None: | ||
| pattern = 'A' | ||
| else: | ||
| option_string_indices[i] = option_tuple | ||
| pattern = 'O' | ||
| arg_string_pattern_parts.append(pattern) | ||
| # join the pieces together to form the pattern | ||
| arg_strings_pattern = ''.join(arg_string_pattern_parts) | ||
| # converts arg strings to the appropriate and then takes the action | ||
| seen_actions = set() | ||
| seen_non_default_actions = set() | ||
| def take_action(action, argument_strings, option_string=None): | ||
| seen_actions.add(action) | ||
| argument_values = self._get_values(action, argument_strings) | ||
| # error if this argument is not allowed with other previously | ||
| # seen arguments, assuming that actions that use the default | ||
| # value don't really count as "present" | ||
| if argument_values is not action.default: | ||
| seen_non_default_actions.add(action) | ||
| for conflict_action in action_conflicts.get(action, []): | ||
| if conflict_action in seen_non_default_actions: | ||
| action_name = _argparse._get_action_name(conflict_action) | ||
| msg = _gettext(f'not allowed with argument {action_name}') | ||
| raise _argparse.ArgumentError(action, msg) | ||
| # take the action if we didn't receive a SUPPRESS value | ||
| # (e.g. from a default) | ||
| if argument_values is not _argparse.SUPPRESS: | ||
| action(self, namespace, argument_values, option_string) | ||
| # function to convert arg_strings into an optional action | ||
| def consume_optional(start_index): | ||
| # get the optional identified at this index | ||
| option_tuple = option_string_indices[start_index] | ||
| if len(option_tuple) == 3: | ||
| action, option_string, explicit_arg = option_tuple | ||
| sep = None | ||
| elif len(option_tuple) == 4: | ||
| action, option_string, sep, explicit_arg = option_tuple | ||
| else: | ||
| # Tell the user that there seem to have been a change in argparse module | ||
| # and if they see this error they should immediately report it in an | ||
| # issue at GitHub.com/MPCodeWriter21/log21 with their Python version | ||
| raise ValueError( | ||
| 'Unknown option tuple length, please report this issue at: ' | ||
| 'https://GitHub.com/MPCodeWriter21/log21\n' | ||
| f'Python version: {_sys.version}' | ||
| f'Option tuple: {option_tuple}' | ||
| f'log21 version: {_log21.__version__}' | ||
| ) | ||
| # identify additional optionals in the same arg string | ||
| # (e.g. -xyz is the same as -x -y -z if no args are required) | ||
| match_argument = self._match_argument | ||
| action_tuples = [] | ||
| while True: | ||
| # if we found no optional action, skip it | ||
| if action is None: | ||
| extras.append(arg_strings[start_index]) | ||
| return start_index + 1 | ||
| # if there is an explicit argument, try to match the | ||
| # optional's string arguments to only this | ||
| if explicit_arg is not None: | ||
| arg_count = match_argument(action, 'A') | ||
| # if the action is a single-dash option and takes no | ||
| # arguments, try to parse more single-dash options out | ||
| # of the tail of the option string | ||
| chars = self.prefix_chars | ||
| if arg_count == 0 and option_string[1] not in chars: | ||
| if sep or explicit_arg[0] in chars: | ||
| msg = _gettext('ignored explicit argument %r') | ||
| raise _argparse.ArgumentError(action, msg % explicit_arg) | ||
| action_tuples.append((action, [], option_string)) | ||
| char = option_string[0] | ||
| option_string = char + explicit_arg[0] | ||
| optionals_map = self._option_string_actions | ||
| if option_string in optionals_map: | ||
| action = optionals_map[option_string] | ||
| explicit_arg = explicit_arg[1:] | ||
| if not explicit_arg: | ||
| sep = explicit_arg = None | ||
| elif explicit_arg[0] == '=': | ||
| sep = '=' | ||
| explicit_arg = explicit_arg[1:] | ||
| else: | ||
| sep = '' | ||
| else: | ||
| extras.append(char + explicit_arg) | ||
| stop = start_index + 1 | ||
| break | ||
| # if the action expect exactly one argument, we've | ||
| # successfully matched the option; exit the loop | ||
| elif arg_count == 1: | ||
| stop = start_index + 1 | ||
| args = [explicit_arg] | ||
| action_tuples.append((action, args, option_string)) | ||
| break | ||
| # error if a double-dash option did not use the | ||
| # explicit argument | ||
| else: | ||
| msg = _gettext('ignored explicit argument %r') | ||
| raise _argparse.ArgumentError(action, msg % explicit_arg) | ||
| # if there is no explicit argument, try to match the | ||
| # optional's string arguments with the following strings | ||
| # if successful, exit the loop | ||
| else: | ||
| start = start_index + 1 | ||
| selected_patterns = arg_strings_pattern[start:] | ||
| arg_count = match_argument(action, selected_patterns) | ||
| stop = start + arg_count | ||
| args = arg_strings[start:stop] | ||
| action_tuples.append((action, args, option_string)) | ||
| break | ||
| # add the Optional to the list and return the index at which | ||
| # the Optional's string args stopped | ||
| assert action_tuples | ||
| for action, args, option_string in action_tuples: | ||
| take_action(action, args, option_string) | ||
| return stop | ||
| # the list of Positionals left to be parsed; this is modified | ||
| # by consume_positionals() | ||
| positionals = self._get_positional_actions() | ||
| # function to convert arg_strings into positional actions | ||
| def consume_positionals(start_index): | ||
| # match as many Positionals as possible | ||
| match_partial = self._match_arguments_partial | ||
| selected_pattern = arg_strings_pattern[start_index:] | ||
| arg_counts = match_partial(positionals, selected_pattern) | ||
| # slice off the appropriate arg strings for each Positional | ||
| # and add the Positional and its args to the list | ||
| for action, arg_count in zip(positionals, arg_counts): | ||
| args = arg_strings[start_index:start_index + arg_count] | ||
| start_index += arg_count | ||
| take_action(action, args) | ||
| # slice off the Positionals that we just parsed and return the | ||
| # index at which the Positionals' string args stopped | ||
| positionals[:] = positionals[len(arg_counts):] | ||
| return start_index | ||
| # consume Positionals and Optionals alternately, until we have | ||
| # passed the last option string | ||
| extras = [] | ||
| start_index = 0 | ||
| if option_string_indices: | ||
| max_option_string_index = max(option_string_indices) | ||
| else: | ||
| max_option_string_index = -1 | ||
| while start_index <= max_option_string_index: | ||
| # consume any Positionals preceding the next option | ||
| next_option_string_index = min( | ||
| [index for index in option_string_indices if index >= start_index] | ||
| ) | ||
| if start_index != next_option_string_index: | ||
| positionals_end_index = consume_positionals(start_index) | ||
| # only try to parse the next optional if we didn't consume | ||
| # the option string during the positionals parsing | ||
| if positionals_end_index > start_index: | ||
| start_index = positionals_end_index | ||
| continue | ||
| else: | ||
| start_index = positionals_end_index | ||
| # if we consumed all the positionals we could and we're not | ||
| # at the index of an option string, there were extra arguments | ||
| if start_index not in option_string_indices: | ||
| strings = arg_strings[start_index:next_option_string_index] | ||
| extras.extend(strings) | ||
| start_index = next_option_string_index | ||
| # consume the next optional and any arguments for it | ||
| start_index = consume_optional(start_index) | ||
| # consume any positionals following the last Optional | ||
| stop_index = consume_positionals(start_index) | ||
| # if we didn't consume all the argument strings, there were extras | ||
| extras.extend(arg_strings[stop_index:]) | ||
| # make sure all required actions were present and also convert | ||
| # action defaults which were not given as arguments | ||
| required_actions = [] | ||
| for action in self._actions: | ||
| if action not in seen_actions: | ||
| if action.required: | ||
| required_actions.append(_argparse._get_action_name(action)) | ||
| else: | ||
| # Convert action default now instead of doing it before | ||
| # parsing arguments to avoid calling convert functions | ||
| # twice (which may fail) if the argument was given, but | ||
| # only if it was defined already in the namespace | ||
| if (action.default is not None and isinstance(action.default, str) | ||
| and hasattr(namespace, action.dest) | ||
| and action.default is getattr(namespace, action.dest)): | ||
| setattr( | ||
| namespace, action.dest, | ||
| self._get_value(action, action.default) | ||
| ) | ||
| if required_actions: | ||
| self.error( | ||
| _gettext( | ||
| 'the following arguments are required: ' + | ||
| ", ".join(required_actions) | ||
| ) | ||
| ) | ||
| # make sure all required groups had one option present | ||
| for group in self._mutually_exclusive_groups: | ||
| if group.required: | ||
| for action in group._group_actions: | ||
| if action in seen_non_default_actions: | ||
| break | ||
| # if no actions were used, report the error | ||
| else: | ||
| names = [ | ||
| _argparse._get_action_name(action) | ||
| for action in group._group_actions | ||
| if action.help is not _argparse.SUPPRESS | ||
| ] | ||
| self.error( | ||
| _gettext( | ||
| 'one of the arguments ' + | ||
| ' '.join(name for name in names if name is not None) + | ||
| ' is required' | ||
| ) | ||
| ) | ||
| for group in self._action_groups: | ||
| if isinstance(group, _ArgumentGroup) and group.required: | ||
| for action in group._group_actions: | ||
| if action in seen_non_default_actions: | ||
| break | ||
| # if no actions were used, report the error | ||
| else: | ||
| names = [ | ||
| _argparse._get_action_name(action) | ||
| for action in group._group_actions | ||
| if action.help is not _argparse.SUPPRESS | ||
| ] | ||
| self.error( | ||
| _gettext( | ||
| 'one of the arguments ' + | ||
| ' '.join(name for name in names if name is not None) + | ||
| ' is required' | ||
| ) | ||
| ) | ||
| # return the updated namespace and the extra arguments | ||
| return namespace, extras | ||
| class _ArgumentGroup(_argparse._ArgumentGroup, _ActionsContainer): | ||
| def __init__( | ||
| self, | ||
| container, | ||
| title=None, | ||
| description=None, | ||
| required: bool = False, | ||
| **kwargs | ||
| ): | ||
| super().__init__(container, title=title, description=description, **kwargs) | ||
| self.required = required | ||
| class _MutuallyExclusiveGroup(_argparse._MutuallyExclusiveGroup, _ArgumentGroup): | ||
| pass |
| # log21.Argparse.py | ||
| # CodeWriter21 | ||
| import re as _re | ||
| import string as _string | ||
| import asyncio as _asyncio | ||
| import inspect as _inspect | ||
| 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, Awaitable as _Awaitable, | ||
| 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 | ||
| import log21.Argparse as _Argparse | ||
| __all__ = [ | ||
| 'argumentify', 'ArgumentifyError', 'ArgumentTypeError', 'FlagGenerationError', | ||
| 'RESERVED_FLAGS', 'Callable', 'Argument', 'FunctionInfo', 'generate_flag', | ||
| 'normalize_name', 'normalize_name_to_snake_case', 'ArgumentError', | ||
| 'IncompatibleArguments', 'RequiredArgument', 'TooFewArguments' | ||
| ] | ||
| 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 | ||
| 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. | ||
| >>> 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: _OrderedDict[str, Argument] = _field(init=False) | ||
| docstring: _Docstring = _field(init=False) | ||
| parser: _Argparse.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( # pylint: disable=too-many-branches | ||
| 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: | ||
| if no_dash: | ||
| raise FlagGenerationError( | ||
| f"Failed to generate a flag for argument: {argument}" | ||
| ) | ||
| else: | ||
| 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[_Argparse.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 argument.annotation == bool: | ||
| config['action'] = 'store_true' | ||
| elif argument.annotation: | ||
| config['type'] = argument.annotation | ||
| 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 = _Argparse.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) | ||
| 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. | ||
| :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 = _Argparse.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) | ||
| 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. | ||
| 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 |
| # log21.Colors.py | ||
| # CodeWriter21 | ||
| import re as _re | ||
| from typing import Union as _Union, Sequence as _Sequence | ||
| import webcolors as _webcolors | ||
| __all__ = [ | ||
| 'Colors', 'get_color', 'get_colors', 'ansi_escape', 'get_color_name', | ||
| 'closest_color', 'hex_escape', 'RESET', 'BLACK', 'RED', 'GREEN', 'YELLOW', | ||
| 'BLUE', 'MAGENTA', 'CYAN', 'WHITE', 'BACK_BLACK', 'BACK_RED', 'BACK_GREEN', | ||
| 'BACK_YELLOW', 'BACK_BLUE', 'BACK_MAGENTA', 'BACK_CYAN', 'BACK_WHITE', | ||
| 'GREY', 'LIGHT_RED', 'LIGHT_GREEN', 'LIGHT_YELLOW', 'LIGHT_BLUE', | ||
| 'LIGHT_MAGENTA', 'LIGHT_CYAN', 'LIGHT_WHITE', 'BACK_GREY', 'BACK_LIGHT_RED', | ||
| 'BACK_LIGHT_GREEN', 'BACK_LIGHT_YELLOW', 'BACK_LIGHT_BLUE', | ||
| 'BACK_LIGHT_MAGENTA', 'BACK_LIGHT_CYAN', 'BACK_LIGHT_WHITE' | ||
| ] | ||
| # Regex pattern to find ansi colors in message | ||
| ansi_escape = _re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m') | ||
| hex_escape = _re.compile(r'\x1b(#[0-9a-fA-F]{6})h([f|b])') | ||
| RESET = '\033[0m' | ||
| BLACK = '\033[30m' | ||
| RED = '\033[31m' | ||
| GREEN = '\033[32m' | ||
| YELLOW = '\033[33m' | ||
| BLUE = '\033[34m' | ||
| MAGENTA = '\033[35m' | ||
| CYAN = '\033[36m' | ||
| WHITE = '\033[37m' | ||
| BACK_BLACK = '\033[40m' | ||
| BACK_RED = '\033[41m' | ||
| BACK_GREEN = '\033[42m' | ||
| BACK_YELLOW = '\033[43m' | ||
| BACK_BLUE = '\033[44m' | ||
| BACK_MAGENTA = '\033[45m' | ||
| BACK_CYAN = '\033[46m' | ||
| BACK_WHITE = '\033[47m' | ||
| GREY = '\033[90m' | ||
| LIGHT_RED = '\033[91m' | ||
| LIGHT_GREEN = '\033[92m' | ||
| LIGHT_YELLOW = '\033[93m' | ||
| LIGHT_BLUE = '\033[94m' | ||
| LIGHT_MAGENTA = '\033[95m' | ||
| LIGHT_CYAN = '\033[96m' | ||
| LIGHT_WHITE = '\033[97m' | ||
| BACK_GREY = '\033[100m' | ||
| BACK_LIGHT_RED = '\033[101m' | ||
| BACK_LIGHT_GREEN = '\033[102m' | ||
| BACK_LIGHT_YELLOW = '\033[103m' | ||
| BACK_LIGHT_BLUE = '\033[104m' | ||
| BACK_LIGHT_MAGENTA = '\033[105m' | ||
| BACK_LIGHT_CYAN = '\033[106m' | ||
| BACK_LIGHT_WHITE = '\033[107m' | ||
| class Colors: | ||
| """A class containing color-maps.""" | ||
| color_map = { | ||
| 'Reset': RESET, | ||
| 'Black': BLACK, | ||
| 'Red': RED, | ||
| 'Green': GREEN, | ||
| 'Yellow': YELLOW, | ||
| 'Blue': BLUE, | ||
| 'Magenta': MAGENTA, | ||
| 'Cyan': CYAN, | ||
| 'White': WHITE, | ||
| 'BackBlack': BACK_BLACK, | ||
| 'BackRed': BACK_RED, | ||
| 'BackGreen': BACK_GREEN, | ||
| 'BackYellow': BACK_YELLOW, | ||
| 'BackBlue': BACK_BLUE, | ||
| 'BackMagenta': BACK_MAGENTA, | ||
| 'BackCyan': BACK_CYAN, | ||
| 'BackWhite': BACK_WHITE, | ||
| 'Grey': GREY, | ||
| 'LightRed': LIGHT_RED, | ||
| 'LightGreen': LIGHT_GREEN, | ||
| 'LightYellow': LIGHT_YELLOW, | ||
| 'LightBlue': LIGHT_BLUE, | ||
| 'LightMagenta': LIGHT_MAGENTA, | ||
| 'LightCyan': LIGHT_CYAN, | ||
| 'LightWhite': LIGHT_WHITE, | ||
| 'BackGrey': BACK_GREY, | ||
| 'BackLightRed': BACK_LIGHT_RED, | ||
| 'BackLightGreen': BACK_LIGHT_GREEN, | ||
| 'BackLightYellow': BACK_LIGHT_YELLOW, | ||
| 'BackLightBlue': BACK_LIGHT_BLUE, | ||
| 'BackLightMagenta': BACK_LIGHT_MAGENTA, | ||
| 'BackLightCyan': BACK_LIGHT_CYAN, | ||
| 'BackLightWhite': BACK_LIGHT_WHITE, | ||
| } | ||
| color_map_ = { | ||
| 'reset': RESET, | ||
| 'black': BLACK, | ||
| 'red': RED, | ||
| 'green': GREEN, | ||
| 'yellow': YELLOW, | ||
| 'blue': BLUE, | ||
| 'magenta': MAGENTA, | ||
| 'cyan': CYAN, | ||
| 'white': WHITE, | ||
| 'backblack': BACK_BLACK, | ||
| 'backred': BACK_RED, | ||
| 'backgreen': BACK_GREEN, | ||
| 'backyellow': BACK_YELLOW, | ||
| 'backblue': BACK_BLUE, | ||
| 'backmagenta': BACK_MAGENTA, | ||
| 'backcyan': BACK_CYAN, | ||
| 'backwhite': BACK_WHITE, | ||
| 'grey': GREY, | ||
| 'gray': GREY, | ||
| 'lightred': LIGHT_RED, | ||
| 'lightgreen': LIGHT_GREEN, | ||
| 'lightyellow': LIGHT_YELLOW, | ||
| 'lightblue': LIGHT_BLUE, | ||
| 'lightmagenta': LIGHT_MAGENTA, | ||
| 'lightcyan': LIGHT_CYAN, | ||
| 'lightwhite': LIGHT_WHITE, | ||
| 'backgrey': BACK_GREY, | ||
| 'backlightred': BACK_LIGHT_RED, | ||
| 'backlightgreen': BACK_LIGHT_GREEN, | ||
| 'backlightyellow': BACK_LIGHT_YELLOW, | ||
| 'backlightblue': BACK_LIGHT_BLUE, | ||
| 'backlightmagenta': BACK_LIGHT_MAGENTA, | ||
| 'backlightcyan': BACK_LIGHT_CYAN, | ||
| 'backlightwhite': BACK_LIGHT_WHITE, | ||
| 'brightblack': GREY, | ||
| 'brightred': LIGHT_RED, | ||
| 'brightgreen': LIGHT_GREEN, | ||
| 'brightyellow': LIGHT_YELLOW, | ||
| 'brightblue': LIGHT_BLUE, | ||
| 'brightmagenta': LIGHT_MAGENTA, | ||
| 'brightcyan': LIGHT_CYAN, | ||
| 'brightwhite': LIGHT_WHITE, | ||
| 'backbrightblack': BACK_GREY, | ||
| 'backbrightred': BACK_LIGHT_RED, | ||
| 'backbrightgreen': BACK_LIGHT_GREEN, | ||
| 'backbrightyellow': BACK_LIGHT_YELLOW, | ||
| 'backbrightblue': BACK_LIGHT_BLUE, | ||
| 'backbrightmagenta': BACK_LIGHT_MAGENTA, | ||
| 'backbrightcyan': BACK_LIGHT_CYAN, | ||
| 'backbrightwhite': BACK_LIGHT_WHITE, | ||
| 'rst': RESET, | ||
| 'bk': BLACK, | ||
| 'r': RED, | ||
| 'g': GREEN, | ||
| 'y': YELLOW, | ||
| 'b': BLUE, | ||
| 'm': MAGENTA, | ||
| 'c': CYAN, | ||
| 'w': WHITE, | ||
| 'bbk': BACK_BLACK, | ||
| 'br': BACK_RED, | ||
| 'bg': BACK_GREEN, | ||
| 'by': BACK_YELLOW, | ||
| 'bb': BACK_BLUE, | ||
| 'bm': BACK_MAGENTA, | ||
| 'bc': BACK_CYAN, | ||
| 'bw': BACK_WHITE, | ||
| 'gr': GREY, | ||
| 'lr': LIGHT_RED, | ||
| 'lg': LIGHT_GREEN, | ||
| 'ly': LIGHT_YELLOW, | ||
| 'lb': LIGHT_BLUE, | ||
| 'lm': LIGHT_MAGENTA, | ||
| 'lc': LIGHT_CYAN, | ||
| 'lw': LIGHT_WHITE, | ||
| 'bgr': BACK_GREY, | ||
| 'blr': BACK_LIGHT_RED, | ||
| 'blg': BACK_LIGHT_GREEN, | ||
| 'bly': BACK_LIGHT_YELLOW, | ||
| 'blb': BACK_LIGHT_BLUE, | ||
| 'blm': BACK_LIGHT_MAGENTA, | ||
| 'blc': BACK_LIGHT_CYAN, | ||
| 'blw': BACK_LIGHT_WHITE, | ||
| } | ||
| change_map = { | ||
| 'aqua': 'LightCyan', | ||
| 'blue': 'LightBlue', | ||
| 'fuchsia': 'LightMagenta', | ||
| 'lime': 'LightGreen', | ||
| 'maroon': 'Red', | ||
| 'navy': 'Blue', | ||
| 'olive': 'Yellow', | ||
| 'purple': 'Magenta', | ||
| 'red': 'LightRed', | ||
| 'silver': 'Grey', | ||
| 'teal': 'Cyan', | ||
| 'white': 'BrightWhite', | ||
| 'yellow': 'LightYellow', | ||
| } | ||
| def closest_color(requested_color: _Sequence[int]): | ||
| """ | ||
| Takes a color in RGB and returns the name of the closest color to the value. | ||
| Uses the `webcolors.CSS2_HEX_TO_NAMES` dictionary to find the closest color. | ||
| :param requested_color: Sequence[int, int, int]: The input color in RGB. | ||
| :return: str: The name of the closest color. | ||
| """ | ||
| min_colors = {} | ||
| for key, name in _webcolors.CSS2_HEX_TO_NAMES.items(): | ||
| r_c, g_c, b_c = _webcolors.hex_to_rgb(key) | ||
| r_d = (r_c - requested_color[0])**2 | ||
| g_d = (g_c - requested_color[1])**2 | ||
| b_d = (b_c - requested_color[2])**2 | ||
| min_colors[(r_d + g_d + b_d)] = name | ||
| return min_colors[min(min_colors.keys())] | ||
| def get_color_name( | ||
| color: _Union[str, _Sequence[int], _webcolors.IntegerRGB, | ||
| _webcolors.HTML5SimpleColor], | ||
| raise_exceptions: bool = False | ||
| ) -> str: | ||
| """ | ||
| Takes a color in RGB format and returns a color name close to the RGB value. | ||
| >>> | ||
| >>> get_color_name('#00FF00') | ||
| 'LightGreen' | ||
| >>> | ||
| >>> get_color_name((128, 0, 128)) | ||
| 'Magenta' | ||
| >>> | ||
| :param color: Union[str, Sequence[int]: The input color. Example: '#00FF00', | ||
| (128, 0, 128) | ||
| :param raise_exceptions: bool = False: Returns empty string when raise_exceptions is | ||
| False and an error occurs. | ||
| :raises TypeError | ||
| :return: str: The color name. | ||
| """ | ||
| # Makes sure that the input parameters has valid values. | ||
| if not isinstance(color, | ||
| (str, tuple, _webcolors.IntegerRGB, _webcolors.HTML5SimpleColor)): | ||
| if raise_exceptions: | ||
| raise TypeError( | ||
| 'Input color must be a str or Tuple[int, int, int] or ' | ||
| 'webcolors.IntegerRGB or webcolors.HTML5SimpleColor' | ||
| ) | ||
| return '' | ||
| if isinstance(color, str): | ||
| if color.startswith('#') and len(color) == 7: | ||
| color = _webcolors.hex_to_rgb(color) | ||
| elif color.isdigit() and len(color) == 9: | ||
| color = (int(color[:3]), int(color[3:6]), int(color[6:9])) | ||
| else: | ||
| if raise_exceptions: | ||
| raise TypeError('String color format must be `#0021ff` or `000033255`!') | ||
| return '' | ||
| if isinstance(color, _Sequence): | ||
| if len(color) == 3: | ||
| if not (isinstance(color[0], int) and isinstance(color[1], int) | ||
| and isinstance(color[2], int)): | ||
| if raise_exceptions: | ||
| raise TypeError('Color sequence format must be (int, int, int)!') | ||
| return '' | ||
| else: | ||
| if raise_exceptions: | ||
| raise TypeError('Color sequence format must be (int, int, int)!') | ||
| return '' | ||
| # Looks for the name of the input RGB color. | ||
| try: | ||
| closest_name = _webcolors.rgb_to_name(tuple(color)) | ||
| except ValueError: | ||
| closest_name = closest_color(color) | ||
| if closest_name in Colors.change_map: | ||
| closest_name = Colors.change_map[closest_name] | ||
| return closest_name | ||
| def get_color(color: _Union[str, _Sequence], raise_exceptions: bool = False) -> str: | ||
| """Gets a color name and returns it in ansi format | ||
| >>> | ||
| >>> get_color('LightRed') | ||
| '\x1b[91m' | ||
| >>> | ||
| >>> import log21 | ||
| >>> log21.get_logger().info(log21.get_color('Blue') + 'Hello World!') | ||
| [21:21:21] [INFO] Hello World! | ||
| >>> # Note that you must run it yourself to see the colorful result ;D | ||
| >>> | ||
| :param color: color name(Example: Blue) | ||
| :param raise_exceptions: bool = False: | ||
| False: It will return '' instead of raising exceptions when an error occurs. | ||
| True: It may raise TypeError or KeyError | ||
| :raises TypeError: `color` must be str | ||
| :raises KeyError: `color` not found! | ||
| :return: str: an ansi color | ||
| """ | ||
| if not isinstance(color, (str, _Sequence)): | ||
| if raise_exceptions: | ||
| raise TypeError('`color` must be str or Sequence!') | ||
| return '' | ||
| if isinstance(color, _Sequence) and not isinstance(color, str): | ||
| color = get_color_name(color) | ||
| return get_color(color) | ||
| color = color.lower() | ||
| color = color.replace(' ', '').replace('_', '').replace('-', '') | ||
| color = color.replace('foreground', '').replace('fore', '').replace('ground', '') | ||
| if (color.startswith('#') and len(color) == 7) or (color.isdigit() | ||
| and len(color) == 9): | ||
| color = get_color_name(color) | ||
| return get_color(color) | ||
| if color in Colors.color_map_: | ||
| return Colors.color_map_[color] | ||
| if ansi_escape.match(color): | ||
| return ansi_escape.match(color).group() | ||
| if color in Colors.change_map: | ||
| return get_color(Colors.change_map[color]) | ||
| if raise_exceptions: | ||
| raise KeyError(f'`{color}` not found!') | ||
| return '' | ||
| def get_colors(*colors: str, raise_exceptions: bool = False) -> str: | ||
| """Gets a list of colors and combines them into one. | ||
| >>> | ||
| >>> get_colors('LightCyan') | ||
| '\x1b[96m' | ||
| >>> | ||
| >>> import log21 | ||
| >>> log21.get_logger().info(log21.get_colors('Green', 'Background White') + | ||
| ... 'Hello World!') | ||
| [21:21:21] [INFO] Hello World! | ||
| >>> # Note that you must run it yourself to see the colorful result ;D | ||
| >>> | ||
| :param colors: Input colors | ||
| :param raise_exceptions: bool = False: | ||
| False: It will return '' instead of raising exceptions when an error occurs. | ||
| True: It may raise TypeError or KeyError | ||
| :raises TypeError: `color` must be str | ||
| :raises KeyError: `color` not found! | ||
| :return: str: a combined color | ||
| """ | ||
| output = '' | ||
| for color in colors: | ||
| output += get_color(str(color), raise_exceptions=raise_exceptions) | ||
| parts = ansi_escape.split(output) | ||
| output = '\033[' | ||
| for part in parts: | ||
| if part: | ||
| output += part + ';' | ||
| if output.endswith(';'): | ||
| output = output[:-1] + 'm' | ||
| return output | ||
| return '' |
| # log21.CrashReporter.__init__.py | ||
| # CodeWriter21 | ||
| from . import Reporters, Formatters | ||
| from .Reporters import Reporter, FileReporter, EmailReporter, ConsoleReporter | ||
| from .Formatters import (FILE_REPORTER_FORMAT, EMAIL_REPORTER_FORMAT, | ||
| CONSOLE_REPORTER_FORMAT, Formatter) | ||
| __all__ = [ | ||
| 'Reporters', 'Formatters', 'Reporter', 'FileReporter', 'EmailReporter', | ||
| 'ConsoleReporter', 'FILE_REPORTER_FORMAT', 'EMAIL_REPORTER_FORMAT', | ||
| 'CONSOLE_REPORTER_FORMAT', 'Formatter' | ||
| ] |
| # log21.CrashReporter.Formatters.py | ||
| # CodeWriter21 | ||
| import traceback | ||
| from typing import (Any as _Any, Union as _Union, Mapping as _Mapping, | ||
| Callable as _Callable, Optional as _Optional) | ||
| from datetime import datetime as _datetime | ||
| __all__ = [ | ||
| 'Formatter', 'CONSOLE_REPORTER_FORMAT', 'FILE_REPORTER_FORMAT', | ||
| 'EMAIL_REPORTER_FORMAT' | ||
| ] | ||
| RESERVED_KEYS = ( | ||
| '__name__', 'type', 'message', 'traceback', 'name', 'file', 'lineno', 'function', | ||
| 'asctime' | ||
| ) | ||
| class Formatter: | ||
| """The base class for all CrashReporter formatters.""" | ||
| def __init__( | ||
| self, | ||
| format_: str, | ||
| style: str = '%', | ||
| datefmt: str = '%Y-%m-%d %H:%M:%S', | ||
| extra_values: _Optional[_Mapping[str, _Union[str, _Callable, _Any]]] = None | ||
| ): | ||
| """Initialize the formatter. | ||
| :param format_: The format string. | ||
| :param style: The style of the format string. Valid styles: %, { | ||
| :param datefmt: The date format string. | ||
| :param extra_values: A mapping of extra values to be added to the log record. | ||
| """ | ||
| self._format = format_ | ||
| if style in ['%', '{']: | ||
| self.__style = style | ||
| else: | ||
| raise ValueError('Invalid style: "' + str(style) + '" Valid styles: %, {') | ||
| self.datefmt = datefmt | ||
| self.extra_values = {} | ||
| if extra_values: | ||
| for key in extra_values: | ||
| if key in RESERVED_KEYS: | ||
| raise ValueError( | ||
| f'`{key}` is a reserved-key and cannot be used in ' | ||
| '`extra_values`.' | ||
| ) | ||
| self.extra_values[key] = extra_values[key] | ||
| def format(self, exception: BaseException) -> str: | ||
| """Format the exception. | ||
| :param exception: The exception to format. | ||
| :raises ValueError: If the style is not either '%' or '{'. | ||
| :return: The formatted exception. | ||
| """ | ||
| exception_dict = { | ||
| '__name__': __name__, | ||
| 'type': type(exception), | ||
| 'message': exception.args[0], | ||
| 'traceback': traceback.format_tb(exception.__traceback__.tb_next), | ||
| 'name': exception.__class__.__name__, | ||
| 'file': exception.__traceback__.tb_next.tb_frame.f_code.co_filename, | ||
| 'lineno': exception.__traceback__.tb_next.tb_lineno, | ||
| 'function': exception.__traceback__.tb_next.tb_frame.f_code.co_name, | ||
| 'asctime': _datetime.now().strftime(self.datefmt), | ||
| } | ||
| for key, value in self.extra_values.items(): | ||
| if callable(value): | ||
| exception_dict[key] = value() | ||
| else: | ||
| exception_dict[key] = value | ||
| if self.__style == '%': | ||
| return self._format % exception_dict | ||
| if self.__style == '{': | ||
| return self._format.format(**exception_dict) | ||
| raise ValueError( | ||
| 'Invalid style: "' + str(self.__style) + '" Valid styles: %, {' | ||
| ) | ||
| CONSOLE_REPORTER_FORMAT = { | ||
| 'format_': | ||
| '\033[91m%(name)s: %(message)s\033[0m\n' # Name and message of the exception. | ||
| '\tFile\033[91m:\033[0m "%(file)s"\n' # The file that exception was raised in. | ||
| '\tLine\033[91m:\033[0m %(lineno)d', # The line that exception was raised on. | ||
| 'style': | ||
| '%' | ||
| } | ||
| FILE_REPORTER_FORMAT = { | ||
| 'format_': | ||
| '[%(asctime)s] %(name)s: %(message)s' # Name and message of the exception. | ||
| '; File: "%(file)s"' # The file that exception was raised in. | ||
| '; Line: %(lineno)d\n', # The line that exception was raised on. | ||
| 'style': | ||
| '%' | ||
| } | ||
| EMAIL_REPORTER_FORMAT = { | ||
| 'format_': """ | ||
| <html> | ||
| <body> | ||
| <h1>Crash Report: %(__name__)s</h1> | ||
| <h2>%(name)s: %(message)s</h2> | ||
| <p> | ||
| <span style="bold">File:</span> "%(file)s"<br> | ||
| <span style="bold">Line:</span> %(lineno)d<br> | ||
| <span style="center">%(asctime)s</span><br> | ||
| </p> | ||
| <body> | ||
| </html> | ||
| """, | ||
| 'style': '%' | ||
| } |
| # log21.CrashReporter.Reporters.py | ||
| # CodeWriter21 | ||
| from __future__ import annotations | ||
| import ssl as _ssl | ||
| import smtplib as _smtplib # This module is used to send emails. | ||
| from os import PathLike as _PathLike | ||
| from typing import (IO as _IO, Any as _Any, Set as _Set, Type as _Type, Union as _Union, | ||
| Callable as _Callable, Iterable as _Iterable, Optional as _Optional) | ||
| from functools import wraps as _wraps | ||
| from email.mime.text import MIMEText as _MIMEText | ||
| from email.mime.multipart import MIMEMultipart as _MIMEMultipart | ||
| import log21 as _log21 | ||
| from .Formatters import (FILE_REPORTER_FORMAT as _FILE_REPORTER_FORMAT, | ||
| EMAIL_REPORTER_FORMAT as _EMAIL_REPORTER_FORMAT, | ||
| CONSOLE_REPORTER_FORMAT as _CONSOLE_REPORTER_FORMAT) | ||
| __all__ = ['Reporter', 'ConsoleReporter', 'FileReporter', 'EmailReporter'] | ||
| # pylint: disable=redefined-builtin | ||
| def print(*msg, args: tuple = (), end='\033[0m\n', **kwargs): | ||
| """Prints a message to the console using the log21.Logger.""" | ||
| logger = _log21.get_logger( | ||
| 'log21.print', level='DEBUG', show_time=False, show_level=False | ||
| ) | ||
| logger.print(*msg, args=args, end=end, **kwargs) | ||
| class Reporter: | ||
| """Reporter is a decorator that wraps a function and calls a function when | ||
| an exception is raised. | ||
| Usage Example: | ||
| >>> | ||
| >>> # Define a function that gets an exception and somehow reports it to you | ||
| >>> def report_function(exception): | ||
| ... print(exception) | ||
| ... | ||
| >>> | ||
| >>> # Create a Reporter object and pass the reporter function you defined to it | ||
| >>> reporter_object = Reporter(report_function, False) | ||
| >>> | ||
| >>> # Define the function you want to wrap | ||
| >>> # This function might raise an exception | ||
| >>> # You can wrap your main function, so that you get notified whenever your | ||
| >>> # app crashes | ||
| >>> @reporter_object.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| division by zero | ||
| >>> | ||
| >>> # You also can wrap a function like this | ||
| >>> import math | ||
| >>> wrapped_sqrt = reporter_object.reporter(math.sqrt) | ||
| >>> wrapped_sqrt(121) | ||
| 11.0 | ||
| >>> wrapped_sqrt(-1) | ||
| math domain error | ||
| >>> | ||
| """ | ||
| _reporter_function: _Callable[[ | ||
| BaseException | ||
| ], _Any] # A function that will be called when an exception is raised. | ||
| _exceptions_to_catch: _Set = None | ||
| _exceptions_to_ignore: _Set = None | ||
| raise_after_report: bool | ||
| def __init__( | ||
| self, | ||
| report_function: _Optional[_Callable[[BaseException], _Any]], | ||
| raise_after_report: bool = False, | ||
| formatter: _Optional[_log21.CrashReporter.Formatter] = None, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ): | ||
| """ | ||
| :param report_function: Function to call when an exception is raised. | ||
| :param raise_after_report: If True, the exception will be raised after the | ||
| report_function is called. | ||
| """ | ||
| self._reporter_function = report_function | ||
| self.raise_after_report = raise_after_report | ||
| self.formatter = formatter | ||
| self._exceptions_to_catch = set( | ||
| exceptions_to_catch | ||
| ) if exceptions_to_catch else None | ||
| self._exceptions_to_ignore = set( | ||
| exceptions_to_ignore | ||
| ) if exceptions_to_ignore else None | ||
| def reporter(self, func): | ||
| """It will wrap the function and call the report_function when an | ||
| exception is raised. | ||
| :param func: Function to wrap. | ||
| :return: Wrapped function. | ||
| """ | ||
| exceptions_to_catch = tuple( | ||
| self._exceptions_to_catch | ||
| ) if self._exceptions_to_catch else BaseException | ||
| exceptions_to_ignore = tuple(self._exceptions_to_ignore | ||
| ) if self._exceptions_to_ignore else tuple() | ||
| @_wraps(func) | ||
| def wrap(*args, **kwargs): | ||
| try: | ||
| return func(*args, **kwargs) | ||
| except BaseException as e: | ||
| if isinstance(e, exceptions_to_catch) and not isinstance( | ||
| e, exceptions_to_ignore): | ||
| self._reporter_function(e) | ||
| if self.raise_after_report: | ||
| raise e | ||
| else: | ||
| raise e | ||
| return wrap | ||
| def catch(self, exception: _Type[BaseException]): | ||
| """Add an exception to the list of exceptions to catch. | ||
| :param exception: Exception to catch. | ||
| """ | ||
| if not issubclass(exception, BaseException): | ||
| raise TypeError('`exception` must be a subclass of BaseException.') | ||
| if self._exceptions_to_catch is None: | ||
| self._exceptions_to_catch = set() | ||
| if exception not in self._exceptions_to_catch: | ||
| self._exceptions_to_catch.add(exception) | ||
| else: | ||
| raise ValueError('exception is already in the list of exceptions to catch') | ||
| def ignore(self, exception: _Type[BaseException]): | ||
| """Add an exception to the list of exceptions to ignore. | ||
| :param exception: Exception to ignore. | ||
| """ | ||
| if not issubclass(exception, BaseException): | ||
| raise TypeError('`exception` must be a subclass of BaseException.') | ||
| if self._exceptions_to_ignore is None: | ||
| self._exceptions_to_ignore = set() | ||
| if exception not in self._exceptions_to_ignore: | ||
| self._exceptions_to_ignore.add(exception) | ||
| else: | ||
| raise ValueError('exception is already in the list of exceptions to ignore') | ||
| def __call__(self, func): | ||
| return self.reporter(func) | ||
| class ConsoleReporter(Reporter): | ||
| """ConsoleReporter is a Reporter that prints the exception to the console. | ||
| Usage Example: | ||
| >>> | ||
| >>> # Define a ConsoleReporter object | ||
| >>> console_reporter = ConsoleReporter() | ||
| >>> | ||
| >>> # Define a function that raises an exception | ||
| >>> @console_reporter.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| ZeroDivisionError: division by zero | ||
| File: "<stdin>" | ||
| Line: 3 | ||
| >>> | ||
| >>> # You can also use costume formatters | ||
| >>> import log21 | ||
| >>> BLUE = log21.get_color('Light Blue') | ||
| >>> RED = log21.get_color('Light Red') | ||
| >>> YELLOW = log21.get_color('LIGHT YELLOW') | ||
| >>> RESET = log21.get_color('reset') | ||
| >>> formatter = log21.CrashReporter.Formatter( | ||
| ... format_='[' + BLUE + '%(asctime)s' + RESET + '] ' + | ||
| ... YELLOW + '%(function)s' + RED + ': ' + | ||
| ... RESET + 'Line ' + RED + '%(lineno)d: %(name)s:' + | ||
| ... RESET + ' %(message)s' | ||
| ... ) | ||
| >>> console_reporter = log21.CrashReporter.ConsoleReporter(formatter=formatter) | ||
| >>> | ||
| >>> @console_reporter.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| [2121-12-21 21:21:21] divide: Line 3: ZeroDivisionError: division by zero | ||
| >>> | ||
| """ | ||
| def __init__( | ||
| self, | ||
| raise_after_report: bool = False, | ||
| formatter: _Optional[_log21.CrashReporter.Formatter] = None, | ||
| print_function: _Optional[_Callable] = print, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ): | ||
| """ | ||
| :param raise_after_report: If True, the exception will be raised after the | ||
| report_function is called. | ||
| :param print_function: Function to use to print the message. | ||
| """ | ||
| super().__init__( | ||
| self._report, raise_after_report, formatter, exceptions_to_catch, | ||
| exceptions_to_ignore | ||
| ) | ||
| if formatter: | ||
| if isinstance(formatter, _log21.CrashReporter.Formatter): | ||
| self.formatter = formatter | ||
| else: | ||
| raise ValueError('formatter must be a log21.CrashReporter.Formatter') | ||
| else: | ||
| self.formatter = _log21.CrashReporter.Formatters.Formatter( | ||
| **_CONSOLE_REPORTER_FORMAT | ||
| ) | ||
| self.print = print_function | ||
| def _report(self, exception: BaseException): | ||
| """Prints the exception to the console. | ||
| :param exception: Exception to print. | ||
| :return: | ||
| """ | ||
| self.print(self.formatter.format(exception)) | ||
| class FileReporter(Reporter): | ||
| """FileReporter is a Reporter that writes the exception to a file.""" | ||
| def __init__( | ||
| self, | ||
| *, | ||
| file: _Union[str, _PathLike, _IO], | ||
| encoding: str = 'utf-8', | ||
| raise_after_report: bool = True, | ||
| formatter: _Optional[_log21.CrashReporter.Formatter] = None, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ): | ||
| super().__init__( | ||
| self._report, raise_after_report, formatter, exceptions_to_catch, | ||
| exceptions_to_ignore | ||
| ) | ||
| # pylint: disable=consider-using-with | ||
| if isinstance(file, str): | ||
| self.file = open(file, 'a', encoding=encoding) | ||
| elif isinstance(file, _PathLike): | ||
| self.file = open(file, 'a', encoding=encoding) | ||
| elif isinstance(file, _IO): | ||
| if file.writable(): | ||
| self.file = file | ||
| else: | ||
| raise ValueError('file must be writable') | ||
| else: | ||
| raise ValueError('file must be a string, PathLike, or IO object') | ||
| if formatter: | ||
| if isinstance(formatter, _log21.CrashReporter.Formatter): | ||
| self.formatter = formatter | ||
| else: | ||
| raise ValueError('formatter must be a log21.CrashReporter.Formatter') | ||
| else: | ||
| self.formatter = _log21.CrashReporter.Formatters.Formatter( | ||
| **_FILE_REPORTER_FORMAT | ||
| ) | ||
| def _report(self, exception: BaseException): | ||
| """Writes the exception to the file. | ||
| :param exception: Exception to write. | ||
| :return: | ||
| """ | ||
| self.file.write(self.formatter.format(exception)) | ||
| self.file.flush() | ||
| class EmailReporter(Reporter): # pylint: disable=too-many-instance-attributes | ||
| """EmailReporter is a Reporter that sends an email with the exception. | ||
| Usage Example: | ||
| >>> | ||
| >>> # Define a EmailReporter object | ||
| >>> email_reporter = EmailReporter( | ||
| ... mail_host='smtp.yandex.ru', | ||
| ... port=465, | ||
| ... from_address='MyEmail@yandex.ru', | ||
| ... to_address='CodeWriter21@gmail.com', | ||
| ... password='My$up3rStr0ngP@assw0rd XD' | ||
| ... ) | ||
| ... | ||
| >>> # Define the function you want to wrap | ||
| >>> @email_reporter.reporter | ||
| ... def divide(a, b): | ||
| ... return a / b | ||
| ... | ||
| >>> | ||
| >>> divide(21, 3) | ||
| 7.0 | ||
| >>> divide(10, 0) | ||
| Traceback (most recent call last): | ||
| File "<stdin>", line 1, in <module> | ||
| File ".../site-packages/log21/CrashReporter/Reporters.py", | ||
| line 81, in wrap | ||
| raise e | ||
| File ".../site-packages/log21/CrashReporter/Reporters.py", | ||
| line 77, in wrap | ||
| return func(*args, **kwargs) | ||
| File "<stdin>", line 3, in divide | ||
| ZeroDivisionError: division by zero | ||
| >>> # At this point a Crash Report is sent to my email: CodeWriter21@gmail.com | ||
| >>> | ||
| """ | ||
| def __init__( | ||
| self, | ||
| mail_host: str, | ||
| port: int, | ||
| from_address: str, | ||
| to_address: str, | ||
| password: str, | ||
| username: str = '', | ||
| tls: bool = True, | ||
| raise_after_report: bool = True, | ||
| formatter: _Optional[_log21.CrashReporter.Formatter] = None, | ||
| exceptions_to_catch: _Optional[_Iterable[BaseException]] = None, | ||
| exceptions_to_ignore: _Optional[_Iterable[BaseException]] = None | ||
| ): | ||
| super().__init__( | ||
| self._report, raise_after_report, formatter, exceptions_to_catch, | ||
| exceptions_to_ignore | ||
| ) | ||
| self.mail_host = mail_host | ||
| self.port = port | ||
| self.from_address = from_address | ||
| self.to_address = to_address | ||
| self.password = password | ||
| if username: | ||
| self.username = username | ||
| else: | ||
| self.username = self.from_address | ||
| self.tls = tls | ||
| # Checks if the sender email is accessible | ||
| try: | ||
| if self.tls: | ||
| context = _ssl.create_default_context() | ||
| with _smtplib.SMTP_SSL(self.mail_host, port, context=context) as server: | ||
| server.ehlo() | ||
| server.login(self.username, self.password) | ||
| else: | ||
| with _smtplib.SMTP(self.mail_host, port) as server: | ||
| server.ehlo() | ||
| server.login(self.username, self.password) | ||
| server.ehlo() | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| raise ex | ||
| if formatter: | ||
| if isinstance(formatter, _log21.CrashReporter.Formatter): | ||
| self.formatter = formatter | ||
| else: | ||
| raise ValueError('formatter must be a log21.CrashReporter.Formatter') | ||
| else: | ||
| self.formatter = _log21.CrashReporter.Formatters.Formatter( | ||
| **_EMAIL_REPORTER_FORMAT | ||
| ) | ||
| def _report(self, exception: BaseException): | ||
| """Sends an email with the exception. | ||
| :param exception: Exception to send. | ||
| :return: | ||
| """ | ||
| message = _MIMEMultipart() | ||
| message['From'] = self.from_address # Sender | ||
| message['To'] = self.to_address # Receiver | ||
| message['Subject'] = f'Crash Report: {exception.__class__.__name__}' # Subject | ||
| message.attach(_MIMEText(self.formatter.format(exception), 'html')) | ||
| if self.tls: | ||
| context = _ssl.create_default_context() | ||
| with _smtplib.SMTP_SSL(self.mail_host, port=self.port, | ||
| context=context) as server: | ||
| server.login(self.username, self.password) | ||
| server.sendmail(self.from_address, self.to_address, message.as_string()) | ||
| else: | ||
| with _smtplib.SMTP(self.username, port=self.port) as server: | ||
| server.login(self.from_address, self.password) | ||
| server.sendmail(self.from_address, self.to_address, message.as_string()) |
| # log21.FileHandler.py | ||
| # CodeWriter21 | ||
| from logging import FileHandler as _FileHandler | ||
| from log21.Formatters import DecolorizingFormatter as _DecolorizingFormatter | ||
| class FileHandler(_FileHandler): | ||
| """A subclass of logging.FileHandler that allows you to specify a | ||
| formatter and a level when you initialize it.""" | ||
| def __init__( | ||
| self, | ||
| filename, | ||
| mode='a', | ||
| encoding=None, | ||
| delay=False, | ||
| errors=None, | ||
| formatter=None, | ||
| level=None | ||
| ): | ||
| """Initialize the handler. | ||
| :param filename: The filename of the log file. | ||
| :param mode: The mode to open the file in. | ||
| :param encoding: The encoding to use when opening the file. | ||
| :param delay: Whether to delay opening the file. | ||
| :param errors: The error handling scheme to use. | ||
| :param formatter: The formatter to use. | ||
| :param level: The level to use. | ||
| """ | ||
| super().__init__(filename, mode, encoding, delay, errors) | ||
| if formatter is not None: | ||
| self.setFormatter(formatter) | ||
| if level is not None: | ||
| self.setLevel(level) | ||
| class DecolorizingFileHandler(FileHandler): | ||
| """A subclass of FileHandler that removes ANSI colors from the log messages before | ||
| writing them to the file.""" | ||
| terminator = '' | ||
| def emit(self, record): | ||
| """Emit a record.""" | ||
| if self.stream is None: | ||
| self.stream = self._open() | ||
| try: | ||
| msg = self.format(record) | ||
| msg = _DecolorizingFormatter.decolorize(msg) | ||
| stream = self.stream | ||
| stream.write(msg + self.terminator) | ||
| self.flush() | ||
| except Exception: # pylint: disable=broad-except | ||
| self.handleError(record) |
| # log21.Formatters.py | ||
| # CodeWriter21 | ||
| import time as _time | ||
| from typing import (Dict as _Dict, Tuple as _Tuple, Mapping as _Mapping, | ||
| Optional as _Optional) | ||
| from logging import Formatter as __Formatter | ||
| from log21.Colors import get_colors as _gc, ansi_escape | ||
| from log21.Levels import INFO, DEBUG, ERROR, INPUT, PRINT, WARNING, CRITICAL | ||
| __all__ = ['ColorizingFormatter', 'DecolorizingFormatter'] | ||
| class _Formatter(__Formatter): | ||
| def __init__( | ||
| self, | ||
| fmt: _Optional[str] = None, | ||
| datefmt: _Optional[str] = None, | ||
| style: str = '%', | ||
| level_names: _Optional[_Mapping[int, str]] = None | ||
| ): | ||
| """ | ||
| `level_names` usage: | ||
| >>> import log21 | ||
| >>> logger = log21.Logger('MyLogger', log21.DEBUG) | ||
| >>> stream_handler = log21.ColorizingStreamHandler() | ||
| >>> formatter = log21.ColorizingFormatter(fmt='[%(levelname)s] %(message)s', | ||
| ... level_names={log21.DEBUG: ' ', log21.INFO: '+', | ||
| ... log21.WARNING: '-', log21.ERROR: '!', | ||
| ... log21.CRITICAL: 'X'}) | ||
| >>> stream_handler.setFormatter(formatter) | ||
| >>> logger.addHandler(stream_handler) | ||
| >>> | ||
| >>> logger.debug('Just wanna see if this works...') | ||
| [ ] Just wanna see if this works... | ||
| >>> logger.info("FYI: I'm glad somebody read this 8)") | ||
| [+] FYI: I'm glad somebody read this 8) | ||
| >>> logger.warning("Oh no! Something's gonna happen!") | ||
| [-] Oh no! something's gonna happen! | ||
| >>> logger.error('AN ERROR OCCURRED! (told ya ;))') | ||
| [!] AN ERROR OCCURRED! (told ya ;)) | ||
| >>> logger.critical('Crashed....') | ||
| [X] Crashed.... | ||
| >>> | ||
| >>> # Hope you've enjoyed | ||
| >>> | ||
| :param fmt: The format string to use. | ||
| :param datefmt: The date format string to use. | ||
| :param style: The style to use. | ||
| :param level_names: A dictionary mapping logging levels to their names. | ||
| """ | ||
| super().__init__(fmt=fmt, datefmt=datefmt, style=style) | ||
| self._level_names: _Dict[int, str] = { | ||
| DEBUG: 'DEBUG', | ||
| INFO: 'INFO', | ||
| WARNING: 'WARNING', | ||
| ERROR: 'ERROR', | ||
| CRITICAL: 'CRITICAL', | ||
| PRINT: 'PRINT', | ||
| INPUT: 'INPUT' | ||
| } | ||
| if level_names: | ||
| for level, name in level_names.items(): | ||
| self.level_names[level] = name | ||
| @property | ||
| def level_names(self): | ||
| """Get the level names mapping.""" | ||
| return self._level_names | ||
| @level_names.setter | ||
| def level_names(self, level_names: _Mapping[int, str]): | ||
| if level_names: | ||
| if not isinstance(level_names, _Mapping): | ||
| raise TypeError( | ||
| '`level_names` must be a Mapping, a dictionary like object!' | ||
| ) | ||
| self._level_names = level_names | ||
| else: | ||
| self._level_names = {} | ||
| def format(self, record) -> str: | ||
| record.message = record.getMessage() | ||
| if self.usesTime(): | ||
| record.asctime = self.formatTime(record, self.datefmt) | ||
| record.levelname = self.level_names.get(record.levelno, 'NOTSET') | ||
| s = self.formatMessage(record) # pylint: disable=invalid-name | ||
| if record.exc_info: | ||
| if not record.exc_text: | ||
| record.exc_text = self.formatException(record.exc_info) | ||
| if record.exc_text: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + record.exc_text | ||
| if record.stack_info: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + self.formatStack(record.stack_info) | ||
| return s | ||
| class ColorizingFormatter(_Formatter): # pylint: disable=too-many-instance-attributes | ||
| """A formatter that helps adding colors to the log records.""" | ||
| time_color: _Tuple[str, ...] = ('lightblue', ) | ||
| name_color = pathname_color = filename_color = module_color = func_name_color = \ | ||
| thread_name_color = message_color = tuple() | ||
| def __init__( | ||
| self, | ||
| fmt: _Optional[str] = None, | ||
| datefmt: _Optional[str] = None, | ||
| style: str = '%', | ||
| level_names: _Optional[_Mapping[int, str]] = None, | ||
| level_colors: _Optional[_Mapping[int, _Tuple[str]]] = None, | ||
| time_color: _Optional[_Tuple[str, ...]] = None, | ||
| name_color: _Optional[_Tuple[str, ...]] = None, | ||
| pathname_color: _Optional[_Tuple[str, ...]] = None, | ||
| filename_color: _Optional[_Tuple[str, ...]] = None, | ||
| module_color: _Optional[_Tuple[str, ...]] = None, | ||
| func_name_color: _Optional[_Tuple[str, ...]] = None, | ||
| thread_name_color: _Optional[_Tuple[str, ...]] = None, | ||
| message_color: _Optional[_Tuple[str, ...]] = None | ||
| ): # pylint: disable=too-many-branches | ||
| """Initialize the formatter. | ||
| :param fmt: The format string to use for the message. | ||
| :param datefmt: The format string to use for the date/time | ||
| portion of the message. | ||
| :param style: The format style to use. | ||
| :param level_names: A mapping of level numbers to level names. | ||
| :param level_colors: A mapping of level numbers to level colors. | ||
| :param time_color: The color to use for the time portion of the | ||
| message. | ||
| :param name_color: The color to use for the logger name portion | ||
| of the message. | ||
| :param pathname_color: The color to use for the pathname portion | ||
| of the message. | ||
| :param filename_color: The color to use for the filename portion | ||
| of the message. | ||
| :param module_color: The color to use for the module portion of | ||
| the message. | ||
| :param func_name_color: The color to use for the function name | ||
| portion of the message. | ||
| :param thread_name_color: The color to use for the thread name | ||
| portion of the message. | ||
| :param message_color: The color to use for the message portion | ||
| of the message. | ||
| """ | ||
| super().__init__(fmt=fmt, datefmt=datefmt, style=style, level_names=level_names) | ||
| self.level_colors: _Dict[int, _Tuple[str, ...]] = { | ||
| DEBUG: ('lightblue', ), | ||
| INFO: ('green', ), | ||
| WARNING: ('lightyellow', ), | ||
| ERROR: ('light red', ), | ||
| CRITICAL: ('background red', 'white'), | ||
| PRINT: ('Cyan', ), | ||
| INPUT: ('Magenta', ) | ||
| } | ||
| # Checks and sets colors | ||
| if level_colors: | ||
| if not isinstance(level_colors, _Mapping): | ||
| raise TypeError('`level_colors` must be a dictionary like object!') | ||
| for level, color in level_colors.items(): | ||
| self.level_colors[level] = (_gc(*color), ) | ||
| if time_color: | ||
| if not isinstance(time_color, tuple): | ||
| raise TypeError('`time_color` must be a tuple!') | ||
| self.time_color = time_color | ||
| if name_color: | ||
| if not isinstance(name_color, tuple): | ||
| raise TypeError('`name_color` must be a tuple!') | ||
| self.name_color = name_color | ||
| if pathname_color: | ||
| if not isinstance(pathname_color, tuple): | ||
| raise TypeError('`pathname_color` must be a tuple!') | ||
| self.pathname_color = pathname_color | ||
| if filename_color: | ||
| if not isinstance(filename_color, tuple): | ||
| raise TypeError('`filename_color` must be a tuple!') | ||
| self.filename_color = filename_color | ||
| if module_color: | ||
| if not isinstance(module_color, tuple): | ||
| raise TypeError('`module_color` must be a tuple!') | ||
| self.module_color = module_color | ||
| if func_name_color: | ||
| if not isinstance(func_name_color, tuple): | ||
| raise TypeError('`func_name_color` must be a tuple!') | ||
| self.func_name_color = func_name_color | ||
| if thread_name_color: | ||
| if not isinstance(thread_name_color, tuple): | ||
| raise TypeError('`thread_name_color` must be a tuple!') | ||
| self.thread_name_color = thread_name_color | ||
| if message_color: | ||
| if not isinstance(message_color, tuple): | ||
| raise TypeError('`message_color` must be a tuple!') | ||
| self.message_color = message_color | ||
| def format(self, record) -> str: | ||
| """Colorizes the record and returns the formatted message.""" | ||
| record.message = record.getMessage() | ||
| if self.usesTime(): | ||
| record.asctime = self.formatTime(record, self.datefmt) | ||
| record.levelname = self.level_names.get(record.levelno, 'NOTSET') | ||
| record = self.colorize(record) | ||
| s = self.formatMessage(record) # pylint: disable=invalid-name | ||
| if record.exc_info: | ||
| # Cache the traceback text to avoid converting it multiple times | ||
| # (it's constant anyway) | ||
| if not record.exc_text: | ||
| record.exc_text = self.formatException(record.exc_info) | ||
| if record.exc_text: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + record.exc_text | ||
| if record.stack_info: | ||
| if s[-1:] != "\n": | ||
| s = s + "\n" | ||
| s = s + self.formatStack(record.stack_info) | ||
| return s | ||
| def colorize(self, record): | ||
| """Colorizes the record attributes. | ||
| :param record: | ||
| :return: colorized record | ||
| """ | ||
| reset = '\033[0m' | ||
| if hasattr(record, 'asctime'): | ||
| record.asctime = _gc(*self.time_color) + record.asctime + reset | ||
| if hasattr(record, 'levelno'): | ||
| record.levelname = _gc( | ||
| *self.level_colors.get(int(record.levelno), ('lw', )) | ||
| ) + getattr(record, 'levelname', 'NOTSET') + reset | ||
| if hasattr(record, 'name'): | ||
| record.name = _gc(*self.name_color) + str(record.name) + reset | ||
| if hasattr(record, 'pathname'): | ||
| record.pathname = _gc(*self.pathname_color) + record.pathname + reset | ||
| if hasattr(record, 'filename'): | ||
| record.filename = _gc(*self.filename_color) + record.filename + reset | ||
| if hasattr(record, 'module'): | ||
| record.module = _gc(*self.module_color) + record.module + reset | ||
| if hasattr(record, 'funcName'): | ||
| record.funcName = _gc(*self.func_name_color) + record.funcName + reset | ||
| if hasattr(record, 'threadName'): | ||
| record.threadName = _gc(*self.thread_name_color) + record.threadName + reset | ||
| if hasattr(record, 'message'): | ||
| record.message = _gc(*self.message_color) + record.message | ||
| return record | ||
| class DecolorizingFormatter(_Formatter): | ||
| """Formatter that removes color codes from the log records.""" | ||
| def formatTime(self, record, datefmt=None): | ||
| """Returns the creation time of the specified LogRecord as formatted | ||
| text.""" | ||
| ct = self.converter(int(record.created)) | ||
| if datefmt: | ||
| s = _time.strftime(datefmt, ct) | ||
| else: | ||
| t = _time.strftime(self.default_time_format, ct) | ||
| s = self.default_msec_format % (t, record.msecs) | ||
| return s | ||
| def format(self, record) -> str: | ||
| """Decolorizes the record and returns the formatted message. | ||
| :param record: | ||
| :return: str | ||
| """ | ||
| return self.decolorize(super().format(record)) | ||
| @staticmethod | ||
| def decolorize(text: str): | ||
| """Removes all ansi colors in the text. | ||
| :param text: str: Input text | ||
| :return: str: decolorized text | ||
| """ | ||
| return ansi_escape.sub('', text) |
| # log21.Levels.py | ||
| # CodeWriter21 | ||
| import logging as _logging | ||
| __all__ = [ | ||
| 'CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET', 'INPUT', | ||
| 'PRINT' | ||
| ] | ||
| INPUT = 70 | ||
| PRINT = 60 | ||
| CRITICAL = _logging.CRITICAL | ||
| FATAL = CRITICAL | ||
| ERROR = _logging.ERROR | ||
| WARNING = _logging.WARNING | ||
| WARN = WARNING | ||
| INFO = _logging.INFO | ||
| DEBUG = _logging.DEBUG | ||
| NOTSET = _logging.NOTSET |
| # log21.Logger.py | ||
| # CodeWriter21 | ||
| import re as _re | ||
| import sys as _sys | ||
| import logging as _logging | ||
| from types import MethodType as _MethodType | ||
| from typing import (Any, List, Union as _Union, Literal as _Literal, Mapping, | ||
| Callable as _Callable, Optional as _Optional, Sequence as _Sequence) | ||
| from getpass import getpass as _getpass | ||
| from logging import raiseExceptions as _raiseExceptions | ||
| import log21 as _log21 | ||
| from log21.Levels import INFO, DEBUG, ERROR, INPUT, PRINT, NOTSET, WARNING, CRITICAL | ||
| __all__ = ['Logger'] | ||
| class Logger(_logging.Logger): | ||
| """A Logger that can print to the console and log to a file.""" | ||
| def __init__( | ||
| self, | ||
| name, | ||
| level: _Union[int, str] = NOTSET, | ||
| handlers: _Optional[_Union[_Sequence[_logging.Handler], | ||
| _logging.Handler]] = None | ||
| ): | ||
| """Initialize a Logger object. | ||
| :param name: The name of the logger. | ||
| :param level: The level of the logger. | ||
| :param handlers: The handlers to add to the logger. | ||
| """ | ||
| super().__init__(name, level) | ||
| self.setLevel(level) | ||
| self._progress_bar = None | ||
| if handlers: | ||
| if not isinstance(handlers, _Sequence): | ||
| if isinstance(handlers, _logging.Handler): | ||
| handlers = [handlers] | ||
| else: | ||
| raise TypeError( | ||
| 'handlers must be a list of logging.Handler objects' | ||
| ) | ||
| for handler in handlers: | ||
| self.addHandler(handler) | ||
| def isEnabledFor(self, level): | ||
| """Is this logger enabled for level 'level'?""" | ||
| return (self.level <= level) or (level in (PRINT, INPUT)) | ||
| def log(self, level: int, *msg, args: tuple = (), end='\n', **kwargs): | ||
| """Log 'msg % args' with the integer severity 'level'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.log(level, "We have a %s", args=("mysterious problem",), exc_info=1) | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| if not isinstance(level, int): | ||
| if _raiseExceptions: | ||
| raise TypeError('level must be an integer') | ||
| return | ||
| if self.isEnabledFor(level): | ||
| self._log(level, msg, args, **kwargs) | ||
| def debug(self, *msg, args: tuple = (), end='\n', **kwargs): | ||
| """Log 'msg % args' with severity 'DEBUG'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.debug("Houston, we have a %s", args=("thorny problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(DEBUG): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(DEBUG, msg, args, **kwargs) | ||
| def info(self, *msg, args: tuple = (), end='\n', **kwargs): | ||
| """Log 'msg % args' with severity 'INFO'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.info("Houston, we have an %s", args=("interesting problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(INFO): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(INFO, msg, args, **kwargs) | ||
| def warning(self, *msg, args: tuple = (), end='\n', **kwargs): | ||
| """Log 'msg % args' with severity 'WARNING'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.warning("Houston, we have a %s", args=("bit of a problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(WARNING): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(WARNING, msg, args, **kwargs) | ||
| warn = warning | ||
| def write(self, *msg, args: tuple = (), end='', **kwargs): | ||
| """Log 'msg % args' with severity 'WARNING'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.write("Houston, we have a %s", args=("bit of a problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(WARNING): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(WARNING, msg, args, **kwargs) | ||
| def error(self, *msg, args: tuple = (), end='\n', **kwargs): | ||
| """Log 'msg % args' with severity 'ERROR'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.error("Houston, we have a %s", args=("major problem",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(ERROR): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(ERROR, msg, args, **kwargs) | ||
| def exception(self, *msg, args: tuple = (), exc_info=True, **kwargs): | ||
| """Convenience method for logging an ERROR with exception information.""" | ||
| self.error(*msg, args=args, exc_info=exc_info, **kwargs) | ||
| def critical(self, *msg, args: tuple = (), end='\n', **kwargs): | ||
| """Log 'msg % args' with severity 'CRITICAL'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.critical("Houston, we have a %s", args=("major disaster",), exc_info=1) | ||
| """ | ||
| if self.isEnabledFor(CRITICAL): | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(CRITICAL, msg, args, **kwargs) | ||
| fatal = critical | ||
| def print(self, *msg, args: tuple = (), end='\n', **kwargs): | ||
| """Log 'msg % args'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value, e.g. | ||
| logger.print("Houston, we have a %s", args=("major disaster",), exc_info=1) | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(PRINT, msg, args, **kwargs) | ||
| def input(self, *msg, args: tuple = (), end='', **kwargs): | ||
| """Log 'msg % args'. | ||
| To pass exception information, use the keyword argument exc_info with a true | ||
| value. | ||
| Usage example: | ||
| age = logger.input("Enter your age: ") | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(INPUT, msg, args, **kwargs) | ||
| return input() | ||
| def getpass(self, *msg, args: tuple = (), end='', **kwargs): | ||
| """Takes a password input from the user. | ||
| :param msg: The message to display to the user. | ||
| :param args: The arguments to pass to the message. | ||
| :param end: The ending character to append to the message. | ||
| :return: The password. | ||
| """ | ||
| msg = ' '.join([str(m) for m in msg]) + end | ||
| self._log(self.level if self.level >= NOTSET else NOTSET, msg, args, **kwargs) | ||
| return _getpass('') | ||
| def print_progress(self, progress: float, total: float, **kwargs): | ||
| """Log progress.""" | ||
| self.progress_bar(progress, total, **kwargs) | ||
| @property | ||
| def progress_bar(self): | ||
| """Return a progress bar instance. | ||
| If not exists, create a new one. | ||
| """ | ||
| if not self._progress_bar: | ||
| # avoid circular import; pylint: disable=import-outside-toplevel | ||
| from log21.ProgressBar import ProgressBar | ||
| self._progress_bar = ProgressBar(logger=self) | ||
| return self._progress_bar | ||
| @progress_bar.setter | ||
| def progress_bar(self, value: '_log21.ProgressBar'): | ||
| self._progress_bar = value | ||
| def clear_line(self, length: _Optional[int] = None): | ||
| """Clear the current line. | ||
| :param length: The length of the line to clear. | ||
| :return: | ||
| """ | ||
| for handler in self.handlers: | ||
| if isinstance(getattr(handler, 'clear_line', None), _Callable): | ||
| handler.clear_line(length) # type: ignore | ||
| def add_level( | ||
| self, | ||
| level: int, | ||
| name: str, | ||
| errors: _Literal['raise', 'ignore', 'handle', 'force'] = 'raise' | ||
| ) -> str: | ||
| """Adds a new method to the logger with a specific level and name. | ||
| :param level: The level of the new method. | ||
| :param name: The name of the new method. | ||
| :param errors: The action to take if the level already exists. | ||
| + ``raise`` (default): Raise an exception if anything goes wrong. | ||
| + ``ignore``: Do nothing. | ||
| + ``handle``: Handle the situation if a method with the same ``name`` | ||
| already exists. Adds a number to the name to avoid the conflict. | ||
| + ``force``: Add the new level with the specified level even if a | ||
| method with the same ``name`` already exists. | ||
| :raises TypeError: If ``level`` is not an integer. | ||
| :raises TypeError: If ``name`` is not a string. | ||
| :raises ValueError: If ``errors`` is not one of "raise", "ignore", "handle", | ||
| or "force". | ||
| :raises ValueError: If ``name`` starts with a number. | ||
| :raises ValueError: If ``name`` is not a valid identifier. | ||
| :raises AttributeError: If ``errors`` is "raise" and a method with the | ||
| same ``name`` already exists. | ||
| :return: The name of the new method. | ||
| """ | ||
| def raise_(error: BaseException): | ||
| if errors == 'ignore': | ||
| return | ||
| raise error | ||
| if not isinstance(level, int): | ||
| raise_(TypeError('level must be an integer')) | ||
| if not isinstance(name, str): | ||
| raise_(TypeError('name must be a string')) | ||
| if errors not in ('raise', 'ignore', 'handle', 'force'): | ||
| raise_( | ||
| ValueError( | ||
| 'errors must be one of "raise", "ignore", "handle", "force"' | ||
| ) | ||
| ) | ||
| name = _re.sub(r'\s', '_', name) | ||
| if _re.match(r'[0-9].*', name): | ||
| raise_(ValueError(f'level name cannot start with a number: "{name}"')) | ||
| if not _re.fullmatch(r'[a-zA-Z_][a-zA-Z0-9_]*', name): | ||
| raise_(ValueError(f'level name must be a valid identifier: "{name}"')) | ||
| if hasattr(self, name): | ||
| if errors == 'raise': | ||
| raise AttributeError(f'level "{name}" already exists') | ||
| if errors == 'ignore': | ||
| return name | ||
| if errors == 'handle': | ||
| return self.add_level(level, _add_one(name), errors) | ||
| def log_for_level(self, *msg, args: tuple = (), end='\n', **kwargs): | ||
| self.log(level, *msg, args=args, end=end, **kwargs) | ||
| setattr(self, name, _MethodType(log_for_level, self)) | ||
| return name | ||
| def add_levels( | ||
| self, | ||
| level_names: Mapping[int, str], | ||
| errors: _Literal['raise', 'ignore', 'handle', 'force'] = 'raise' | ||
| ) -> None: | ||
| """Adds new methods to the logger with specific levels and names. | ||
| :param level_names: A mapping of levels to names. | ||
| :param errors: The action to take if the level already exists | ||
| :return: | ||
| """ | ||
| for level, name in level_names.items(): | ||
| self.add_level(level, name, errors) | ||
| def __lshift__(self, obj): | ||
| """Prints the object to the output stream. | ||
| This operator is meant to make the Logger object be usable in a | ||
| std::cout-like way. | ||
| :param obj: The object to print. | ||
| :return: The Logger object. | ||
| """ | ||
| logger = self | ||
| found = 0 | ||
| while logger: | ||
| for handler in logger.handlers: | ||
| if (isinstance(handler, _logging.StreamHandler) | ||
| and hasattr(handler.stream, 'write') | ||
| and hasattr(handler.stream, 'flush')): | ||
| found = found + 1 | ||
| handler.stream.write(str(obj)) | ||
| handler.stream.flush() | ||
| if not logger.propagate: | ||
| break | ||
| logger = logger.parent | ||
| if found == 0: | ||
| _sys.stderr.write( | ||
| f"No handlers could be found for logger \"{self.name}\"\n" | ||
| ) | ||
| return self | ||
| def __rshift__(self, obj: List[Any]): | ||
| """A way of receiving input from the stdin. | ||
| This operator is meant to make a std::cin-like operation possible in Python. | ||
| Usage examples: | ||
| >>> import log21 | ||
| >>> cout = cin = log21.get_logger() | ||
| >>> | ||
| >>> # Example 1 | ||
| >>> # Get three inputs of type: str, str or None, and float | ||
| >>> data = [str, None, float] # first name, last name and age | ||
| >>> cout << "Please enter a first name, last name and age(separated by space): " | ||
| Please enter a first name, last name and age(separated by space): | ||
| >>> cin >> data; | ||
| M 21 | ||
| >>> name = data[0] + (data[1] if data[1] is not None else '') | ||
| >>> age = data[2] | ||
| >>> cout << name << " is " << age << " years old." << log21.endl; | ||
| M is 21.0 years old. | ||
| >>> | ||
| >>> # Example 2 | ||
| >>> # Get any number of inputs | ||
| >>> data = [] | ||
| >>> cout << "Enter something: "; | ||
| Enter something: | ||
| >>> cin >> data; | ||
| What ever man 1 2 3 ! | ||
| >>> cout << "Here are the items you chose: " << data << log21.endl; | ||
| Here are the items you chose: ['What', 'ever', 'man', '1', '2', '3', '!'] | ||
| >>> | ||
| >>> # Example 3 | ||
| >>> # Get two inputs of type int with defaults: 1280 and 720 | ||
| >>> data = [1280, 720] | ||
| >>> cout << "Enter the width and the height: "; | ||
| Enter the width and the height: | ||
| >>> cin >> data; | ||
| 500 | ||
| >>> cout << "Width: " << data[0] << " Height: " << data[1] << log21.endl; | ||
| Width: 500 Height: 720 | ||
| >>> | ||
| :param obj: The object to redirect the output to. | ||
| :return: The Logger object. | ||
| """ | ||
| n = len(obj) - 1 | ||
| if n >= 0: | ||
| data = [] | ||
| while n >= 0: | ||
| tmp = _sys.stdin.readline()[:-1].split(' ', maxsplit=n) | ||
| if tmp: | ||
| data.extend(tmp) | ||
| else: | ||
| data.append('') | ||
| n -= len(tmp) | ||
| tmp = [] | ||
| for i, item in enumerate(data): | ||
| if obj[i] is None: | ||
| tmp.append(item or None) | ||
| elif isinstance(obj[i], type): | ||
| try: | ||
| tmp.append(obj[i](item)) | ||
| except ValueError: | ||
| tmp.append(obj[i]()) | ||
| else: | ||
| try: | ||
| tmp.append(obj[i].__class__(item)) | ||
| except ValueError: | ||
| tmp.append(obj[i]) | ||
| obj[:] = tmp | ||
| else: | ||
| obj[:] = _sys.stdin.readline()[:-1].split() | ||
| return self | ||
| def _add_one(name: str) -> str: | ||
| """Add one to the end of a string. | ||
| :param name: The string to add one to. | ||
| :return: The string with one added to the end. | ||
| """ | ||
| match = _re.match(r'([\S]+)_([0-9]+)', name) | ||
| if not match: | ||
| return name + '_1' | ||
| return f'{match.group(1)}{int(match.group(2)) + 1}' |
| # log21.LoggingWindow.py | ||
| # CodeWriter2 | ||
| from __future__ import annotations | ||
| import re as _re | ||
| import threading as _threading | ||
| import subprocess as _subprocess | ||
| from enum import Enum as _Enum | ||
| from time import sleep as _sleep | ||
| from uuid import uuid4 as _uuid4 | ||
| from string import printable as _printable | ||
| from typing import Union as _Union | ||
| from logging import FileHandler as _FileHandler | ||
| from argparse import Namespace as _Namespace | ||
| from log21.Colors import hex_escape as _hex_escape, ansi_escape as _ansi_escape | ||
| from log21.Levels import NOTSET as _NOTSET | ||
| from log21.Logger import Logger as _Logger | ||
| from log21.StreamHandler import StreamHandler as _StreamHandler | ||
| __all__ = ['LoggingWindow', 'LoggingWindowHandler'] | ||
| try: | ||
| import tkinter as _tkinter | ||
| except ImportError: | ||
| _tkinter = None | ||
| ansi_to_hex_color_map = { | ||
| # https://chrisyeh96.github.io/2020/03/28/terminal-colors.html | ||
| '30': ('#000000', 'foreground'), # Black foreground | ||
| '31': ('#cc0000', 'foreground'), # Red foreground | ||
| '32': ('#4e9a06', 'foreground'), # Green foreground | ||
| '33': ('#c4a000', 'foreground'), # Yellow foreground | ||
| '34': ('#729fcf', 'foreground'), # Blue foreground | ||
| '35': ('#75507b', 'foreground'), # Magenta foreground | ||
| '36': ('#06989a', 'foreground'), # Cyan foreground | ||
| '37': ('#d3d7cf', 'foreground'), # White foreground | ||
| '90': ('#555753', 'foreground'), # Bright black foreground | ||
| '91': ('#ef2929', 'foreground'), # Bright red foreground | ||
| '92': ('#8ae234', 'foreground'), # Bright green foreground | ||
| '93': ('#fce94f', 'foreground'), # Bright yellow foreground | ||
| '94': ('#32afff', 'foreground'), # Bright blue foreground | ||
| '95': ('#ad7fa8', 'foreground'), # Bright magenta foreground | ||
| '96': ('#34e2e2', 'foreground'), # Bright cyan foreground | ||
| '97': ('#ffffff', 'foreground'), # Bright white foreground | ||
| '40': ('#000000', 'background'), # Black background | ||
| '41': ('#cc0000', 'background'), # Red background | ||
| '42': ('#4e9a06', 'background'), # Green background | ||
| '43': ('#c4a000', 'background'), # Yellow background | ||
| '44': ('#729fcf', 'background'), # Blue background | ||
| '45': ('#75507b', 'background'), # Magenta background | ||
| '46': ('#06989a', 'background'), # Cyan background | ||
| '47': ('#d3d7cf', 'background'), # White background | ||
| '100': ('#555753', 'background'), # Bright black background | ||
| '101': ('#ef2929', 'background'), # Bright red background | ||
| '102': ('#8ae234', 'background'), # Bright green background | ||
| '103': ('#fce94f', 'background'), # Bright yellow background | ||
| '104': ('#32afff', 'background'), # Bright blue background | ||
| '105': ('#ad7fa8', 'background'), # Bright magenta background | ||
| '106': ('#34e2e2', 'background'), # Bright cyan background | ||
| '107': ('#ffffff', 'background'), # Bright white background | ||
| } | ||
| _lock = _threading.RLock() | ||
| class GettingInputStatus(_Enum): | ||
| """An enum for the status of getting input.""" | ||
| NOT_GETTING_INPUT = 0 | ||
| GETTING_INPUT = 1 | ||
| CANCELLED = 2 | ||
| class CancelledInputError(InterruptedError, Exception): | ||
| """An exception raised when the input is cancelled.""" | ||
| class LoggingWindowHandler(_StreamHandler): | ||
| """A handler for logging to a LoggingWindow.""" | ||
| def __init__( | ||
| self, | ||
| logging_window: LoggingWindow, | ||
| handle_carriage_return: bool = True, | ||
| handle_new_line: bool = True | ||
| ): | ||
| """Initialize the LoggingWindowHandler. | ||
| :param logging_window: The LoggingWindow to log to. | ||
| :param handle_carriage_return: Whether to handle carriage | ||
| returns. | ||
| :param handle_new_line: Whether to handle new lines. | ||
| """ | ||
| self.HandleCR = handle_carriage_return | ||
| self.HandleNL = handle_new_line | ||
| self.__carriage_return: bool = False | ||
| self.LoggingWindow = logging_window # pylint: disable=invalid-name | ||
| super().__init__(stream=None) | ||
| def emit(self, record): | ||
| try: | ||
| if self.HandleCR: | ||
| self.check_cr(record) | ||
| if self.HandleNL: | ||
| self.check_nl(record) | ||
| msg = self.format(record) | ||
| self.write(msg) | ||
| self.write(self.terminator) | ||
| except Exception: # pylint: disable=broad-except | ||
| self.handleError(record) | ||
| def write(self, message): # pylint: disable=too-many-branches | ||
| """Write a message to the LoggingWindow. | ||
| :param message: The message to write. | ||
| """ | ||
| if self.LoggingWindow is not None: # pylint: disable=too-many-nested-blocks | ||
| # Sets the element's state to normal so that it can be modified. | ||
| self.LoggingWindow.logs.config(state=_tkinter.NORMAL) | ||
| # Handles carriage return | ||
| parts = _re.split(r'(\r)', message) | ||
| while parts: | ||
| part = parts.pop(0) | ||
| if self.__carriage_return: | ||
| # Checks if the part is printable | ||
| if any((char in _printable[:-6]) | ||
| for char in _hex_escape.sub('', _ansi_escape.sub('', part))): | ||
| # Removes the last line | ||
| self.LoggingWindow.logs.delete('end - 1 lines', _tkinter.END) | ||
| if self.LoggingWindow.logs.count('0.0', 'end')[0] != 1: | ||
| self.LoggingWindow.logs.insert('end', '\n') | ||
| self.__carriage_return = False | ||
| tags = [] | ||
| # Handles ANSI color codes | ||
| ansi_parts = _ansi_escape.split(part) | ||
| while ansi_parts: | ||
| ansi_text = ansi_parts.pop(0) | ||
| if ansi_text: | ||
| # Handles HEX color codes | ||
| hex_parts = _hex_escape.split(ansi_text) | ||
| while hex_parts: | ||
| hex_text = hex_parts.pop(0) | ||
| if hex_text: | ||
| self.LoggingWindow.logs.insert(_tkinter.END, hex_text) | ||
| if hex_parts: | ||
| hex_color = hex_parts.pop(0) | ||
| tag = str(_uuid4()) | ||
| # Foreground color | ||
| if hex_parts.pop(0) == 'f': | ||
| tags.append( | ||
| { | ||
| 'name': tag, | ||
| 'start': | ||
| self.LoggingWindow.logs.index('end-1c'), | ||
| 'config': { | ||
| 'foreground': hex_color | ||
| } | ||
| } | ||
| ) | ||
| # Background color | ||
| else: | ||
| tags.append( | ||
| { | ||
| 'name': tag, | ||
| 'start': | ||
| self.LoggingWindow.logs.index('end-1c'), | ||
| 'config': { | ||
| 'background': hex_color | ||
| } | ||
| } | ||
| ) | ||
| if ansi_parts: | ||
| ansi_params = ansi_parts.pop(0).split(';') | ||
| ansi_color = {'foreground': None, 'background': None} | ||
| for part in ansi_params: | ||
| if part in ansi_to_hex_color_map: | ||
| color_ = ansi_to_hex_color_map[part] | ||
| ansi_color[color_[1]] = color_[0] | ||
| elif part == '0': | ||
| ansi_color[ | ||
| 'foreground' | ||
| ] = self.LoggingWindow.default_foreground_color | ||
| ansi_color[ | ||
| 'background' | ||
| ] = self.LoggingWindow.default_background_color | ||
| else: | ||
| pass # error condition ignored | ||
| if ansi_color['foreground'] or ansi_color['background']: | ||
| tags.append( | ||
| { | ||
| 'name': str(_uuid4()), | ||
| 'start': self.LoggingWindow.logs.index('end-1c'), | ||
| 'config': ansi_color | ||
| } | ||
| ) | ||
| # Applies the color tags | ||
| for tag in tags: | ||
| self.LoggingWindow.logs.tag_add(tag['name'], tag['start'], 'end') | ||
| self.LoggingWindow.logs.tag_config(tag['name'], **tag['config']) | ||
| if parts: | ||
| parts.pop(0) | ||
| self.__carriage_return = True | ||
| self.LoggingWindow.logs.config(state=_tkinter.DISABLED) | ||
| self.LoggingWindow.logs.see(_tkinter.END) | ||
| class LoggingWindow(_Logger): # pylint: disable=too-many-instance-attributes | ||
| """ | ||
| Usage Example: | ||
| >>> # Manual creation | ||
| >>> # Imports the LoggingWindow and LoggingWindowHandler classes | ||
| >>> from log21 import LoggingWindow, LoggingWindowHandler | ||
| >>> # Creates a new LoggingWindow object | ||
| >>> window = LoggingWindow('Test Window', level='DEBUG') | ||
| >>> # Creates a new LoggingWindowHandler object and adds it to the LoggingWindow | ||
| >>> # we created earlier | ||
| >>> window.addHandler(LoggingWindowHandler(window)) | ||
| >>> window.debug('A debug message') | ||
| >>> window.info('An info message') | ||
| >>> # Run these lines to see the messages in the window | ||
| >>> | ||
| >>> # Automatic creation | ||
| >>> # Imports log21 and time modules | ||
| >>> import log21, time | ||
| >>> # Creates a new LoggingWindow object | ||
| >>> window = log21.get_logging_window('Test Window') | ||
| >>> # Use it without any additional steps to add handlers and formatters | ||
| >>> window.info('This works properly!') | ||
| >>> # ANSI colors usage: | ||
| >>> window.info('This is a \033[91mred\033[0m message.') | ||
| >>> window.info('\033[102mThis is a message with green background.') | ||
| >>> # HEX colors usage: | ||
| >>> window.info('\033#00FFFFhfThis is a message with cyan foreground.') | ||
| >>> window.info('\033#0000FFhbThis is a message with blue background.') | ||
| >>> # Progressbar usage: | ||
| >>> for i in range(100): | ||
| ... window.print_progress(i + 1, 100) | ||
| ... time.sleep(0.1) | ||
| ... | ||
| >>> # Gettig input from the user: | ||
| >>> name: str = window.input('Enter your name: ') | ||
| >>> window.print('Hello, ' + name + '!') | ||
| >>> # Run these lines to see the messages in the window | ||
| >>> | ||
| """ | ||
| def __init__( | ||
| self, | ||
| name, | ||
| level=_NOTSET, | ||
| width: int = 80, | ||
| height: int = 20, | ||
| default_foreground_color='white', | ||
| default_background_color='black', | ||
| font=('Courier', 10), | ||
| allow_python: bool = False, | ||
| allow_shell: bool = False, | ||
| command_history_buffer_size: int = 100 | ||
| ): # pylint: disable=too-many-statements | ||
| """Creates a new LoggingWindow object. | ||
| :param name: The name of the logger. | ||
| :param level: The level of the logger. | ||
| :param width: The width of the LoggingWindow. | ||
| :param height: The height of the LoggingWindow. | ||
| :param default_foreground_color: The default foreground color of | ||
| the LoggingWindow. | ||
| :param default_background_color: The default background color of | ||
| the LoggingWindow. | ||
| :param font: The font of the LoggingWindow. | ||
| """ | ||
| super().__init__(name, level) | ||
| self.window = _tkinter.Tk() | ||
| self.window.title(name) | ||
| # Hides window instead of closing it | ||
| self.window.protocol("WM_DELETE_WINDOW", self.hide) | ||
| self.window.resizable(False, False) | ||
| self.logs = _tkinter.Text(self.window) | ||
| self.logs.grid(row=0, column=0, sticky='nsew') | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.logs.config(wrap=_tkinter.NONE) | ||
| # Commands entry | ||
| self.command_entry = _tkinter.Entry(self.window) | ||
| self.command_entry.grid(row=1, column=0, sticky='nsew') | ||
| self.command_entry.bind('<Return>', self.execute_command) | ||
| self.command_entry.bind('<Up>', self.history_up) | ||
| self.command_entry.bind('<Down>', self.history_down) | ||
| self.command_history = [] | ||
| self.command_history_index = 0 | ||
| if not isinstance(command_history_buffer_size, (int, float)): | ||
| raise TypeError('command_history_buffer_size must be a number') | ||
| self.command_history_buffer_size = ( | ||
| command_history_buffer_size if command_history_buffer_size > 0 else 0 | ||
| ) | ||
| # Hides the command entry if allow_python and allow_shell are False | ||
| if not allow_python and not allow_shell: | ||
| self.command_entry.grid_remove() | ||
| if allow_python: | ||
| raise NotImplementedError('Python commands are not supported yet!') | ||
| self.__allow_python = False | ||
| self.__allow_shell = allow_shell | ||
| # Scroll bars | ||
| self.logs.config( | ||
| xscrollcommand=_tkinter.Scrollbar(self.window, orient=_tkinter.HORIZONTAL | ||
| ).set | ||
| ) | ||
| self.logs.config(yscrollcommand=_tkinter.Scrollbar(self.window).set) | ||
| # Input related lines | ||
| self.getting_input_status: GettingInputStatus = ( | ||
| GettingInputStatus.NOT_GETTING_INPUT | ||
| ) | ||
| self.getting_pass = False | ||
| self.input_text = '' | ||
| # cursor counter is used for making a nice blinking cursor | ||
| self.__cursor_counter = 1 | ||
| self._cursor_position = None | ||
| self.cursor_position = 0 | ||
| # KeyPress event for self.logs | ||
| self.logs.bind('<KeyPress>', self.key_press) | ||
| self.font = font | ||
| self.width = width | ||
| self.height = height | ||
| self.default_foreground_color = default_foreground_color | ||
| self.default_background_color = default_background_color | ||
| # Events for multi-threading support | ||
| self.window.bind('<<hide>>', self.__hide) | ||
| self.window.bind('<<show>>', self.__show) | ||
| self.window.bind('<<clear>>', self.__clear) | ||
| self.window.bind('<<log>>', self.__log) | ||
| self.window.bind('<<input>>', self.__input) | ||
| self.window.bind('<<type input>>', self.__type_input) | ||
| self.window.bind('<<getpass>>', self.__getpass) | ||
| self.window.bind('<<SetAllowPython>>', self.__set_allow_python) | ||
| self.window.bind('<<SetAllowShell>>', self.__set_allow_shell) | ||
| self.window.bind('<<SetCursorPosition>>', self.__set_cursor_position) | ||
| self.window.bind( | ||
| '<<SetDefaultForegroundColor>>', self.__set_default_foreground_color | ||
| ) | ||
| self.window.bind( | ||
| '<<SetDefaultBackgroundColor>>', self.__set_default_background_color | ||
| ) | ||
| self.window.bind('<<SetFont>>', self.__set_font) | ||
| self.window.bind('<<SetWidth>>', self.__set_width) | ||
| self.window.bind('<<SetHeight>>', self.__set_height) | ||
| def addHandler(self, hdlr: _Union[_FileHandler, LoggingWindowHandler]): | ||
| if not isinstance(hdlr, LoggingWindowHandler, _FileHandler): | ||
| raise TypeError("Handler must be a FileHandler or LoggingWindowHandler") | ||
| super().addHandler(hdlr) | ||
| def __hide(self, _): | ||
| self.window.withdraw() | ||
| def __show(self, _): | ||
| self.window.deiconify() | ||
| def __clear(self, _): | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete('1.0', _tkinter.END) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| def __log(self, event): | ||
| data = event.data | ||
| if self.getting_input_status == GettingInputStatus.GETTING_INPUT: | ||
| raise RuntimeError( | ||
| 'Cannot log while getting input from the user! ' | ||
| 'Please cancel the input first.' | ||
| ) | ||
| super()._log( | ||
| data.level, data.msg, data.args, data.exc_info, data.extra, data.stack_info, | ||
| data.stacklevel | ||
| ) | ||
| def __input(self, event): | ||
| data = event.data | ||
| msg = ' '.join([str(m) for m in data.msg]) + data.end | ||
| self._log( | ||
| self.level if self.level >= _NOTSET else _NOTSET, msg, data.args, | ||
| **data.kwargs | ||
| ) | ||
| self.input_text = '' | ||
| self.getting_input_status = GettingInputStatus.GETTING_INPUT | ||
| self.cursor_position = 0 | ||
| self.logs.focus() | ||
| try: | ||
| while self.getting_input_status == GettingInputStatus.GETTING_INPUT: | ||
| self.cursor_position = self.cursor_position | ||
| self.window.update() | ||
| self.window.after(10) | ||
| except KeyboardInterrupt: | ||
| self.input_text = '' | ||
| self.getting_input_status = GettingInputStatus.NOT_GETTING_INPUT | ||
| if self.getting_input_status == GettingInputStatus.NOT_GETTING_INPUT: | ||
| data.output = self.input_text | ||
| elif self.getting_input_status == GettingInputStatus.CANCELLED: | ||
| if data.raise_error: | ||
| raise CancelledInputError('Input cancelled!') | ||
| data.output = '' | ||
| def __type_input(self, event): | ||
| data = event.data | ||
| if (self.getting_input_status | ||
| != GettingInputStatus.GETTING_INPUT) and data.wait <= 0: | ||
| raise RuntimeError( | ||
| 'The logger must be getting input for this method to work! ' | ||
| 'Use `input` method or set the `wait` argument to ' | ||
| 'a value greater than 0.' | ||
| ) | ||
| while self.getting_input_status != GettingInputStatus.GETTING_INPUT: | ||
| _sleep(data.wait) | ||
| self.input_text += data.text | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete(f'end-{len(self.input_text) + 1}c', 'end-1c') | ||
| self.logs.insert(_tkinter.END, self.input_text) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.cursor_position = len(self.input_text) - 1 | ||
| def __getpass(self, event): | ||
| data = event.data | ||
| msg = ' '.join([str(m) for m in data.msg]) + data.end | ||
| self._log( | ||
| self.level if self.level >= _NOTSET else _NOTSET, msg, data.args, | ||
| **data.kwargs | ||
| ) | ||
| self.input_text = '' | ||
| self.getting_pass = True | ||
| self.cursor_position = 0 | ||
| self.logs.focus() | ||
| try: | ||
| while self.getting_pass: | ||
| self.cursor_position = self.cursor_position | ||
| self.window.update() | ||
| self.window.after(10) | ||
| except KeyboardInterrupt: | ||
| self.input_text = '' | ||
| self.getting_pass = False | ||
| data.output = self.input_text | ||
| def hide(self): | ||
| """Hides the LoggingWindow. | ||
| :return: | ||
| """ | ||
| self.window.event_generate('<<hide>>') | ||
| def show(self): | ||
| """Shows the LoggingWindow. | ||
| :return: | ||
| """ | ||
| self.window.event_generate('<<show>>') | ||
| def clear(self): | ||
| """Clears the LoggingWindow. | ||
| :return: | ||
| """ | ||
| self.window.event_generate('<<clear>>') | ||
| def _log( | ||
| self, | ||
| level, | ||
| msg, | ||
| args, | ||
| exc_info=None, | ||
| extra=None, | ||
| stack_info=False, | ||
| stacklevel=1 | ||
| ): | ||
| _lock.acquire() | ||
| self.window.event_generate( | ||
| '<<log>>', | ||
| when='tail', | ||
| data=_Namespace( | ||
| level=level, | ||
| msg=msg, | ||
| args=args, | ||
| exc_info=exc_info, | ||
| extra=extra, | ||
| stack_info=stack_info, | ||
| stacklevel=stacklevel | ||
| ) | ||
| ) | ||
| _lock.release() | ||
| def input( | ||
| self, | ||
| *msg, | ||
| args: tuple = (), | ||
| end='', | ||
| raise_error: str = False, | ||
| **kwargs | ||
| ) -> str: | ||
| """Prints a message and waits for input. | ||
| :param msg: The message to print. | ||
| :param args: The arguments to pass to the message. | ||
| :param end: The end of the message. | ||
| :param raise_error: If True, raises an error instead of | ||
| returning an empty string. | ||
| :param kwargs: | ||
| :return: The input. | ||
| """ | ||
| _lock.acquire() | ||
| data = _Namespace( | ||
| msg=msg, args=args, end=end, raise_error=raise_error, kwargs=kwargs | ||
| ) | ||
| self.window.event_generate('<<input>>', when='tail', data=data) | ||
| _lock.release() | ||
| return data.output | ||
| def cancel_input(self) -> str: | ||
| """Cancels the input. | ||
| :return: Part of the input that the user has typed | ||
| """ | ||
| if self.getting_input_status != GettingInputStatus.GETTING_INPUT: | ||
| raise RuntimeError( | ||
| 'The logger must be getting input for this method to work! ' | ||
| 'Use `input` method.' | ||
| ) | ||
| self.getting_input_status = GettingInputStatus.CANCELLED | ||
| return self.input_text | ||
| def type_input(self, text: str, wait: _Union[int, float, bool] = False): | ||
| """Types some text as a part of the input that the user can edit and | ||
| enter. | ||
| :param text: The text to type for the user | ||
| :param wait: Wait until the input function is called and then | ||
| type the text | ||
| :return: | ||
| """ | ||
| self.window.event_generate( | ||
| '<<type input>>', when='tail', data=_Namespace(text=text, wait=wait) | ||
| ) | ||
| def getpass(self, *msg, args: tuple = (), end='', **kwargs) -> str: | ||
| """Prints a message and waits for input. | ||
| :param msg: The message to print. | ||
| :param args: The arguments to pass to the message. | ||
| :param end: The end of the message. | ||
| :param kwargs: | ||
| :return: The input. | ||
| """ | ||
| _lock.acquire() | ||
| data = _Namespace(msg=msg, args=args, end=end, kwargs=kwargs) | ||
| self.window.event_generate('<<getpass>>', when='tail', data=data) | ||
| _lock.release() | ||
| return data.output | ||
| def key_press(self, event): # pylint: disable=too-many-branches | ||
| """KeyPress event callback for self.logs.""" | ||
| if (self.getting_input_status == GettingInputStatus.GETTING_INPUT | ||
| or self.getting_pass): | ||
| # Handles Enter key | ||
| if event.keysym == 'Return': | ||
| self.getting_input_status = GettingInputStatus.NOT_GETTING_INPUT | ||
| self.getting_pass = False | ||
| self.cursor_position = 0 | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.insert(_tkinter.END, '\n') | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.logs.see(_tkinter.END) | ||
| # Handles Backspace key | ||
| elif event.keysym == 'BackSpace': | ||
| if self.input_text: | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete(f'end-{len(self.input_text) + 1}c', 'end-1c') | ||
| self.input_text = self.input_text[:self.cursor_position - 1] + \ | ||
| self.input_text[self.cursor_position:] | ||
| if self.getting_pass: | ||
| self.logs.insert(_tkinter.END, '*' * len(self.input_text)) | ||
| else: | ||
| self.logs.insert(_tkinter.END, self.input_text) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.cursor_position -= 1 | ||
| # Handles Right Arrow | ||
| elif event.keysym == 'Right': | ||
| if self.cursor_position < len(self.input_text | ||
| ) and not self.getting_pass: | ||
| self.cursor_position += 1 | ||
| # Handles Left Arrow | ||
| elif event.keysym == 'Left': | ||
| if self.cursor_position > 0 and not self.getting_pass: | ||
| self.cursor_position -= 1 | ||
| # Handles other keys | ||
| elif event.char: | ||
| self.logs.config(state=_tkinter.NORMAL) | ||
| self.logs.delete(f'end-{len(self.input_text) + 1}c', 'end-1c') | ||
| self.input_text = ( | ||
| self.input_text[:self.cursor_position] + event.char + | ||
| self.input_text[self.cursor_position:] | ||
| ) | ||
| if self.getting_pass: | ||
| self.logs.insert(_tkinter.END, '*' * len(self.input_text)) | ||
| else: | ||
| self.logs.insert(_tkinter.END, self.input_text) | ||
| self.logs.config(state=_tkinter.DISABLED) | ||
| self.cursor_position += 1 | ||
| def execute_command(self, _): | ||
| """Executes the command in self.command_entry.""" | ||
| command = self.command_entry.get() | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| self.command_history.append(command) | ||
| self.command_history = self.command_history[-self.command_history_buffer_size:] | ||
| # FIXME: It doesn't support multiline commands yet | ||
| # Shell commands: | ||
| if command.startswith('!'): | ||
| if self.allow_shell: | ||
| try: | ||
| # TODO: Add the support of interactive programmes such as python | ||
| # shell and bash | ||
| output = _subprocess.check_output(command[1:].strip(), shell=False) | ||
| self.print(output.decode('utf-8').strip('\r\n')) | ||
| except _subprocess.CalledProcessError as ex: | ||
| self.error( | ||
| 'Error code:', ex.returncode, | ||
| ex.output.decode('utf-8').strip('\r\n') | ||
| ) | ||
| except FileNotFoundError: | ||
| self.error('File not found: Unrecognized command.') | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| self.error(ex) | ||
| else: | ||
| self.error('Shell commands are not allowed!') | ||
| # Python commands: | ||
| else: | ||
| if self.allow_python: | ||
| try: | ||
| # TODO: Add the support of python commands | ||
| raise NotImplementedError | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| self.error(ex) | ||
| else: | ||
| try: | ||
| output = _subprocess.check_output(command.strip(), shell=False) | ||
| self.print(output.decode('utf-8').strip('\r\n')) | ||
| except _subprocess.CalledProcessError as ex: | ||
| self.error( | ||
| 'Error code:', ex.returncode, | ||
| ex.output.decode('utf-8').strip('\r\n') | ||
| ) | ||
| except FileNotFoundError: | ||
| self.error('File not found: Unrecognized command.') | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| self.error(ex) | ||
| self.command_history_index = len(self.command_history) | ||
| def history_up(self, _): | ||
| """Moves up the command history.""" | ||
| _lock.acquire() | ||
| if self.command_history_index > 0: | ||
| self.command_history_index -= 1 | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| self.command_entry.insert( | ||
| 0, self.command_history[self.command_history_index] | ||
| ) | ||
| _lock.release() | ||
| def history_down(self, _): | ||
| """Moves down the command history.""" | ||
| _lock.acquire() | ||
| if self.command_history_index < len(self.command_history) - 1: | ||
| self.command_history_index += 1 | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| self.command_entry.insert( | ||
| 0, self.command_history[self.command_history_index] | ||
| ) | ||
| else: | ||
| self.command_entry.delete(0, _tkinter.END) | ||
| _lock.release() | ||
| def __set_allow_python(self, event): | ||
| """Sets the allow_python attribute.""" | ||
| self.__allow_python = event.data | ||
| # Hides the command entry if allow_python and allow_shell are False | ||
| if not self.__allow_python and not self.__allow_shell: | ||
| self.command_entry.grid_remove() | ||
| # Shows the command entry if allow_python or allow_shell are True | ||
| else: | ||
| self.command_entry.grid(row=1, column=0, sticky='nsew') | ||
| def __set_allow_shell(self, event): | ||
| """Sets the allow_shell attribute.""" | ||
| self.__allow_shell = event.data | ||
| # Hides the command entry if allow_python and allow_shell are False | ||
| if not self.__allow_python and not self.__allow_shell: | ||
| self.command_entry.grid_remove() | ||
| # Shows the command entry if allow_python or allow_shell are True | ||
| else: | ||
| self.command_entry.grid(row=1, column=0, sticky='nsew') | ||
| def __set_cursor_position(self, event): | ||
| """Sets the cursor_position attribute.""" | ||
| new_value = self.cursor_position != event.data | ||
| # Removes the cursor from the last position | ||
| if self.cursor_position is not None and (self.__cursor_counter % 50 == 0 | ||
| or new_value): | ||
| index = self.logs.index( | ||
| f'end-{len(self.input_text) - self.cursor_position + 2}c' | ||
| ) | ||
| self.logs.tag_add( | ||
| index, index, f'end-{len(self.input_text) - self.cursor_position + 1}c' | ||
| ) | ||
| self.logs.tag_config( | ||
| index, | ||
| background=self.default_background_color, | ||
| foreground=self.default_foreground_color | ||
| ) | ||
| self._cursor_position = event.data | ||
| self.__cursor_counter += 1 | ||
| # Places the new cursor | ||
| if self.getting_input_status == GettingInputStatus.GETTING_INPUT and ( | ||
| self.__cursor_counter % 100 == 0 or new_value): | ||
| self.__cursor_counter = 1 | ||
| index = self.logs.index( | ||
| f'end-{len(self.input_text) - self.cursor_position + 2}c' | ||
| ) | ||
| self.logs.tag_add( | ||
| index, index, f'end-{len(self.input_text) - self.cursor_position + 1}c' | ||
| ) | ||
| self.logs.tag_config( | ||
| index, | ||
| background=self.default_foreground_color, | ||
| foreground=self.default_background_color | ||
| ) | ||
| def __set_default_foreground_color(self, event): | ||
| """Sets the default_foreground_color attribute.""" | ||
| self._default_foreground_color = event.data | ||
| self.logs.config(foreground=event.data) | ||
| def __set_default_background_color(self, event): | ||
| """Sets the default_background_color attribute.""" | ||
| self._default_background_color = event.data | ||
| self.logs.config(background=event.data) | ||
| def __set_font(self, event): | ||
| """Sets the font attribute.""" | ||
| self.logs.config(font=event.data) | ||
| def __set_width(self, event): | ||
| """Sets the width attribute.""" | ||
| self.logs.config(width=event.data) | ||
| def __set_height(self, event): | ||
| """Sets the height attribute.""" | ||
| self.logs.config(height=event.data) | ||
| @property | ||
| def allow_python(self): | ||
| return self.__allow_python | ||
| @allow_python.setter | ||
| def allow_python(self, value): | ||
| raise NotImplementedError('Python commands are not supported yet!') | ||
| self.window.event_generate('<<SetAllowPython>>', when='tail', data=value) | ||
| @property | ||
| def allow_shell(self): | ||
| return self.__allow_shell | ||
| @allow_shell.setter | ||
| def allow_shell(self, value): | ||
| self.window.event_generate('<<SetAllowShell>>', when='tail', data=value) | ||
| @property | ||
| def cursor_position(self): | ||
| return self._cursor_position | ||
| @cursor_position.setter | ||
| def cursor_position(self, value): | ||
| self.window.event_generate('<<SetCursorPosition>>', when='tail', data=value) | ||
| @property | ||
| def default_foreground_color(self): | ||
| return self._default_foreground_color | ||
| @default_foreground_color.setter | ||
| def default_foreground_color(self, value): | ||
| self.window.event_generate( | ||
| '<<SetDefaultForegroundColor>>', when='tail', data=value | ||
| ) | ||
| @property | ||
| def default_background_color(self): | ||
| return self._default_background_color | ||
| @default_background_color.setter | ||
| def default_background_color(self, value): | ||
| self.window.event_generate( | ||
| '<<SetDefaultBackgroundColor>>', when='tail', data=value | ||
| ) | ||
| @property | ||
| def font(self): | ||
| return self.logs.config()['font'] | ||
| @font.setter | ||
| def font(self, value): | ||
| self.window.event_generate('<<SetFont>>', when='tail', data=value) | ||
| @property | ||
| def width(self): | ||
| return self.logs.config()['width'][-1] | ||
| @width.setter | ||
| def width(self, value): | ||
| self.window.event_generate('<<SetWidth>>', when='tail', data=value) | ||
| @property | ||
| def height(self): | ||
| return self.logs.config()['height'][-1] | ||
| @height.setter | ||
| def height(self, value): | ||
| self.window.event_generate('<<SetHeight>>', when='tail', data=value) | ||
| @property | ||
| def progress_bar(self): | ||
| if not self._progress_bar: | ||
| # Import here to avoid circular import | ||
| # pylint: disable=import-outside-toplevel | ||
| from log21.ProgressBar import ProgressBar | ||
| self._progress_bar = ProgressBar(logger=self, width=self.width) | ||
| self.window.update() | ||
| return self._progress_bar | ||
| def __del__(self): | ||
| self.window.withdraw() | ||
| self.window.destroy() | ||
| del self.window | ||
| if not _tkinter: | ||
| class LoggingWindow: # pylint: disable=function-redefined | ||
| """LoggingWindow requires tkinter to be installed.""" | ||
| def __init__(self, *args, **kwargs): | ||
| raise ImportError('LoggingWindow requires tkinter to be installed.') | ||
| class LoggingWindowHandler: # pylint: disable=function-redefined | ||
| """LoggingWindow requires tkinter to be installed.""" | ||
| def __init__(self, *args, **kwargs): | ||
| raise ImportError('LoggingWindow requires tkinter to be installed.') |
| # log21.Manager.py | ||
| # CodeWriter21 | ||
| import logging as _logging | ||
| from typing import Union as _Union | ||
| from log21.Levels import INFO as _INFO | ||
| from log21.Logger import Logger as _loggerClass | ||
| root = _logging.RootLogger(_INFO) | ||
| class Manager(_logging.Manager): | ||
| """The Manager class is a subclass of the logging.Manager class. It | ||
| overrides the getLogger method to make it more compatible with the | ||
| log21.Logger class. It also overrides the constructor.""" | ||
| def __init__(self): | ||
| self.root = root | ||
| self.disable = 0 | ||
| self.emittedNoHandlerWarning = False | ||
| self.loggerDict = {} | ||
| self.loggerClass = None | ||
| self.logRecordFactory = None | ||
| def getLogger(self, name: str) -> _Union[_logging.Logger, _loggerClass, None]: | ||
| """Takes the name of a logger and if there was a logger with that name | ||
| in the loggerDict it will return the logger otherwise it'll return | ||
| None. | ||
| :param name: The name of the logger. | ||
| :raises TypeError: A logger name must be a string | ||
| :return: | ||
| """ | ||
| if not isinstance(name, str): | ||
| raise TypeError('A logger name must be a string') | ||
| try: | ||
| if name in self.loggerDict: | ||
| rv = self.loggerDict[name] | ||
| if isinstance(rv, _logging.PlaceHolder): | ||
| rv = (self.loggerClass or _loggerClass)(name) | ||
| rv.manager = self | ||
| self.loggerDict[name] = rv | ||
| else: | ||
| return None | ||
| except Exception: # pylint: disable=broad-except | ||
| return None | ||
| return rv | ||
| def addLogger(self, name: str, logger) -> None: # pylint: disable=invalid-name | ||
| """Adds a logger to the loggerDict dictionary. | ||
| :param name: str: The name of the logger. | ||
| :param logger: The logger to save. | ||
| :raises TypeError: A logger name must be a string | ||
| :return: None | ||
| """ | ||
| if not isinstance(name, str): | ||
| raise TypeError('A logger name must be a string') | ||
| self.loggerDict[name] = logger |
| # log21.PPrint.py | ||
| # CodeWriter21 | ||
| import re as _re | ||
| import sys as _sys | ||
| import types as _types | ||
| import collections as _collections | ||
| import dataclasses as _dataclasses | ||
| from pprint import PrettyPrinter as _PrettyPrinter | ||
| from typing import Dict as _Dict, Mapping as _Mapping, Optional as _Optional | ||
| from log21.Colors import get_colors as _gc | ||
| _builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)}) | ||
| def _recursion(obj): | ||
| return f"<Recursion on {type(obj).__name__} with id={id(obj)}>" | ||
| def _safe_tuple(t): | ||
| """Helper function for comparing 2-tuples.""" | ||
| return _SafeKey(t[0]), _SafeKey(t[1]) | ||
| def _wrap_bytes_repr(obj, width, allowance): | ||
| current = b'' | ||
| last = len(obj) // 4 * 4 | ||
| for i in range(0, len(obj), 4): | ||
| part = obj[i:i + 4] | ||
| candidate = current + part | ||
| if i == last: | ||
| width -= allowance | ||
| if len(repr(candidate)) > width: | ||
| if current: | ||
| yield repr(current) | ||
| current = part | ||
| else: | ||
| current = candidate | ||
| if current: | ||
| yield repr(current) | ||
| class _SafeKey: | ||
| """Helper function for key functions when sorting unorderable objects. | ||
| The wrapped-object will fallback to a Py2.x style comparison for unorderable types | ||
| (sorting first comparing the type name and then by the obj ids). Does not work | ||
| recursively, so dict.items() must have _safe_key applied to both the key and the | ||
| value. | ||
| """ | ||
| __slots__ = ['obj'] | ||
| def __init__(self, obj): | ||
| self.obj = obj | ||
| def __lt__(self, other): | ||
| try: | ||
| return self.obj < other.obj | ||
| except TypeError: | ||
| return (str(type(self.obj)), id(self.obj | ||
| )) < (str(type(other.obj)), id(other.obj)) | ||
| class PrettyPrinter(_PrettyPrinter): | ||
| def __init__( | ||
| self, | ||
| indent=1, | ||
| width=80, | ||
| depth=None, | ||
| stream=None, | ||
| sign_colors: _Optional[_Mapping[str, str]] = None, | ||
| *, | ||
| compact=False, | ||
| sort_dicts=True, | ||
| underscore_numbers=False, | ||
| **kwargs | ||
| ): | ||
| super().__init__( | ||
| indent=indent, | ||
| width=width, | ||
| depth=depth, | ||
| stream=stream, | ||
| compact=compact, | ||
| **kwargs | ||
| ) | ||
| self._depth = depth | ||
| self._indent_per_level = indent | ||
| self._width = width | ||
| if stream is not None: | ||
| self._stream = stream | ||
| else: | ||
| self._stream = _sys.stdout | ||
| self._compact = bool(compact) | ||
| self._sort_dicts = sort_dicts | ||
| self._underscore_numbers = underscore_numbers | ||
| self.sign_colors: _Dict[str, str] = { | ||
| 'square-brackets': _gc('LightCyan'), | ||
| 'curly-braces': _gc('LightBlue'), | ||
| 'parenthesis': _gc('LightGreen'), | ||
| 'comma': _gc('LightRed'), | ||
| 'colon': _gc('LightRed'), | ||
| '...': _gc('LightMagenta'), | ||
| 'data': _gc('Green') | ||
| } | ||
| if sign_colors: | ||
| for sign, color in sign_colors.items(): | ||
| self.sign_colors[sign.lower()] = _gc(color) | ||
| def _format(self, obj, stream, indent, allowance, context, level): | ||
| objid = id(obj) | ||
| if objid in context: | ||
| stream.write(_recursion(obj)) | ||
| self._recursive = True | ||
| self._readable = False | ||
| return | ||
| rep = self._repr(obj, context, level) | ||
| max_width = self._width - indent - allowance | ||
| if len(rep) > max_width: | ||
| p = self._dispatch.get(type(obj).__repr__, None) | ||
| if p is not None: | ||
| context[objid] = 1 | ||
| p(self, obj, stream, indent, allowance, context, level + 1) | ||
| del context[objid] | ||
| return | ||
| elif (_dataclasses.is_dataclass(obj) and not isinstance(obj, type) | ||
| and obj.__dataclass_params__.repr | ||
| and # Check dataclass has generated repr method. | ||
| hasattr(obj.__repr__, "__wrapped__") and "__create_fn__" | ||
| in obj.__repr__.__wrapped__.__qualname__): | ||
| context[objid] = 1 | ||
| self._pprint_dataclass( | ||
| obj, stream, indent, allowance, context, level + 1 | ||
| ) | ||
| del context[objid] | ||
| return | ||
| stream.write(rep) | ||
| def _pprint_dataclass(self, obj, stream, indent, allowance, context, level): | ||
| cls_name = obj.__class__.__name__ | ||
| indent += len(cls_name) + 1 | ||
| items = [ | ||
| (f.name, getattr(obj, f.name)) for f in _dataclasses.fields(obj) if f.repr | ||
| ] | ||
| stream.write(cls_name + '(') | ||
| self._format_namespace_items(items, stream, indent, allowance, context, level) | ||
| stream.write(')') | ||
| def _repr(self, obj, context, level): | ||
| repr, readable, recursive = self.format(obj, context.copy(), self._depth, level) | ||
| if not readable: | ||
| self._readable = False | ||
| if recursive: | ||
| self._recursive = True | ||
| return repr | ||
| def _safe_repr(self, object_, context, max_levels, level): | ||
| # Return triple (repr_string, isreadable, isrecursive). | ||
| type_ = type(object_) | ||
| if type_ in _builtin_scalars: | ||
| return repr(object_), True, False | ||
| representation = getattr(type_, "__repr__", None) | ||
| if issubclass(type_, int) and representation is int.__repr__: | ||
| if self._underscore_numbers: | ||
| return f"{object_:_d}", True, False | ||
| else: | ||
| return repr(object_), True, False | ||
| if issubclass(type_, dict) and representation is dict.__repr__: | ||
| if not object_: | ||
| return self.sign_colors.get( | ||
| 'curly-braces', '' | ||
| ) + "{}" + self.sign_colors.get('data', ''), True, False | ||
| object_id = id(object_) | ||
| if max_levels and level >= max_levels: | ||
| return ( | ||
| self.sign_colors.get('curly-braces', '') + "{" + | ||
| self.sign_colors.get('...', '') + "..." + | ||
| self.sign_colors.get('curly-braces', '') + "}" + | ||
| self.sign_colors.get('data', ''), False, object_id in context | ||
| ) | ||
| if object_id in context: | ||
| return _recursion(object_), False, True | ||
| context[object_id] = 1 | ||
| readable = True | ||
| recursive = False | ||
| components = [] | ||
| append = components.append | ||
| level += 1 | ||
| if self._sort_dicts: | ||
| items = sorted(object_.items(), key=_safe_tuple) | ||
| else: | ||
| items = object_.items() | ||
| for k, v in items: | ||
| krepr, kreadable, krecur = self.format(k, context, max_levels, level) | ||
| vrepr, vreadable, vrecur = self.format(v, context, max_levels, level) | ||
| append( | ||
| f"{krepr}{self.sign_colors.get('colon')}:" | ||
| f"{self.sign_colors.get('data')} {vrepr}" | ||
| ) | ||
| readable = readable and kreadable and vreadable | ||
| if krecur or vrecur: | ||
| recursive = True | ||
| del context[object_id] | ||
| return ( | ||
| self.sign_colors.get('curly-braces', '') + "{" + | ||
| self.sign_colors.get('data', '') + ( | ||
| self.sign_colors.get('comma', '') + ", " + | ||
| self.sign_colors.get('data', '') | ||
| ).join(components) + self.sign_colors.get('curly-braces', '') + "}", | ||
| readable, recursive | ||
| ) | ||
| if (issubclass(type_, list) and representation is list.__repr__) or \ | ||
| (issubclass(type_, tuple) and representation is tuple.__repr__): | ||
| if issubclass(type_, list): | ||
| if not object_: | ||
| return self.sign_colors.get( | ||
| 'square-brackets', '' | ||
| ) + "[]" + self.sign_colors.get('data', ''), True, False | ||
| format_ = ( | ||
| self.sign_colors.get('square-brackets', '') + "[" + | ||
| self.sign_colors.get('data', '') + "%s" + | ||
| self.sign_colors.get('square-brackets', '') + "]" + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| elif len(object_) == 1: | ||
| format_ = ( | ||
| self.sign_colors.get('parenthesis', '') + "(" + | ||
| self.sign_colors.get('data', '') + "%s" + | ||
| self.sign_colors.get('comma', '') + "," + | ||
| self.sign_colors.get('parenthesis', '') + ")" + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| if not object_: | ||
| return self.sign_colors.get( | ||
| 'parenthesis', '' | ||
| ) + "()" + self.sign_colors.get('data', ''), True, False | ||
| format_ = ( | ||
| self.sign_colors.get('parenthesis', '') + "(" + | ||
| self.sign_colors.get('data', '') + "%s" + | ||
| self.sign_colors.get('parenthesis', '') + ")" + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| object_id = id(object_) | ||
| if max_levels and level >= max_levels: | ||
| return format_ % self.sign_colors.get( | ||
| '...' | ||
| ) + "...", False, object_id in context | ||
| if object_id in context: | ||
| return _recursion(object_), False, True | ||
| context[object_id] = 1 | ||
| readable = True | ||
| recursive = False | ||
| components = [] | ||
| append = components.append | ||
| level += 1 | ||
| for o in object_: | ||
| orepr, oreadable, orecur = self.format(o, context, max_levels, level) | ||
| append(orepr) | ||
| if not oreadable: | ||
| readable = False | ||
| if orecur: | ||
| recursive = True | ||
| del context[object_id] | ||
| return ( | ||
| format_ % ( | ||
| self.sign_colors.get('comma', '') + ", " + | ||
| self.sign_colors.get('data', '') | ||
| ).join(components), readable, recursive | ||
| ) | ||
| rep = repr(object_) | ||
| return rep, (rep and not rep.startswith('<')), False | ||
| _dispatch = {} | ||
| def _pprint_dict(self, obj, stream, indent, allowance, context, level): | ||
| write = stream.write | ||
| write( | ||
| self.sign_colors.get('curly-braces', '') + '{' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| if self._indent_per_level > 1: | ||
| write((self._indent_per_level - 1) * ' ') | ||
| length = len(obj) | ||
| if length: | ||
| if self._sort_dicts: | ||
| items = sorted(obj.items(), key=_safe_tuple) | ||
| else: | ||
| items = obj.items() | ||
| self._format_dict_items( | ||
| items, stream, indent, allowance + 1, context, level | ||
| ) | ||
| write( | ||
| self.sign_colors.get('curly-braces', '') + '}' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[dict.__repr__] = _pprint_dict | ||
| def _pprint_ordered_dict(self, obj, stream, indent, allowance, context, level): | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis') + '(' + | ||
| self.sign_colors.get('data') | ||
| ) | ||
| self._format( | ||
| list(obj.items()), stream, indent + len(cls.__name__) + 1, allowance + 1, | ||
| context, level | ||
| ) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict | ||
| def _pprint_list(self, obj, stream, indent, allowance, context, level): | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + '[' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format_items(obj, stream, indent, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + ']' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[list.__repr__] = _pprint_list | ||
| def _pprint_tuple(self, obj, stream, indent, allowance, context, level): | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| endchar = ( | ||
| self.sign_colors.get('comma', '') + ',' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| if len(obj) == 1 else self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format_items( | ||
| obj, stream, indent, allowance + len(endchar), context, level | ||
| ) | ||
| stream.write(endchar) | ||
| _dispatch[tuple.__repr__] = _pprint_tuple | ||
| def _pprint_set(self, obj, stream, indent, allowance, context, level): | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| typ = obj.__class__ | ||
| if typ is set: | ||
| stream.write( | ||
| self.sign_colors.get('curly-braces', '') + '{' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| endchar = self.sign_colors.get('curly-braces', | ||
| '') + '}' + self.sign_colors.get('data', '') | ||
| else: | ||
| stream.write( | ||
| typ.__name__ + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('curly-braces', '') + '{' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| endchar = ( | ||
| self.sign_colors.get('curly-braces', '') + '}' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| indent += len(typ.__name__) + 1 | ||
| obj = sorted(obj, key=_SafeKey) | ||
| self._format_items( | ||
| obj, stream, indent, allowance + len(endchar), context, level | ||
| ) | ||
| stream.write(endchar) | ||
| _dispatch[set.__repr__] = _pprint_set | ||
| _dispatch[frozenset.__repr__] = _pprint_set | ||
| def _pprint_str(self, object_, stream, indent, allowance, context, level): | ||
| write = stream.write | ||
| if not len(object_): | ||
| write(repr(object_)) | ||
| return | ||
| chunks = [] | ||
| lines = object_.splitlines(True) | ||
| if level == 1: | ||
| indent += 1 | ||
| allowance += 1 | ||
| max_width1 = max_width = self._width - indent | ||
| representation = '' | ||
| for i, line in enumerate(lines): | ||
| representation = repr(line) | ||
| if i == len(lines) - 1: | ||
| max_width1 -= allowance | ||
| if len(representation) <= max_width1: | ||
| chunks.append(representation) | ||
| else: | ||
| # A list of alternating (non-space, space) strings | ||
| parts = _re.findall(r'\S*\s*', line) | ||
| assert parts | ||
| assert not parts[-1] | ||
| parts.pop() # drop empty last part | ||
| max_width2 = max_width | ||
| current = '' | ||
| for j, part in enumerate(parts): | ||
| candidate = current + part | ||
| if j == len(parts) - 1 and i == len(lines) - 1: | ||
| max_width2 -= allowance | ||
| if len(repr(candidate)) > max_width2: | ||
| if current: | ||
| chunks.append(repr(current)) | ||
| current = part | ||
| else: | ||
| current = candidate | ||
| if current: | ||
| chunks.append(repr(current)) | ||
| if len(chunks) == 1: | ||
| write(representation) | ||
| return | ||
| if level == 1: | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| for i, representation in enumerate(chunks): | ||
| if i > 0: | ||
| write('\n' + ' ' * indent) | ||
| write(representation) | ||
| if level == 1: | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[str.__repr__] = _pprint_str | ||
| def _pprint_bytes(self, obj, stream, indent, allowance, context, level): | ||
| write = stream.write | ||
| if len(obj) <= 4: | ||
| write(repr(obj)) | ||
| return | ||
| parens = level == 1 | ||
| if parens: | ||
| indent += 1 | ||
| allowance += 1 | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| delim = '' | ||
| for rep in _wrap_bytes_repr(obj, self._width - indent, allowance): | ||
| write(delim) | ||
| write(rep) | ||
| if not delim: | ||
| delim = '\n' + ' ' * indent | ||
| if parens: | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[bytes.__repr__] = _pprint_bytes | ||
| def _pprint_bytearray(self, obj, stream, indent, allowance, context, level): | ||
| write = stream.write | ||
| write( | ||
| 'bytearray' + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._pprint_bytes( | ||
| bytes(obj), stream, indent + 10, allowance + 1, context, level + 1 | ||
| ) | ||
| write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[bytearray.__repr__] = _pprint_bytearray | ||
| def _pprint_mappingproxy(self, obj, stream, indent, allowance, context, level): | ||
| stream.write( | ||
| 'mappingproxy' + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format(obj.copy(), stream, indent + 13, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy | ||
| def _pprint_simplenamespace(self, obj, stream, indent, allowance, context, level): | ||
| if isinstance(obj, _types.SimpleNamespace): | ||
| # The SimpleNamespace repr is "namespace" instead of the class | ||
| # name, so we do the same here. For subclasses; use the class name. | ||
| cls_name = 'namespace' | ||
| else: | ||
| cls_name = obj.__class__.__name__ | ||
| indent += len(cls_name) + 1 | ||
| items = obj.__dict__.items() | ||
| stream.write( | ||
| cls_name + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format_namespace_items(items, stream, indent, allowance, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace | ||
| def _format_dict_items(self, items, stream, indent, allowance, context, level): | ||
| write = stream.write | ||
| indent += self._indent_per_level | ||
| delimnl = self.sign_colors.get('comma', '') + ',\n' + self.sign_colors.get( | ||
| 'data', '' | ||
| ) + ' ' * indent | ||
| last_index = len(items) - 1 | ||
| for i, (key, ent) in enumerate(items): | ||
| last = i == last_index | ||
| rep = self._repr(key, context, level) | ||
| write(rep) | ||
| write( | ||
| self.sign_colors.get('colon', '') + ': ' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| self._format( | ||
| ent, stream, indent + len(rep) + 2, allowance if last else 1, context, | ||
| level | ||
| ) | ||
| if not last: | ||
| write(delimnl) | ||
| def _format_namespace_items(self, items, stream, indent, allowance, context, level): | ||
| write = stream.write | ||
| delimnl = self.sign_colors.get('comma', '') + ',\n' + self.sign_colors.get( | ||
| 'data', '' | ||
| ) + ' ' * indent | ||
| last_index = len(items) - 1 | ||
| for i, (key, ent) in enumerate(items): | ||
| last = i == last_index | ||
| write(key) | ||
| write('=') | ||
| if id(ent) in context: | ||
| # Special-case representation of recursion to match standard | ||
| # recursive dataclass repr. | ||
| write( | ||
| self.sign_colors.get('...', '') + "..." + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| self._format( | ||
| ent, stream, indent + len(key) + 1, allowance if last else 1, | ||
| context, level | ||
| ) | ||
| if not last: | ||
| write(delimnl) | ||
| def _format_items(self, items, stream, indent, allowance, context, level): | ||
| write = stream.write | ||
| indent += self._indent_per_level | ||
| if self._indent_per_level > 1: | ||
| write((self._indent_per_level - 1) * ' ') | ||
| delimnl = self.sign_colors.get('comma', '') + ',\n' + self.sign_colors.get( | ||
| 'data', '' | ||
| ) + ' ' * indent | ||
| delim = '' | ||
| width = max_width = self._width - indent + 1 | ||
| it = iter(items) | ||
| try: | ||
| next_ent = next(it) | ||
| except StopIteration: | ||
| return | ||
| last = False | ||
| while not last: | ||
| ent = next_ent | ||
| try: | ||
| next_ent = next(it) | ||
| except StopIteration: | ||
| last = True | ||
| max_width -= allowance | ||
| width -= allowance | ||
| if self._compact: | ||
| rep = self._repr(ent, context, level) | ||
| w = len(rep) + 2 | ||
| if width < w: | ||
| width = max_width | ||
| if delim: | ||
| delim = delimnl | ||
| if width >= w: | ||
| width -= w | ||
| write(delim) | ||
| delim = self.sign_colors.get( | ||
| 'comma', '' | ||
| ) + ', ' + self.sign_colors.get('data', '') | ||
| write(rep) | ||
| continue | ||
| write(delim) | ||
| delim = delimnl | ||
| self._format(ent, stream, indent, allowance if last else 1, context, level) | ||
| def _pprint_default_dict(self, obj, stream, indent, allowance, context, level): | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| rdf = self._repr(obj.default_factory, context, level) | ||
| cls = obj.__class__ | ||
| indent += len(cls.__name__) + 1 | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis') + '(' + | ||
| self.sign_colors.get('data') + rdf + self.sign_colors.get('comma') + ',\n' + | ||
| self.sign_colors.get('data') + (' ' * indent) | ||
| ) | ||
| self._pprint_dict(obj, stream, indent, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict | ||
| def _pprint_counter(self, obj, stream, indent, allowance, context, level): | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis') + '(' + | ||
| self.sign_colors.get('curly-braces') + '{' + self.sign_colors.get('data') | ||
| ) | ||
| if self._indent_per_level > 1: | ||
| stream.write((self._indent_per_level - 1) * ' ') | ||
| items = obj.most_common() | ||
| self._format_dict_items( | ||
| items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level | ||
| ) | ||
| stream.write( | ||
| self.sign_colors.get('curly-braces', '') + '}' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| _dispatch[_collections.Counter.__repr__] = _pprint_counter | ||
| def _pprint_chain_map(self, obj, stream, indent, allowance, context, level): | ||
| if not len(obj.maps): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| indent += len(cls.__name__) + 1 | ||
| for i, m in enumerate(obj.maps): | ||
| if i == len(obj.maps) - 1: | ||
| self._format(m, stream, indent, allowance + 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| self._format(m, stream, indent, 1, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('comma', '') + ',\n' + | ||
| self.sign_colors.get('data', '') + ' ' * indent | ||
| ) | ||
| _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map | ||
| def _pprint_deque(self, obj, stream, indent, allowance, context, level): | ||
| if not len(obj): | ||
| stream.write(repr(obj)) | ||
| return | ||
| cls = obj.__class__ | ||
| stream.write( | ||
| cls.__name__ + self.sign_colors.get('parenthesis', '') + '(' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| indent += len(cls.__name__) + 1 | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + '[' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| if obj.maxlen is None: | ||
| self._format_items(obj, stream, indent, allowance + 2, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + ']' + | ||
| self.sign_colors.get('parenthesis', '') + ')' + | ||
| self.sign_colors.get('data', '') | ||
| ) | ||
| else: | ||
| self._format_items(obj, stream, indent, 2, context, level) | ||
| rml = self._repr(obj.maxlen, context, level) | ||
| stream.write( | ||
| self.sign_colors.get('square-brackets', '') + ']' + | ||
| self.sign_colors.get('comma', '') + ',' + | ||
| self.sign_colors.get('data', '') + '\n' + (' ' * indent) + 'maxlen=' + | ||
| rml + self.sign_colors.get('parenthesis', '') + ')' | ||
| ) | ||
| _dispatch[_collections.deque.__repr__] = _pprint_deque | ||
| def _pprint_user_dict(self, obj, stream, indent, allowance, context, level): | ||
| self._format(obj.data, stream, indent, allowance, context, level - 1) | ||
| _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict | ||
| def _pprint_user_list(self, obj, stream, indent, allowance, context, level): | ||
| self._format(obj.data, stream, indent, allowance, context, level - 1) | ||
| _dispatch[_collections.UserList.__repr__] = _pprint_user_list | ||
| def _pprint_user_string(self, obj, stream, indent, allowance, context, level): | ||
| self._format(obj.data, stream, indent, allowance, context, level - 1) | ||
| _dispatch[_collections.UserString.__repr__] = _pprint_user_string | ||
| def pformat( | ||
| obj, | ||
| indent=1, | ||
| width=80, | ||
| depth=None, | ||
| signs_colors: _Optional[_Mapping[str, str]] = None, | ||
| *, | ||
| compact=False, | ||
| sort_dicts=True, | ||
| underscore_numbers=False, | ||
| **kwargs | ||
| ): | ||
| """Format a Python object into a pretty-printed representation. | ||
| :param obj: the object to format. | ||
| :param indent: the number of spaces to indent for each level of nesting. | ||
| :param width: the maximum width of the formatted representation. | ||
| :param depth: the maximum depth to print out nested structures. | ||
| :param signs_colors: a mapping of signs and colors. | ||
| :param compact: if `True`, several items will be combined in one line. | ||
| :param sort_dicts: if `True`, dictionaries will be sorted by key. (py38+) | ||
| :param underscore_numbers: if `True`, numbers will be represented with an | ||
| underscore between every digit. (py310+) | ||
| :param kwargs: additional keyword arguments to pass to the underlying pretty | ||
| printer. | ||
| :return: the formatted representation. | ||
| """ | ||
| return PrettyPrinter( | ||
| indent=indent, | ||
| width=width, | ||
| depth=depth, | ||
| compact=compact, | ||
| sign_colors=signs_colors, | ||
| sort_dicts=sort_dicts, | ||
| underscore_numbers=underscore_numbers, | ||
| **kwargs | ||
| ).pformat(obj) |
| # log21.ProgressBar.py | ||
| # CodeWriter21 | ||
| from __future__ import annotations | ||
| import shutil as _shutil | ||
| from typing import Any as _Any, Mapping as _Mapping, Optional as _Optional | ||
| import log21 as _log21 | ||
| from log21.Colors import get_colors as _gc | ||
| from log21.Logger import Logger as _Logger | ||
| from log21.StreamHandler import ColorizingStreamHandler as _ColorizingStreamHandler | ||
| _logger = _Logger('ProgressBar') | ||
| _logger.addHandler(_ColorizingStreamHandler()) | ||
| __all__ = ['ProgressBar'] | ||
| class ProgressBar: # pylint: disable=too-many-instance-attributes, line-too-long | ||
| """ | ||
| Usage Example: | ||
| >>> pb = ProgressBar(width=20, show_percentage=False, prefix='[', suffix=']', | ||
| ... fill='=', empty='-') | ||
| >>> pb(0, 10) | ||
| [/-----------------] | ||
| >>> pb(1, 10) | ||
| [==----------------] | ||
| >>> pb(2, 10) | ||
| [====\\-------------] | ||
| >>> | ||
| >>> # A better example | ||
| >>> import time | ||
| >>> pb = ProgressBar() | ||
| >>> for i in range(500): | ||
| ... pb(i + 1, 500) | ||
| ... time.sleep(0.01) | ||
| ... | ||
| |███████████████████████████████████████████████████████████████████| 100% | ||
| >>> # Of course, You should try it yourself to see the progress! XD | ||
| >>> | ||
| """ | ||
| def __init__( | ||
| self, | ||
| *, | ||
| width: _Optional[int] = None, | ||
| show_percentage: bool = True, | ||
| prefix: str = '|', | ||
| suffix: str = '|', | ||
| fill: str = '█', | ||
| empty: str = ' ', | ||
| format_: _Optional[str] = None, | ||
| style: str = '%', | ||
| new_line_when_complete: bool = True, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| no_color: bool = False, | ||
| logger: _log21.Logger = _logger, | ||
| additional_variables: _Optional[_Mapping[str, _Any]] = None | ||
| ): # pylint: disable=too-many-branches, too-many-statements | ||
| """ | ||
| :param args: Prevents the use of positional arguments | ||
| :param width: The width of the progress bar | ||
| :param show_percentage: Whether to show the percentage of the progress | ||
| :param prefix: The prefix of the progress bar | ||
| :param suffix: The suffix of the progress bar | ||
| :param fill: The fill character of the progress bar | ||
| :param empty: The empty character of the progress bar | ||
| :param format_: The format of the progress bar | ||
| :param style: The style that is used to format the progress bar | ||
| :param new_line_when_complete: Whether to print a new line when the progress is | ||
| complete or failed | ||
| :param colors: The colors of the progress bar | ||
| :param no_color: If True, removes the colors of the progress bar | ||
| :param logger: The logger to use | ||
| :param additional_variables: Additional variables to use in the format and their | ||
| default values | ||
| """ | ||
| # Sets a default value for the width | ||
| if width is None: | ||
| try: | ||
| width = _shutil.get_terminal_size().columns - 1 | ||
| except OSError: | ||
| width = 50 | ||
| if width < 1: | ||
| width = 50 | ||
| self.width = width | ||
| if self.width < 3: | ||
| raise ValueError('`width` must be greater than 1') | ||
| if not isinstance(fill, str): | ||
| raise TypeError('`fill` must be a string') | ||
| if not isinstance(empty, str): | ||
| raise TypeError('`empty` must be a string') | ||
| if not isinstance(prefix, str): | ||
| raise TypeError('`prefix` must be a string') | ||
| if not isinstance(suffix, str): | ||
| raise TypeError('`suffix` must be a string') | ||
| if len(fill) != 1: | ||
| raise ValueError('`fill` must be a single character') | ||
| if len(empty) != 1: | ||
| raise ValueError('`empty` must be a single character') | ||
| if style not in ['%', '{']: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| if colors and no_color: | ||
| raise PermissionError( | ||
| 'You cannot use `no_color` and `colors` parameters together!' | ||
| ) | ||
| if additional_variables: | ||
| if not isinstance(additional_variables, _Mapping): | ||
| raise TypeError( | ||
| '`additional_variables` must be a dictionary like object.' | ||
| ) | ||
| for key, value in additional_variables.items(): | ||
| if not isinstance(key, str): | ||
| raise TypeError('`additional_variables` keys must be strings') | ||
| if not isinstance(value, str): | ||
| additional_variables[key] = str(value) | ||
| else: | ||
| additional_variables = {} | ||
| self.colors = { | ||
| 'progress in-progress': _gc('LightYellow'), | ||
| 'progress complete': _gc('LightGreen'), | ||
| 'progress failed': _gc('LightRed'), | ||
| 'percentage in-progress': _gc('LightBlue'), | ||
| 'percentage complete': _gc('LightCyan'), | ||
| 'percentage failed': _gc('LightRed'), | ||
| 'prefix-color in-progress': _gc('Yellow'), | ||
| 'prefix-color complete': _gc('Green'), | ||
| 'prefix-color failed': _gc('Red'), | ||
| 'suffix-color in-progress': _gc('Yellow'), | ||
| 'suffix-color complete': _gc('Green'), | ||
| 'suffix-color failed': _gc('Red'), | ||
| 'reset-color': _gc('Reset'), | ||
| } | ||
| self.spinner = ['|', '/', '-', '\\'] | ||
| self.fill = fill | ||
| self.empty = empty | ||
| self.prefix = prefix | ||
| self.suffix = suffix | ||
| if format_: | ||
| self.format = format_ | ||
| else: | ||
| self.format = ( | ||
| '%(prefix)s%(bar)s%(suffix)s %(percentage)s%%' | ||
| if show_percentage else '%(prefix)s%(bar)s%(suffix)s' | ||
| ) | ||
| style = '%' | ||
| self.style = style | ||
| self.new_line_when_complete = new_line_when_complete | ||
| if colors: | ||
| for key, value in colors.items(): | ||
| self.colors[key] = value | ||
| if no_color: | ||
| self.colors = {name: '' for name in self.colors} | ||
| self.logger = logger | ||
| self.additional_variables = additional_variables | ||
| self.i = 0 | ||
| def get_bar(self, progress: float, total: float, **kwargs) -> str: | ||
| """Return the progress bar as a string. | ||
| :param progress: The current progress. (e.g. 21) | ||
| :param total: The total progress. (e.g. 100) | ||
| :param kwargs: Additional variables to be used in the format | ||
| string. | ||
| :raises ValueError: If the style is not supported. | ||
| Set the style to one of the following: | ||
| + '%' | ||
| + '{' | ||
| e.g. bar = ProgressBar(style='{') | ||
| :return: The progress bar as a string. | ||
| """ | ||
| if progress == total: | ||
| return self.progress_complete(**kwargs) | ||
| if progress > total or progress < 0: | ||
| return self.progress_failed(progress, total, **kwargs) | ||
| return self.progress_in_progress(progress, total, **kwargs) | ||
| def progress_in_progress(self, progress: float, total: float, **kwargs) -> str: | ||
| """Return the progress bar as a string when the progress is in progress. | ||
| :param progress: The current progress. (e.g. 21) | ||
| :param total: The total progress. (e.g. 100) | ||
| :param kwargs: Additional variables to be used in the format | ||
| string. | ||
| :raises ValueError: If the style is not supported. (supported | ||
| styles: '%', '{') | ||
| :return: The progress bar as a string. | ||
| """ | ||
| percentage = str(round(progress / total * 100, 2)) | ||
| progress_dict = { | ||
| 'prefix': self.prefix, | ||
| 'bar': '', | ||
| 'suffix': self.suffix, | ||
| 'percentage': percentage, | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| if key in ['prefix', 'bar', 'suffix', 'percentage']: | ||
| raise ValueError(f'`{key}` is a reserved keyword') | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| used_characters = len(self.format % progress_dict) | ||
| elif self.style == '{': | ||
| used_characters = len(self.format.format(**progress_dict)) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| fill_length = round(progress / total * (self.width - used_characters)) | ||
| empty_length = (self.width - (fill_length + used_characters)) - 1 | ||
| if self.i >= 3: | ||
| self.i = 0 | ||
| else: | ||
| self.i += 1 | ||
| spinner_char = self.spinner[self.i] if empty_length > 0 else '' | ||
| progress_dict = { | ||
| 'prefix': | ||
| self.colors['prefix-color in-progress'] + self.prefix + | ||
| self.colors['reset-color'], | ||
| 'bar': | ||
| self.colors['progress in-progress'] + | ||
| (self.fill * fill_length + spinner_char + self.empty * empty_length) + | ||
| self.colors['reset-color'], | ||
| 'suffix': | ||
| self.colors['suffix-color in-progress'] + self.suffix + | ||
| self.colors['reset-color'], | ||
| 'percentage': | ||
| self.colors["percentage in-progress"] + str(percentage) + | ||
| self.colors['reset-color'], | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| return '\r' + self.format % progress_dict + self.colors['reset-color'] | ||
| if self.style == '{': | ||
| return '\r' + self.format.format(**progress_dict | ||
| ) + self.colors['reset-color'] | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| def progress_complete(self, **kwargs) -> str: | ||
| """Prints the progress bar as complete. | ||
| :param kwargs: Additional variables to be passed to the format string. | ||
| :raises ValueError: If the style is not either `%` or `{`. | ||
| :return: The formatted progress bar. | ||
| """ | ||
| progress_dict = { | ||
| 'prefix': self.prefix, | ||
| 'bar': '', | ||
| 'suffix': self.suffix, | ||
| 'percentage': '100', | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| if key in ['prefix', 'bar', 'suffix', 'percentage']: | ||
| raise ValueError(f'`{key}` is a reserved keyword') | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| bar_length = self.width - len(self.format % progress_dict) | ||
| elif self.style == '{': | ||
| bar_length = self.width - len(self.format.format(**progress_dict)) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| progress_dict = { | ||
| 'prefix': | ||
| self.colors['prefix-color complete'] + self.prefix + | ||
| self.colors['reset-color'], | ||
| 'bar': | ||
| self.colors['progress complete'] + (self.fill * bar_length) + | ||
| self.colors['reset-color'], | ||
| 'suffix': | ||
| self.colors['suffix-color complete'] + self.suffix + | ||
| self.colors['reset-color'], | ||
| 'percentage': | ||
| self.colors["percentage complete"] + '100' + self.colors['reset-color'], | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| return ( | ||
| '\r' + self.format % progress_dict + self.colors['reset-color'] + | ||
| ('\n' if self.new_line_when_complete else '') | ||
| ) | ||
| if self.style == '{': | ||
| return ( | ||
| '\r' + self.format.format(**progress_dict) + | ||
| self.colors['reset-color'] + | ||
| ('\n' if self.new_line_when_complete else '') | ||
| ) | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| def progress_failed(self, progress: float, total: float, **kwargs): | ||
| """Returns a progress bar with a failed state. | ||
| :param progress: The current progress. | ||
| :param total: The total progress. | ||
| :param kwargs: Additional variables to be passed to the format string. | ||
| :raises ValueError: If the style is not `%` or `{`. | ||
| :return: A progress bar with a failed state. | ||
| """ | ||
| progress_dict = { | ||
| 'prefix': self.prefix, | ||
| 'bar': '', | ||
| 'suffix': self.suffix, | ||
| 'percentage': str(round(progress / total * 100, 2)), | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| if key in ['prefix', 'bar', 'suffix', 'percentage']: | ||
| raise ValueError(f'`{key}` is a reserved keyword') | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| bar_length = self.width - len(self.format % progress_dict) | ||
| elif self.style == '{': | ||
| bar_length = self.width - len(self.format.format(**progress_dict)) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| if progress > total: | ||
| bar_char = self.fill | ||
| else: | ||
| bar_char = self.empty | ||
| progress_dict = { | ||
| 'prefix': | ||
| self.colors['prefix-color failed'] + self.prefix + | ||
| self.colors['reset-color'], | ||
| 'bar': | ||
| self.colors['progress failed'] + (bar_char * bar_length) + | ||
| self.colors['reset-color'], | ||
| 'suffix': | ||
| self.colors['suffix-color failed'] + self.suffix + | ||
| self.colors['reset-color'], | ||
| 'percentage': | ||
| self.colors["percentage failed"] + progress_dict['percentage'] + | ||
| self.colors['reset-color'], | ||
| **self.additional_variables | ||
| } | ||
| for key, value in kwargs.items(): | ||
| progress_dict[key] = value | ||
| if self.style == '%': | ||
| progress_bar = self.format % progress_dict | ||
| elif self.style == '{': | ||
| progress_bar = self.format.format(**progress_dict) | ||
| else: | ||
| raise ValueError('`style` must be either `%` or `{`') | ||
| return '\r' + progress_bar + self.colors['reset-color'] + ( | ||
| '\n' if self.new_line_when_complete else '' | ||
| ) | ||
| def __call__( | ||
| self, | ||
| progress: float, | ||
| total: float, | ||
| logger: _Optional[_log21.Logger] = None, | ||
| **kwargs | ||
| ): | ||
| if not logger: | ||
| logger = self.logger | ||
| logger.print(self.get_bar(progress, total, **kwargs), end='') | ||
| def update( | ||
| self, | ||
| progress: float, | ||
| total: float, | ||
| logger: _Optional[_log21.Logger] = None, | ||
| **kwargs | ||
| ): | ||
| """Update the progress bar. | ||
| :param progress: The current progress. | ||
| :param total: The total progress. | ||
| :param logger: The logger to use. If not specified, the logger specified in the | ||
| constructor will be used. | ||
| :param kwargs: Additional variables to be used in the format string. | ||
| :raises ValueError: If the style is not `%` or `{`. | ||
| """ | ||
| self(progress, total, logger, **kwargs) |
| # log21.StreamHandler.py | ||
| # CodeWriter21 | ||
| import os as _os | ||
| import re as _re | ||
| import shutil as _shutil | ||
| from typing import Optional as _Optional | ||
| from logging import StreamHandler as _StreamHandler | ||
| from log21.Colors import (get_colors as _gc, hex_escape as _hex_escape, | ||
| ansi_escape as _ansi_escape) | ||
| __all__ = ['IS_WINDOWS', 'ColorizingStreamHandler', 'StreamHandler'] | ||
| IS_WINDOWS = _os.name == 'nt' | ||
| if IS_WINDOWS: | ||
| import ctypes | ||
| class StreamHandler(_StreamHandler): | ||
| """A StreamHandler that can handle carriage returns and new lines.""" | ||
| terminator = '' | ||
| def __init__( | ||
| self, | ||
| handle_carriage_return: bool = True, | ||
| handle_new_line: bool = True, | ||
| stream=None, | ||
| formatter=None, | ||
| level=None | ||
| ): | ||
| """Initialize the StreamHandler. | ||
| :param handle_carriage_return: Whether to handle carriage | ||
| returns. | ||
| :param handle_new_line: Whether to handle new lines. | ||
| :param stream: The stream to write to. | ||
| :param formatter: The formatter to use. | ||
| :param level: The level to log at. | ||
| """ | ||
| self.HandleCR = handle_carriage_return | ||
| self.HandleNL = handle_new_line | ||
| super().__init__(stream=stream) | ||
| if formatter is not None: | ||
| self.setFormatter(formatter) | ||
| if level is not None: | ||
| self.setLevel(level) | ||
| def check_cr(self, record): | ||
| """Check if the record contains a carriage return and handle it.""" | ||
| if record.msg: | ||
| msg = _hex_escape.sub( | ||
| '', _ansi_escape.sub('', record.msg.strip(' \t\n\x0b\x0c')) | ||
| ) | ||
| if '\r' == msg[:1]: | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): # stdout or stderr | ||
| self.stream.write( | ||
| '\r' + ( | ||
| ' ' * ( | ||
| _shutil.get_terminal_size(file_descriptor).columns - | ||
| 1 | ||
| ) | ||
| ) + '\r' | ||
| ) | ||
| index = record.msg.rfind('\r') | ||
| find = _re.compile(r'(\x1b\[(?:\d+(?:;(?:\d+))*)m)') | ||
| record.msg = _gc(*find.split(record.msg[:index]) | ||
| ) + record.msg[index + 1:] | ||
| def check_nl(self, record): | ||
| """Check if the record contains a newline and handle it.""" | ||
| while record.msg and record.msg[0] == '\n': | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): # stdout or stderr | ||
| self.stream.write('\n') | ||
| record.msg = record.msg[1:] | ||
| def emit(self, record): | ||
| if self.HandleCR: | ||
| self.check_cr(record) | ||
| if self.HandleNL: | ||
| self.check_nl(record) | ||
| super().emit(record) | ||
| def clear_line(self, length: _Optional[int] = None): | ||
| """Clear the current line. | ||
| :param length: The length of the line to clear. | ||
| :return: | ||
| """ | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): | ||
| if length is None: | ||
| length = _shutil.get_terminal_size(file_descriptor).columns | ||
| self.stream.write('\r' + (' ' * (length - 1)) + '\r') | ||
| # A stream handler that supports colorizing. | ||
| class ColorizingStreamHandler(StreamHandler): | ||
| """A stream handler that supports colorizing even in Windows.""" | ||
| def emit(self, record): | ||
| try: | ||
| if self.HandleCR: | ||
| self.check_cr(record) | ||
| if self.HandleNL: | ||
| self.check_nl(record) | ||
| msg = self.format(record) | ||
| if IS_WINDOWS: | ||
| self.convert_and_write(msg) | ||
| self.convert_and_write(self.terminator) | ||
| else: | ||
| self.write(msg) | ||
| self.write(self.terminator) | ||
| self.flush() | ||
| except Exception: # pylint: disable=broad-except | ||
| self.handleError(record) | ||
| # Writes colorized text to the Windows console. | ||
| def convert_and_write(self, message): | ||
| """Convert the message to a Windows console colorized message and write | ||
| it to the stream.""" | ||
| nt_color_map = { | ||
| 30: 0, # foreground: black - 0b00000000 | ||
| 31: 4, # foreground: red - 0b00000100 | ||
| 32: 2, # foreground: green - 0b00000010 | ||
| 33: 6, # foreground: yellow - 0b00000110 | ||
| 34: 1, # foreground: blue - 0b00000001 | ||
| 35: 5, # foreground: magenta - 0b00000101 | ||
| 36: 3, # foreground: cyan - 0b00000011 | ||
| 37: 7, # foreground: white - 0b00000111 | ||
| 40: 0, # background: black - 0b00000000 = 0 << 4 | ||
| 41: 64, # background: red - 0b01000000 = 4 << 4 | ||
| 42: 32, # background: green - 0b00100000 = 2 << 4 | ||
| 43: 96, # background: yellow - 0b01100000 = 6 << 4 | ||
| 44: 16, # background: blue - 0b00010000 = 1 << 4 | ||
| 45: 80, # background: magenta - 0b01010000 = 5 << 4 | ||
| 46: 48, # background: cyan - 0b00110000 = 3 << 4 | ||
| 47: 112, # background: white - 0b01110000 = 7 << 4 | ||
| 90: 8, # foreground: gray - 0b00001000 | ||
| 91: 12, # foreground: light red - 0b00001100 | ||
| 92: 10, # foreground: light green - 0b00001010 | ||
| 93: 14, # foreground: light yellow - 0b00001110 | ||
| 94: 9, # foreground: light blue - 0b00001001 | ||
| 95: 13, # foreground: light magenta - 0b00001101 | ||
| 96: 11, # foreground: light cyan - 0b00001011 | ||
| 97: 15, # foreground: light white - 0b00001111 | ||
| 100: 128, # background: gray - 0b10000000 = 8 << 4 | ||
| 101: 192, # background: light red - 0b11000000 = 12 << 4 | ||
| 102: 160, # background: light green - 0b10100000 = 10 << 4 | ||
| 103: 224, # background: light yellow - 0b11100000 = 14 << 4 | ||
| 104: 144, # background: light blue - 0b10010000 = 9 << 4 | ||
| 105: 208, # background: light magenta - 0b11010000 = 13 << 4 | ||
| 106: 176, # background: light cyan - 0b10110000 = 11 << 4 | ||
| 107: 240, # background: light white - 0b11110000 = 15 << 4 | ||
| 2: 8, | ||
| 0: 7 | ||
| } | ||
| parts = _ansi_escape.split(message) | ||
| win_handle = None | ||
| file_descriptor = getattr(self.stream, 'fileno', None) | ||
| if file_descriptor: | ||
| file_descriptor = file_descriptor() | ||
| if file_descriptor in (1, 2): # stdout or stderr | ||
| win_handle = ctypes.windll.kernel32.GetStdHandle(-10 - file_descriptor) | ||
| while parts: | ||
| text = parts.pop(0) | ||
| if text: | ||
| self.write(text) | ||
| if parts: | ||
| params = parts.pop(0) | ||
| if win_handle is not None: | ||
| params = [int(p) for p in params.split(';')] | ||
| color = 0 | ||
| for param in params: | ||
| if param in nt_color_map: | ||
| color |= nt_color_map[param] | ||
| else: | ||
| pass # error condition ignored | ||
| ctypes.windll.kernel32.SetConsoleTextAttribute(win_handle, color) | ||
| # Writes the message to the console. | ||
| def write(self, message): | ||
| """Write the message to the stream.""" | ||
| self.stream.write(message) | ||
| self.flush() |
| # log21.TreePrint.py | ||
| # CodeWriter21 | ||
| from __future__ import annotations | ||
| from typing import (List as _List, Union as _Union, Mapping as _Mapping, | ||
| Optional as _Optional, Sequence as _Sequence) | ||
| from log21.Colors import get_colors as _gc | ||
| class TreePrint: | ||
| """A class to help you print objects in a tree-like format.""" | ||
| class Node: | ||
| """A class to represent a node in a tree.""" | ||
| def __init__( | ||
| self, | ||
| value: _Union[str, int], | ||
| children: _Optional[_List[TreePrint.Node]] = None, | ||
| indent: int = 4, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| mode: str = '-' | ||
| ): | ||
| """Initialize a node. | ||
| :param value: The value of the node. | ||
| :param children: The children of the node. | ||
| :param indent: Number of spaces to indent the node. | ||
| :param colors: Colors to use for the node. | ||
| :param mode: Choose between '-' and '='. | ||
| """ | ||
| self.value = str(value) | ||
| if children: | ||
| self._children = children | ||
| else: | ||
| self._children = [] | ||
| self.indent = indent | ||
| self.colors = { | ||
| 'branches': _gc('Green'), | ||
| 'fruit': _gc('LightMagenta'), | ||
| } | ||
| if colors: | ||
| for key, value_ in colors.items(): | ||
| if key in self.colors: | ||
| self.colors[key] = _gc(value_) | ||
| if not mode: | ||
| self.mode = 1 | ||
| else: | ||
| self.mode = self._get_mode(mode) | ||
| if self.mode == -1: | ||
| raise ValueError('`mode` must be - or =') | ||
| def _get_mode(self, mode: _Optional[_Union[str, int]] = None) -> int: | ||
| if not mode: | ||
| mode = self.mode | ||
| if isinstance(mode, int): | ||
| if mode in [1, 2]: | ||
| return mode | ||
| elif isinstance(mode, str): | ||
| if mode in '-_─┌│|└┬├└': | ||
| return 1 | ||
| if mode in '=═╔║╠╚╦╚': | ||
| return 2 | ||
| return -1 | ||
| def __str__(self, level=0, prefix='', mode=None): | ||
| mode = self._get_mode(mode) | ||
| if mode == -1: | ||
| raise ValueError('`mode` must be - or =') | ||
| chars = '─┌│└┬├└' | ||
| if mode == 2: | ||
| chars = '═╔║╚╦╠╚' | ||
| text = _gc(self.colors['branches']) + prefix | ||
| if level == 0: | ||
| text += chars[0] # ─ OR ═ | ||
| prefix += ' ' | ||
| if self.has_child(): | ||
| text += chars[4] # ┬ OR ╦ | ||
| else: | ||
| text += chars[0] # ─ OR ═ | ||
| text += ' ' + _gc(self.colors['fruit']) + str(self.value) + '\n' | ||
| for i, child in enumerate(self._children): | ||
| prefix_ = '' | ||
| for part in prefix: | ||
| if part in '┌│├┬╔║╠╦': | ||
| prefix_ += chars[2] # │ OR ║ | ||
| elif part in chars: | ||
| prefix_ += ' ' | ||
| else: | ||
| prefix_ += part | ||
| if i + 1 == len(self._children): | ||
| prefix_ += chars[6] # └ OR ╚ | ||
| else: | ||
| prefix_ += chars[5] # ├ OR ╠ | ||
| prefix_ += chars[0] * (self.indent - 1) # ─ OR ═ | ||
| prefix_ = prefix_[:len(prefix)] + _gc(self.colors['branches'] | ||
| ) + prefix_[len(prefix):] | ||
| text += child.__str__(level=level + 1, prefix=prefix_, mode=mode) | ||
| return text | ||
| def has_child(self): | ||
| """Return True if node has children, False otherwise.""" | ||
| return len(self._children) > 0 | ||
| def add_child(self, child: TreePrint.Node): | ||
| """Add a child to the node.""" | ||
| if not isinstance(child, TreePrint.Node): | ||
| raise TypeError('`child` must be TreePrint.Node') | ||
| self._children.append(child) | ||
| def get_child(self, value: _Optional[str] = None, index: _Optional[int] = None): | ||
| """Get a child by value or index.""" | ||
| if value and index: | ||
| raise ValueError('`value` and `index` can not be both set') | ||
| if not value and not index: | ||
| raise ValueError('`value` or `index` must be set') | ||
| if value: | ||
| for child in self._children: | ||
| if child.value == value: | ||
| return child | ||
| if index: | ||
| return self._children[index] | ||
| raise ValueError(f'Failed to find child: {value = }, {index = }') | ||
| def add_to( | ||
| self: TreePrint.Node, | ||
| data: _Union[_Mapping, _Sequence, str, int], | ||
| indent: int = 4, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| mode='-' | ||
| ): # pylint: disable=too-many-branches | ||
| """Add data to the node.""" | ||
| if isinstance(data, _Mapping): | ||
| if len(data) == 1: | ||
| child = TreePrint.Node( | ||
| list(data.keys())[0], indent=indent, colors=colors, mode=mode | ||
| ) | ||
| child.add_to( | ||
| list(data.values())[0], indent=indent, colors=colors, mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| else: | ||
| for key, value in data.items(): | ||
| child = TreePrint.Node( | ||
| key, indent=indent, colors=colors, mode=mode | ||
| ) | ||
| child.add_to(value, indent=indent, colors=colors, mode=mode) | ||
| self.add_child(child) | ||
| elif isinstance(data, _Sequence) and not isinstance(data, str): | ||
| if len(data) == 1: | ||
| self.add_child( | ||
| TreePrint.Node( | ||
| data[0], indent=indent, colors=colors, mode=mode | ||
| ) | ||
| ) | ||
| else: | ||
| for value in data: | ||
| if isinstance(value, _Mapping): | ||
| for key, dict_value in value.items(): | ||
| child = TreePrint.Node( | ||
| key, indent=indent, colors=colors, mode=mode | ||
| ) | ||
| child.add_to( | ||
| dict_value, indent=indent, colors=colors, mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| elif isinstance(value, _Sequence): | ||
| child = TreePrint.Node( | ||
| '>', indent=indent, colors=colors, mode=mode | ||
| ) | ||
| child.add_to(value, indent=indent, colors=colors, mode=mode) | ||
| self.add_child(child) | ||
| else: | ||
| child = TreePrint.Node( | ||
| str(value), indent=indent, colors=colors, mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| else: | ||
| child = TreePrint.Node( | ||
| str(data), indent=indent, colors=colors, mode=mode | ||
| ) | ||
| self.add_child(child) | ||
| def __init__( | ||
| self, | ||
| data: _Union[_Mapping, _Sequence, str, int], | ||
| indent: int = 4, | ||
| colors: _Optional[_Mapping[str, str]] = None, | ||
| mode='-' | ||
| ): | ||
| self.indent = indent | ||
| self.mode = mode | ||
| if isinstance(data, _Mapping): | ||
| if len(data) == 1: | ||
| self.root = self.Node( | ||
| list(data.keys())[0], indent=indent, colors=colors | ||
| ) | ||
| self.add_to_root(list(data.values()), colors=colors) | ||
| else: | ||
| self.root = self.Node('root', indent=indent, colors=colors) | ||
| self.add_to_root(data, colors=colors) | ||
| elif isinstance(data, _Sequence): | ||
| self.root = self.Node('root', indent=indent, colors=colors) | ||
| self.add_to_root(data, colors=colors) | ||
| else: | ||
| self.root = self.Node(str(data), indent=indent, colors=colors) | ||
| def add_to_root( | ||
| self, | ||
| data: _Union[_Mapping, _Sequence, str, int], | ||
| colors: _Optional[_Mapping[str, str]] = None | ||
| ): | ||
| """Add data to root node.""" | ||
| self.root.add_to(data, indent=self.indent, colors=colors, mode=self.mode) | ||
| def __str__(self, mode=None): | ||
| if not mode: | ||
| mode = self.mode | ||
| return self.root.__str__(mode=mode) | ||
| def tree_format( | ||
| data: _Union[_Mapping, _Sequence, str, int], | ||
| indent: int = 4, | ||
| mode='-', | ||
| colors: _Optional[_Mapping[str, str]] = None | ||
| ) -> str: | ||
| """Return a tree representation of data. | ||
| :param data: data to be represented as a tree (dict, list, str, int) | ||
| :param indent: number of spaces to indent each level of the tree | ||
| :param mode: mode of tree representation ('-', '=') | ||
| :param colors: colors to use for each level of the tree | ||
| :return: tree representation of data | ||
| """ | ||
| return str(TreePrint(data, indent=indent, colors=colors, mode=mode)) |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
382001
37.18%8333
44.42%22
-18.52%