1010
1111import threading
1212import warnings
13+ from collections .abc import Callable
1314from contextlib import contextmanager
1415
1516from hypothesis .errors import HypothesisWarning , InvalidArgument
1617from hypothesis .internal .reflection import (
1718 get_pretty_function_description ,
19+ is_first_param_referenced_in_function ,
1820 is_identity_function ,
1921)
2022from hypothesis .internal .validation import check_type
2325 SearchStrategy ,
2426 check_strategy ,
2527)
28+ from hypothesis .utils .deprecation import note_deprecation
2629
2730
2831class LimitReached (BaseException ):
@@ -76,27 +79,25 @@ def capped(self, max_templates):
7679
7780
7881class RecursiveStrategy (SearchStrategy ):
79- def __init__ (self , base , extend , min_leaves , max_leaves ):
82+ def __init__ (
83+ self ,
84+ base : SearchStrategy ,
85+ extend : Callable [[SearchStrategy ], SearchStrategy ],
86+ min_leaves : int | None ,
87+ max_leaves : int ,
88+ ):
8089 super ().__init__ ()
8190 self .min_leaves = min_leaves
8291 self .max_leaves = max_leaves
8392 self .base = base
8493 self .limited_base = LimitedStrategy (base )
8594 self .extend = extend
8695
87- if is_identity_function (extend ):
88- warnings .warn (
89- "extend=lambda x: x is a no-op; you probably want to use a "
90- "different extend function, or just use the base strategy directly." ,
91- HypothesisWarning ,
92- stacklevel = 5 ,
93- )
94-
9596 strategies = [self .limited_base , self .extend (self .limited_base )]
9697 while 2 ** (len (strategies ) - 1 ) <= max_leaves :
9798 strategies .append (extend (OneOfStrategy (tuple (strategies ))))
9899 # If min_leaves > 1, we can never draw from base directly
99- if min_leaves > 1 :
100+ if isinstance ( min_leaves , int ) and min_leaves > 1 :
100101 strategies = strategies [1 :]
101102 self .strategy = OneOfStrategy (strategies )
102103
@@ -115,17 +116,39 @@ def do_validate(self) -> None:
115116 check_strategy (extended , f"extend({ self .limited_base !r} )" )
116117 self .limited_base .validate ()
117118 extended .validate ()
118- check_type (int , self .min_leaves , "min_leaves" )
119+ if is_identity_function (self .extend ):
120+ warnings .warn (
121+ "extend=lambda x: x is a no-op; you probably want to use a "
122+ "different extend function, or just use the base strategy directly." ,
123+ HypothesisWarning ,
124+ stacklevel = 5 ,
125+ )
126+ if not is_first_param_referenced_in_function (self .extend ):
127+ msg = (
128+ f"extend={ get_pretty_function_description (self .extend )} doesn't use "
129+ "it's argument, and thus can't actually recurse!"
130+ )
131+ if self .min_leaves is None :
132+ note_deprecation (
133+ msg ,
134+ since = "RELEASEDAY" ,
135+ has_codemod = False ,
136+ stacklevel = 1 ,
137+ )
138+ else :
139+ raise InvalidArgument (msg )
140+ if self .min_leaves is not None :
141+ check_type (int , self .min_leaves , "min_leaves" )
119142 check_type (int , self .max_leaves , "max_leaves" )
120- if self .min_leaves <= 0 :
143+ if self .min_leaves is not None and self . min_leaves <= 0 :
121144 raise InvalidArgument (
122145 f"min_leaves={ self .min_leaves !r} must be greater than zero"
123146 )
124147 if self .max_leaves <= 0 :
125148 raise InvalidArgument (
126149 f"max_leaves={ self .max_leaves !r} must be greater than zero"
127150 )
128- if self .min_leaves > self .max_leaves :
151+ if ( self .min_leaves or 1 ) > self .max_leaves :
129152 raise InvalidArgument (
130153 f"min_leaves={ self .min_leaves !r} must be less than or equal to "
131154 f"max_leaves={ self .max_leaves !r} "
@@ -138,7 +161,7 @@ def do_draw(self, data):
138161 with self .limited_base .capped (self .max_leaves ):
139162 result = data .draw (self .strategy )
140163 leaves_drawn = self .max_leaves - self .limited_base .marker
141- if leaves_drawn < self .min_leaves :
164+ if self . min_leaves and leaves_drawn < self .min_leaves :
142165 data .events [
143166 f"Draw for { self !r} had fewer than "
144167 f"min_leaves={ self .min_leaves } and had to be retried"
0 commit comments