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

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

436 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 datetime as dt 

12import operator as op 

13import sys 

14import warnings 

15import zoneinfo 

16from functools import cache, partial 

17from importlib import resources 

18from pathlib import Path 

19from typing import TYPE_CHECKING, Annotated, Any, overload 

20 

21from hypothesis.errors import CannotInvert, InvalidArgument 

22from hypothesis.internal.conjecture.choice import ChoiceT 

23from hypothesis.internal.validation import check_type, check_valid_interval 

24from hypothesis.strategies._internal.core import sampled_from 

25from hypothesis.strategies._internal.lazy import unwrap_strategies 

26from hypothesis.strategies._internal.misc import just, none, nothing 

27from hypothesis.strategies._internal.strategies import ( 

28 FilteredStrategy, 

29 OneOfStrategy, 

30 SampledFromStrategy, 

31 SearchStrategy, 

32 current_filter_call_site, 

33 one_of, 

34) 

35from hypothesis.strategies._internal.utils import defines_strategy 

36 

37if TYPE_CHECKING: 

38 from annotated_types import Timezone 

39 

40 NaiveDatetime = Annotated[dt.datetime, Timezone(None)] 

41 AwareDatetime = Annotated[dt.datetime, Timezone(...)] 

42elif at := sys.modules.get("annotated_types"): 

43 NaiveDatetime = Annotated[dt.datetime, at.Timezone(None)] 

44 AwareDatetime = Annotated[dt.datetime, at.Timezone(...)] 

45else: 

46 NaiveDatetime = AwareDatetime = dt.datetime 

47 

48DATENAMES = ("year", "month", "day") 

49TIMENAMES = ("hour", "minute", "second", "microsecond") 

50 

51_MICROSECOND = dt.timedelta(microseconds=1) 

52 

53 

54def _comparator_bound(condition): 

55 """Return ``(op, bound)`` for filter conditions like ``partial(op, bound)``, 

56 with a single positional argument to one of the five comparison operators.""" 

57 if ( 

58 isinstance(condition, partial) 

59 and len(condition.args) == 1 

60 and not condition.keywords 

61 and condition.func in (op.lt, op.le, op.eq, op.ge, op.gt) 

62 ): 

63 return condition.func, condition.args[0] 

64 return None 

65 

66 

67def _narrowed_bounds(func, arg, min_value, max_value, shift): 

68 """Narrow [min_value, max_value] to satisfy the condition ``func(arg, x)``. 

69 

70 ``shift(value, steps)`` moves value by that many of the smallest representable 

71 steps, raising OverflowError if the result would be unrepresentable. Returns 

72 the narrowed (min_value, max_value), or None if no values can satisfy the 

73 condition. 

74 """ 

75 if func in (op.lt, op.gt): 

76 try: 

77 arg = shift(arg, 1 if func is op.lt else -1) 

78 except OverflowError: # gt the maximum value, or lt the minimum 

79 return None 

80 lo, hi = { 

81 # We're talking about op(arg, x) - the reverse of our usual intuition! 

82 op.lt: (arg, max_value), # lambda x: arg < x 

83 op.le: (arg, max_value), # lambda x: arg <= x 

84 op.eq: (arg, arg), # lambda x: arg == x 

85 op.ge: (min_value, arg), # lambda x: arg >= x 

86 op.gt: (min_value, arg), # lambda x: arg > x 

87 }[func] 

88 lo = max(lo, min_value) 

89 hi = min(hi, max_value) 

90 if hi < lo: 

91 return None 

92 return lo, hi 

93 

94 

95def _timezones_kind(strat): 

96 """Classify the values a timezones= strategy can generate: "none" if only 

97 None, "aware" if only tzinfo instances, or "unknown" if we can't tell.""" 

98 strat = unwrap_strategies(strat) 

99 if isinstance(strat, SampledFromStrategy) and all( 

100 name == "filter" for name, *_ in strat._transformations 

101 ): 

102 kinds = { 

103 "none" if e is None else "aware" if isinstance(e, dt.tzinfo) else "unknown" 

104 for e in strat.elements 

105 } 

106 return kinds.pop() if len(kinds) == 1 else "unknown" 

107 if isinstance(strat, OneOfStrategy): 

108 kinds = {_timezones_kind(s) for s in strat.original_strategies} 

109 return kinds.pop() if len(kinds) == 1 else "unknown" 

110 return "unknown" 

111 

112 

113def is_pytz_timezone(tz): 

114 if not isinstance(tz, dt.tzinfo): 

115 return False 

116 module = type(tz).__module__ 

117 return module == "pytz" or module.startswith("pytz.") 

118 

119 

120def replace_tzinfo(value, timezone): 

121 if is_pytz_timezone(timezone): 

122 # Pytz timezones are a little complicated, and using the .replace method 

123 # can cause some weird issues, so we use their special "localize" instead. 

124 # 

125 # We use the fold attribute as a convenient boolean for is_dst, even though 

126 # they're semantically distinct. For ambiguous or imaginary hours, fold says 

127 # whether you should use the offset that applies before the gap (fold=0) or 

128 # the offset that applies after the gap (fold=1). is_dst says whether you 

129 # should choose the side that is "DST" or "STD" (STD->STD or DST->DST 

130 # transitions are unclear as you might expect). 

131 # 

132 # WARNING: this is INCORRECT for timezones with negative DST offsets such as 

133 # "Europe/Dublin", but it's unclear what we could do instead beyond 

134 # documenting the problem and recommending use of `dateutil` instead. 

