@@ -98,12 +98,24 @@ def __eq__(self, other: object) -> bool:
9898 )
9999
100100
101+ class TooManyConstants (Exception ):
102+ # a control flow exception which we raise in ConstantsVisitor when the
103+ # number of constants in a module gets too large.
104+ pass
105+
106+
101107class ConstantVisitor (NodeVisitor ):
102- def __init__ (self ):
108+ CONSTANTS_LIMIT : int = 1024
109+
110+ def __init__ (self , * , limit : bool ):
103111 super ().__init__ ()
104112 self .constants = Constants ()
113+ self .limit = limit
105114
106115 def _add_constant (self , value : object ) -> None :
116+ if self .limit and len (self .constants ) >= self .CONSTANTS_LIMIT :
117+ raise TooManyConstants
118+
107119 if isinstance (value , str ) and (
108120 value .isspace ()
109121 or value == ""
@@ -166,33 +178,49 @@ def visit_Constant(self, node):
166178 self .generic_visit (node )
167179
168180
169- def _constants_from_source (source : Union [str , bytes ]) -> Constants :
181+ def _constants_from_source (source : Union [str , bytes ], * , limit : bool ) -> Constants :
170182 tree = ast .parse (source )
171- visitor = ConstantVisitor ()
172- visitor .visit (tree )
183+ visitor = ConstantVisitor (limit = limit )
184+
185+ try :
186+ visitor .visit (tree )
187+ except TooManyConstants :
188+ # in the case of an incomplete collection, return nothing, to avoid
189+ # muddying caches etc.
190+ return Constants ()
191+
173192 return visitor .constants
174193
175194
176195@lru_cache (4096 )
177- def constants_from_module (module : ModuleType ) -> Constants :
196+ def constants_from_module (module : ModuleType , * , limit : bool = True ) -> Constants :
178197 try :
179198 module_file = inspect .getsourcefile (module )
180199 # use type: ignore because we know this might error
181200 source_bytes = Path (module_file ).read_bytes () # type: ignore
182201 except Exception :
183202 return Constants ()
184203
204+ if limit and len (source_bytes ) > 512 * 1024 :
205+ # Skip files over 512kb. For reference, the largest source file
206+ # in Hypothesis is strategies/_internal/core.py at 107kb at time
207+ # of writing.
208+ return Constants ()
209+
185210 source_hash = hashlib .sha1 (source_bytes ).hexdigest ()[:16 ]
186- cache_p = storage_directory ("constants" ) / source_hash
211+ # separate cache files for each limit param. see discussion in pull/4398
212+ cache_p = storage_directory ("constants" ) / (
213+ source_hash + ("" if limit else "_nolimit" )
214+ )
187215 try :
188- return _constants_from_source (cache_p .read_bytes ())
216+ return _constants_from_source (cache_p .read_bytes (), limit = limit )
189217 except Exception :
190218 # if the cached location doesn't exist, or it does exist but there was
191219 # a problem reading it, fall back to standard computation of the constants
192220 pass
193221
194222 try :
195- constants = _constants_from_source (source_bytes )
223+ constants = _constants_from_source (source_bytes , limit = limit )
196224 except Exception :
197225 # A bunch of things can go wrong here.
198226 # * ast.parse may fail on the source code
0 commit comments