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 min_leaves is not None and min_leaves > 1 :
100101 strategies = strategies [1 :]
101102 self .strategy = OneOfStrategy (strategies )
102103
@@ -115,17 +116,42 @@ 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+
120+ if is_identity_function (self .extend ):
121+ warnings .warn (
122+ "extend=lambda x: x is a no-op; you probably want to use a "
123+ "different extend function, or just use the base strategy directly." ,
124+ HypothesisWarning ,
125+ stacklevel = 5 ,
126+ )
127+
128+ if not is_first_param_referenced_in_function (self .extend ):
129+ msg = (
130+ f"extend={ get_pretty_function_description (self .extend )} doesn't use "
131+ "it's argument, and thus can't actually recurse!"
132+ )
133+ if self .min_leaves is None :
134+ note_deprecation (
135+ msg ,
136+ since = "RELEASEDAY" ,
137+ has_codemod = False ,
138+ stacklevel = 1 ,
139+ )
140+ else :
141+ raise InvalidArgument (msg )
142+
143+ if self .min_leaves is not None :
144+ check_type (int , self .min_leaves , "min_leaves" )
119145 check_type (int , self .max_leaves , "max_leaves" )
120- if self .min_leaves <= 0 :
146+ if self .min_leaves is not None and self . min_leaves <= 0 :
121147 raise InvalidArgument (
122148 f"min_leaves={ self .min_leaves !r} must be greater than zero"
123149 )
124150 if self .max_leaves <= 0 :
125151 raise InvalidArgument (
126152 f"max_leaves={ self .max_leaves !r} must be greater than zero"
127153 )
128- if self .min_leaves > self .max_leaves :
154+ if ( self .min_leaves or 1 ) > self .max_leaves :
129155 raise InvalidArgument (
130156 f"min_leaves={ self .min_leaves !r} must be less than or equal to "
131157 f"max_leaves={ self .max_leaves !r} "
@@ -138,7 +164,7 @@ def do_draw(self, data):
138164 with self .limited_base .capped (self .max_leaves ):
139165 result = data .draw (self .strategy )
140166 leaves_drawn = self .max_leaves - self .limited_base .marker
141- if leaves_drawn < self .min_leaves :
167+ if self . min_leaves and leaves_drawn < self .min_leaves :
142168 data .events [
143169 f"Draw for { self !r} had fewer than "
144170 f"min_leaves={ self .min_leaves } and had to be retried"
0 commit comments