135 return timezone.localize(value, is_dst=not value.fold) 

136 return value.replace(tzinfo=timezone) 

137 

138 

139def _instant(value): 

140 """A sort key ordering aware datetimes by the moment they refer to. 

141 

142 Unlike comparison of datetimes which share a tzinfo - which falls back to 

143 ignoring both the timezone and the fold attribute - this respects the fold, 

144 and unlike .astimezone() it cannot overflow near datetime.min/max. 

145 """ 

146 return value.replace(tzinfo=None) - dt.datetime.min - value.utcoffset() 

147 

148 

149def _ambiguous(value, tz): 

150 # Whether the naive value is inside a DST fold, i.e. is a wall time which 

151 # occurs twice in tz, so that its utcoffset depends on the fold attribute. 

152 return ( 

153 replace_tzinfo(value.replace(fold=0), tz).utcoffset() 

154 != replace_tzinfo(value.replace(fold=1), tz).utcoffset() 

155 ) 

156 

157 

158def datetime_does_not_exist(value): 

159 """This function tests whether the given datetime can be round-tripped to and 

160 from UTC. It is an exact inverse of (and very similar to) the dateutil method 

161 https://dateutil.readthedocs.io/en/stable/tz.html#dateutil.tz.datetime_exists 

162 """ 

163 # Naive datetimes cannot be imaginary, but we need this special case because 

164 # chaining .astimezone() ends with *the system local timezone*, not None. 

165 # See bug report in https://github.com/HypothesisWorks/hypothesis/issues/2662 

166 if value.tzinfo is None: 

167 return False 

168 try: 

169 # Does the naive portion of the datetime change when round-tripped to 

170 # UTC? If so, or if this overflows, we say that it does not exist. 

171 roundtrip = value.astimezone(dt.timezone.utc).astimezone(value.tzinfo) 

172 except OverflowError: 

173 # Overflows at datetime.min or datetime.max boundary condition. 

174 # Rejecting these is acceptable, because timezones are close to 

175 # meaningless before ~1900 and subject to a lot of change by 

176 # 9999, so it should be a very small fraction of possible values. 

177 return True 

178 

179 if ( 

180 value.tzinfo is not roundtrip.tzinfo 

181 and value.utcoffset() != roundtrip.utcoffset() 

182 ): 

183 # This only ever occurs during imaginary (i.e. nonexistent) datetimes, 

184 # and only for pytz timezones which do not follow PEP-495 semantics. 

185 # (may exclude a few other edge cases, but you should use zoneinfo anyway) 

186 return True 

187 

188 assert value.tzinfo is roundtrip.tzinfo, "so only the naive portions are compared" 

189 return value != roundtrip 

190 

191 

192def _num_days_in_month(year, month): 

193 """Branchless equivalent of ``monthrange(year, month)[1]`` for valid inputs. 

194 

195 Written using only arithmetic and (in)equality, with no branching or indexing. 

196 This avoids concretizing the input or adding more path constraints than necessary. 

197 """ 

198 leap = (year % 4 == 0) * (1 - (year % 100 == 0) * (year % 400 != 0)) 

199 is_feb = month == 2 

200 is_30_day = 1 - (month != 4) * (month != 6) * (month != 9) * (month != 11) 

201 return 31 - is_30_day - is_feb * (3 - leap) 

202 

203 

204def draw_capped_multipart( 

205 data, min_value, max_value, duration_names=DATENAMES + TIMENAMES 

206): 

207 assert isinstance(min_value, (dt.date, dt.time, dt.datetime)) 

208 assert type(min_value) == type(max_value) 

209 assert min_value <= max_value 

210 

211 # cap_{low, high} records whether every field drawn so far has equalled 

212 # ``min_value``'s / ``max_value``'s, i.e. whether that bound is still "active" and 

213 # constrains the next field. 

214 # 

215 # cap_{low, high} are conceptually booleans. We define them as integers and interpret 

216 # boolean operations on them as multiplication, so that we don't concretize or 

217 # branch under symbolic backends. See 

218 # https://github.com/HypothesisWorks/hypothesis/issues/4759. 

219 cap_low = 1 

220 cap_high = 1 

221 result = {} 

222 for name in duration_names: 

223 natural_low = getattr(dt.datetime.min, name) 

224 if name == "day": 

225 natural_high = _num_days_in_month(result["year"], result["month"]) 

226 else: 

227 natural_high = getattr(dt.datetime.max, name) 

228 # equivalent to: 

229 # low = min_value.<name> if cap_low else natural_low 

230 # high = max_value.<name> if cap_high else natural_high 

231 low = natural_low + cap_low * (getattr(min_value, name) - natural_low) 

232 high = natural_high + cap_high * (getattr(max_value, name) - natural_high) 

233 if name == "year": 

234 val = data.draw_integer(low, high, shrink_towards=2000) 

235 else: 

236 val = data.draw_integer(low, high) 

237 result[name] = val 

238 cap_low = cap_low * (val == low) 

239 cap_high = cap_high * (val == high) 

240 if hasattr(min_value, "fold"): 

241 # The `fold` attribute is ignored in comparison of naive datetimes. 

242 # In tz-aware datetimes it would require *very* invasive changes to 

243 # the logic above, and be very sensitive to the specific timezone 

244 # (at the cost of efficient shrinking and mutation), so at least for 

245 # now we stick with the status quo and generate it independently. 

246 result["fold"] = data.draw_integer(0, 1) 

247 return result 

248 

249 

250def _shift_datetime(value, steps): 

251 return value + steps * _MICROSECOND 

