├── .github └── workflows │ └── python-publish.yml ├── LICENSE ├── setup.py ├── .gitignore ├── README_CN.md ├── README.md └── exceptionx ├── __init__.py └── i exceptionx.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 16 | uses: actions/setup-python@v3 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install build 23 | - name: Build package 24 | run: python -m build 25 | - name: Publish package 26 | uses: pypa/gh-action-pypi-publish@master 27 | with: 28 | user: __token__ 29 | password: ${{ secrets.PYPI_API_TOKEN }} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 GQYLPY . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name='exceptionx', 5 | version='4.1.9', 6 | author='Unnamed great master', 7 | author_email='', 8 | license='MIT', 9 | url='http://gqylpy.com', 10 | project_urls={'Source': 'https://github.com/gqylpy/exceptionx'}, 11 | description=''' 12 | The `exceptionx` is a flexible and convenient Python exception handling 13 | library that allows you to dynamically create exception classes and 14 | provides various exception handling mechanisms. 15 | '''.strip().replace('\n ', ''), 16 | long_description=open('README.md', encoding='utf8').read(), 17 | long_description_content_type='text/markdown', 18 | packages=['exceptionx'], 19 | python_requires='>=3.8', 20 | classifiers=[ 21 | 'Development Status :: 4 - Beta', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Natural Language :: Chinese (Simplified)', 25 | 'Natural Language :: English', 26 | 'Operating System :: OS Independent', 27 | 'Topic :: Software Development :: Libraries :: Python Modules', 28 | 'Topic :: Software Development :: Bug Tracking', 29 | 'Topic :: Software Development :: Widget Sets', 30 | 'Topic :: Artistic Software', 31 | 'Programming Language :: Python :: 3.8', 32 | 'Programming Language :: Python :: 3.9', 33 | 'Programming Language :: Python :: 3.10', 34 | 'Programming Language :: Python :: 3.11', 35 | 'Programming Language :: Python :: 3.12', 36 | 'Programming Language :: Python :: 3.13' 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *.pyc 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | py2env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | .mypy_cache/ 31 | .idea/ 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # IPython Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv/ 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # Mac 101 | .DS_Store 102 | 103 | # vim 104 | *.swp 105 | 106 | # netCDF Files 107 | *.nc 108 | conda-requirements.txt 109 | 110 | tests/ 111 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | [LOGO](http://gqylpy.com) 2 | [![Release](https://img.shields.io/github/release/gqylpy/exceptionx.svg?style=flat-square")](https://github.com/gqylpy/exceptionx/releases/latest) 3 | [![Python Versions](https://img.shields.io/pypi/pyversions/exceptionx)](https://pypi.org/project/exceptionx) 4 | [![License](https://img.shields.io/pypi/l/exceptionx)](https://github.com/gqylpy/exceptionx/blob/main/LICENSE) 5 | [![Downloads](https://static.pepy.tech/badge/exceptionx)](https://pepy.tech/project/exceptionx) 6 | 7 | # exceptionx 8 | [English](README.md) | 中文 9 | 10 | __exceptionx__ 是一个灵活且便捷的Python异常处理库,允许你动态创建异常类,并提供多种异常处理机制。 11 | > exceptionx 的前身是 [gqylpy-exception](https://github.com/gqylpy/gqylpy-exception)。 12 | 13 | pip3 install exceptionx 14 | 15 | ## 动态创建异常 16 | 17 | 使用 exceptionx,你可以在需要时即时创建异常类,而无需提前定义。例如,如果你希望抛出一个名为 `NotUnderstandError` 的异常,只需导入库并以如下方式调用: 18 | ```python 19 | import exceptionx as ex 20 | 21 | raise ex.NotUnderstandError(...) 22 | ``` 23 | 24 | 在这里,`NotUnderstandError` 并不是 exceptionx 预先定义的,而是在你尝试访问 `e.NotUnderstandError` 时通过魔化方法 `__getattr__` 动态创建的。这种灵活性意味着你可以根据需要创建任何名称的异常类。 25 | 26 | 此外,exceptionx 还确保不会重复创建相同的异常类。所有已创建的异常类都会被存储在 `e.__history__` 字典中,以便后续快速访问。 27 | 28 | 还有一种用法,导入即创建: 29 | ```python 30 | from exceptionx import NotUnderstandError 31 | 32 | raise NotUnderstandError(...) 33 | ``` 34 | 35 | ## 强大的异常处理功能 36 | 37 | exceptionx 还提供了一系列强大的异常处理工具: 38 | - `TryExcept`: 装饰器,捕获被装饰的函数中引发的异常,并将异常信息输出到终端(不是抛出)。这有助于避免程序因未处理的异常而崩溃。 39 | - `Retry`: 装饰器,同上,并会尝试重新执行,通过参数控制次数和每次重试之间的间隔时间,在达到最大次数后抛出异常。 40 | - `TryContext`: 上下文管理器,使用 `with` 语句,你可以轻松捕获代码块中引发的异常,并将异常信息输出到终端。 41 | 42 | **使用 `TryExcept` 处理函数中引发的异常** 43 | ```python 44 | from exceptionx import TryExcept 45 | 46 | @TryExcept(ValueError) 47 | def func(): 48 | int('a') 49 | ``` 50 | 默认的处理方案是将异常简要信息输出到终端,不会中断程序执行。当然,也可以输出到日志或做其它处理,通过参数控制。 51 | 52 | > 根据 Python 编程规范,处理异常时应明确指定异常类型。因此,在使用 `TryExcept` 装饰器时,需要明确传递所处理的异常类型。 53 | 54 | **使用 `Retry` 重试函数中引发的异常** 55 | ```python 56 | from exceptionx import Retry 57 | 58 | @Retry(sleep=1, count=3) 59 | def func(): 60 | int('a') 61 | ``` 62 | 若被装饰的函数中引发了异常,会尝试重新执行被装饰的函数,默认重试 `Exception` 及其所有子类的异常。像上面这样调用 `Retry(sleep=1, count=3)` 表示最大执行3次,每次间隔1秒。 63 | 64 | `Retry` 可以配合 `TryExcept` 使用,将先重试异常,若重试无果,则在最后处理异常: 65 | ```python 66 | from exceptionx import TryExcept, Retry 67 | 68 | @TryExcept(ValueError) 69 | @Retry(sleep=1, count=3) 70 | def func(): 71 | int('a') 72 | ``` 73 | 74 | **使用 `TryContext` 处理上下文中引发的异常** 75 | ```python 76 | from exceptionx import TryContext 77 | 78 | with TryContext(ValueError): 79 | int('a') 80 | ``` 81 | 82 | 通过 exceptionx,你可以更加灵活和高效地处理Python程序中的异常,提升代码的健壮性和可靠性。 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [LOGO](http://gqylpy.com) 2 | [![Release](https://img.shields.io/github/release/gqylpy/exceptionx.svg?style=flat-square")](https://github.com/gqylpy/exceptionx/releases/latest) 3 | [![Python Versions](https://img.shields.io/pypi/pyversions/exceptionx)](https://pypi.org/project/exceptionx) 4 | [![License](https://img.shields.io/pypi/l/exceptionx)](https://github.com/gqylpy/exceptionx/blob/main/LICENSE) 5 | [![Downloads](https://static.pepy.tech/badge/exceptionx)](https://pepy.tech/project/exceptionx) 6 | 7 | # exceptionx 8 | English | [中文](https://github.com/gqylpy/exceptionx/blob/main/README_CN.md) 9 | 10 | __exceptionx__ is a flexible and convenient Python exception handling library that allows you to dynamically create exception classes and provides various exception handling mechanisms. 11 | > The predecessor of exceptionx is [gqylpy-exception](https://github.com/gqylpy/gqylpy-exception). 12 | 13 | pip3 install exceptionx 14 | 15 | ## Dynamically Creating Exceptions 16 | 17 | With exceptionx, you can instantly create exception classes when needed, without the need for advance definition. For example, if you want to throw an exception named `NotUnderstandError`, you can simply import the library and call it as follows: 18 | 19 | ```python 20 | import exceptionx as ex 21 | 22 | raise ex.NotUnderstandError(...) 23 | ``` 24 | 25 | Here, `NotUnderstandError` is not predefined by exceptionx but is dynamically created through the magic method `__getattr__` when you try to access `e.NotUnderstandError`. This flexibility means you can create exception classes with any name as needed. 26 | 27 | Additionally, exceptionx ensures that the same exception class is not created repeatedly. All created exception classes are stored in the `e.__history__` dictionary for quick access later. 28 | 29 | There is another usage, import and create immediately: 30 | 31 | ```python 32 | from exceptionx import NotUnderstandError 33 | 34 | raise NotUnderstandError(...) 35 | ``` 36 | 37 | ## Powerful Exception Handling Capabilities 38 | 39 | exceptionx also provides a series of powerful exception handling tools: 40 | 41 | - `TryExcept`: A decorator that catches exceptions raised in the decorated function and outputs the exception information to the terminal (instead of throwing it). This helps prevent the program from crashing due to unhandled exceptions. 42 | - `Retry`: A decorator that works similarly to `TryExcept` but attempts to re-execute the function, controlling the number of attempts and the interval between each retry through parameters. It throws an exception after reaching the maximum number of attempts. 43 | - `TryContext`: A context manager that allows you to easily catch exceptions raised in a code block using the `with` statement and output the exception information to the terminal. 44 | 45 | **Handling Exceptions in Functions with `TryExcept`** 46 | 47 | ```python 48 | from exceptionx import TryExcept 49 | 50 | @TryExcept(ValueError) 51 | def func(): 52 | int('a') 53 | ``` 54 | 55 | The default handling scheme is to output brief exception information to the terminal without interrupting program execution. Of course, it can also be output to logs or processed in other ways through parameters. 56 | 57 | > According to Python programming conventions, exception types should be explicitly specified when handling exceptions. Therefore, when using the `TryExcept` decorator, it is necessary to explicitly pass the handled exception types. 58 | 59 | **Retrying Exceptions in Functions with `Retry`** 60 | 61 | ```python 62 | from exceptionx import Retry 63 | 64 | @Retry(sleep=1, count=3) 65 | def func(): 66 | int('a') 67 | ``` 68 | 69 | If an exception is raised in the decorated function, it will attempt to re-execute the decorated function. The default behavior is to retry exceptions of type `Exception` and all its subclasses. Calling `Retry(sleep=1, count=3)` as above means a maximum of 3 attempts will be made, with a 1-second interval between each attempt. 70 | 71 | `Retry` can be used in combination with `TryExcept` to retry exceptions first and then handle them if the retries are unsuccessful: 72 | 73 | ```python 74 | from exceptionx import TryExcept, Retry 75 | 76 | @TryExcept(ValueError) 77 | @Retry(sleep=1, count=3) 78 | def func(): 79 | int('a') 80 | ``` 81 | 82 | **Handling Exceptions in Contexts with `TryContext`** 83 | 84 | ```python 85 | from exceptionx import TryContext 86 | 87 | with TryContext(ValueError): 88 | int('a') 89 | ``` 90 | 91 | With exceptionx, you can handle exceptions in Python programs more flexibly and efficiently, enhancing the robustness and reliability of your code. 92 | -------------------------------------------------------------------------------- /exceptionx/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The `exceptionx` is a flexible and convenient Python exception handling library 3 | that allows you to dynamically create exception classes and provides various 4 | exception handling mechanisms. 5 | 6 | Key Features: 7 | 8 | - Dynamic Exception Creation: 9 | Dynamically generate exception classes through simple APIs for easy project 10 | management and reuse. 11 | 12 | - Powerful Exception Handling: 13 | Offers decorators (`TryExcept`, `Retry`) and context managers (`TryContext`) 14 | for flexible handling of exceptions within functions or code blocks. 15 | 16 | - Configurable Options: 17 | Supports various exception handling options such as silent handling, raw 18 | exception output, logging, custom callbacks, and more. 19 | 20 | Example Usage: 21 | 22 | Dynamic Exception Creation: 23 | >>> import exceptionx as ex 24 | >>> raise ex.AnError(...) 25 | 26 | Handling Exceptions with Decorators: 27 | >>> from exceptionx import TryExcept, Retry 28 | 29 | >>> @TryExcept(ValueError) 30 | >>> def func(): 31 | >>> int('a') 32 | 33 | >>> @Retry(sleep=1, count=3) 34 | >>> def func(): 35 | >>> int('a') 36 | 37 | Handling Exceptions with Context Managers: 38 | >>> from exceptionx import TryContext 39 | 40 | >>> with TryContext(ValueError): 41 | >>> int('a') 42 | 43 | For more information please visit https://github.com/gqylpy/exceptionx. 44 | """ 45 | import sys 46 | import typing 47 | 48 | from typing import Type, TypeVar, Protocol, Optional, Tuple, Dict, Callable, Any 49 | 50 | if sys.version_info >= (3, 10): 51 | from typing import TypeAlias 52 | else: 53 | TypeAlias = TypeVar('TypeAlias') 54 | 55 | if typing.TYPE_CHECKING: 56 | import threading 57 | 58 | 59 | class HasWarningMethod(Protocol): 60 | def warning(self, msg: Any): ... 61 | 62 | 63 | class HasErrorMethod(Protocol): 64 | def error(self, msg: Any): ... 65 | 66 | 67 | ETypes: TypeAlias = \ 68 | TypeVar('ETypes', Type[Exception], Tuple[Type[Exception], ...]) 69 | 70 | ELogger: TypeAlias = TypeVar('ELogger', HasWarningMethod, HasErrorMethod, '...') 71 | ECallback: TypeAlias = TypeVar('ECallback', bound=Callable[..., None]) 72 | 73 | WrappedClosure: TypeAlias = TypeVar('WrappedClosure', bound=Callable[..., Any]) 74 | Second: TypeAlias = TypeVar('Second', int, float, str) 75 | 76 | 77 | class Error(Exception): 78 | """ 79 | All exception classes created with `exceptionx` inherit from it. 80 | You can use it to handle any exception created by `exceptionx`. 81 | """ 82 | msg: Any = Exception.args 83 | 84 | 85 | __history__: Dict[str, Type[Error]] 86 | # All the exception classes you've ever created are here. 87 | # This dictionary is read-only. 88 | 89 | 90 | def __getattr__(ename: str, /) -> Type[Error]: 91 | """ 92 | Create an exception type called `ename` and return it. 93 | 94 | The created exception type will be stored to the dictionary `__history__`, 95 | and when you create an exception type with the same name again, directly get 96 | the value from this dictionary, rather than being created repeatedly. 97 | 98 | For Python built-in exception types, returned directly, are not repeatedly 99 | creation, and not stored to dictionary `__history__`. 100 | """ 101 | return __history__.setdefault(ename, type(ename, (Error,), {})) 102 | 103 | 104 | def TryExcept( 105 | etype: ETypes, 106 | /, *, 107 | emsg: Optional[str] = None, 108 | silent: Optional[bool] = None, 109 | raw: Optional[bool] = None, 110 | invert: Optional[bool] = None, 111 | last_tb: Optional[bool] = None, 112 | logger: Optional[ELogger] = None, 113 | ereturn: Optional[Any] = None, 114 | ecallback: Optional[ECallback] = None, 115 | eexit: Optional[bool] = None 116 | ) -> WrappedClosure: 117 | """ 118 | `TryExcept` is a decorator that handles exceptions raised by the function it 119 | decorates (support decorating asynchronous functions). 120 | 121 | >>> @TryExcept(ValueError) 122 | >>> def func(): 123 | >>> int('a') 124 | 125 | @param etype: 126 | The types of exceptions to be handled, multiple types can be passed in 127 | using a tuple. 128 | 129 | @param emsg: 130 | The exception message. Only when the information of the captured 131 | exception contains this string, a retry will be performed; otherwise, 132 | the encountered exception will be thrown immediately. This is used to 133 | filter the exception messages that need to be retried, but it is not 134 | recommended to use it. 135 | 136 | @param silent: 137 | If True, exceptions will be silently handled without any output. The 138 | default is False. 139 | 140 | @param raw: 141 | If True, raw exception information will be directly output. The default 142 | is False. Note that its priority is lower than the `silent` parameter. 143 | 144 | @param invert: 145 | Used for inverting the exception type. If set to True, it will not 146 | handle the exception specified by the parameter `etype`, but instead 147 | handle all other exceptions that inherit from `Exception`. The default 148 | is False. 149 | 150 | @param last_tb: 151 | Whether to trace to the last traceback object of the exception. The 152 | default is False, tracing only to the current code segment. 153 | 154 | @param logger: 155 | By default, exception information is output to the terminal via 156 | `sys.stderr`. If you want to use your own logger to record exception 157 | information, you can pass the logger to this parameter, and the `error` 158 | method of the logger will be called internally. 159 | 160 | @param ereturn: 161 | The value to be returned when the decorated function raises an 162 | exception. The default is None. 163 | 164 | @param ecallback: 165 | Accepts a callable object and invokes it when an exception is raised. 166 | The callable object takes one argument, the raised exception object. 167 | 168 | @param eexit: 169 | If True, the program will execute `raise SystemExit(4)` and exit after 170 | an exception is raised, with an exit code of 4. If the ecallback 171 | parameter is provided, the program will execute the callback function 172 | first before exiting. The default is False. 173 | """ 174 | 175 | 176 | def Retry( 177 | etype: Optional[ETypes] = None, 178 | /, *, 179 | emsg: Optional[str] = None, 180 | sleep: Optional[Second] = None, 181 | count: Optional[int] = None, 182 | limit_time: Optional[Second] = None, 183 | event: Optional['threading.Event'] = None, 184 | silent: Optional[bool] = None, 185 | raw: Optional[bool] = None, 186 | invert: Optional[bool] = None, 187 | last_tb: Optional[bool] = None, 188 | logger: Optional[ELogger] = None 189 | ) -> WrappedClosure: 190 | """ 191 | `Retry` is a decorator that retries exceptions raised by the function it 192 | decorates (support decorating asynchronous functions). When an exception is 193 | raised in the decorated function, it attempts to re-execute the decorated 194 | function. 195 | 196 | >>> @Retry(sleep=1, count=3) 197 | >>> def func(): 198 | >>> int('a') 199 | 200 | >>> @TryExcept(ValueError) 201 | >>> @Retry(sleep=1, count=3) 202 | >>> def func(): 203 | >>> int('a') 204 | 205 | @param etype: 206 | The types of exceptions to be handled, multiple types can be specified 207 | by passing them in a tuple. The default is `Exception`. 208 | 209 | @param emsg: 210 | The exception message. Only when the information of the captured 211 | exception contains this string, a retry will be performed; otherwise, 212 | the encountered exception will be thrown immediately. This is used to 213 | filter the exception messages that need to be retried, but it is not 214 | recommended to use it. 215 | 216 | @param sleep: 217 | The interval time between each retry, default is 0 seconds. The interval 218 | time will always be slightly longer than the actual value (almost 219 | negligible). Note that the interval time will be reduced by the time 220 | consumed by the execution of the decorated function. `sleep` supports 221 | passing time in the format of "1h2m3s". 222 | 223 | @param count: 224 | The number of retries, 0 means infinite retries, infinite by default. 225 | 226 | @param limit_time: 227 | This parameter is used to set the total time limit for retry operations 228 | in seconds. 0 indicates no time limit, default is no time limit. If the 229 | total time taken by the retry operation (including the time to execute 230 | the function and the interval time, with the interval time always added 231 | beforehand) exceeds this limit, the retry will be stopped immediately 232 | and the last encountered exception will be thrown. `limit_time` supports 233 | passing time in the format of "1h2m3s". 234 | 235 | @param event: 236 | An optional `threading.Event` object used to control the retry 237 | mechanism. During the retry process, this event can be set at any time 238 | to stop retrying. Once the event is set, even if the retry count has 239 | not reached the set upper limit or the time limit has not been exceeded, 240 | retrying will stop immediately and the last encountered exception will 241 | be thrown. 242 | 243 | @param silent: 244 | If True, exceptions will be silently handled without any output. The 245 | default is False. 246 | 247 | @param raw: 248 | If True, raw exception information will be directly output. The default 249 | is False. Note that its priority is lower than the `silent` parameter. 250 | 251 | @param invert: 252 | Used for inverting the exception type. If set to True, it will not retry 253 | the exception specified by the parameter `etype`, but instead retry all 254 | other exceptions that inherit from `Exception`. The default is False. 255 | 256 | @param last_tb: 257 | Whether to trace to the last traceback object of the exception. The 258 | default is False, tracing only to the current code segment. 259 | 260 | @param logger: 261 | By default, exception information is output to the terminal via 262 | `sys.stderr`. If you want to use your own logger to record exception 263 | information, you can pass the logger to this parameter, and the 264 | `warning` method of the logger will be called internally. 265 | """ 266 | 267 | 268 | def TryContext( 269 | etype: ETypes, 270 | /, *, 271 | emsg: Optional[str] = None, 272 | silent: Optional[bool] = None, 273 | raw: Optional[bool] = None, 274 | invert: Optional[bool] = None, 275 | last_tb: Optional[bool] = None, 276 | logger: Optional[ELogger] = None, 277 | ecallback: Optional[ECallback] = None, 278 | eexit: Optional[bool] = None 279 | ) -> None: 280 | """ 281 | `TryContext` is a context manager that handles exceptions raised within the 282 | context. 283 | 284 | >>> with TryContext(ValueError): 285 | >>> int('a') 286 | 287 | @param etype: 288 | The types of exceptions to be handled, multiple types can be passed in 289 | using a tuple. 290 | 291 | @param emsg: 292 | The exception message. Only when the information of the captured 293 | exception contains this string, a retry will be performed; otherwise, 294 | the encountered exception will be thrown immediately. This is used to 295 | filter the exception messages that need to be retried, but it is not 296 | recommended to use it. 297 | 298 | @param silent: 299 | If True, exceptions will be silently handled without any output. The 300 | default is False. 301 | 302 | @param raw: 303 | If True, raw exception information will be directly output. The default 304 | is False. Note that its priority is lower than the `silent` parameter. 305 | 306 | @param invert: 307 | Used for inverting the exception type. If set to True, it will not 308 | handle the exception specified by the parameter `etype`, but instead 309 | handle all other exceptions that inherit from `Exception`. The default 310 | is False. 311 | 312 | @param last_tb: 313 | Whether to trace to the last traceback object of the exception. The 314 | default is False, tracing only to the current code segment. 315 | 316 | @param logger: 317 | By default, exception information is output to the terminal via 318 | `sys.stderr`. If you want to use your own logger to record exception 319 | information, you can pass the logger to this parameter, and the `error` 320 | method of the logger will be called internally. 321 | 322 | @param ecallback: 323 | Accepts a callable object and invokes it when an exception is raised. 324 | The callable object takes one argument, the raised exception object. 325 | 326 | @param eexit: 327 | If True, the program will execute `raise SystemExit(4)` and exit after 328 | an exception is raised, with an exit code of 4. If the ecallback 329 | parameter is provided, the program will execute the callback function 330 | first before exiting. The default is False. 331 | """ 332 | 333 | 334 | class _xe6_xad_x8c_xe7_x90_xaa_xe6_x80_xa1_xe7_x8e_xb2_xe8_x90_x8d_xe4_xba_x91: 335 | gpack = globals() 336 | gpath = f'{__name__}.i {__name__}' 337 | gcode = __import__(gpath, fromlist=...) 338 | 339 | gpack['Error'] = gcode.Error 340 | gpack['__history__'] = gcode.__history__ 341 | 342 | for gname in gcode.__dir__(): 343 | gfunc = getattr(gcode, gname) 344 | if gname in gpack and getattr(gfunc, '__module__', None) == gpath: 345 | gfunc.__module__ = __package__ 346 | gfunc.__doc__ = gpack[gname].__doc__ 347 | gpack[gname] = gfunc 348 | -------------------------------------------------------------------------------- /exceptionx/i exceptionx.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import time 4 | import logging 5 | import asyncio 6 | import builtins 7 | import warnings 8 | import functools 9 | import traceback 10 | import threading 11 | 12 | from copy import copy, deepcopy 13 | from contextlib import contextmanager 14 | 15 | from types import FrameType, TracebackType 16 | 17 | from typing import ( 18 | Type, TypeVar, Final, Protocol, Optional, Union, Tuple, Callable, NoReturn, 19 | Any 20 | ) 21 | 22 | if sys.version_info >= (3, 9): 23 | from typing import Annotated 24 | else: 25 | class Annotated(metaclass=type('', (type,), { 26 | '__new__': lambda *a: type.__new__(*a)() 27 | })): 28 | def __getitem__(self, *a): ... 29 | 30 | if sys.version_info >= (3, 10): 31 | from typing import TypeAlias 32 | else: 33 | TypeAlias = TypeVar('TypeAlias') 34 | 35 | 36 | class HasWarningMethod(Protocol): 37 | def warning(self, msg: Any): ... 38 | 39 | 40 | class HasErrorMethod(Protocol): 41 | def error(self, msg: Any): ... 42 | 43 | 44 | Wrapped = WrappedClosure = TypeVar('Wrapped', bound=Callable[..., Any]) 45 | WrappedReturn: TypeAlias = TypeVar('WrappedReturn') 46 | 47 | ETypes: TypeAlias = Union[Type[Exception], Tuple[Type[Exception], ...]] 48 | ELogger: TypeAlias = Union[HasWarningMethod, HasErrorMethod] 49 | ECallback: TypeAlias = Callable[..., None] 50 | Second: TypeAlias = Union[int, float, str] 51 | 52 | UNIQUE: Final[Annotated[object, 'A unique object.']] = object() 53 | 54 | CO_QUALNAME: Final[Annotated[str, ''' 55 | The alternative solution of the old version for `co_qualname` attribute. 56 | ''']] = 'co_qualname' if sys.version_info >= (3, 11) else 'co_name' 57 | 58 | 59 | class Error(Exception): 60 | __module__ = builtins.__name__ 61 | 62 | def __init_subclass__(cls) -> None: 63 | cls.__module__ = builtins.__name__ 64 | setattr(builtins, cls.__name__, cls) 65 | 66 | msg: Any = Exception.args 67 | 68 | 69 | builtins.Error = Error 70 | 71 | 72 | class MasqueradeClass(type): 73 | """ 74 | Masquerade one class as another (default masquerade as first parent class). 75 | Warning, masquerade the class can cause unexpected problems, use caution. 76 | """ 77 | __module__ = builtins.__name__ 78 | 79 | __qualname__ = type.__qualname__ 80 | # Warning, masquerade (modify) this attribute will cannot create the 81 | # portable serialized representation. In practice, however, this metaclass 82 | # often does not need to be serialized, so we try to ignore it. 83 | 84 | def __new__(mcs, __name__: str, __bases__: tuple, __dict__: dict): 85 | __masquerade_class__: Type[object] = __dict__.setdefault( 86 | '__masquerade_class__', __bases__[0] if __bases__ else object 87 | ) 88 | 89 | if not isinstance(__masquerade_class__, type): 90 | raise TypeError('"__masquerade_class__" is not a class.') 91 | 92 | cls = type.__new__( 93 | mcs, __masquerade_class__.__name__, __bases__, __dict__ 94 | ) 95 | 96 | if cls.__module__ != __masquerade_class__.__module__: 97 | setattr(sys.modules[__masquerade_class__.__module__], __name__, cls) 98 | 99 | cls.__realname__ = __name__ 100 | cls.__realmodule__ = cls.__module__ 101 | cls.__module__ = __masquerade_class__.__module__ 102 | 103 | # cls.__qualname__ = __masquerade_class__.__qualname__ 104 | # Masquerade (modify) this attribute will cannot create the portable 105 | # serialized representation. We have not yet found an effective 106 | # solution, and we will continue to follow up. 107 | 108 | return cls 109 | 110 | def __hash__(cls) -> int: 111 | if sys._getframe(1).f_code in (deepcopy.__code__, copy.__code__): 112 | return type.__hash__(cls) 113 | return hash(cls.__masquerade_class__) 114 | 115 | def __eq__(cls, o) -> bool: 116 | return True if o is cls.__masquerade_class__ else type.__eq__(cls, o) 117 | 118 | def __init_subclass__(mcs) -> None: 119 | setattr(builtins, mcs.__name__, mcs) 120 | mcs.__name__ = MasqueradeClass.__name__ 121 | mcs.__qualname__ = MasqueradeClass.__qualname__ 122 | mcs.__module__ = MasqueradeClass.__module__ 123 | 124 | 125 | MasqueradeClass.__name__ = type.__name__ 126 | builtins.MasqueradeClass = MasqueradeClass 127 | 128 | 129 | class __history__(dict, metaclass=type('SingletonMode', (MasqueradeClass,), { 130 | '__new__': lambda *a: MasqueradeClass.__new__(*a)() 131 | })): 132 | 133 | def __setitem__(self, *a, **kw) -> NoReturn: 134 | raise __getattr__('ReadOnlyError')('this dictionary is read-only.') 135 | 136 | __delitem__ = setdefault = update = pop = popitem = clear = __setitem__ 137 | 138 | def __reduce_ex__(self, protocol: int) -> ...: 139 | return self.__class__, (dict(self),) 140 | 141 | def copy(self) -> '__history__.__class__': 142 | return copy(self) 143 | 144 | 145 | def __getattr__(ename: str, /) -> Union[Type[BaseException], Type[Error]]: 146 | if ename in __history__: 147 | return __history__[ename] 148 | 149 | if ename[:2] == ename[-2:] == '__' and ename[2] != '_' and ename[-3] != '_': 150 | # Some special modules may attempt to call non-built-in magic method, 151 | # such as `copy`, `pickle`. Compatible for this purpose. 152 | raise AttributeError(f'"{__package__}" has no attribute "{ename}".') 153 | 154 | etype = getattr(builtins, ename, None) 155 | if isinstance(etype, type) and issubclass(etype, BaseException): 156 | return etype 157 | 158 | if ename[-5:] != 'Error': 159 | warnings.warn( 160 | f'strange exception class "{ename}", exception class name should ' 161 | 'end with "Error".', stacklevel=2 162 | ) 163 | 164 | etype = type(ename, (Error,), {}) 165 | dict.__setitem__(__history__, ename, etype) 166 | 167 | return etype 168 | 169 | 170 | class TryExcept: 171 | 172 | def __new__( 173 | cls, etype: Union[ETypes, Wrapped], /, **kw 174 | ) -> Union['TryExcept', WrappedClosure]: 175 | ins = object.__new__(cls) 176 | if isinstance(etype, type) and issubclass(etype, Exception): 177 | return ins 178 | if callable(etype): 179 | ins.__init__(Exception) 180 | return ins(etype) 181 | if isinstance(etype, tuple): 182 | for et in etype: 183 | if not (isinstance(et, type) and issubclass(et, Exception)): 184 | break 185 | else: 186 | return ins 187 | raise __getattr__('ParameterError')( 188 | 'parameter "etype" must be a subclass inherited from "Exception" ' 189 | f'or multiple ones packaged using a tuple, not {etype!r}.' 190 | ) 191 | 192 | def __init__( 193 | self, 194 | etype: ETypes, 195 | /, *, 196 | emsg: Optional[str] = None, 197 | silent: Optional[bool] = None, 198 | silent_exc: bool = UNIQUE, 199 | raw: Optional[bool] = None, 200 | raw_exc: bool = UNIQUE, 201 | invert: bool = False, 202 | last_tb: bool = False, 203 | logger: Optional[ELogger] = None, 204 | ereturn: Optional[Any] = None, 205 | ecallback: Optional[ECallback] = None, 206 | eexit: bool = False 207 | ): 208 | if not (emsg is None or isinstance(emsg, str)): 209 | raise __getattr__('ParameterError')( 210 | 'parameter "emsg" must be of type str, ' 211 | f'not "{emsg.__class__.__name__}".' 212 | ) 213 | 214 | if silent_exc is not UNIQUE: 215 | warnings.warn( 216 | 'parameter "silent_exc" will be deprecated soon, replaced to ' 217 | '"silent". (Did you switch from "gqylpy-exception"?)', 218 | category=DeprecationWarning, 219 | stacklevel=2 if self.__class__ is TryExcept else 3 220 | ) 221 | if silent is None: 222 | silent = silent_exc 223 | 224 | if raw_exc is not UNIQUE: 225 | warnings.warn( 226 | 'parameter "raw_exc" will be deprecated soon, replaced to ' 227 | '"raw". (Did you switch from "gqylpy-exception"?)', 228 | category=DeprecationWarning, 229 | stacklevel=2 if self.__class__ is TryExcept else 3 230 | ) 231 | if raw is None: 232 | raw = raw_exc 233 | 234 | self.etype = etype 235 | self.emsg = emsg 236 | self.silent = silent 237 | self.raw = raw 238 | self.invert = invert 239 | self.last_tb = last_tb 240 | self.logger = get_logger(logger) 241 | self.ereturn = ereturn 242 | self.ecallback = ecallback 243 | self.eexit = eexit 244 | 245 | def __call__(self, func: Wrapped, /) -> WrappedClosure: 246 | if asyncio.iscoroutinefunction(func): 247 | async def inner(*a, **kw) -> Any: 248 | return await self.acore(func, *a, **kw) 249 | else: 250 | def inner(*a, **kw) -> Any: 251 | return self.core(func, *a, **kw) 252 | 253 | inner.__self = self 254 | return functools.wraps(func)(inner) 255 | 256 | def core(self, func: Wrapped, *a, **kw) -> WrappedReturn: 257 | try: 258 | return func(*a, **kw) 259 | except self.etype as e: 260 | if self.invert or not (self.emsg is None or self.emsg in str(e)): 261 | raise 262 | self.exception_handling(func, e, *a, **kw) 263 | except Exception as e: 264 | if not (self.invert and (self.emsg is None or self.emsg in str(e))): 265 | raise 266 | self.exception_handling(func, e, *a, **kw) 267 | return self.ereturn 268 | 269 | async def acore(self, func: Wrapped, *a, **kw) -> WrappedReturn: 270 | try: 271 | return await func(*a, **kw) 272 | except self.etype as e: 273 | if self.invert or not (self.emsg is None or self.emsg in str(e)): 274 | raise 275 | self.exception_handling(func, e, *a, **kw) 276 | except Exception as e: 277 | if not (self.invert and (self.emsg is None or self.emsg in str(e))): 278 | raise 279 | self.exception_handling(func, e, *a, **kw) 280 | return self.ereturn 281 | 282 | def exception_handling(self, func: Wrapped, e: Exception, *a, **kw) -> None: 283 | if not self.silent: 284 | self.logger(get_einfo(e, raw=self.raw, last_tb=self.last_tb)) 285 | if self.ecallback is not None: 286 | self.ecallback(e, func, *a, **kw) 287 | if self.eexit: 288 | raise SystemExit(4) 289 | 290 | 291 | class Retry(TryExcept): 292 | 293 | def __new__( 294 | cls, etype: Union[ETypes, Wrapped] = Exception, /, **kw 295 | ) -> Union['Retry', WrappedClosure]: 296 | ins = TryExcept.__new__(cls, etype) 297 | if not isinstance(ins, Retry): 298 | ins._TryExcept__self.silent = True 299 | return ins 300 | 301 | def __init__( 302 | self, 303 | etype: ETypes = Exception, 304 | /, *, 305 | emsg: Optional[str] = None, 306 | sleep: Optional[Second] = None, 307 | cycle: Second = UNIQUE, 308 | count: int = 0, 309 | limit_time: Second = 0, 310 | event: Optional[threading.Event] = None, 311 | silent: Optional[bool] = None, 312 | silent_exc: bool = UNIQUE, 313 | raw: Optional[bool] = None, 314 | raw_exc: bool = UNIQUE, 315 | invert: bool = False, 316 | last_tb: bool = None, 317 | logger: Optional[ELogger] = None 318 | ): 319 | x = 'sleep' 320 | if cycle is not UNIQUE: 321 | warnings.warn( 322 | 'parameter "cycle" will be deprecated soon, replaced to ' 323 | '"sleep". (Did you switch from "gqylpy-exception"?)', 324 | category=DeprecationWarning, stacklevel=2 325 | ) 326 | if sleep is None: 327 | sleep = cycle 328 | x = 'cycle' 329 | 330 | if sleep is None: 331 | sleep = 0 332 | elif isinstance(sleep, str): 333 | sleep = time2second(sleep) 334 | elif not (isinstance(sleep, (int, float)) and sleep >= 0): 335 | raise __getattr__('ParameterError')( 336 | f'parameter "{x}" is expected to be of type int or float and ' 337 | f'greater than or equal to 0, not {sleep!r}.' 338 | ) 339 | elif isinstance(sleep, float) and sleep.is_integer(): 340 | sleep = int(sleep) 341 | 342 | if count == 0: 343 | count = float('inf') 344 | elif not (isinstance(count, int) and count > 0): 345 | raise __getattr__('ParameterError')( 346 | 'parameter "count" must be of type int and greater than or ' 347 | f'equal to 0, not {count!r}.' 348 | ) 349 | 350 | if limit_time == 0: 351 | limit_time = float('inf') 352 | elif isinstance(limit_time, str): 353 | limit_time = time2second(limit_time) 354 | elif not (isinstance(limit_time, (int, float)) and limit_time > 0): 355 | raise __getattr__('ParameterError')( 356 | 'parameter "limit_time" is expected to be of type int or float ' 357 | f'and greater than or equal to 0, not {limit_time!r}.' 358 | ) 359 | elif isinstance(limit_time, float) and limit_time.is_integer(): 360 | limit_time = int(limit_time) 361 | 362 | if not (event is None or isinstance(event, threading.Event)): 363 | raise __getattr__('ParameterError')( 364 | 'parameter "event" must be of type "threading.Event", ' 365 | f'not "{event.__class__.__name__}".' 366 | ) 367 | 368 | self.sleep = sleep 369 | self.count = count 370 | self.limit_time = limit_time 371 | self.event = event 372 | 373 | TryExcept.__init__( 374 | self, etype, emsg=emsg, silent=silent, silent_exc=silent_exc, 375 | raw=raw, raw_exc=raw_exc, invert=invert, last_tb=last_tb, 376 | logger=logger 377 | ) 378 | 379 | def core(self, func: Wrapped, *a, **kw) -> WrappedReturn: 380 | count = 1 381 | before = time.monotonic() 382 | while True: 383 | start = time.monotonic() 384 | try: 385 | return func(*a, **kw) 386 | except Exception as e: 387 | count, sleep = self.retry_handling( 388 | e, count=count, start=start, before=before 389 | ) 390 | time.sleep(sleep) 391 | 392 | async def acore(self, func: Wrapped, *a, **kw) -> WrappedReturn: 393 | count = 1 394 | before = time.monotonic() 395 | while True: 396 | start = time.monotonic() 397 | try: 398 | return await func(*a, **kw) 399 | except Exception as e: 400 | count, sleep = self.retry_handling( 401 | e, count=count, start=start, before=before 402 | ) 403 | await asyncio.sleep(sleep) 404 | 405 | def retry_handling( 406 | self, e: Exception, *, count: int, start: float, before: float 407 | ) -> Tuple[float, float]: 408 | contain_emsg: bool = self.emsg is None or self.emsg in str(e) 409 | if isinstance(e, self.etype): 410 | if self.invert or not contain_emsg: 411 | raise 412 | elif not (self.invert and contain_emsg): 413 | raise 414 | if not ( 415 | self.silent 416 | or time.monotonic() - start + self.sleep < .1 417 | and self.count >= 30 418 | and 1 < count < self.count 419 | and (self.event is None or not self.event.is_set()) 420 | ): 421 | self.output_einfo(e, count=count, start=start, before=before) 422 | end = time.monotonic() 423 | sleep = max(.0, self.sleep - (end - start)) 424 | if ( 425 | count == self.count 426 | or end - before + sleep >= self.limit_time 427 | or self.event is not None and self.event.is_set() 428 | ): 429 | raise 430 | return count + 1, sleep 431 | 432 | def output_einfo( 433 | self, e: Exception, *, count: int, start: float, before: float 434 | ) -> None: 435 | einfo: str = get_einfo(e, raw=self.raw, last_tb=self.last_tb) 436 | 437 | max_count = 'N' if self.count == float('inf') else self.count 438 | x = f'[try:{count}/{max_count}' 439 | 440 | if self.limit_time != float('inf'): 441 | x += f':{second2time(self.sleep)}' 442 | spent_time = second2time(self.get_spent_time(start, before)) 443 | x += f',limit_time:{spent_time}/{second2time(self.limit_time)}' 444 | elif self.sleep >= 90: 445 | x += f':{second2time(self.sleep)}' 446 | else: 447 | x += f':{self.sleep}' 448 | 449 | if self.event is not None: 450 | x += f',event={self.event.is_set()}' 451 | 452 | self.logger(f'{x}] {einfo}') 453 | 454 | def get_spent_time(self, start: float, before: float) -> Union[int, float]: 455 | now = time.monotonic() 456 | 457 | if isinstance(self.sleep, float) or isinstance(self.limit_time, float): 458 | spent_time = round(now - before, 2) 459 | if spent_time.is_integer(): 460 | spent_time = int(spent_time) 461 | else: 462 | spent_time = now - before 463 | if now - start + self.sleep >= 3: 464 | spent_time = round(spent_time) 465 | 466 | return spent_time 467 | 468 | 469 | @contextmanager 470 | def TryContext( 471 | etype: ETypes, 472 | /, *, 473 | emsg: Optional[str] = None, 474 | silent: bool = False, 475 | raw: bool = False, 476 | invert: bool = False, 477 | last_tb: bool = False, 478 | logger: Optional[ELogger] = None, 479 | ecallback: Optional[ECallback] = None, 480 | eexit: bool = False 481 | ) -> None: 482 | logger = get_logger(logger) 483 | try: 484 | yield 485 | except Exception as e: 486 | contain_emsg: bool = emsg is None or emsg in str(e) 487 | if isinstance(e, etype): 488 | if invert or not contain_emsg: 489 | raise 490 | elif not (invert and contain_emsg): 491 | raise 492 | if not silent: 493 | logger(get_einfo(e, raw=raw, last_tb=last_tb)) 494 | if ecallback is not None: 495 | ecallback(e) 496 | if eexit: 497 | raise SystemExit(4) 498 | 499 | 500 | def stderr(einfo: str) -> None: 501 | now: str = time.strftime('%F %T', time.localtime()) 502 | sys.stderr.write(f'[{now}] {einfo}\n') 503 | 504 | 505 | def get_logger(logger: Optional[ELogger]) -> Callable[[str], None]: 506 | if logger is None: 507 | return stderr 508 | 509 | previous_frame: FrameType = sys._getframe(1) 510 | 511 | if previous_frame.f_back.f_code is Retry.__init__.__code__: 512 | name = 'warning' 513 | else: 514 | name = 'error' 515 | 516 | if not ((method := getattr(logger, name, None)) and callable(method)): 517 | raise __getattr__('ParameterError')( 518 | f'parameter "logger" must have a "{name}" method.' 519 | ) 520 | 521 | if isinstance(logger, logging.Logger): 522 | stacklevel = 4 523 | elif getattr(logger, '__package__', None) == 'gqylpy_log': 524 | stacklevel = 5 525 | else: 526 | return method 527 | 528 | if previous_frame.f_code is TryContext.__wrapped__.__code__: 529 | stacklevel -= 1 530 | 531 | return functools.partial(method, stacklevel=stacklevel) 532 | 533 | 534 | def get_einfo(e: Exception, /, *, raw: bool, last_tb: bool) -> str: 535 | try: 536 | if raw: 537 | return traceback.format_exc() 538 | 539 | tb: TracebackType = e.__traceback__.tb_next 540 | 541 | if last_tb: 542 | while tb.tb_next: 543 | tb = tb.tb_next 544 | else: 545 | while tb.tb_frame.f_code.co_filename == __file__: 546 | tb = tb.tb_next 547 | 548 | module: str = tb.tb_frame.f_globals['__name__'] 549 | name: str = getattr(tb.tb_frame.f_code, CO_QUALNAME) 550 | lineno: int = tb.tb_lineno 551 | ename: str = e.__class__.__name__ 552 | 553 | return f'[{module}.{name}.line{lineno}.{ename}] {e}' 554 | except Exception: 555 | return traceback.format_exc() + '\nPlease note that this exception ' \ 556 | 'occurred within the exceptionx library, not in your code.\n' \ 557 | 'Another exception occurred while we were handling the exception ' \ 558 | 'in your code, very sorry. \nPlease report the error to ' \ 559 | 'https://github.com/gqylpy/exceptionx/issues, thank you.\n' 560 | 561 | 562 | def time2second(unit_time: str, /, *, __pattern__ = re.compile(r''' 563 | (?:(\d+(?:\.\d+)?)d)? 564 | (?:(\d+(?:\.\d+)?)h)? 565 | (?:(\d+(?:\.\d+)?)m)? 566 | (?:(\d+(?:\.\d+)?)s?)? 567 | ''', flags=re.X | re.I)) -> Union[int, float]: 568 | if unit_time.isdigit(): 569 | return int(unit_time) 570 | 571 | if not (unit_time and (m := __pattern__.fullmatch(unit_time))): 572 | raise ValueError(f'unit time {unit_time!r} format is incorrect.') 573 | 574 | r = 0 575 | 576 | for x, s in zip(m.groups(), (86400, 3600, 60, 1)): 577 | if x is not None: 578 | x = int(x) if x.isdigit() else float(x) 579 | r += x * s 580 | 581 | return int(r) if isinstance(r, float) and r.is_integer() else r 582 | 583 | 584 | def second2time(second: Union[int, float], /) -> str: 585 | sec = int(second) 586 | dec = round(second - sec, 2) 587 | 588 | r = '' 589 | 590 | for u, s in ('d', 86400), ('h', 3600), ('m', 60): 591 | if sec >= s: 592 | v, sec = divmod(sec, s) 593 | r += f'{v}{u}' 594 | 595 | if sec or dec: 596 | sec += dec 597 | if isinstance(sec, float): 598 | sec = int(sec) if sec.is_integer() else round(sec, 2) 599 | r += f'{sec}s' 600 | 601 | return r or '0s' 602 | --------------------------------------------------------------------------------