11import  inspect 
2- import  types 
32import  warnings 
43from  collections .abc  import  Callable 
5- from  typing  import  Any , TypeVar , Union , get_args , get_origin 
6- 
7- 
8- def  accepts_single_positional_arg (func : Callable [..., Any ]) ->  bool :
9-     """ 
10-     True if the function accepts at least one positional argument, otherwise false. 
11- 
12-     This function intentionally does not define behavior for `func`s that 
13-     contain more than one positional argument, or any required keyword 
14-     arguments without defaults. 
15-     """ 
16-     try :
17-         sig  =  inspect .signature (func )
18-     except  (ValueError , TypeError ):
19-         return  False 
20- 
21-     params  =  dict (sig .parameters .items ())
22- 
23-     if  len (params ) ==  0 :
24-         # No parameters at all - can't accept single argument 
25-         return  False 
26- 
27-     # Check if ALL remaining parameters are keyword-only 
28-     all_keyword_only  =  all (param .kind  ==  inspect .Parameter .KEYWORD_ONLY  for  param  in  params .values ())
29- 
30-     if  all_keyword_only :
31-         # If all params are keyword-only, check if they ALL have defaults 
32-         # If they do, the function can be called with no arguments -> no argument 
33-         all_have_defaults  =  all (param .default  is  not inspect .Parameter .empty  for  param  in  params .values ())
34-         if  all_have_defaults :
35-             return  False 
36-         # otherwise, undefined (doesn't accept a positional argument, and requires at least one keyword only) 
37- 
38-     # Check if the ONLY parameter is **kwargs (VAR_KEYWORD) 
39-     # A function with only **kwargs can't accept a positional argument 
40-     if  len (params ) ==  1 :
41-         only_param  =  next (iter (params .values ()))
42-         if  only_param .kind  ==  inspect .Parameter .VAR_KEYWORD :
43-             return  False   # Can't pass positional argument to **kwargs 
44- 
45-     # Has at least one positional or variadic parameter - can accept argument 
46-     # Important note: this is designed to _not_ handle the situation where 
47-     # there are multiple keyword only arguments with no defaults. In those 
48-     # situations it's an invalid handler function, and will error. But it's 
49-     # not the responsibility of this function to check the validity of a 
50-     # callback. 
51-     return  True 
52- 
53- 
54- def  get_first_parameter_type (func : Callable [..., Any ]) ->  Any :
55-     """ 
56-     Get the type annotation of the first parameter of a function. 
57- 
58-     Returns None if: 
59-     - The function has no parameters 
60-     - The first parameter has no type annotation 
61-     - The signature cannot be inspected 
62- 
63-     Returns the actual annotation otherwise (could be a type, Any, Union, TypeVar, etc.) 
64-     """ 
65-     try :
66-         sig  =  inspect .signature (func )
67-     except  (ValueError , TypeError ):
68-         return  None 
69- 
70-     params  =  list (sig .parameters .values ())
71-     if  not  params :
72-         return  None 
73- 
74-     first_param  =  params [0 ]
75- 
76-     # Skip *args and **kwargs 
77-     if  first_param .kind  in  (inspect .Parameter .VAR_POSITIONAL , inspect .Parameter .VAR_KEYWORD ):
78-         return  None 
79- 
80-     annotation  =  first_param .annotation 
81-     if  annotation  ==  inspect .Parameter .empty :
82-         return  None 
83- 
84-     return  annotation 
85- 
86- 
87- def  type_accepts_request (param_type : Any , request_type : type ) ->  bool :
88-     """ 
89-     Check if a parameter type annotation can accept the request type. 
90- 
91-     Handles: 
92-     - Exact type match 
93-     - Union types (checks if request_type is in the Union) 
94-     - TypeVars (checks if request_type matches the bound or constraints) 
95-     - Generic types (basic support) 
96-     - Any (always returns True) 
97- 
98-     Returns False for None or incompatible types. 
99-     """ 
100-     if  param_type  is  None :
101-         return  False 
102- 
103-     # Check for Any type 
104-     if  param_type  is  Any :
105-         return  True 
106- 
107-     # Exact match 
108-     if  param_type  ==  request_type :
109-         return  True 
110- 
111-     # Handle Union types (both typing.Union and | syntax) 
112-     origin  =  get_origin (param_type )
113-     if  origin  is  Union  or  origin  is  types .UnionType :
114-         args  =  get_args (param_type )
115-         # Check if request_type is in the Union 
116-         for  arg  in  args :
117-             if  arg  ==  request_type :
118-                 return  True 
119-             # Recursively check each union member 
120-             if  type_accepts_request (arg , request_type ):
121-                 return  True 
122-         return  False 
123- 
124-     # Handle TypeVar 
125-     if  isinstance (param_type , TypeVar ):
126-         # Check if request_type matches the bound 
127-         if  param_type .__bound__  is  not None :
128-             if  request_type  ==  param_type .__bound__ :
129-                 return  True 
130-             # Check if request_type is a subclass of the bound 
131-             try :
132-                 if  issubclass (request_type , param_type .__bound__ ):
133-                     return  True 
134-             except  TypeError :
135-                 pass 
136- 
137-         # Check constraints 
138-         if  param_type .__constraints__ :
139-             for  constraint  in  param_type .__constraints__ :
140-                 if  request_type  ==  constraint :
141-                     return  True 
142-                 try :
143-                     if  issubclass (request_type , constraint ):
144-                         return  True 
145-                 except  TypeError :
146-                     pass 
147- 
148-         return  False 
149- 
150-     # For other generic types, check if request_type matches the origin 
151-     if  origin  is  not None :
152-         # Get the base generic type (e.g., list from list[str]) 
153-         return  request_type  ==  origin 
154- 
155-     return  False 
156- 
157- 
158- def  should_pass_request (func : Callable [..., Any ], request_type : type ) ->  tuple [bool , bool ]:
159-     """ 
160-     Determine if a request should be passed to the function based on parameter type inspection. 
161- 
162-     Returns a tuple of (should_pass_request, should_deprecate): 
163-     - should_pass_request: True if the request should be passed to the function 
164-     - should_deprecate: True if a deprecation warning should be issued 
165- 
166-     The decision logic: 
167-     1. If the function has no parameters -> (False, True) - old style without params, deprecate 
168-     2. If the function has parameters but can't accept positional args -> (False, False) 
169-     3. If the first parameter type accepts the request type -> (True, False) - pass request, no deprecation 
170-     4. If the first parameter is typed as Any -> (True, True) - pass request but deprecate (effectively untyped) 
171-     5. If the first parameter is typed with something incompatible -> (False, True) - old style, deprecate 
172-     6. If the first parameter is untyped but accepts positional args -> (True, True) - pass request, deprecate 
173-     """ 
174-     can_accept_arg  =  accepts_single_positional_arg (func )
175- 
176-     if  not  can_accept_arg :
177-         # Check if it has no parameters at all (old style) 
178-         try :
179-             sig  =  inspect .signature (func )
180-             if  len (sig .parameters ) ==  0 :
181-                 # Old style handler with no parameters - don't pass request but deprecate 
182-                 return  False , True 
183-         except  (ValueError , TypeError ):
184-             pass 
185-         # Can't accept positional arguments for other reasons 
186-         return  False , False 
187- 
188-     param_type  =  get_first_parameter_type (func )
189- 
190-     if  param_type  is  None :
191-         # Untyped parameter - this is the old style, pass request but deprecate 
192-         return  True , True 
193- 
194-     # Check if the parameter type can accept the request 
195-     if  type_accepts_request (param_type , request_type ):
196-         # Check if it's Any - if so, we should deprecate 
197-         if  param_type  is  Any :
198-             return  True , True 
199-         # Properly typed to accept the request - pass request, no deprecation 
200-         return  True , False 
201- 
202-     # Parameter is typed with something incompatible - this is an old style handler expecting 
203-     # a different signature, don't pass request, issue deprecation 
204-     return  False , True 
4+ from  typing  import  Any , get_type_hints 
2055
2066
2077def  issue_deprecation_warning (func : Callable [..., Any ], request_type : type ) ->  None :
@@ -215,3 +15,54 @@ def issue_deprecation_warning(func: Callable[..., Any], request_type: type) -> N
21515        DeprecationWarning ,
21616        stacklevel = 4 ,
21717    )
18+ 
19+ 
20+ def  create_call_wrapper (func : Callable [..., Any ], request_type : type ) ->  tuple [Callable [[Any ], Any ], bool ]:
21+     """ 
22+     Create a wrapper function that knows how to call func with the request object. 
23+ 
24+     Returns a tuple of (wrapper_func, should_deprecate): 
25+     - wrapper_func: A function that takes the request and calls func appropriately 
26+     - should_deprecate: True if a deprecation warning should be issued 
27+ 
28+     The wrapper handles three calling patterns: 
29+     1. Positional-only parameter typed as request_type (no default): func(req) 
30+     2. Positional/keyword parameter typed as request_type (no default): func(**{param_name: req}) 
31+     3. No request parameter or parameter with default (deprecated): func() 
32+     """ 
33+     try :
34+         sig  =  inspect .signature (func )
35+         type_hints  =  get_type_hints (func )
36+     except  (ValueError , TypeError , NameError ):
37+         # Can't inspect signature or resolve type hints, assume no request parameter (deprecated) 
38+         return  lambda  _ : func (), True 
39+ 
40+     # Check for positional-only parameter typed as request_type 
41+     for  param_name , param  in  sig .parameters .items ():
42+         if  param .kind  ==  inspect .Parameter .POSITIONAL_ONLY :
43+             param_type  =  type_hints .get (param_name )
44+             if  param_type  ==  request_type :
45+                 # Check if it has a default - if so, treat as old style (deprecated) 
46+                 if  param .default  is  not inspect .Parameter .empty :
47+                     return  lambda  _ : func (), True 
48+                 # Found positional-only parameter with correct type and no default 
49+                 return  lambda  req : func (req ), False 
50+ 
51+     # Check for any positional/keyword parameter typed as request_type 
52+     for  param_name , param  in  sig .parameters .items ():
53+         if  param .kind  in  (inspect .Parameter .POSITIONAL_OR_KEYWORD , inspect .Parameter .KEYWORD_ONLY ):
54+             param_type  =  type_hints .get (param_name )
55+             if  param_type  ==  request_type :
56+                 # Check if it has a default - if so, treat as old style (deprecated) 
57+                 if  param .default  is  not inspect .Parameter .empty :
58+                     return  lambda  _ : func (), True 
59+ 
60+                 # Found keyword parameter with correct type and no default 
61+                 # Need to capture param_name in closure properly 
62+                 def  make_keyword_wrapper (name : str ) ->  Callable [[Any ], Any ]:
63+                     return  lambda  req : func (** {name : req })
64+ 
65+                 return  make_keyword_wrapper (param_name ), False 
66+ 
67+     # No request parameter found - use old style (deprecated) 
68+     return  lambda  _ : func (), True 
0 commit comments