252 

253 

254class _UnrepresentableBound(Exception): 

255 """No wall time in the timezone lies within the strategy's bounds.""" 

256 

257 

258class DatetimeStrategy(SearchStrategy): 

259 def __init__(self, min_value, max_value, timezones_strat, allow_imaginary): 

260 super().__init__() 

261 assert isinstance(timezones_strat, SearchStrategy) 

262 assert isinstance(allow_imaginary, bool) 

263 self.aware = (min_value is not None and min_value.tzinfo is not None) or ( 

264 max_value is not None and max_value.tzinfo is not None 

265 ) 

266 if self.aware: 

267 for value in (min_value, max_value): 

268 assert value is None or ( 

269 isinstance(value, dt.datetime) and value.tzinfo is not None 

270 ) 

271 # The instants bounding this strategy, as _instant() sort keys. 

272 # UTC offsets are less than a day, so a None bound is replaced by 

273 # a key which lies outside the representable range. 

274 self.min_instant = ( 

275 dt.timedelta(days=-2) if min_value is None else _instant(min_value) 

276 ) 

277 self.max_instant = ( 

278 dt.datetime.max - dt.datetime.min + dt.timedelta(days=2) 

279 if max_value is None 

280 else _instant(max_value) 

281 ) 

282 assert self.min_instant <= self.max_instant 

283 else: 

284 for value in (min_value, max_value): 

285 assert isinstance(value, dt.datetime) 

286 assert value.tzinfo is None 

287 assert min_value <= max_value 

288 self.min_value = min_value 

289 self.max_value = max_value 

290 self.tz_strat = timezones_strat 

291 self.allow_imaginary = allow_imaginary 

292 

293 def do_draw(self, data): 

294 # We start by drawing a timezone, and an initial datetime. 

295 tz = data.draw(self.tz_strat) 

296 if self.aware: 

297 if not isinstance(tz, dt.tzinfo): 

298 raise InvalidArgument( 

299 f"Drew {tz!r} from the timezones strategy {self.tz_strat!r}, " 

300 "but with aware min_value/max_value bounds the timezones " 

301 "strategy must only generate tzinfo objects (not None)" 

302 ) 

303 result = self.draw_aware_datetime(data, tz) 

304 else: 

305 result = self.draw_naive_datetime_and_combine(data, tz) 

306 

307 # TODO: with some probability, systematically search for one of 

308 # - an imaginary time (if allowed), 

309 # - a time within 24hrs of a leap second (if there any are within bounds), 

310 # - other subtle, little-known, or nasty issues as described in 

311 # https://github.com/HypothesisWorks/hypothesis/issues/69 

312 

313 # If we happened to end up with a disallowed imaginary time, reject it. 

314 if (not self.allow_imaginary) and datetime_does_not_exist(result): 

315 data.mark_invalid(f"{result} does not exist (usually a DST transition)") 

316 return result 

317 

318 def in_bounds(self, value): 

319 return self.min_instant <= _instant(value) <= self.max_instant 

320 

321 def draw_aware_datetime(self, data, tz): 

322 try: 

323 window = self._wall_clock_window(tz) 

324 except _UnrepresentableBound as err: 

325 data.mark_invalid(str(err)) 

326 if window is None: 

327 # A large fraction of the wall times between bounds inside or close 

328 # to a DST fold would risk rejection below - and bounds inside the 

329 # same fold may even be in inverted wall-clock order, like 

330 # 01:59 EDT < 01:01 EST - so we recurse to draw in UTC, where wall 

331 # times are unambiguous and ordered, and convert. This is the 

332 # standard draw with the standard shrink order, except that 

333 # simplicity is judged on the UTC wall time rather than the local. 

334 value = self.draw_aware_datetime(data, dt.timezone.utc) 

335 try: 

336 return value.astimezone(tz) 

337 except OverflowError: 

338 data.mark_invalid(f"{value!r} is not representable in {tz!r}") 

339 result = draw_capped_multipart(data, *window) 

340 value = replace_tzinfo(dt.datetime(**result), timezone=tz) 

341 if not self.in_bounds(value): 

342 # An ambiguous wall time next to a bound, with the out-of-bounds fold. 

343 data.mark_invalid(f"{value!r} is outside the bounds") 

344 return value 

345 

346 def _wall_clock_window(self, tz): 

347 """The naive (min, max) wall-clock bounds for drawing in ``tz``, or 

348 None to draw in UTC and convert. A pure function of (bounds, tz), 

349 shared by generation and inversion; raises _UnrepresentableBound when 

350 no wall time in ``tz`` lies within the bounds.""" 

351 

352 def wall_clock(bound, extreme): 

353 if bound is None: 

354 return extreme 

355 try: 

356 return bound.astimezone(tz).replace(tzinfo=None) 

357 except OverflowError: 

358 # UTC offsets are less than a day, so an overflowing bound 

359 # must be within a day of datetime.min/max, converting to a 

360 # moment beyond them. If every wall time representable in tz 

361 # is on the in-bounds side, the bound is simply vacuous here; 

362 # otherwise nothing in tz is in bounds. 

363 near_min = bound.replace(tzinfo=None) - dt.datetime.min < dt.timedelta( 

364 days=2 

365 ) 

366 if near_min == (extreme is dt.datetime.min): 

367 return extreme 

368 raise _UnrepresentableBound( 

369 f"{bound!r} is not representable in {tz!r}" 

370 ) from None 

371 

372 min_local = wall_clock(self.min_value, dt.datetime.min) 

