graft

Examples · a real benchmark sync

Asyncio support carried onto typer 0.9.1

borissmidt/typer, a fork of fastapi/typer with 5 changes on commit f83c66d, between releases, synced onto upstream's 0.9.1. Measured 2026-10-28.

Moved onto 0.9.1, tests pass

5 kept. Read by hand afterwards: correct. Five commits (asyncio support) carried unchanged over 46 upstream commits; identical content, and the fork's async tests pass with upstream's 881.

Each change

ChangeOutcomeWhy
#88: Add asyncio supportkeptapplies to the new release as is
#88: Fix unrelated failing test of powershell autocompletekeptapplies to the new release as is
#88: Fix the mypy linterkeptapplies to the new release as is
#88: Remove the loop factory to simplify the PR and make mypy happykeptapplies to the new release as is
[pre-commit.ci] Auto format from pre-commit.com hookskeptapplies to the new release as is

Tests

pass The project's own tests ran on the carried copy (119.6 s) and on 0.9.1 as shipped (108.9 s), and the changes added no failure.

Agents

No agent was needed: every change applied as it was or was already upstream. The free plan does this on its own.

The carried diff against 0.9.1

What the pull request would change: the fork's work on top of the new release.

tests/test_annotated.py                          | 16 ++++++++++++++++ tests/test_completion/test_completion_install.py |  9 +-------- typer/main.py                                    | 21 +++++++++++++++++++-- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/tests/test_annotated.py b/tests/test_annotated.pyindex 6436ad6..5c6d91b 100644--- a/tests/test_annotated.py+++ b/tests/test_annotated.py@@ -57,3 +57,19 @@ def test_annotated_option_with_argname_doesnt_mutate_multiple_calls():     result = runner.invoke(app, ["--force"])     assert result.exit_code == 0, result.output     assert "Forcing operation" in result.output+++def test_runner_can_use_an_async_method():+    app = typer.Typer()++    @app.command()+    async def cmd(val: Annotated[int, typer.Argument()] = 0):+        print(f"hello {val}")++    result = runner.invoke(app)+    assert result.exit_code == 0, result.output+    assert "hello 0" in result.output++    result = runner.invoke(app, ["42"])+    assert result.exit_code == 0, result.output+    assert "hello 42" in result.outputdiff --git a/tests/test_completion/test_completion_install.py b/tests/test_completion/test_completion_install.pyindex 0856b9b..1a5b5f3 100644--- a/tests/test_completion/test_completion_install.py+++ b/tests/test_completion/test_completion_install.py@@ -5,14 +5,12 @@ from pathlib import Path from unittest import mock  import shellingham-import typer from typer.testing import CliRunner  from docs_src.commands.index import tutorial001 as mod  runner = CliRunner()-app = typer.Typer()-app.command()(mod.main)+app = mod.app   def test_completion_install_no_shell():@@ -145,11 +143,6 @@ def test_completion_install_fish():     assert "Completion will take effect once you restart the terminal" in result.stdout  -runner = CliRunner()-app = typer.Typer()-app.command()(mod.main)-- def test_completion_install_powershell():     completion_path: Path = (         Path.home() / f".config/powershell/Microsoft.PowerShell_profile.ps1"diff --git a/typer/main.py b/typer/main.pyindex c739a29..c662001 100644--- a/typer/main.py+++ b/typer/main.py@@ -1,10 +1,11 @@+import asyncio import inspect import os import sys import traceback from datetime import datetime from enum import Enum-from functools import update_wrapper+from functools import update_wrapper, wraps from pathlib import Path from traceback import FrameSummary, StackSummary from types import TracebackType@@ -46,6 +47,7 @@ try: except ImportError:  # pragma: nocover     rich = None  # type: ignore + _original_except_hook = sys.excepthook _typer_developer_exception_attr_name = "__typer_developer_exception__" @@ -235,12 +237,27 @@ class Typer:             cls = TyperCommand          def decorator(f: CommandFunctionType) -> CommandFunctionType:+            def add_runner(f: CommandFunctionType) -> CommandFunctionType:+                @wraps(f)+                def run_wrapper(*args: Any, **kwargs: Any) -> Any:+                    if sys.version_info >= (3, 7):+                        return asyncio.run(f(*args, **kwargs))+                    else:+                        asyncio.get_event_loop().run_until_complete(f(*args, **kwargs))++                return run_wrapper  # type: ignore++            if inspect.iscoroutinefunction(f):+                callback = add_runner(f)+            else:+                callback = f+             self.registered_commands.append(                 CommandInfo(                     name=name,                     cls=cls,                     context_settings=context_settings,-                    callback=f,+                    callback=callback,                     help=help,                     epilog=epilog,                     short_help=short_help,

From data/bench/sync-real.json, run with scripts/bench-sync-real.ts on 2026-10-28. Every result on the results page, moved or stayed. Try your own fork.