mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 12:05:57 +00:00 
			
		
		
		
	 4ecf1d205a
			
		
	
	
		4ecf1d205a
		
			
		
	
	
	
	
		
			
			## Summary - Add support for the return types of `async` functions - Add type inference for `await` expressions - Add support for `async with` / async context managers - Add support for `yield from` expressions This PR is generally lacking proper error handling in some cases (e.g. illegal `__await__` attributes). I'm planning to work on this in a follow-up. part of https://github.com/astral-sh/ty/issues/151 closes https://github.com/astral-sh/ty/issues/736 ## Ecosystem There are a lot of true positives on `prefect` which look similar to: ```diff prefect (https://github.com/PrefectHQ/prefect) + src/integrations/prefect-aws/tests/workers/test_ecs_worker.py:406:12: error[unresolved-attribute] Type `str` has no attribute `status_code` ``` This is due to a wrong return type annotation [here](e926b8c4c1/src/integrations/prefect-aws/tests/workers/test_ecs_worker.py (L355-L391)). ```diff mitmproxy (https://github.com/mitmproxy/mitmproxy) + test/mitmproxy/addons/test_clientplayback.py:18:1: error[invalid-argument-type] Argument to function `asynccontextmanager` is incorrect: Expected `(...) -> AsyncIterator[Unknown]`, found `def tcp_server(handle_conn, **server_args) -> Unknown | tuple[str, int]` ``` [This](a4d794c59a/test/mitmproxy/addons/test_clientplayback.py (L18-L19)) is a true positive. That function should return `AsyncIterator[Address]`, not `Address`. I looked through almost all of the other new diagnostics and they all look like known problems or true positives. ## Typing conformance The typing conformance diff looks good. ## Test Plan New Markdown tests
		
			
				
	
	
		
			123 lines
		
	
	
	
		
			2.3 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			123 lines
		
	
	
	
		
			2.3 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # `async` / `await`
 | |
| 
 | |
| ## Basic
 | |
| 
 | |
| ```py
 | |
| async def retrieve() -> int:
 | |
|     return 42
 | |
| 
 | |
| async def main():
 | |
|     result = await retrieve()
 | |
| 
 | |
|     reveal_type(result)  # revealed: int
 | |
| ```
 | |
| 
 | |
| ## Generic `async` functions
 | |
| 
 | |
| ```py
 | |
| from typing import TypeVar
 | |
| 
 | |
| T = TypeVar("T")
 | |
| 
 | |
| async def persist(x: T) -> T:
 | |
|     return x
 | |
| 
 | |
| async def f(x: int):
 | |
|     result = await persist(x)
 | |
| 
 | |
|     reveal_type(result)  # revealed: int
 | |
| ```
 | |
| 
 | |
| ## Use cases
 | |
| 
 | |
| ### `Future`
 | |
| 
 | |
| ```py
 | |
| import asyncio
 | |
| import concurrent.futures
 | |
| 
 | |
| def blocking_function() -> int:
 | |
|     return 42
 | |
| 
 | |
| async def main():
 | |
|     loop = asyncio.get_event_loop()
 | |
|     with concurrent.futures.ThreadPoolExecutor() as pool:
 | |
|         result = await loop.run_in_executor(pool, blocking_function)
 | |
| 
 | |
|         # TODO: should be `int`
 | |
|         reveal_type(result)  # revealed: Unknown
 | |
| ```
 | |
| 
 | |
| ### `asyncio.Task`
 | |
| 
 | |
| ```py
 | |
| import asyncio
 | |
| 
 | |
| async def f() -> int:
 | |
|     return 1
 | |
| 
 | |
| async def main():
 | |
|     task = asyncio.create_task(f())
 | |
| 
 | |
|     result = await task
 | |
| 
 | |
|     # TODO: this should be `int`
 | |
|     reveal_type(result)  # revealed: Unknown
 | |
| ```
 | |
| 
 | |
| ### `asyncio.gather`
 | |
| 
 | |
| ```py
 | |
| import asyncio
 | |
| 
 | |
| async def task(name: str) -> int:
 | |
|     return len(name)
 | |
| 
 | |
| async def main():
 | |
|     (a, b) = await asyncio.gather(
 | |
|         task("A"),
 | |
|         task("B"),
 | |
|     )
 | |
| 
 | |
|     # TODO: these should be `int`
 | |
|     reveal_type(a)  # revealed: Unknown
 | |
|     reveal_type(b)  # revealed: Unknown
 | |
| ```
 | |
| 
 | |
| ## Under the hood
 | |
| 
 | |
| ```toml
 | |
| [environment]
 | |
| python-version = "3.12"  # Use 3.12 to be able to use PEP 695 generics
 | |
| ```
 | |
| 
 | |
| Let's look at the example from the beginning again:
 | |
| 
 | |
| ```py
 | |
| async def retrieve() -> int:
 | |
|     return 42
 | |
| ```
 | |
| 
 | |
| When we look at the signature of this function, we see that it actually returns a `CoroutineType`:
 | |
| 
 | |
| ```py
 | |
| reveal_type(retrieve)  # revealed: def retrieve() -> CoroutineType[Any, Any, int]
 | |
| ```
 | |
| 
 | |
| The expression `await retrieve()` desugars into a call to the `__await__` dunder method on the
 | |
| `CoroutineType` object, followed by a `yield from`. Let's first see the return type of `__await__`:
 | |
| 
 | |
| ```py
 | |
| reveal_type(retrieve().__await__())  # revealed: Generator[Any, None, int]
 | |
| ```
 | |
| 
 | |
| We can see that this returns a `Generator` that yields `Any`, and eventually returns `int`. For the
 | |
| final type of the `await` expression, we retrieve that third argument of the `Generator` type:
 | |
| 
 | |
| ```py
 | |
| from typing import Generator
 | |
| 
 | |
| def _():
 | |
|     result = yield from retrieve().__await__()
 | |
|     reveal_type(result)  # revealed: int
 | |
| ```
 |