373 max_local = wall_clock(self.max_value, dt.datetime.max) 

374 if min_local > max_local or ( 

375 max_local - min_local <= dt.timedelta(days=1) 

376 and (_ambiguous(min_local, tz) or _ambiguous(max_local, tz)) 

377 ): 

378 return None 

379 return min_local, max_local 

380 

381 def draw_naive_datetime_and_combine(self, data, tz): 

382 result = draw_capped_multipart(data, self.min_value, self.max_value) 

383 try: 

384 return replace_tzinfo(dt.datetime(**result), timezone=tz) 

385 except (ValueError, OverflowError): 

386 data.mark_invalid( 

387 f"Failed to draw a datetime between {self.min_value!r} and " 

388 f"{self.max_value!r} with timezone from {self.tz_strat!r}." 

389 ) 

390 

391 def _invert(self, value: Any) -> tuple[ChoiceT, ...]: 

392 if self.aware: 

393 if type(value) is not dt.datetime or value.tzinfo is None: 

394 raise CannotInvert(f"{value!r} is not an aware datetime") 

395 try: 

396 in_bounds = self.in_bounds(value) 

397 imaginary = datetime_does_not_exist(value) 

398 except Exception: 

399 raise CannotInvert( 

400 f"could not locate {value!r} relative to {self!r}" 

401 ) from None 

402 if not in_bounds: 

403 raise CannotInvert(f"{value!r} outside the instant bounds of {self!r}") 

404 if imaginary and not self.allow_imaginary: 

405 raise CannotInvert( 

406 f"{value!r} is an imaginary datetime, but allow_imaginary=False" 

407 ) 

408 return ( 

409 *self.tz_strat._invert(value.tzinfo), 

410 *self._invert_aware_fields(value, value.tzinfo, imaginary=imaginary), 

411 ) 

412 if type(value) is not dt.datetime: 

413 raise CannotInvert(f"{value!r} is not a datetime") 

414 naive = value.replace(tzinfo=None) 

415 if not (self.min_value <= naive <= self.max_value): 

416 raise CannotInvert( 

417 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]" 

418 ) 

419 if not self.allow_imaginary and datetime_does_not_exist(value): 

420 raise CannotInvert( 

421 f"{value!r} is an imaginary datetime, but allow_imaginary=False" 

422 ) 

423 # do_draw draws the timezone first, then the naive parts (with fold 

424 # drawn last, since it is ignored in datetime comparisons). 

425 return ( 

426 *self.tz_strat._invert(value.tzinfo), 

427 value.year, 

428 value.month, 

429 value.day, 

430 value.hour, 

431 value.minute, 

432 value.second, 

433 value.microsecond, 

434 value.fold, 

435 ) 

436 

437 def _invert_aware_fields(self, value, tz, *, imaginary): 

438 # The multipart fields draw_aware_datetime would consume to produce 

439 # ``value``, expressed in the frame it would draw in for ``tz``. 

440 try: 

441 window = self._wall_clock_window(tz) 

442 except _UnrepresentableBound as err: 

443 raise CannotInvert(str(err)) from None 

444 if window is None: 

445 # do_draw would draw in UTC and convert; converting an imaginary 

446 # wall time to UTC and back does not round-trip, so be conservative. 

447 if imaginary: 

448 raise CannotInvert( 

449 f"the UTC drawing frame cannot reproduce imaginary {value!r}" 

450 ) 

451 try: 

452 utc_value = value.astimezone(dt.timezone.utc) 

453 except OverflowError: 

454 raise CannotInvert(f"{value!r} is not representable in UTC") from None 

455 return self._invert_aware_fields( 

456 utc_value, dt.timezone.utc, imaginary=False 

457 ) 

458 min_local, max_local = window 

459 naive = value.replace(tzinfo=None) 

460 if not (min_local <= naive <= max_local): 

461 raise CannotInvert( 

462 f"{value!r} is outside the wall-clock window of {self!r} in {tz!r}" 

463 ) 

464 return ( 

465 naive.year, 

466 naive.month, 

467 naive.day, 

468 naive.hour, 

469 naive.minute, 

470 naive.second, 

471 naive.microsecond, 

472 value.fold, 

473 ) 

474 

475 def filter(self, condition): 

476 if (parsed := _comparator_bound(condition)) is not None and isinstance( 

477 arg := parsed[1], dt.datetime 

478 ): 

479 func = parsed[0] 

480 try: 

481 bound_aware = arg.utcoffset() is not None 

482 except Exception: 

483 # A tzinfo whose utcoffset() raises; comparing against this 

484 # bound will raise the same error at draw time. 

485 return super().filter(condition) 

486 if not bound_aware: 

487 # The bound compares as naive (either no tzinfo, or a tzinfo 

488 # without a UTC offset), so we can only rewrite it into the 

489 # naive wall-clock bounds if it really is naive and every 

490 # generated value is too. 

491 if ( 

492 arg.tzinfo is None 

493 and not self.aware 

494 and _timezones_kind(self.tz_strat) == "none" 

495 ): 

496 bounds = _narrowed_bounds( 

497 func, arg, self.min_value, self.max_value, _shift_datetime 

498 ) 

499 if bounds is None: 

500 return nothing() 

501 if bounds == (self.min_value, self.max_value): 

502 return self 

503 return datetimes( 

504 *bounds, 

505 timezones=self.tz_strat, 

506 allow_imaginary=self.allow_imaginary, 

507 ) 

508 else: 

509 # An aware bound constrains the instant of generated values, 

510 # so we narrow our aware bounds to the closed interval of 

