Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-tenacity for openSUSE:Factory checked in at 2023-06-01 17:19:29 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-tenacity (Old) and /work/SRC/openSUSE:Factory/.python-tenacity.new.2531 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tenacity" Thu Jun 1 17:19:29 2023 rev:19 rq:1090091 version:8.2.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-tenacity/python-tenacity.changes 2023-05-16 14:27:17.379695052 +0200 +++ /work/SRC/openSUSE:Factory/.python-tenacity.new.2531/python-tenacity.changes 2023-06-01 17:19:37.590198127 +0200 @@ -1,0 +2,11 @@ +Wed May 31 19:54:37 UTC 2023 - Dirk Müller <dmuel...@suse.com> + +- update to 8.2.2: + * feat: accept datetime.timedelta instances as argument to `stop_after.. + * fix: docstring for wait_exponential_jitter + * fix: remove __iter__ from AsyncRetring + * Add retry_if_exception_cause_type and wait_exponential_jitter + * better wait.WaitBaseT annotation + * CI conversion to GitHub actions + +------------------------------------------------------------------- Old: ---- tenacity-8.1.0.tar.gz New: ---- tenacity-8.2.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-tenacity.spec ++++++ --- /var/tmp/diff_new_pack.Giz9JJ/_old 2023-06-01 17:19:38.074200996 +0200 +++ /var/tmp/diff_new_pack.Giz9JJ/_new 2023-06-01 17:19:38.078201020 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-tenacity -Version: 8.1.0 +Version: 8.2.2 Release: 0 Summary: Python module for retrying code until it succeeeds License: Apache-2.0 ++++++ tenacity-8.1.0.tar.gz -> tenacity-8.2.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/.circleci/config.yml new/tenacity-8.2.2/.circleci/config.yml --- old/tenacity-8.1.0/.circleci/config.yml 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/.circleci/config.yml 1970-01-01 01:00:00.000000000 +0100 @@ -1,98 +0,0 @@ -version: 2 - -jobs: - pep8: - docker: - - image: circleci/python:3.9 - steps: - - checkout - - run: - command: | - sudo pip install tox - tox -e pep8 - black: - docker: - - image: circleci/python:3.9 - steps: - - checkout - - run: - command: | - sudo pip install tox - tox -e black-ci - py36: - docker: - - image: circleci/python:3.6 - steps: - - checkout - - run: - command: | - sudo pip install tox - tox -e py36 - py37: - docker: - - image: circleci/python:3.7 - steps: - - checkout - - run: - command: | - sudo pip install tox - tox -e py37 - py38: - docker: - - image: circleci/python:3.8 - steps: - - checkout - - run: - command: | - sudo pip install tox - tox -e py38 - py39: - docker: - - image: circleci/python:3.9 - steps: - - checkout - - run: - command: | - sudo pip install tox - tox -e py39 - deploy: - docker: - - image: circleci/python:3.9 - steps: - - checkout - - run: | - python -m venv venv - - run: | - venv/bin/pip install twine wheel - - run: - name: init .pypirc - command: | - echo -e "[pypi]" >> ~/.pypirc - echo -e "username = __token__" >> ~/.pypirc - echo -e "password = $PYPI_TOKEN" >> ~/.pypirc - - run: - name: create packages - command: | - venv/bin/python setup.py sdist bdist_wheel - - run: - name: upload to PyPI - command: | - venv/bin/twine upload dist/* - -workflows: - version: 2 - - test: - jobs: - - pep8 - - black - - py36 - - py37 - - py38 - - py39 - - deploy: - filters: - tags: - only: /[0-9]+(\.[0-9]+)*/ - branches: - ignore: /.*/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/.github/workflows/ci.yaml new/tenacity-8.2.2/.github/workflows/ci.yaml --- old/tenacity-8.1.0/.github/workflows/ci.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/.github/workflows/ci.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,53 @@ +name: Continuous Integration +permissions: read-all + +on: + pull_request: + branches: + - main + +concurrency: + # yamllint disable-line rule:line-length + group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}" + cancel-in-progress: true + +jobs: + test: + timeout-minutes: 20 + runs-on: ubuntu-20.04 + strategy: + matrix: + include: + - python: "3.6" + tox: py36 + - python: "3.7" + tox: py37 + - python: "3.8" + tox: py38 + - python: "3.9" + tox: py39 + - python: "3.10" + tox: py310 + - python: "3.11" + tox: py311 + - python: "3.11" + tox: pep8 + - python: "3.11" + tox: black-ci + - python: "3.11" + tox: mypy + steps: + - name: Checkout ðï¸ + uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + + - name: Setup Python ð§ + uses: actions/setup-python@v4.5.0 + with: + python-version: ${{ matrix.python }} + + - name: Build ð§ & Test ð + run: | + pip install tox + tox -e ${{ matrix.tox }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/.github/workflows/deploy.yaml new/tenacity-8.2.2/.github/workflows/deploy.yaml --- old/tenacity-8.1.0/.github/workflows/deploy.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/.github/workflows/deploy.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,33 @@ +name: Release deploy + +on: + push: + tags: + +jobs: + test: + timeout-minutes: 20 + runs-on: ubuntu-20.04 + steps: + - name: Checkout ðï¸ + uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + + - name: Setup Python ð§ + uses: actions/setup-python@v4.5.0 + with: + python-version: 3.11 + + - name: Build ð§ & Deploy ð + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: | + pip install tox twine wheel + + echo -e "[pypi]" >> ~/.pypirc + echo -e "username = __token__" >> ~/.pypirc + echo -e "password = $PYPI_TOKEN" >> ~/.pypirc + + python setup.py sdist bdist_wheel + twine upload dist/* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/.mergify.yml new/tenacity-8.2.2/.mergify.yml --- old/tenacity-8.1.0/.mergify.yml 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/.mergify.yml 2023-02-28 15:21:48.000000000 +0100 @@ -1,12 +1,15 @@ queue_rules: - name: default - conditions: - - "status-success=ci/circleci: pep8" - - "status-success=ci/circleci: black" - - "status-success=ci/circleci: py36" - - "status-success=ci/circleci: py37" - - "status-success=ci/circleci: py38" - - "status-success=ci/circleci: py39" + conditions: &CheckRuns + - "check-success=test (3.6, py36)" + - "check-success=test (3.7, py37)" + - "check-success=test (3.8, py38)" + - "check-success=test (3.9, py39)" + - "check-success=test (3.10, py310)" + - "check-success=test (3.11, py311)" + - "check-success=test (3.11, black-ci)" + - "check-success=test (3.11, pep8)" + - "check-success=test (3.11, mypy)" pull_request_rules: - name: warn on no changelog @@ -22,12 +25,7 @@ a changelog entry. - name: automatic merge without changelog conditions: - - "status-success=ci/circleci: pep8" - - "status-success=ci/circleci: black" - - "status-success=ci/circleci: py36" - - "status-success=ci/circleci: py37" - - "status-success=ci/circleci: py38" - - "status-success=ci/circleci: py39" + - and: *CheckRuns - "#approved-reviews-by>=1" - label=no-changelog actions: @@ -36,12 +34,7 @@ method: squash - name: automatic merge with changelog conditions: - - "status-success=ci/circleci: pep8" - - "status-success=ci/circleci: black" - - "status-success=ci/circleci: py36" - - "status-success=ci/circleci: py37" - - "status-success=ci/circleci: py38" - - "status-success=ci/circleci: py39" + - and: *CheckRuns - "#approved-reviews-by>=1" - files~=^releasenotes/notes/ actions: @@ -51,12 +44,7 @@ - name: automatic merge for jd without changelog conditions: - author=jd - - "status-success=ci/circleci: pep8" - - "status-success=ci/circleci: black" - - "status-success=ci/circleci: py36" - - "status-success=ci/circleci: py37" - - "status-success=ci/circleci: py38" - - "status-success=ci/circleci: py39" + - and: *CheckRuns - label=no-changelog actions: queue: @@ -65,12 +53,7 @@ - name: automatic merge for jd with changelog conditions: - author=jd - - "status-success=ci/circleci: pep8" - - "status-success=ci/circleci: black" - - "status-success=ci/circleci: py36" - - "status-success=ci/circleci: py37" - - "status-success=ci/circleci: py38" - - "status-success=ci/circleci: py39" + - and: *CheckRuns - files~=^releasenotes/notes/ actions: queue: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/PKG-INFO new/tenacity-8.2.2/PKG-INFO --- old/tenacity-8.1.0/PKG-INFO 2022-09-21 14:24:11.075567200 +0200 +++ new/tenacity-8.2.2/PKG-INFO 2023-02-28 15:22:05.633156000 +0100 @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: tenacity -Version: 8.1.0 +Version: 8.2.2 Summary: Retry code until it succeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: jul...@danjou.info License: Apache 2.0 -Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python @@ -17,10 +16,10 @@ Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Utilities Requires-Python: >=3.6 Provides-Extra: doc License-File: LICENSE Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/README.rst new/tenacity-8.2.2/README.rst --- old/tenacity-8.1.0/README.rst 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/README.rst 2023-02-28 15:21:48.000000000 +0100 @@ -6,7 +6,7 @@ .. image:: https://circleci.com/gh/jd/tenacity.svg?style=svg :target: https://circleci.com/gh/jd/tenacity -.. image:: https://img.shields.io/endpoint.svg?url=https://dashboard.mergify.io/badges/jd/tenacity&style=flat +.. image:: https://img.shields.io/endpoint.svg?url=https://api.mergify.com/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status @@ -274,8 +274,12 @@ Error Handling ~~~~~~~~~~~~~~ -While callables that "timeout" retrying raise a `RetryError` by default, -we can reraise the last attempt's exception if needed: +Normally when your function fails its final time (and will not be retried again based on your settings), +a `RetryError` is raised. The exception your code encountered will be shown somewhere in the *middle* +of the stack trace. + +If you would rather see the exception your code encountered at the *end* of the stack trace (where it +is most visible), you can set `reraise=True`. .. testcode:: @@ -298,6 +302,7 @@ .. testcode:: import logging + import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) @@ -312,6 +317,7 @@ .. testcode:: import logging + import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) @@ -328,6 +334,7 @@ .. testcode:: import logging + import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) @@ -566,6 +573,22 @@ except RetryError: pass +In both cases, you may want to set the result to the attempt so it's available +in retry strategies like ``retry_if_result``. This can be done accessing the +``retry_state`` property: + +.. testcode:: + + from tenacity import AsyncRetrying, retry_if_result + + async def function(): + async for attempt in AsyncRetrying(retry=retry_if_result(lambda x: x < 3)): + with attempt: + result = 1 # Some complex calculation, function call, etc. + if not attempt.retry_state.outcome.failed: + attempt.retry_state.set_result(result) + return result + Async and retry ~~~~~~~~~~~~~~~ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/doc/source/index.rst new/tenacity-8.2.2/doc/source/index.rst --- old/tenacity-8.1.0/doc/source/index.rst 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/doc/source/index.rst 2023-02-28 15:21:48.000000000 +0100 @@ -6,7 +6,7 @@ .. image:: https://circleci.com/gh/jd/tenacity.svg?style=svg :target: https://circleci.com/gh/jd/tenacity -.. image:: https://img.shields.io/endpoint.svg?url=https://dashboard.mergify.io/badges/jd/tenacity&style=flat +.. image:: https://img.shields.io/endpoint.svg?url=https://api.mergify.com/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status @@ -274,8 +274,12 @@ Error Handling ~~~~~~~~~~~~~~ -While callables that "timeout" retrying raise a `RetryError` by default, -we can reraise the last attempt's exception if needed: +Normally when your function fails its final time (and will not be retried again based on your settings), +a `RetryError` is raised. The exception your code encountered will be shown somewhere in the *middle* +of the stack trace. + +If you would rather see the exception your code encountered at the *end* of the stack trace (where it +is most visible), you can set `reraise=True`. .. testcode:: @@ -298,6 +302,7 @@ .. testcode:: import logging + import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) @@ -312,6 +317,7 @@ .. testcode:: import logging + import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) @@ -328,6 +334,7 @@ .. testcode:: import logging + import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) @@ -566,6 +573,22 @@ except RetryError: pass +In both cases, you may want to set the result to the attempt so it's available +in retry strategies like ``retry_if_result``. This can be done accessing the +``retry_state`` property: + +.. testcode:: + + from tenacity import AsyncRetrying, retry_if_result + + async def function(): + async for attempt in AsyncRetrying(retry=retry_if_result(lambda x: x < 3)): + with attempt: + result = 1 # Some complex calculation, function call, etc. + if not attempt.retry_state.outcome.failed: + attempt.retry_state.set_result(result) + return result + Async and retry ~~~~~~~~~~~~~~~ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/pyproject.toml new/tenacity-8.2.2/pyproject.toml --- old/tenacity-8.1.0/pyproject.toml 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/pyproject.toml 2023-02-28 15:21:48.000000000 +0100 @@ -11,6 +11,15 @@ [tool.black] line-length = 120 safe = true -target-version = ["py36", "py37", "py38", "py39"] +target-version = ["py36", "py37", "py38", "py39", "py310", "py311"] + +[tool.mypy] +strict = true +files = ["tenacity"] +show_error_codes = true + +[[tool.mypy.overrides]] +module = "tornado.*" +ignore_missing_imports = true [tool.setuptools_scm] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml new/tenacity-8.2.2/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml --- old/tenacity-8.1.0/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,3 @@ +--- +other: + - Add `retry_if_exception_cause_type`and `wait_exponential_jitter` to __all__ of init.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml new/tenacity-8.2.2/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml --- old/tenacity-8.1.0/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,3 @@ +--- +prelude: > + Clarify usage of `reraise` keyword argument diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml new/tenacity-8.2.2/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml --- old/tenacity-8.1.0/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,3 @@ +--- +features: + - Explicitly export convenience symbols from tenacity root module diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml new/tenacity-8.2.2/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml --- old/tenacity-8.1.0/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fix async loop with retrying code block when result is available. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml new/tenacity-8.2.2/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml --- old/tenacity-8.1.0/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,5 @@ +--- +fixes: + - | + Argument `wait` was improperly annotated, making mypy checks fail. + Now it's annotated as `typing.Union[wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]]` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml new/tenacity-8.2.2/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml --- old/tenacity-8.1.0/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,7 @@ +--- +fixes: + - | + `AsyncRetrying` was erroneously implementing `__iter__()`, making tenacity + retrying mechanism working but in a synchronous fashion and not waiting as + expected. This interface has been removed, `__aiter__()` should be used + instead. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml new/tenacity-8.2.2/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml --- old/tenacity-8.1.0/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/tenacity-8.2.2/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml 2023-02-28 15:21:48.000000000 +0100 @@ -0,0 +1,4 @@ +--- +features: + - | + - accept ``datetime.timedelta`` instances as argument to ``tenacity.stop.stop_after_delay`` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/setup.cfg new/tenacity-8.2.2/setup.cfg --- old/tenacity-8.1.0/setup.cfg 2022-09-21 14:24:11.075567200 +0200 +++ new/tenacity-8.2.2/setup.cfg 2023-02-28 15:22:05.637156000 +0100 @@ -18,6 +18,7 @@ Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Topic :: Utilities [options] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/__init__.py new/tenacity-8.2.2/tenacity/__init__.py --- old/tenacity-8.1.0/tenacity/__init__.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/__init__.py 2023-02-28 15:21:48.000000000 +0100 @@ -16,6 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + import functools import sys import threading @@ -79,58 +80,20 @@ from .before_sleep import before_sleep_nothing # noqa try: - import tornado # type: ignore + import tornado except ImportError: - tornado = None # type: ignore + tornado = None if t.TYPE_CHECKING: import types - from .wait import wait_base - from .stop import stop_base - - -WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable) -_RetValT = t.TypeVar("_RetValT") - - -@t.overload -def retry(fn: WrappedFn) -> WrappedFn: - pass - - -@t.overload -def retry(*dargs: t.Any, **dkw: t.Any) -> t.Callable[[WrappedFn], WrappedFn]: # noqa - pass + from .retry import RetryBaseT + from .stop import StopBaseT + from .wait import WaitBaseT -def retry(*dargs: t.Any, **dkw: t.Any) -> t.Union[WrappedFn, t.Callable[[WrappedFn], WrappedFn]]: # noqa - """Wrap a function with a new `Retrying` object. - - :param dargs: positional arguments passed to Retrying object - :param dkw: keyword arguments passed to the Retrying object - """ - # support both @retry and @retry() as valid syntax - if len(dargs) == 1 and callable(dargs[0]): - return retry()(dargs[0]) - else: - - def wrap(f: WrappedFn) -> WrappedFn: - if isinstance(f, retry_base): - warnings.warn( - f"Got retry_base instance ({f.__class__.__name__}) as callable argument, " - f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)" - ) - if iscoroutinefunction(f): - r: "BaseRetrying" = AsyncRetrying(*dargs, **dkw) - elif tornado and hasattr(tornado.gen, "is_coroutine_function") and tornado.gen.is_coroutine_function(f): - r = TornadoRetrying(*dargs, **dkw) - else: - r = Retrying(*dargs, **dkw) - - return r.wraps(f) - - return wrap +WrappedFnReturnT = t.TypeVar("WrappedFnReturnT") +WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any]) class TryAgain(Exception): @@ -214,7 +177,7 @@ exc_value: t.Optional[BaseException], traceback: t.Optional["types.TracebackType"], ) -> t.Optional[bool]: - if isinstance(exc_value, BaseException): + if exc_type is not None and exc_value is not None: self.retry_state.set_exception((exc_type, exc_value, traceback)) return True # Swallow exception. else: @@ -227,9 +190,9 @@ def __init__( self, sleep: t.Callable[[t.Union[int, float]], None] = sleep, - stop: "stop_base" = stop_never, - wait: "wait_base" = wait_none(), - retry: retry_base = retry_if_exception_type(), + stop: "StopBaseT" = stop_never, + wait: "WaitBaseT" = wait_none(), + retry: "RetryBaseT" = retry_if_exception_type(), before: t.Callable[["RetryCallState"], None] = before_nothing, after: t.Callable[["RetryCallState"], None] = after_nothing, before_sleep: t.Optional[t.Callable[["RetryCallState"], None]] = None, @@ -252,8 +215,8 @@ def copy( self, sleep: t.Union[t.Callable[[t.Union[int, float]], None], object] = _unset, - stop: t.Union["stop_base", object] = _unset, - wait: t.Union["wait_base", object] = _unset, + stop: t.Union["StopBaseT", object] = _unset, + wait: t.Union["WaitBaseT", object] = _unset, retry: t.Union[retry_base, object] = _unset, before: t.Union[t.Callable[["RetryCallState"], None], object] = _unset, after: t.Union[t.Callable[["RetryCallState"], None], object] = _unset, @@ -310,9 +273,9 @@ statistics from each thread). """ try: - return self._local.statistics + return self._local.statistics # type: ignore[no-any-return] except AttributeError: - self._local.statistics = {} + self._local.statistics = t.cast(t.Dict[str, t.Any], {}) return self._local.statistics def wraps(self, f: WrappedFn) -> WrappedFn: @@ -328,10 +291,10 @@ def retry_with(*args: t.Any, **kwargs: t.Any) -> WrappedFn: return self.copy(*args, **kwargs).wraps(f) - wrapped_f.retry = self - wrapped_f.retry_with = retry_with + wrapped_f.retry = self # type: ignore[attr-defined] + wrapped_f.retry_with = retry_with # type: ignore[attr-defined] - return wrapped_f + return wrapped_f # type: ignore[return-value] def begin(self) -> None: self.statistics.clear() @@ -346,15 +309,15 @@ self.before(retry_state) return DoAttempt() - is_explicit_retry = retry_state.outcome.failed and isinstance(retry_state.outcome.exception(), TryAgain) - if not (is_explicit_retry or self.retry(retry_state=retry_state)): + is_explicit_retry = fut.failed and isinstance(fut.exception(), TryAgain) + if not (is_explicit_retry or self.retry(retry_state)): return fut.result() if self.after is not None: self.after(retry_state) self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start - if self.stop(retry_state=retry_state): + if self.stop(retry_state): if self.retry_error_callback: return self.retry_error_callback(retry_state) retry_exc = self.retry_error_cls(fut) @@ -363,7 +326,7 @@ raise retry_exc from fut.exception() if self.wait: - sleep = self.wait(retry_state=retry_state) + sleep = self.wait(retry_state) else: sleep = 0.0 retry_state.next_action = RetryAction(sleep) @@ -391,14 +354,24 @@ break @abstractmethod - def __call__(self, fn: t.Callable[..., _RetValT], *args: t.Any, **kwargs: t.Any) -> _RetValT: + def __call__( + self, + fn: t.Callable[..., WrappedFnReturnT], + *args: t.Any, + **kwargs: t.Any, + ) -> WrappedFnReturnT: pass class Retrying(BaseRetrying): """Retrying controller.""" - def __call__(self, fn: t.Callable[..., _RetValT], *args: t.Any, **kwargs: t.Any) -> _RetValT: + def __call__( + self, + fn: t.Callable[..., WrappedFnReturnT], + *args: t.Any, + **kwargs: t.Any, + ) -> WrappedFnReturnT: self.begin() retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) @@ -408,17 +381,23 @@ try: result = fn(*args, **kwargs) except BaseException: # noqa: B902 - retry_state.set_exception(sys.exc_info()) + retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() self.sleep(do) else: - return do + return do # type: ignore[no-any-return] -class Future(futures.Future): +if sys.version_info[1] >= 9: + FutureGenericT = futures.Future[t.Any] +else: + FutureGenericT = futures.Future + + +class Future(FutureGenericT): """Encapsulates a (future or past) attempted call to a target function.""" def __init__(self, attempt_number: int) -> None: @@ -491,13 +470,15 @@ fut.set_result(val) self.outcome, self.outcome_timestamp = fut, ts - def set_exception(self, exc_info: t.Tuple[t.Type[BaseException], BaseException, "types.TracebackType"]) -> None: + def set_exception( + self, exc_info: t.Tuple[t.Type[BaseException], BaseException, "types.TracebackType| None"] + ) -> None: ts = time.monotonic() fut = Future(self.attempt_number) fut.set_exception(exc_info[1]) self.outcome, self.outcome_timestamp = fut, ts - def __repr__(self): + def __repr__(self) -> str: if self.outcome is None: result = "none yet" elif self.outcome.failed: @@ -511,7 +492,115 @@ return f"<{clsname} {id(self)}: attempt #{self.attempt_number}; slept for {slept}; last result: {result}>" +@t.overload +def retry(func: WrappedFn) -> WrappedFn: + ... + + +@t.overload +def retry( + sleep: t.Callable[[t.Union[int, float]], None] = sleep, + stop: "StopBaseT" = stop_never, + wait: "WaitBaseT" = wait_none(), + retry: "RetryBaseT" = retry_if_exception_type(), + before: t.Callable[["RetryCallState"], None] = before_nothing, + after: t.Callable[["RetryCallState"], None] = after_nothing, + before_sleep: t.Optional[t.Callable[["RetryCallState"], None]] = None, + reraise: bool = False, + retry_error_cls: t.Type["RetryError"] = RetryError, + retry_error_callback: t.Optional[t.Callable[["RetryCallState"], t.Any]] = None, +) -> t.Callable[[WrappedFn], WrappedFn]: + ... + + +def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any: + """Wrap a function with a new `Retrying` object. + + :param dargs: positional arguments passed to Retrying object + :param dkw: keyword arguments passed to the Retrying object + """ + # support both @retry and @retry() as valid syntax + if len(dargs) == 1 and callable(dargs[0]): + return retry()(dargs[0]) + else: + + def wrap(f: WrappedFn) -> WrappedFn: + if isinstance(f, retry_base): + warnings.warn( + f"Got retry_base instance ({f.__class__.__name__}) as callable argument, " + f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)" + ) + r: "BaseRetrying" + if iscoroutinefunction(f): + r = AsyncRetrying(*dargs, **dkw) + elif tornado and hasattr(tornado.gen, "is_coroutine_function") and tornado.gen.is_coroutine_function(f): + r = TornadoRetrying(*dargs, **dkw) + else: + r = Retrying(*dargs, **dkw) + + return r.wraps(f) + + return wrap + + from tenacity._asyncio import AsyncRetrying # noqa:E402,I100 if tornado: from tenacity.tornadoweb import TornadoRetrying + + +__all__ = [ + "retry_base", + "retry_all", + "retry_always", + "retry_any", + "retry_if_exception", + "retry_if_exception_type", + "retry_if_exception_cause_type", + "retry_if_not_exception_type", + "retry_if_not_result", + "retry_if_result", + "retry_never", + "retry_unless_exception_type", + "retry_if_exception_message", + "retry_if_not_exception_message", + "sleep", + "sleep_using_event", + "stop_after_attempt", + "stop_after_delay", + "stop_all", + "stop_any", + "stop_never", + "stop_when_event_set", + "wait_chain", + "wait_combine", + "wait_exponential", + "wait_fixed", + "wait_incrementing", + "wait_none", + "wait_random", + "wait_random_exponential", + "wait_full_jitter", + "wait_exponential_jitter", + "before_log", + "before_nothing", + "after_log", + "after_nothing", + "before_sleep_log", + "before_sleep_nothing", + "retry", + "WrappedFn", + "TryAgain", + "NO_RESULT", + "DoAttempt", + "DoSleep", + "BaseAction", + "RetryAction", + "RetryError", + "AttemptManager", + "BaseRetrying", + "Retrying", + "Future", + "RetryCallState", + "AsyncRetrying", +] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/_asyncio.py new/tenacity-8.2.2/tenacity/_asyncio.py --- old/tenacity-8.1.0/tenacity/_asyncio.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/_asyncio.py 2023-02-28 15:21:48.000000000 +0100 @@ -17,7 +17,7 @@ import functools import sys -import typing +import typing as t from asyncio import sleep from tenacity import AttemptManager @@ -26,21 +26,20 @@ from tenacity import DoSleep from tenacity import RetryCallState -WrappedFn = typing.TypeVar("WrappedFn", bound=typing.Callable) -_RetValT = typing.TypeVar("_RetValT") +WrappedFnReturnT = t.TypeVar("WrappedFnReturnT") +WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]]) class AsyncRetrying(BaseRetrying): - def __init__(self, sleep: typing.Callable[[float], typing.Awaitable] = sleep, **kwargs: typing.Any) -> None: + sleep: t.Callable[[float], t.Awaitable[t.Any]] + + def __init__(self, sleep: t.Callable[[float], t.Awaitable[t.Any]] = sleep, **kwargs: t.Any) -> None: super().__init__(**kwargs) self.sleep = sleep - async def __call__( # type: ignore # Change signature from supertype - self, - fn: typing.Callable[..., typing.Awaitable[_RetValT]], - *args: typing.Any, - **kwargs: typing.Any, - ) -> _RetValT: + async def __call__( # type: ignore[override] + self, fn: WrappedFn, *args: t.Any, **kwargs: t.Any + ) -> WrappedFnReturnT: self.begin() retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) @@ -50,21 +49,24 @@ try: result = await fn(*args, **kwargs) except BaseException: # noqa: B902 - retry_state.set_exception(sys.exc_info()) + retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() await self.sleep(do) else: - return do + return do # type: ignore[no-any-return] + + def __iter__(self) -> t.Generator[AttemptManager, None, None]: + raise TypeError("AsyncRetrying object is not iterable") def __aiter__(self) -> "AsyncRetrying": self.begin() self._retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) return self - async def __anext__(self) -> typing.Union[AttemptManager, typing.Any]: + async def __anext__(self) -> AttemptManager: while True: do = self.iter(retry_state=self._retry_state) if do is None: @@ -75,18 +77,18 @@ self._retry_state.prepare_for_next_attempt() await self.sleep(do) else: - return do + raise StopAsyncIteration def wraps(self, fn: WrappedFn) -> WrappedFn: fn = super().wraps(fn) # Ensure wrapper is recognized as a coroutine function. @functools.wraps(fn) - async def async_wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: + async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any: return await fn(*args, **kwargs) # Preserve attributes - async_wrapped.retry = fn.retry - async_wrapped.retry_with = fn.retry_with + async_wrapped.retry = fn.retry # type: ignore[attr-defined] + async_wrapped.retry_with = fn.retry_with # type: ignore[attr-defined] - return async_wrapped + return async_wrapped # type: ignore[return-value] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/_utils.py new/tenacity-8.2.2/tenacity/_utils.py --- old/tenacity-8.1.0/tenacity/_utils.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/_utils.py 2023-02-28 15:21:48.000000000 +0100 @@ -16,6 +16,7 @@ import sys import typing +from datetime import timedelta # sys.maxsize: @@ -66,3 +67,10 @@ except AttributeError: pass return ".".join(segments) + + +time_unit_type = typing.Union[int, float, timedelta] + + +def to_seconds(time_unit: time_unit_type) -> float: + return float(time_unit.total_seconds() if isinstance(time_unit, timedelta) else time_unit) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/after.py new/tenacity-8.2.2/tenacity/after.py --- old/tenacity-8.1.0/tenacity/after.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/after.py 2023-02-28 15:21:48.000000000 +0100 @@ -36,9 +36,14 @@ """After call strategy that logs to some logger the finished attempt.""" def log_it(retry_state: "RetryCallState") -> None: + if retry_state.fn is None: + # NOTE(sileht): can't really happen, but we must please mypy + fn_name = "<unknown>" + else: + fn_name = _utils.get_callback_name(retry_state.fn) logger.log( log_level, - f"Finished call to '{_utils.get_callback_name(retry_state.fn)}' " + f"Finished call to '{fn_name}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/before.py new/tenacity-8.2.2/tenacity/before.py --- old/tenacity-8.1.0/tenacity/before.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/before.py 2023-02-28 15:21:48.000000000 +0100 @@ -32,9 +32,14 @@ """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state: "RetryCallState") -> None: + if retry_state.fn is None: + # NOTE(sileht): can't really happen, but we must please mypy + fn_name = "<unknown>" + else: + fn_name = _utils.get_callback_name(retry_state.fn) logger.log( log_level, - f"Starting call to '{_utils.get_callback_name(retry_state.fn)}', " + f"Starting call to '{fn_name}', " f"this is the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/before_sleep.py new/tenacity-8.2.2/tenacity/before_sleep.py --- old/tenacity-8.1.0/tenacity/before_sleep.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/before_sleep.py 2023-02-28 15:21:48.000000000 +0100 @@ -36,6 +36,14 @@ """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state: "RetryCallState") -> None: + local_exc_info: BaseException | bool | None + + if retry_state.outcome is None: + raise RuntimeError("log_it() called before outcome was set") + + if retry_state.next_action is None: + raise RuntimeError("log_it() called before next_action was set") + if retry_state.outcome.failed: ex = retry_state.outcome.exception() verb, value = "raised", f"{ex.__class__.__name__}: {ex}" @@ -48,10 +56,15 @@ verb, value = "returned", retry_state.outcome.result() local_exc_info = False # exc_info does not apply when no exception + if retry_state.fn is None: + # NOTE(sileht): can't really happen, but we must please mypy + fn_name = "<unknown>" + else: + fn_name = _utils.get_callback_name(retry_state.fn) + logger.log( log_level, - f"Retrying {_utils.get_callback_name(retry_state.fn)} " - f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.", + f"Retrying {fn_name} " f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.", exc_info=local_exc_info, ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/retry.py new/tenacity-8.2.2/tenacity/retry.py --- old/tenacity-8.1.0/tenacity/retry.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/retry.py 2023-02-28 15:21:48.000000000 +0100 @@ -36,6 +36,9 @@ return retry_any(self, other) +RetryBaseT = typing.Union[retry_base, typing.Callable[["RetryCallState"], bool]] + + class _retry_never(retry_base): """Retry strategy that never rejects any result.""" @@ -63,8 +66,14 @@ self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: + if retry_state.outcome is None: + raise RuntimeError("__call__() called before outcome was set") + if retry_state.outcome.failed: - return self.predicate(retry_state.outcome.exception()) + exception = retry_state.outcome.exception() + if exception is None: + raise RuntimeError("outcome failed but the exception is None") + return self.predicate(exception) else: return False @@ -111,10 +120,17 @@ super().__init__(lambda e: not isinstance(e, exception_types)) def __call__(self, retry_state: "RetryCallState") -> bool: + if retry_state.outcome is None: + raise RuntimeError("__call__() called before outcome was set") + # always retry if no exception was raised if not retry_state.outcome.failed: return True - return self.predicate(retry_state.outcome.exception()) + + exception = retry_state.outcome.exception() + if exception is None: + raise RuntimeError("outcome failed but the exception is None") + return self.predicate(exception) class retry_if_exception_cause_type(retry_base): @@ -134,6 +150,9 @@ self.exception_cause_types = exception_types def __call__(self, retry_state: "RetryCallState") -> bool: + if retry_state.outcome is None: + raise RuntimeError("__call__ called before outcome was set") + if retry_state.outcome.failed: exc = retry_state.outcome.exception() while exc is not None: @@ -151,6 +170,9 @@ self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: + if retry_state.outcome is None: + raise RuntimeError("__call__() called before outcome was set") + if not retry_state.outcome.failed: return self.predicate(retry_state.outcome.result()) else: @@ -164,6 +186,9 @@ self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: + if retry_state.outcome is None: + raise RuntimeError("__call__() called before outcome was set") + if not retry_state.outcome.failed: return not self.predicate(retry_state.outcome.result()) else: @@ -215,9 +240,16 @@ self.predicate = lambda *args_, **kwargs_: not if_predicate(*args_, **kwargs_) def __call__(self, retry_state: "RetryCallState") -> bool: + if retry_state.outcome is None: + raise RuntimeError("__call__() called before outcome was set") + if not retry_state.outcome.failed: return True - return self.predicate(retry_state.outcome.exception()) + + exception = retry_state.outcome.exception() + if exception is None: + raise RuntimeError("outcome failed but the exception is None") + return self.predicate(exception) class retry_any(retry_base): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/stop.py new/tenacity-8.2.2/tenacity/stop.py --- old/tenacity-8.1.0/tenacity/stop.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/stop.py 2023-02-28 15:21:48.000000000 +0100 @@ -16,6 +16,8 @@ import abc import typing +from tenacity import _utils + if typing.TYPE_CHECKING: import threading @@ -36,6 +38,9 @@ return stop_any(self, other) +StopBaseT = typing.Union[stop_base, typing.Callable[["RetryCallState"], bool]] + + class stop_any(stop_base): """Stop if any of the stop condition is valid.""" @@ -89,8 +94,10 @@ class stop_after_delay(stop_base): """Stop when the time from the first attempt >= limit.""" - def __init__(self, max_delay: float) -> None: - self.max_delay = max_delay + def __init__(self, max_delay: _utils.time_unit_type) -> None: + self.max_delay = _utils.to_seconds(max_delay) def __call__(self, retry_state: "RetryCallState") -> bool: + if retry_state.seconds_since_start is None: + raise RuntimeError("__call__() called but seconds_since_start is not set") return retry_state.seconds_since_start >= self.max_delay diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/tornadoweb.py new/tenacity-8.2.2/tenacity/tornadoweb.py --- old/tenacity-8.1.0/tenacity/tornadoweb.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/tornadoweb.py 2023-02-28 15:21:48.000000000 +0100 @@ -33,8 +33,8 @@ super().__init__(**kwargs) self.sleep = sleep - @gen.coroutine - def __call__( # type: ignore # Change signature from supertype + @gen.coroutine # type: ignore[misc] + def __call__( self, fn: "typing.Callable[..., typing.Union[typing.Generator[typing.Any, typing.Any, _RetValT], Future[_RetValT]]]", *args: typing.Any, @@ -49,7 +49,7 @@ try: result = yield fn(*args, **kwargs) except BaseException: # noqa: B902 - retry_state.set_exception(sys.exc_info()) + retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity/wait.py new/tenacity-8.2.2/tenacity/wait.py --- old/tenacity-8.1.0/tenacity/wait.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tenacity/wait.py 2023-02-28 15:21:48.000000000 +0100 @@ -17,19 +17,12 @@ import abc import random import typing -from datetime import timedelta from tenacity import _utils if typing.TYPE_CHECKING: from tenacity import RetryCallState -wait_unit_type = typing.Union[int, float, timedelta] - - -def to_seconds(wait_unit: wait_unit_type) -> float: - return float(wait_unit.total_seconds() if isinstance(wait_unit, timedelta) else wait_unit) - class wait_base(abc.ABC): """Abstract base class for wait strategies.""" @@ -43,16 +36,19 @@ def __radd__(self, other: "wait_base") -> typing.Union["wait_combine", "wait_base"]: # make it possible to use multiple waits with the built-in sum function - if other == 0: + if other == 0: # type: ignore[comparison-overlap] return self return self.__add__(other) +WaitBaseT = typing.Union[wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]] + + class wait_fixed(wait_base): """Wait strategy that waits a fixed amount of time between each retry.""" - def __init__(self, wait: wait_unit_type) -> None: - self.wait_fixed = to_seconds(wait) + def __init__(self, wait: _utils.time_unit_type) -> None: + self.wait_fixed = _utils.to_seconds(wait) def __call__(self, retry_state: "RetryCallState") -> float: return self.wait_fixed @@ -68,9 +64,9 @@ class wait_random(wait_base): """Wait strategy that waits a random amount of time between min/max.""" - def __init__(self, min: wait_unit_type = 0, max: wait_unit_type = 1) -> None: # noqa - self.wait_random_min = to_seconds(min) - self.wait_random_max = to_seconds(max) + def __init__(self, min: _utils.time_unit_type = 0, max: _utils.time_unit_type = 1) -> None: # noqa + self.wait_random_min = _utils.to_seconds(min) + self.wait_random_max = _utils.to_seconds(max) def __call__(self, retry_state: "RetryCallState") -> float: return self.wait_random_min + (random.random() * (self.wait_random_max - self.wait_random_min)) @@ -120,13 +116,13 @@ def __init__( self, - start: wait_unit_type = 0, - increment: wait_unit_type = 100, - max: wait_unit_type = _utils.MAX_WAIT, # noqa + start: _utils.time_unit_type = 0, + increment: _utils.time_unit_type = 100, + max: _utils.time_unit_type = _utils.MAX_WAIT, # noqa ) -> None: - self.start = to_seconds(start) - self.increment = to_seconds(increment) - self.max = to_seconds(max) + self.start = _utils.to_seconds(start) + self.increment = _utils.to_seconds(increment) + self.max = _utils.to_seconds(max) def __call__(self, retry_state: "RetryCallState") -> float: result = self.start + (self.increment * (retry_state.attempt_number - 1)) @@ -149,13 +145,13 @@ def __init__( self, multiplier: typing.Union[int, float] = 1, - max: wait_unit_type = _utils.MAX_WAIT, # noqa + max: _utils.time_unit_type = _utils.MAX_WAIT, # noqa exp_base: typing.Union[int, float] = 2, - min: wait_unit_type = 0, # noqa + min: _utils.time_unit_type = 0, # noqa ) -> None: self.multiplier = multiplier - self.min = to_seconds(min) - self.max = to_seconds(max) + self.min = _utils.to_seconds(min) + self.max = _utils.to_seconds(max) self.exp_base = exp_base def __call__(self, retry_state: "RetryCallState") -> float: @@ -206,7 +202,7 @@ This implements the strategy described here: https://cloud.google.com/storage/docs/retry-strategy - The wait time is min(initial * (2**n + random.uniform(0, jitter)), maximum) + The wait time is min(initial * 2**n + random.uniform(0, jitter), maximum) where n is the retry count. """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity.egg-info/PKG-INFO new/tenacity-8.2.2/tenacity.egg-info/PKG-INFO --- old/tenacity-8.1.0/tenacity.egg-info/PKG-INFO 2022-09-21 14:24:11.000000000 +0200 +++ new/tenacity-8.2.2/tenacity.egg-info/PKG-INFO 2023-02-28 15:22:05.000000000 +0100 @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: tenacity -Version: 8.1.0 +Version: 8.2.2 Summary: Retry code until it succeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: jul...@danjou.info License: Apache 2.0 -Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python @@ -17,10 +16,10 @@ Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Utilities Requires-Python: >=3.6 Provides-Extra: doc License-File: LICENSE Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tenacity.egg-info/SOURCES.txt new/tenacity-8.2.2/tenacity.egg-info/SOURCES.txt --- old/tenacity-8.1.0/tenacity.egg-info/SOURCES.txt 2022-09-21 14:24:11.000000000 +0200 +++ new/tenacity-8.2.2/tenacity.egg-info/SOURCES.txt 2023-02-28 15:22:05.000000000 +0100 @@ -9,7 +9,8 @@ setup.cfg setup.py tox.ini -.circleci/config.yml +.github/workflows/ci.yaml +.github/workflows/deploy.yaml doc/source/api.rst doc/source/changelog.rst doc/source/conf.py @@ -17,21 +18,28 @@ releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml releasenotes/notes/add-reno-d1ab5710f272650a.yaml releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml +releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml releasenotes/notes/after_log-50f4d73b24ce9203.yaml releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml releasenotes/notes/annotate_code-197b93130df14042.yaml releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml +releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml +releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml +releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml +releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml +releasenotes/notes/no-async-iter-6132a42e52348a75.yaml releasenotes/notes/pr320-py3-only-wheel-tag.yaml releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml +releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml tenacity/__init__.py tenacity/_asyncio.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tests/test_after.py new/tenacity-8.2.2/tests/test_after.py --- old/tenacity-8.1.0/tests/test_after.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tests/test_after.py 2023-02-28 15:21:48.000000000 +0100 @@ -2,8 +2,8 @@ import random import unittest.mock -from tenacity import after_log from tenacity import _utils # noqa +from tenacity import after_log from . import test_tenacity @@ -24,9 +24,10 @@ retry_state = test_tenacity.make_retry_state(self.previous_attempt_number, delay_since_first_attempt) fun = after_log(logger=logger, log_level=self.log_level) # use default sec_format fun(retry_state) + fn_name = "<unknown>" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn) log.assert_called_once_with( self.log_level, - f"Finished call to '{_utils.get_callback_name(retry_state.fn)}' " + f"Finished call to '{fn_name}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) @@ -42,9 +43,10 @@ retry_state = test_tenacity.make_retry_state(self.previous_attempt_number, delay_since_first_attempt) fun = after_log(logger=logger, log_level=self.log_level, sec_format=sec_format) fun(retry_state) + fn_name = "<unknown>" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn) log.assert_called_once_with( self.log_level, - f"Finished call to '{_utils.get_callback_name(retry_state.fn)}' " + f"Finished call to '{fn_name}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tests/test_asyncio.py new/tenacity-8.2.2/tests/test_asyncio.py --- old/tenacity-8.1.0/tests/test_asyncio.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tests/test_asyncio.py 2023-02-28 15:21:48.000000000 +0100 @@ -18,9 +18,11 @@ import unittest from functools import wraps +import pytest + from tenacity import AsyncRetrying, RetryError from tenacity import _asyncio as tasyncio -from tenacity import retry, stop_after_attempt +from tenacity import retry, retry_if_result, stop_after_attempt from tenacity.wait import wait_fixed from .test_tenacity import NoIOErrorAfterCount, current_time_ms @@ -88,7 +90,6 @@ @asynctest async def test_attempt_number_is_correct_for_interleaved_coroutines(self): - attempts = [] def after(retry_state): @@ -156,6 +157,28 @@ t = current_time_ms() - start self.assertLess(t, 1.1) + @asynctest + async def test_retry_with_result(self): + async def test(): + attempts = 0 + async for attempt in tasyncio.AsyncRetrying(retry=retry_if_result(lambda x: x < 3)): + with attempt: + attempts += 1 + attempt.retry_state.set_result(attempts) + return attempts + + result = await test() + + self.assertEqual(3, result) + + @asynctest + async def test_async_retying_iterator(self): + thing = NoIOErrorAfterCount(5) + with pytest.raises(TypeError): + for attempts in AsyncRetrying(): + with attempts: + await _async_function(thing) + if __name__ == "__main__": unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tests/test_tenacity.py new/tenacity-8.2.2/tests/test_tenacity.py --- old/tenacity-8.1.0/tests/test_tenacity.py 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tests/test_tenacity.py 2023-02-28 15:21:48.000000000 +0100 @@ -155,10 +155,12 @@ self.assertTrue(r.stop(make_retry_state(4, 6546))) def test_stop_after_delay(self): - r = Retrying(stop=tenacity.stop_after_delay(1)) - self.assertFalse(r.stop(make_retry_state(2, 0.999))) - self.assertTrue(r.stop(make_retry_state(2, 1))) - self.assertTrue(r.stop(make_retry_state(2, 1.001))) + for delay in (1, datetime.timedelta(seconds=1)): + with self.subTest(): + r = Retrying(stop=tenacity.stop_after_delay(delay)) + self.assertFalse(r.stop(make_retry_state(2, 0.999))) + self.assertTrue(r.stop(make_retry_state(2, 1))) + self.assertTrue(r.stop(make_retry_state(2, 1.001))) def test_legacy_explicit_stop_type(self): Retrying(stop="stop_after_attempt") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tenacity-8.1.0/tox.ini new/tenacity-8.2.2/tox.ini --- old/tenacity-8.1.0/tox.ini 2022-09-21 14:23:59.000000000 +0200 +++ new/tenacity-8.2.2/tox.ini 2023-02-28 15:21:48.000000000 +0100 @@ -1,5 +1,5 @@ [tox] -envlist = py3{6,7,8,9,10}, pep8, pypy3 +envlist = py3{6,7,8,9,10,11}, pep8, pypy3 skip_missing_interpreters = True [testenv] @@ -10,9 +10,9 @@ pytest typeguard commands = - py3{6,7,8,9,10},pypy3: pytest {posargs} - py3{6,7,8,9,10},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build - py3{6,7,8,9,10},pypy3: sphinx-build -a -E -W -b html doc/source doc/build + py3{6,7,8,9,10,11},pypy3: pytest {posargs} + py3{6,7,8,9,10,11},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build + py3{6,7,8,9,10,11},pypy3: sphinx-build -a -E -W -b html doc/source doc/build [testenv:pep8] basepython = python3 @@ -31,6 +31,12 @@ commands = black . +[testenv:mypy] +deps = + mypy>=1.0.0 +commands = + mypy tenacity + [testenv:black-ci] deps = black