Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/hypothesis/strategies/_internal/strings.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

162 statements  

1# This file is part of Hypothesis, which may be found at 

2# https://github.com/HypothesisWorks/hypothesis/ 

3# 

4# Copyright the Hypothesis Authors. 

5# Individual contributors are listed in AUTHORS.rst and the git log. 

6# 

7# This Source Code Form is subject to the terms of the Mozilla Public License, 

8# v. 2.0. If a copy of the MPL was not distributed with this file, You can 

9# obtain one at https://mozilla.org/MPL/2.0/. 

10 

11import copy 

12import re 

13import warnings 

14from collections.abc import Collection 

15from functools import cache, lru_cache, partial 

16from typing import cast 

17 

18from hypothesis.errors import HypothesisWarning, InvalidArgument 

19from hypothesis.internal import charmap 

20from hypothesis.internal.charmap import Categories 

21from hypothesis.internal.conjecture.data import ConjectureData 

22from hypothesis.internal.conjecture.providers import COLLECTION_DEFAULT_MAX_SIZE 

23from hypothesis.internal.filtering import max_len, min_len 

24from hypothesis.internal.intervalsets import IntervalSet 

25from hypothesis.internal.reflection import get_pretty_function_description 

26from hypothesis.strategies._internal.collections import ListStrategy 

27from hypothesis.strategies._internal.lazy import unwrap_strategies 

28from hypothesis.strategies._internal.strategies import ( 

29 OneOfStrategy, 

30 SampledFromStrategy, 

31 SearchStrategy, 

32) 

33from hypothesis.vendor.pretty import pretty 

34 

35 

36# Cache size is limited by sys.maxunicode, but passing None makes it slightly faster. 

37@cache 

38# this is part of our forward-facing validation, so we do *not* tell mypyc that c 

39# should be a str, because we don't want it to validate it before we can. 

40def _check_is_single_character(c: object) -> str: 

41 # In order to mitigate the performance cost of this check, we use a shared cache, 

42 # even at the cost of showing the culprit strategy in the error message. 

43 if not isinstance(c, str): 

44 type_ = get_pretty_function_description(type(c)) 

45 raise InvalidArgument(f"Got non-string {c!r} (type {type_})") 

46 if len(c) != 1: 

47 raise InvalidArgument(f"Got {c!r} (length {len(c)} != 1)") 

48 return c 

49 

50 

51def _check_alphabet_elements(alphabet: Collection[str]) -> None: 

52 non_string = [c for c in alphabet if not isinstance(c, str)] 

53 if non_string: 

54 raise InvalidArgument( 

55 "The following elements in alphabet are not unicode " 

56 f"strings: {non_string!r}" 

57 ) 

58 not_one_char = [c for c in alphabet if len(c) != 1] 

59 if not_one_char: 

60 raise InvalidArgument( 

61 "The following elements in alphabet are not of length one, " 

62 f"which leads to violation of size constraints: {not_one_char!r}" 

63 ) 

64 if alphabet in ["ascii", "utf-8"]: 

65 warnings.warn( 

66 f"alphabet={alphabet!r}: it seems like you are trying to use the " 

67 f"codec {alphabet!r}, but this generates strings using the literal " 

68 f"characters {list(alphabet)!r}. To specify the {alphabet} codec, " 

69 f"use alphabet=st.characters(codec={alphabet!r}). If you intended " 

70 "to use character literals, you can silence this warning by " 

71 "reordering the characters.", 

72 HypothesisWarning, 

73 # this stacklevel is of course incorrect, but breaking out of the 

74 # levels of LazyStrategy and validation isn't worthwhile. 

75 stacklevel=1, 

76 ) 

77 

78 

79class OneCharStringStrategy(SearchStrategy[str]): 

80 """A strategy which generates single character strings of text type.""" 

81 

82 def __init__(self, intervals: IntervalSet, force_repr: str | None = None) -> None: 

83 super().__init__() 

84 assert isinstance(intervals, IntervalSet) 

85 self.intervals = intervals 

86 self._force_repr = force_repr 

87 

88 @classmethod 

89 def from_characters_args( 

90 cls, 

91 *, 

92 codec: str | None = None, 

93 min_codepoint: int | None = None, 

94 max_codepoint: int | None = None, 

95 categories: Categories | None = None, 

96 exclude_characters: Collection[str] = "", 

97 include_characters: Collection[str] = "", 

98 ) -> "OneCharStringStrategy": 