511 # satisfying instants - retaining strict predicates below, 

512 # which then reject at most the boundary instant per timezone. 

513 # We compare bounds by their _instant() key, since comparison 

514 # of datetimes which share a tzinfo would fall back to 

515 # wall-clock order, ignoring the fold. 

516 if self.aware: 

517 min_value, max_value = self.min_value, self.max_value 

518 elif (self.min_value, self.max_value) == ( 

519 dt.datetime.min, 

520 dt.datetime.max, 

521 ) and _timezones_kind(self.tz_strat) != "none": 

522 # An unbounded naive-mode strategy whose values are all 

523 # aware: promote to aware mode, bounded by the filter. 

524 min_value = max_value = None 

525 else: 

526 return super().filter(condition) 

527 key = _instant(arg) 

528 if func in (op.lt, op.le, op.eq) and ( 

529 min_value is None or _instant(min_value) < key 

530 ): 

531 min_value = arg 

532 if func in (op.gt, op.ge, op.eq) and ( 

533 max_value is None or key < _instant(max_value) 

534 ): 

535 max_value = arg 

536 if min_value is not None and max_value is not None: 

537 lo, hi = _instant(min_value), _instant(max_value) 

538 if hi < lo or (func in (op.lt, op.gt) and lo == hi == key): 

539 # Only aware-mode strategies can reach this, and they 

540 # generate only aware values (or raise for a bad 

541 # timezones strategy), so this is provably empty. 

542 return nothing() 

543 if min_value is self.min_value and max_value is self.max_value: 

544 result = self 

545 else: 

546 result = DatetimeStrategy( 

547 min_value, max_value, self.tz_strat, self.allow_imaginary 

548 ) 

549 if func in (op.lt, op.gt): 

550 return FilteredStrategy( 

551 result, (condition,), (current_filter_call_site(),) 

552 ) 

553 return result 

554 return super().filter(condition) 

555 

556 

557@overload 

558def datetimes( 

559 min_value: NaiveDatetime | None = None, 

560 max_value: NaiveDatetime | None = None, 

561 *, 

562 timezones: SearchStrategy[None] | None = None, 

563) -> SearchStrategy[NaiveDatetime]: ... 

564 

565 

566@overload 

567def datetimes( 

568 min_value: dt.datetime | None = None, 

569 max_value: dt.datetime | None = None, 

570 *, 

571 timezones: SearchStrategy[dt.tzinfo], 

572 allow_imaginary: bool = True, 

573) -> SearchStrategy[AwareDatetime]: ... 

574 

575 

576@overload 

577def datetimes( 

578 min_value: None = None, 

579 max_value: None = None, 

580 *, 

581 timezones: SearchStrategy[dt.tzinfo | None], 

582 allow_imaginary: bool = True, 

583) -> SearchStrategy[dt.datetime]: ... 

584 

585 

586@defines_strategy(force_reusable_values=True) 

587def datetimes( 

588 min_value: dt.datetime | None = None, 

589 max_value: dt.datetime | None = None, 

590 *, 

591 timezones: SearchStrategy[dt.tzinfo | None] | None = None, 

592 allow_imaginary: bool = True, 

593) -> SearchStrategy[dt.datetime]: 

594 """datetimes(min_value=None, max_value=None, *, timezones=None, allow_imaginary=True) 

595 

596 A strategy for generating datetimes, which may be timezone-aware. 

597 

598 If ``min_value`` and ``max_value`` are naive datetimes, or omitted, this 

599 strategy works by drawing a naive datetime between them - defaulting to 

600 ``datetime.min`` and ``datetime.max`` respectively - and then attaching 

601 a timezone drawn from ``timezones``, which defaults to 

602 :func:`~hypothesis.strategies.none`. 

603 

604 If instead both bounds are timezone-aware, they are treated as moments in 

605 time, and ``timezones`` defaults to :func:`~hypothesis.strategies.timezones`. 

606 Each generated datetime is aware, in a timezone drawn from ``timezones`` - 

607 which must not generate ``None`` - and lies between the two moments. 

608 Passing one aware and one naive bound is an error. 

609 

610 ``timezones`` must be a strategy that generates either ``None``, for naive 

611 datetimes, or :class:`~python:datetime.tzinfo` objects for 'aware' datetimes. 

612 You can construct your own, though we recommend using one of these built-in 

613 strategies: 

614 

615 * with the standard library: :func:`hypothesis.strategies.timezones`; 

616 * with :pypi:`dateutil <python-dateutil>`: 

617 :func:`hypothesis.extra.dateutil.timezones`; or 

618 * with :pypi:`pytz`: :func:`hypothesis.extra.pytz.timezones`. 

619 

620 You may pass ``allow_imaginary=False`` to filter out "imaginary" datetimes 

621 which did not (or will not) occur due to daylight savings, leap seconds, 

622 timezone and calendar adjustments, etc. Imaginary datetimes are allowed 

623 by default, because malformed timestamps are a common source of bugs. 

624 

625 .. note:: 

626 

627 Arithmetic and comparisons on timezone-aware datetimes can be very 

628 surprising around daylight-savings changes. See `this CPython issue 

629 <https://github.com/python/cpython/issues/116035>`__ for details 

630 and discussion. 

631 

632 Examples from this strategy shrink towards midnight on January 1st 2000, 

633 local time. 

634 """ 

635 check_type(bool, allow_imaginary, "allow_imaginary") 

636 if min_value is not None: 

637 check_type(dt.datetime, min_value, "min_value") 

638 if max_value is not None: 

