Skip to content

fix: Preserve the types of annotated async functions wrapped with on_exception or on_predicate - #202

Merged
edgarrmondragon merged 2 commits into
python-backoff:mainfrom
MeisQuietude:fix/async-decorator-typing
Aug 28, 2026
Merged

fix: Preserve the types of annotated async functions wrapped with on_exception or on_predicate#202
edgarrmondragon merged 2 commits into
python-backoff:mainfrom
MeisQuietude:fix/async-decorator-typing

Conversation

@MeisQuietude

@MeisQuietude MeisQuietude commented Aug 28, 2026

Copy link
Copy Markdown

What changed

  • Preserve the complete callable type through on_exception and on_predicate while giving the internal async retry wrappers accurate coroutine contracts.
  • Add exact sync and async typing regression coverage for mypy, ty, and pyrefly, including strict disallow_any_decorated and unannotated sync functions.

Why

Version 2.4.0 reconstructs decorated async callables in a way that exposes Coroutine[Any, Any, T] to mypy and fails disallow_any_decorated. An identity transformation preserves the public callable contract without an overlapping async overload that can make pyrefly misclassify unannotated sync functions.

Fixes #200

@read-the-docs-community

read-the-docs-community Bot commented Aug 28, 2026

Copy link
Copy Markdown

Documentation build overview

📚 backoff | 🛠️ Build #34282625 | 📁 Comparing c186219 against latest (0430374)

  🔍 Preview build  

1 file changed
± api/reference.html

@MeisQuietude
MeisQuietude marked this pull request as draft August 28, 2026 19:37
@MeisQuietude
MeisQuietude marked this pull request as ready for review August 28, 2026 19:38
@edgarrmondragon edgarrmondragon added bug Something isn't working fix Bug fix and removed bug Something isn't working labels Aug 28, 2026

@edgarrmondragon edgarrmondragon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @MeisQuietude, just a couple of comments

Comment thread CHANGELOG.md
Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread tests/typing_pyrefly.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually exercise something? i.e. does this fail somehow without the decorator typing changes? Otherwise, it's not clear to me what this tests.

@MeisQuietude MeisQuietude Aug 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers the sync side of the decorator typing change. Pyrefly infers str for these unannotated sync functions, and the decorated calls must preserve that inferred type instead of becoming coroutines. The strict mypy test cannot cover this case because it requires explicit return annotations.

it guards against an alternative overload-based implementation of this fix.

For this sync function:

@backoff.on_exception(backoff.expo, ValueError)
def load_value(value: str):
    return value

load_value("hello").upper()

The results are:

  • Callable[[_CallableT], _CallableT]: Pyrefly preserves the inferred sync return type, so this passes.
  • Async-first + generic-sync overloads: Pyrefly resolves the call as a coroutine and reports Coroutine has no attribute upper.

This test prevents fixing the async case by accidentally breaking unannotated sync functions.

--

But ofc if you're willing to remove it, just say and I do it. I appeciate your work :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. I did consider exploring the @overload option, so it's good to have in case I forget that it won't work 😅.

Thanks!

@edgarrmondragon edgarrmondragon changed the title fix: Preserve async decorator types fix: Preserve the types of annotated async functions wrapped with on_exception or on_predicate Aug 28, 2026
Model async retry implementations as coroutine transformations while keeping the public decorators as identity transformations. This avoids mypy's disallow_any_decorated regression without introducing ambiguous overloads for other type checkers.
@edgarrmondragon
edgarrmondragon added this pull request to the merge queue Aug 28, 2026
Merged via the queue into python-backoff:main with commit 6886aed Aug 28, 2026
20 checks passed
@MeisQuietude
MeisQuietude deleted the fix/async-decorator-typing branch September 1, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Async decorator typing regression in 2.4.0 with mypy disallow_any_decorated

2 participants