99 assert set(categories or ()).issubset(charmap.categories()) 

100 intervals = charmap.query( 

101 min_codepoint=min_codepoint, 

102 max_codepoint=max_codepoint, 

103 categories=categories, 

104 exclude_characters=exclude_characters, 

105 include_characters=include_characters, 

106 ) 

107 if codec is not None: 

108 intervals &= charmap.intervals_from_codec(codec) 

109 

110 _arg_repr = ", ".join( 

111 f"{k}={v!r}" 

112 for k, v in [ 

113 ("codec", codec), 

114 ("min_codepoint", min_codepoint), 

115 ("max_codepoint", max_codepoint), 

116 ("categories", categories), 

117 ("exclude_characters", exclude_characters), 

118 ("include_characters", include_characters), 

119 ] 

120 if v not in (None, "") 

121 and not ( 

122 k == "categories" 

123 # v has to be `categories` here. Help mypy along to infer that. 

124 and set(cast(Categories, v)) == set(charmap.categories()) - {"Cs"} 

125 ) 

126 ) 

127 if not intervals: 

128 raise InvalidArgument( 

129 "No characters are allowed to be generated by this " 

130 f"combination of arguments: {_arg_repr}" 

131 ) 

132 return cls(intervals, force_repr=f"characters({_arg_repr})") 

133 

134 @classmethod 

135 def from_alphabet( 

136 cls, alphabet: Collection[str] | SearchStrategy[str] 

137 ) -> "OneCharStringStrategy | None": 

138 # Shared logic for the `alphabet=` parameter of st.text and st.from_regex. 

139 # Returns None if `alphabet` cannot be statically resolved to a set of characters, 

140 # since each caller may wish to handle this case differently. 

141 if not isinstance(alphabet, SearchStrategy): 

142 _check_alphabet_elements(alphabet) 

143 return cls.from_characters_args(categories=(), include_characters=alphabet) 

144 

145 char_strategy = unwrap_strategies(alphabet) 

146 if isinstance(char_strategy, cls): 

147 return char_strategy 

148 elif isinstance(char_strategy, SampledFromStrategy): 

149 if char_strategy._transformations: 

150 # resolving from .elements would ignore the .map/.filter calls 

151 return None 

152 _check_alphabet_elements(char_strategy.elements) 

153 return cls.from_characters_args( 

154 categories=(), 

155 include_characters=char_strategy.elements, 

156 ) 

157 elif isinstance(char_strategy, OneOfStrategy): 

158 intervals = IntervalSet() 

159 for s in char_strategy.element_strategies: 

160 resolved = cls.from_alphabet(s) 

161 if resolved is None: 

162 return None 

163 intervals = intervals.union(resolved.intervals) 

164 return cls(intervals, force_repr=repr(alphabet)) 

165 return None 

166 

167 def __repr__(self) -> str: 

168 return self._force_repr or f"OneCharStringStrategy({self.intervals!r})" 

169 

170 def do_draw(self, data: ConjectureData) -> str: 

171 return data.draw_string(self.intervals, min_size=1, max_size=1) 

172 

173 

174_nonempty_names = ( 

175 "capitalize", 

176 "expandtabs", 

177 "join", 

178 "lower", 

179 "rsplit", 

180 "split", 

181 "splitlines", 

182 "swapcase", 

183 "title", 

184 "upper", 

185) 

186_nonempty_and_content_names = ( 

187 "islower", 

188 "isupper", 

189 "isalnum", 

190 "isalpha", 

191 "isascii", 

192 "isdigit", 

193 "isspace", 

194 "istitle", 

195 "lstrip", 

196 "rstrip", 

197 "strip", 

198) 

199 

200 

201class TextStrategy(ListStrategy[str]): 

202 def do_draw(self, data): 

203 # if our element strategy is OneCharStringStrategy, we can skip the 

204 # ListStrategy draw and jump right to data.draw_string. 

205 # Doing so for user-provided element strategies is not correct in 

206 # general, as they may define a different distribution than data.draw_string. 

207 elems = unwrap_strategies(self.element_strategy) 

208 if isinstance(elems, OneCharStringStrategy): 

209 return data.draw_string( 

210 elems.intervals, 

211 min_size=self.min_size, 

212 max_size=( 

213 COLLECTION_DEFAULT_MAX_SIZE 

214 if self.max_size == float("inf") 

215 else self.max_size 

216 ), 

217 ) 