639 check_type(dt.datetime, max_value, "max_value") 

640 if timezones is not None and not isinstance(timezones, SearchStrategy): 

641 raise InvalidArgument( 

642 f"{timezones=} must be a SearchStrategy that can " 

643 "provide tzinfo for datetimes (either None or dt.tzinfo objects)" 

644 ) 

645 if (min_value is None or min_value.tzinfo is None) and ( 

646 max_value is None or max_value.tzinfo is None 

647 ): 

648 min_value = dt.datetime.min if min_value is None else min_value 

649 max_value = dt.datetime.max if max_value is None else max_value 

650 if timezones is None: 

651 timezones = none() 

652 check_valid_interval(min_value, max_value, "min_value", "max_value") 

653 else: 

654 # Aware bounds describe moments in time; we check both are aware here, 

655 # and then at draw time convert them to the drawn timezone and proceed 

656 # as in the naive case. 

657 for name, value in [("min_value", min_value), ("max_value", max_value)]: 

658 if value is not None and value.tzinfo is None: 

659 raise InvalidArgument( 

660 f"{name}={value!r} is naive, but the other bound is " 

661 "timezone-aware; the bounds must be both naive or both aware" 

662 ) 

663 if timezones is None: 

664 timezones = _timezones() 

665 # Compare explicitly as moments in time: comparison of datetimes which 

666 # share a tzinfo falls back to wall-clock order, ignoring the fold. 

667 if ( 

668 min_value is not None 

669 and max_value is not None 

670 and _instant(max_value) < _instant(min_value) 

671 ): 

672 raise InvalidArgument( 

673 f"Cannot have {max_value=} < {min_value=}, comparing as " 

674 "moments in time" 

675 ) 

676 return DatetimeStrategy(min_value, max_value, timezones, allow_imaginary) 

677 

678 

679_ARBITRARY_DATE = dt.date(2000, 1, 1) 

680 

681 

682def _shift_time(value, steps): 

683 # dt.time supports no arithmetic, so we go via a datetime on a fixed day 

684 # and treat crossing midnight as overflowing the representable range. 

685 shifted = dt.datetime.combine(_ARBITRARY_DATE, value) + steps * _MICROSECOND 

686 if shifted.date() != _ARBITRARY_DATE: 

687 raise OverflowError 

688 return shifted.time() 

689 

690 

691class TimeStrategy(SearchStrategy): 

692 def __init__(self, min_value, max_value, timezones_strat): 

693 super().__init__() 

694 self.min_value = min_value 

695 self.max_value = max_value 

696 self.tz_strat = timezones_strat 

697 

698 def do_draw(self, data): 

699 result = draw_capped_multipart(data, self.min_value, self.max_value, TIMENAMES) 

700 tz = data.draw(self.tz_strat) 

701 return dt.time(**result, tzinfo=tz) 

702 

703 def _invert(self, value: Any) -> tuple[ChoiceT, ...]: 

704 if type(value) is not dt.time: 

705 raise CannotInvert(f"{value!r} is not a time") 

706 naive = value.replace(tzinfo=None) 

707 if not (self.min_value <= naive <= self.max_value): 

708 raise CannotInvert( 

709 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]" 

710 ) 

711 # unlike DatetimeStrategy, do_draw draws the naive parts first - with 

712 # fold at the end, via draw_capped_multipart - and the timezone last. 

713 return ( 

714 value.hour, 

715 value.minute, 

716 value.second, 

717 value.microsecond, 

718 value.fold, 

719 *self.tz_strat._invert(value.tzinfo), 

720 ) 

721 

722 def filter(self, condition): 

723 # We only rewrite naive times: ordering aware times works in terms of 

724 # utcoffset(), which is None for e.g. ZoneInfo tzinfos on a time - so 

725 # such values compare as naive anyway, and rewriting fixed-offset aware 

726 # times isn't worth the extra complexity. 

727 if ( 

728 (parsed := _comparator_bound(condition)) is not None 

729 and isinstance(arg := parsed[1], dt.time) 

730 and arg.tzinfo is None 

731 and _timezones_kind(self.tz_strat) == "none" 

732 ): 

733 bounds = _narrowed_bounds( 

734 parsed[0], arg, self.min_value, self.max_value, _shift_time 

735 ) 

736 if bounds is None: 

737 return nothing() 

738 if bounds == (self.min_value, self.max_value): 

739 return self 

740 return times(*bounds, timezones=self.tz_strat) 

741 return super().filter(condition) 

742 

743 

744@defines_strategy(force_reusable_values=True) 

745def times( 

746 min_value: dt.time = dt.time.min, 

747 max_value: dt.time = dt.time.max, 

748 *, 

749 timezones: SearchStrategy[dt.tzinfo | None] = none(), 

750) -> SearchStrategy[dt.time]: 

751 """times(min_value=datetime.time.min, max_value=datetime.time.max, *, timezones=none()) 

752 

753 A strategy for times between ``min_value`` and ``max_value``. 

754 

755 The ``timezones`` argument is handled as for :py:func:`datetimes`. 

756 

757 Examples from this strategy shrink towards midnight, with the timezone 

758 component shrinking as for the strategy that provided it. 

759 """ 

760 check_type(dt.time, min_value, "min_value") 

761 check_type(dt.time, max_value, "max_value") 

762 if min_value.tzinfo is not None: 

763 raise InvalidArgument(f"{min_value=} must not have tzinfo") 

764 if max_value.tzinfo is not None: 

765 raise InvalidArgument(f"{max_value=} must not have tzinfo") 

