mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-28 02:39:59 +00:00 
			
		
		
		
	[ty] Handle decorators which return unions of Callables (#20858)
				
					
				
			## Summary If a function is decorated with a decorator that returns a union of `Callable`s, also treat it as a union of function-like `Callable`s. Labeling as `internal`, since the previous change has not been released yet. ## Test Plan New regression test.
This commit is contained in:
		
							parent
							
								
									c69fa75cd5
								
							
						
					
					
						commit
						ac2c530377
					
				
					 2 changed files with 43 additions and 9 deletions
				
			
		|  | @ -145,22 +145,38 @@ class C2: | ||||||
| C2().method_decorated(1) | C2().method_decorated(1) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | And with unions of `Callable` types: | ||||||
|  | 
 | ||||||
|  | ```py | ||||||
|  | from typing import Callable | ||||||
|  | 
 | ||||||
|  | def expand(f: Callable[[C3, int], int]) -> Callable[[C3, int], int] | Callable[[C3, int], str]: | ||||||
|  |     raise NotImplementedError | ||||||
|  | 
 | ||||||
|  | class C3: | ||||||
|  |     @expand | ||||||
|  |     def method_decorated(self, x: int) -> int: | ||||||
|  |         return x | ||||||
|  | 
 | ||||||
|  | reveal_type(C3().method_decorated(1))  # revealed: int | str | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| Note that we currently only apply this heuristic when calling a function such as `memoize` via the | Note that we currently only apply this heuristic when calling a function such as `memoize` via the | ||||||
| decorator syntax. This is inconsistent, because the above *should* be equivalent to the following, | decorator syntax. This is inconsistent, because the above *should* be equivalent to the following, | ||||||
| but here we emit errors: | but here we emit errors: | ||||||
| 
 | 
 | ||||||
| ```py | ```py | ||||||
| def memoize3(f: Callable[[C3, int], str]) -> Callable[[C3, int], str]: | def memoize3(f: Callable[[C4, int], str]) -> Callable[[C4, int], str]: | ||||||
|     raise NotImplementedError |     raise NotImplementedError | ||||||
| 
 | 
 | ||||||
| class C3: | class C4: | ||||||
|     def method(self, x: int) -> str: |     def method(self, x: int) -> str: | ||||||
|         return str(x) |         return str(x) | ||||||
|     method_decorated = memoize3(method) |     method_decorated = memoize3(method) | ||||||
| 
 | 
 | ||||||
| # error: [missing-argument] | # error: [missing-argument] | ||||||
| # error: [invalid-argument-type] | # error: [invalid-argument-type] | ||||||
| C3().method_decorated(1) | C4().method_decorated(1) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| The reason for this is that the heuristic is problematic. We don't *know* that the `Callable` in the | The reason for this is that the heuristic is problematic. We don't *know* that the `Callable` in the | ||||||
|  |  | ||||||
|  | @ -2199,19 +2199,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | ||||||
|                 .map(|bindings| bindings.return_type(self.db())) |                 .map(|bindings| bindings.return_type(self.db())) | ||||||
|             { |             { | ||||||
|                 Ok(return_ty) => { |                 Ok(return_ty) => { | ||||||
|  |                     fn into_function_like_callable<'d>( | ||||||
|  |                         db: &'d dyn Db, | ||||||
|  |                         ty: Type<'d>, | ||||||
|  |                     ) -> Option<Type<'d>> { | ||||||
|  |                         match ty { | ||||||
|  |                             Type::Callable(callable) => Some(Type::Callable(CallableType::new( | ||||||
|  |                                 db, | ||||||
|  |                                 callable.signatures(db), | ||||||
|  |                                 true, | ||||||
|  |                             ))), | ||||||
|  |                             Type::Union(union) => union | ||||||
|  |                                 .try_map(db, |element| into_function_like_callable(db, *element)), | ||||||
|  |                             // Intersections are currently not handled here because that would require
 | ||||||
|  |                             // the decorator to be explicitly annotated as returning an intersection.
 | ||||||
|  |                             _ => None, | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|                     let is_input_function_like = inferred_ty |                     let is_input_function_like = inferred_ty | ||||||
|                         .try_upcast_to_callable(self.db()) |                         .try_upcast_to_callable(self.db()) | ||||||
|                         .and_then(Type::as_callable) |                         .and_then(Type::as_callable) | ||||||
|                         .is_some_and(|callable| callable.is_function_like(self.db())); |                         .is_some_and(|callable| callable.is_function_like(self.db())); | ||||||
|                     if is_input_function_like && let Some(callable_type) = return_ty.as_callable() { | 
 | ||||||
|  |                     if is_input_function_like | ||||||
|  |                         && let Some(return_ty_function_like) = | ||||||
|  |                             into_function_like_callable(self.db(), return_ty) | ||||||
|  |                     { | ||||||
|                         // When a method on a class is decorated with a function that returns a `Callable`, assume that
 |                         // When a method on a class is decorated with a function that returns a `Callable`, assume that
 | ||||||
|                         // the returned callable is also function-like. See "Decorating a method with a `Callable`-typed
 |                         // the returned callable is also function-like. See "Decorating a method with a `Callable`-typed
 | ||||||
|                         // decorator" in `callables_as_descriptors.md` for the extended explanation.
 |                         // decorator" in `callables_as_descriptors.md` for the extended explanation.
 | ||||||
|                         Type::Callable(CallableType::new( |                         return_ty_function_like | ||||||
|                             self.db(), |  | ||||||
|                             callable_type.signatures(self.db()), |  | ||||||
|                             true, |  | ||||||
|                         )) |  | ||||||
|                     } else { |                     } else { | ||||||
|                         return_ty |                         return_ty | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 David Peter
						David Peter