218 return "".join(super().do_draw(data)) 

219 

220 def __repr__(self) -> str: 

221 args = [] 

222 if repr(self.element_strategy) != "characters()": 

223 args.append(repr(self.element_strategy)) 

224 if self.min_size: 

225 args.append(f"min_size={self.min_size}") 

226 if self.max_size < float("inf"): 

227 args.append(f"max_size={self.max_size}") 

228 return f"text({', '.join(args)})" 

229 

230 # See https://docs.python.org/3/library/stdtypes.html#string-methods 

231 # These methods always return Truthy values for any nonempty string. 

232 _nonempty_filters = ( 

233 *ListStrategy._nonempty_filters, 

234 str, 

235 str.casefold, 

236 str.encode, 

237 *(getattr(str, n) for n in _nonempty_names), 

238 ) 

239 _nonempty_and_content_filters = ( 

240 str.isdecimal, 

241 str.isnumeric, 

242 *(getattr(str, n) for n in _nonempty_and_content_names), 

243 ) 

244 

245 def filter(self, condition): 

246 elems = unwrap_strategies(self.element_strategy) 

247 if ( 

248 condition is str.isidentifier 

249 and self.max_size >= 1 

250 and isinstance(elems, OneCharStringStrategy) 

251 ): 

252 from hypothesis.strategies import builds, nothing 

253 

254 id_start, id_continue = _identifier_characters() 

255 if not (elems.intervals & id_start): 

256 return nothing() 

257 return builds( 

258 "{}{}".format, 

259 OneCharStringStrategy(elems.intervals & id_start), 

260 TextStrategy( 

261 OneCharStringStrategy(elems.intervals & id_continue), 

262 min_size=max(0, self.min_size - 1), 

263 max_size=self.max_size - 1, 

264 ), 

265 # Filter to ensure that NFKC normalization keeps working in future 

266 ).filter(str.isidentifier) 

267 if (new := _string_filter_rewrite(self, str, condition)) is not None: 

268 return new 

269 return super().filter(condition) 

270 

271 

272def _string_filter_rewrite(self, kind, condition): 

273 if condition in (kind.lower, kind.title, kind.upper): 

274 k = kind.__name__ 

275 warnings.warn( 

276 f"You applied {k}.{condition.__name__} as a filter, but this allows " 

277 f"all nonempty strings! Did you mean {k}.is{condition.__name__}?", 

278 HypothesisWarning, 

279 stacklevel=2, 

280 ) 

281 

282 if ( 

283 ( 

284 kind is bytes 

285 or isinstance( 

286 unwrap_strategies(self.element_strategy), OneCharStringStrategy 

287 ) 

288 ) 

289 and isinstance(pattern := getattr(condition, "__self__", None), re.Pattern) 

290 and isinstance(pattern.pattern, kind) 

291 ): 

292 from hypothesis.strategies._internal.regex import regex_strategy 

293 

294 if condition.__name__ == "match": 

295 # Replace with an easier-to-handle equivalent condition 

296 caret, close = ("^(?:", ")") if kind is str else (b"^(?:", b")") 

297 pattern = re.compile(caret + pattern.pattern + close, flags=pattern.flags) 

298 condition = pattern.search 

299 

300 if condition.__name__ in ("search", "findall", "fullmatch"): 

301 s = regex_strategy( 

302 pattern, 

303 fullmatch=condition.__name__ == "fullmatch", 

304 alphabet=self.element_strategy if kind is str else None, 

305 ) 

306 if self.min_size > 0: 

307 s = s.filter(partial(min_len, self.min_size)) 

308 if self.max_size < 1e999: 

309 s = s.filter(partial(max_len, self.max_size)) 

310 return s 

311 elif condition.__name__ in ("finditer", "scanner"): 

312 # PyPy implements `finditer` as an alias to their `scanner` method 

313 warnings.warn( 

314 f"You applied {pretty(condition)} as a filter, but this allows " 

315 f"any string at all! Did you mean .findall ?", 

316 HypothesisWarning, 

317 stacklevel=3, 

318 ) 

319 return self 

320 elif condition.__name__ == "split": 

321 warnings.warn( 

322 f"You applied {pretty(condition)} as a filter, but this allows " 

323 f"any nonempty string! Did you mean .search ?", 

324 HypothesisWarning, 

325 stacklevel=3, 

326 ) 