766 check_valid_interval(min_value, max_value, "min_value", "max_value") 

767 return TimeStrategy(min_value, max_value, timezones) 

768 

769 

770def _shift_date(value, steps): 

771 return value + steps * dt.timedelta(days=1) 

772 

773 

774class DateStrategy(SearchStrategy): 

775 def __init__(self, min_value, max_value): 

776 super().__init__() 

777 assert isinstance(min_value, dt.date) 

778 assert isinstance(max_value, dt.date) 

779 assert min_value < max_value 

780 self.min_value = min_value 

781 self.max_value = max_value 

782 

783 def do_draw(self, data): 

784 return dt.date( 

785 **draw_capped_multipart(data, self.min_value, self.max_value, DATENAMES) 

786 ) 

787 

788 def _invert(self, value: Any) -> tuple[ChoiceT, ...]: 

789 if type(value) is not dt.date: 

790 raise CannotInvert(f"{value!r} is not a date") 

791 if not (self.min_value <= value <= self.max_value): 

792 raise CannotInvert( 

793 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]" 

794 ) 

795 return (value.year, value.month, value.day) 

796 

797 def filter(self, condition): 

798 if ( 

799 (parsed := _comparator_bound(condition)) is not None 

800 # datetime is a date subclass, but not comparable with dates 

801 and isinstance(arg := parsed[1], dt.date) 

802 and not isinstance(arg, dt.datetime) 

803 ): 

804 bounds = _narrowed_bounds( 

805 parsed[0], arg, self.min_value, self.max_value, _shift_date 

806 ) 

807 if bounds is None: 

808 return nothing() 

809 if bounds == (self.min_value, self.max_value): 

810 return self 

811 return dates(*bounds) 

812 

813 return super().filter(condition) 

814 

815 

816@defines_strategy(force_reusable_values=True) 

817def dates( 

818 min_value: dt.date = dt.date.min, max_value: dt.date = dt.date.max 

819) -> SearchStrategy[dt.date]: 

820 """dates(min_value=datetime.date.min, max_value=datetime.date.max) 

821 

822 A strategy for dates between ``min_value`` and ``max_value``. 

823 

824 Examples from this strategy shrink towards January 1st 2000. 

825 """ 

826 check_type(dt.date, min_value, "min_value") 

827 check_type(dt.date, max_value, "max_value") 

828 # datetime is a subclass of date, so check_type() accepts it - but a datetime 

829 # bound is almost certainly a mistake, and breaks our drawing logic downstream. 

830 if isinstance(min_value, dt.datetime): 

831 raise InvalidArgument(f"{min_value=} is a datetime, but expected a date") 

832 if isinstance(max_value, dt.datetime): 

833 raise InvalidArgument(f"{max_value=} is a datetime, but expected a date") 

834 check_valid_interval(min_value, max_value, "min_value", "max_value") 

835 if min_value == max_value: 

836 return just(min_value) 

837 return DateStrategy(min_value, max_value) 

838 

839 

840class TimedeltaStrategy(SearchStrategy): 

841 def __init__(self, min_value, max_value): 

842 super().__init__() 

843 assert isinstance(min_value, dt.timedelta) 

844 assert isinstance(max_value, dt.timedelta) 

845 assert min_value < max_value 

846 self.min_value = min_value 

847 self.max_value = max_value 

848 

849 def do_draw(self, data): 

850 result = {} 

851 low_bound = True 

852 high_bound = True 

853 for name in ("days", "seconds", "microseconds"): 

854 low = getattr(self.min_value if low_bound else dt.timedelta.min, name) 

855 high = getattr(self.max_value if high_bound else dt.timedelta.max, name) 

856 val = data.draw_integer(low, high) 

857 result[name] = val 

858 low_bound = low_bound and val == low 

859 high_bound = high_bound and val == high 

860 return dt.timedelta(**result) 

861 

862 def _invert(self, value: Any) -> tuple[ChoiceT, ...]: 

863 if type(value) is not dt.timedelta: 

864 raise CannotInvert(f"{value!r} is not a timedelta") 

865 if not (self.min_value <= value <= self.max_value): 

866 raise CannotInvert( 

867 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]" 

868 ) 

869 return (value.days, value.seconds, value.microseconds) 

870 

871 

872@defines_strategy(force_reusable_values=True) 

873def timedeltas( 

874 min_value: dt.timedelta = dt.timedelta.min, 

875 max_value: dt.timedelta = dt.timedelta.max, 

876) -> SearchStrategy[dt.timedelta]: 

877 """timedeltas(min_value=datetime.timedelta.min, max_value=datetime.timedelta.max) 

878 

879 A strategy for timedeltas between ``min_value`` and ``max_value``. 

880 

881 Examples from this strategy shrink towards zero. 

882 """ 

883 check_type(dt.timedelta, min_value, "min_value") 

884 check_type(dt.timedelta, max_value, "max_value") 

885 check_valid_interval(min_value, max_value, "min_value", "max_value") 

886 if min_value == max_value: 

887 return just(min_value) 

888 return TimedeltaStrategy(min_value=min_value, max_value=max_value) 

889 

890 

891@cache 

892def _valid_key_cacheable(tzpath, key): 

893 assert isinstance(tzpath, tuple) # zoneinfo changed, better update this function! 

894 for root in tzpath: 

895 if Path(root).joinpath(key).exists(): # pragma: no branch 

896 # No branch because most systems only have one TZPATH component. 

897 return True 

898 else: 

899 # Taken for names which are known to zoneinfo but not present on the 