327 return self.filter(bool) 

328 

329 # We use ListStrategy filter logic for the conditions that *only* imply 

330 # the string is nonempty. Here, we increment the min_size but still apply 

331 # the filter for conditions that imply nonempty *and specific contents*. 

332 if condition in self._nonempty_and_content_filters and self.max_size >= 1: 

333 self = copy.copy(self) 

334 self.min_size = max(1, self.min_size) 

335 return ListStrategy.filter(self, condition) 

336 

337 return None 

338 

339 

340# Excerpted from https://www.unicode.org/Public/15.0.0/ucd/PropList.txt 

341# Python updates it's Unicode version between minor releases, but fortunately 

342# these properties do not change between the Unicode versions in question. 

343_PROPLIST = """ 

344# ================================================ 

345 

3461885..1886 ; Other_ID_Start # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA 

3472118 ; Other_ID_Start # Sm SCRIPT CAPITAL P 

348212E ; Other_ID_Start # So ESTIMATED SYMBOL 

349309B..309C ; Other_ID_Start # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK 

350 

351# Total code points: 6 

352 

353# ================================================ 

354 

35500B7 ; Other_ID_Continue # Po MIDDLE DOT 

3560387 ; Other_ID_Continue # Po GREEK ANO TELEIA 

3571369..1371 ; Other_ID_Continue # No [9] ETHIOPIC DIGIT ONE..ETHIOPIC DIGIT NINE 

35819DA ; Other_ID_Continue # No NEW TAI LUE THAM DIGIT ONE 

359 

360# Total code points: 12 

361""" 

362 

363 

364@lru_cache 

365def _identifier_characters() -> tuple[IntervalSet, IntervalSet]: 

366 """See https://docs.python.org/3/reference/lexical_analysis.html#identifiers""" 

367 # Start by computing the set of special characters 

368 chars = {"Other_ID_Start": "", "Other_ID_Continue": ""} 

369 for line in _PROPLIST.splitlines(): 

370 if m := re.match(r"([0-9A-F.]+) +; (\w+) # ", line): 

371 codes, prop = m.groups() 

372 span = range(int(codes[:4], base=16), int(codes[-4:], base=16) + 1) 

373 chars[prop] += "".join(chr(x) for x in span) 

374 

375 # Then get the basic set by Unicode category and known extras 

376 id_start = charmap.query( 

377 categories=("Lu", "Ll", "Lt", "Lm", "Lo", "Nl"), 

378 include_characters="_" + chars["Other_ID_Start"], 

379 ) 

380 id_start -= IntervalSet.from_string( 

381 # Magic value: the characters which NFKC-normalize to be invalid identifiers. 

382 # Conveniently they're all in `id_start`, so we only need to do this once. 

383 "\u037a\u0e33\u0eb3\u2e2f\u309b\u309c\ufc5e\ufc5f\ufc60\ufc61\ufc62\ufc63" 

384 "\ufdfa\ufdfb\ufe70\ufe72\ufe74\ufe76\ufe78\ufe7a\ufe7c\ufe7e\uff9e\uff9f" 

385 ) 

386 id_continue = id_start | charmap.query( 

387 categories=("Mn", "Mc", "Nd", "Pc"), 

388 include_characters=chars["Other_ID_Continue"], 

389 ) 

390 return id_start, id_continue 

391 

392 

393class BytesStrategy(SearchStrategy): 

394 def __init__(self, min_size: int, max_size: int | None): 

395 super().__init__() 

396 self.min_size = min_size 

397 self.max_size = ( 

398 max_size if max_size is not None else COLLECTION_DEFAULT_MAX_SIZE 

399 ) 

400 

401 def do_draw(self, data: ConjectureData) -> bytes: 

402 return data.draw_bytes(self.min_size, self.max_size) 

403 

404 _nonempty_filters = ( 

405 *ListStrategy._nonempty_filters, 

406 bytes, 

407 *(getattr(bytes, n) for n in _nonempty_names), 

408 ) 

409 _nonempty_and_content_filters = ( 

410 *(getattr(bytes, n) for n in _nonempty_and_content_names), 

411 ) 

412 

413 def filter(self, condition): 

414 if (new := _string_filter_rewrite(self, bytes, condition)) is not None: 

415 return new 

416 return ListStrategy.filter(self, condition)