900 # filesystem, e.g. with the tzdata package installed. 

901 *package_loc, resource_name = key.split("/") 

902 package = "tzdata.zoneinfo." + ".".join(package_loc) 

903 try: 

904 return (resources.files(package) / resource_name).exists() 

905 except ModuleNotFoundError: 

906 return False 

907 

908 

909def _timezone_key_strategies(*, allow_prefix): 

910 """SampledFromStrategy branches for IANA keys: plain keys first, then one 

911 branch per allowed prefix, with the prefix applied as a sampled_from 

912 transformation. one_of's branch selector is therefore the prefix choice, 

913 which shrinks towards - and can be re-encoded as - an unprefixed key.""" 

914 with warnings.catch_warnings(): 

915 try: 

916 warnings.simplefilter("ignore", EncodingWarning) 

917 except NameError: # pragma: no cover 

918 pass 

919 # On Python 3.12 (and others?), `available_timezones()` opens files 

920 # without specifying an encoding - which our selftests make an error. 

921 available_timezones = ("UTC", *sorted(zoneinfo.available_timezones())) 

922 

923 # TODO: filter out alias and deprecated names if disallowed 

924 

925 def valid_key(key): 

926 return key == "UTC" or _valid_key_cacheable(zoneinfo.TZPATH, key) 

927 

928 # TODO: work out how to place a higher priority on "weird" timezones 

929 # For details see https://github.com/HypothesisWorks/hypothesis/issues/2414 

930 plain = [key for key in available_timezones if valid_key(key)] 

931 branches = [sampled_from(plain)] 

932 if allow_prefix: 

933 for prefix in ("posix", "right"): 

934 keys = [key for key in plain if valid_key(f"{prefix}/{key}")] 

935 if keys: 

936 branches.append(sampled_from(keys).map(f"{prefix}/{{}}".format)) 

937 return branches 

938 

939 

940@defines_strategy(force_reusable_values=True) 

941def timezone_keys( 

942 *, 

943 # allow_alias: bool = True, 

944 # allow_deprecated: bool = True, 

945 allow_prefix: bool = True, 

946) -> SearchStrategy[str]: 

947 """A strategy for :wikipedia:`IANA timezone names <List_of_tz_database_time_zones>`. 

948 

949 As well as timezone names like ``"UTC"``, ``"Australia/Sydney"``, or 

950 ``"America/New_York"``, this strategy can generate: 

951 

952 - Aliases such as ``"Antarctica/McMurdo"``, which links to ``"Pacific/Auckland"``. 

953 - Deprecated names such as ``"Antarctica/South_Pole"``, which *also* links to 

954 ``"Pacific/Auckland"``. Note that most but 

955 not all deprecated timezone names are also aliases. 

956 - Timezone names with the ``"posix/"`` or ``"right/"`` prefixes, unless 

957 ``allow_prefix=False``. 

958 

959 These strings are provided separately from Tzinfo objects - such as ZoneInfo 

960 instances from the timezones() strategy - to facilitate testing of timezone 

961 logic without needing workarounds to access non-canonical names. 

962 

963 .. note:: 

964 

965 `The tzdata package is required on Windows 

966 <https://docs.python.org/3/library/zoneinfo.html#data-sources>`__. 

967 ``pip install hypothesis[zoneinfo]`` installs it, if and only if needed. 

968 

969 On Windows, you may need to access IANA timezone data via the :pypi:`tzdata` 

970 package. For non-IANA timezones, such as Windows-native names or GNU TZ 

971 strings, we recommend using :func:`~hypothesis.strategies.sampled_from` with 

972 the :pypi:`dateutil <python-dateutil>` package, e.g. 

973 :meth:`dateutil:dateutil.tz.tzwin.list`. 

974 """ 

975 # check_type(bool, allow_alias, "allow_alias") 

976 # check_type(bool, allow_deprecated, "allow_deprecated") 

977 check_type(bool, allow_prefix, "allow_prefix") 

978 return one_of(_timezone_key_strategies(allow_prefix=allow_prefix)) 

979 

980 

981@defines_strategy(force_reusable_values=True) 

982def timezones(*, no_cache: bool = False) -> SearchStrategy["zoneinfo.ZoneInfo"]: 

983 """A strategy for :class:`python:zoneinfo.ZoneInfo` objects. 

984 

985 If ``no_cache=True``, the generated instances are constructed using 

986 :meth:`ZoneInfo.no_cache <python:zoneinfo.ZoneInfo.no_cache>` instead 

987 of the usual constructor. This may change the semantics of your datetimes 

988 in surprising ways, so only use it if you know that you need to! 

989 

990 .. note:: 

991 

992 `The tzdata package is required on Windows 

993 <https://docs.python.org/3/library/zoneinfo.html#data-sources>`__. 

994 ``pip install hypothesis[zoneinfo]`` installs it, if and only if needed. 

995 """ 

996 check_type(bool, no_cache, "no_cache") 

997 ctor = zoneinfo.ZoneInfo.no_cache if no_cache else zoneinfo.ZoneInfo 

998 # Mapping each sampled_from branch folds ctor into its transformations, 

999 # keeping the whole strategy re-encodable (unlike mapping the one_of). 

1000 return one_of( 

1001 [keys.map(ctor) for keys in _timezone_key_strategies(allow_prefix=True)] 

1002 ) 

1003 

1004 

1005# In datetimes() above, the ``timezones`` argument shadows this module's 

1006# timezones() strategy, so we refer to it by this alias instead. 

1007_timezones = timezones