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

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

354 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, overload 

20 

21from hypothesis.errors import InvalidArgument 

22from hypothesis.internal.validation import check_type, check_valid_interval 

23from hypothesis.strategies._internal.core import sampled_from 

24from hypothesis.strategies._internal.lazy import unwrap_strategies 

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

26from hypothesis.strategies._internal.strategies import ( 

27 FilteredStrategy, 

28 OneOfStrategy, 

29 SampledFromStrategy, 

30 SearchStrategy, 

31) 

32from hypothesis.strategies._internal.utils import defines_strategy 

33 

34if TYPE_CHECKING: 

35 from annotated_types import Timezone 

36 

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

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

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

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

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

42else: 

43 NaiveDatetime = AwareDatetime = dt.datetime 

44 

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

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

47 

48_MICROSECOND = dt.timedelta(microseconds=1) 

49 

50 

51def _comparator_bound(condition): 

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

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

54 if ( 

55 isinstance(condition, partial) 

56 and len(condition.args) == 1 

57 and not condition.keywords 

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

59 ): 

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

61 return None 

62 

63 

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

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

66 

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

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

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

70 condition. 

71 """ 

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

73 try: 

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

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

76 return None 

77 lo, hi = { 

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

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

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

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

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

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

84 }[func] 

85 lo = max(lo, min_value) 

86 hi = min(hi, max_value) 

87 if hi < lo: 

88 return None 

89 return lo, hi 

90 

91 

92def _timezones_kind(strat): 

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

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

95 strat = unwrap_strategies(strat) 

96 if isinstance(strat, SampledFromStrategy) and all( 

97 name == "filter" for name, _ in strat._transformations 

98 ): 

99 kinds = { 

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

101 for e in strat.elements 

102 } 

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

104 if isinstance(strat, OneOfStrategy): 

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

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

107 return "unknown" 

108 

109 

110def is_pytz_timezone(tz): 

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

112 return False 

113 module = type(tz).__module__ 

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

115 

116 

117def replace_tzinfo(value, timezone): 

118 if is_pytz_timezone(timezone): 

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

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

121 # 

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

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

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

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

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

127 # transitions are unclear as you might expect). 

128 # 

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

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

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

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

133 return value.replace(tzinfo=timezone) 

134 

135 

136def _instant(value): 

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

138 

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

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

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

142 """ 

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

144 

145 

146def _ambiguous(value, tz): 

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

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

149 return ( 

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

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

152 ) 

153 

154 

155def datetime_does_not_exist(value): 

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

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

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

159 """ 

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

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

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

163 if value.tzinfo is None: 

164 return False 

165 try: 

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

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

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

169 except OverflowError: 

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

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

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

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

174 return True 

175 

176 if ( 

177 value.tzinfo is not roundtrip.tzinfo 

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

179 ): 

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

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

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

183 return True 

184 

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

186 return value != roundtrip 

187 

188 

189def _num_days_in_month(year, month): 

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

191 

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

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

194 """ 

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

196 is_feb = month == 2 

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

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

199 

200 

201def draw_capped_multipart( 

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

203): 

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

205 assert type(min_value) == type(max_value) 

206 assert min_value <= max_value 

207 

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

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

210 # constrains the next field. 

211 # 

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

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

214 # branch under symbolic backends. See 

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

216 cap_low = 1 

217 cap_high = 1 

218 result = {} 

219 for name in duration_names: 

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

221 if name == "day": 

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

223 else: 

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

225 # equivalent to: 

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

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

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

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

230 if name == "year": 

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

232 else: 

233 val = data.draw_integer(low, high) 

234 result[name] = val 

235 cap_low = cap_low * (val == low) 

236 cap_high = cap_high * (val == high) 

237 if hasattr(min_value, "fold"): 

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

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

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

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

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

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

244 return result 

245 

246 

247def _shift_datetime(value, steps): 

248 return value + steps * _MICROSECOND 

249 

250 

251class DatetimeStrategy(SearchStrategy): 

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

253 super().__init__() 

254 assert isinstance(timezones_strat, SearchStrategy) 

255 assert isinstance(allow_imaginary, bool) 

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

257 max_value is not None and max_value.tzinfo is not None 

258 ) 

259 if self.aware: 

260 for value in (min_value, max_value): 

261 assert value is None or ( 

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

263 ) 

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

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

266 # a key which lies outside the representable range. 

267 self.min_instant = ( 

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

269 ) 

270 self.max_instant = ( 

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

272 if max_value is None 

273 else _instant(max_value) 

274 ) 

275 assert self.min_instant <= self.max_instant 

276 else: 

277 for value in (min_value, max_value): 

278 assert isinstance(value, dt.datetime) 

279 assert value.tzinfo is None 

280 assert min_value <= max_value 

281 self.min_value = min_value 

282 self.max_value = max_value 

283 self.tz_strat = timezones_strat 

284 self.allow_imaginary = allow_imaginary 

285 

286 def do_draw(self, data): 

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

288 tz = data.draw(self.tz_strat) 

289 if self.aware: 

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

291 raise InvalidArgument( 

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

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

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

295 ) 

296 result = self.draw_aware_datetime(data, tz) 

297 else: 

298 result = self.draw_naive_datetime_and_combine(data, tz) 

299 

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

301 # - an imaginary time (if allowed), 

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

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

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

305 

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

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

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

309 return result 

310 

311 def in_bounds(self, value): 

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

313 

314 def draw_aware_datetime(self, data, tz): 

315 def wall_clock(bound, extreme): 

316 if bound is None: 

317 return extreme 

318 try: 

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

320 except OverflowError: 

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

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

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

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

325 # otherwise nothing in tz is in bounds. 

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

327 days=2 

328 ) 

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

330 return extreme 

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

332 

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

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

335 if min_local > max_local or ( 

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

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

338 ): 

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

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

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

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

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

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

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

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

347 try: 

348 return value.astimezone(tz) 

349 except OverflowError: 

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

351 result = draw_capped_multipart(data, min_local, max_local) 

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

353 if not self.in_bounds(value): 

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

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

356 return value 

357 

358 def draw_naive_datetime_and_combine(self, data, tz): 

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

360 try: 

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

362 except (ValueError, OverflowError): 

363 data.mark_invalid( 

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

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

366 ) 

367 

368 def filter(self, condition): 

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

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

371 ): 

372 func = parsed[0] 

373 try: 

374 bound_aware = arg.utcoffset() is not None 

375 except Exception: 

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

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

378 return super().filter(condition) 

379 if not bound_aware: 

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

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

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

383 # generated value is too. 

384 if ( 

385 arg.tzinfo is None 

386 and not self.aware 

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

388 ): 

389 bounds = _narrowed_bounds( 

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

391 ) 

392 if bounds is None: 

393 return nothing() 

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

395 return self 

396 return datetimes( 

397 *bounds, 

398 timezones=self.tz_strat, 

399 allow_imaginary=self.allow_imaginary, 

400 ) 

401 else: 

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

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

404 # satisfying instants - retaining strict predicates below, 

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

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

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

408 # wall-clock order, ignoring the fold. 

409 if self.aware: 

410 min_value, max_value = self.min_value, self.max_value 

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

412 dt.datetime.min, 

413 dt.datetime.max, 

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

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

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

417 min_value = max_value = None 

418 else: 

419 return super().filter(condition) 

420 key = _instant(arg) 

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

422 min_value is None or _instant(min_value) < key 

423 ): 

424 min_value = arg 

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

426 max_value is None or key < _instant(max_value) 

427 ): 

428 max_value = arg 

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

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

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

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

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

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

435 return nothing() 

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

437 result = self 

438 else: 

439 result = DatetimeStrategy( 

440 min_value, max_value, self.tz_strat, self.allow_imaginary 

441 ) 

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

443 return FilteredStrategy(result, (condition,)) 

444 return result 

445 return super().filter(condition) 

446 

447 

448@overload 

449def datetimes( 

450 min_value: NaiveDatetime | None = None, 

451 max_value: NaiveDatetime | None = None, 

452 *, 

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

454) -> SearchStrategy[NaiveDatetime]: # pragma: no cover 

455 ... 

456 

457 

458@overload 

459def datetimes( 

460 min_value: dt.datetime | None = None, 

461 max_value: dt.datetime | None = None, 

462 *, 

463 timezones: SearchStrategy[dt.tzinfo], 

464 allow_imaginary: bool = True, 

465) -> SearchStrategy[AwareDatetime]: # pragma: no cover 

466 ... 

467 

468 

469@overload 

470def datetimes( 

471 min_value: None = None, 

472 max_value: None = None, 

473 *, 

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

475 allow_imaginary: bool = True, 

476) -> SearchStrategy[dt.datetime]: # pragma: no cover 

477 ... 

478 

479 

480@defines_strategy(force_reusable_values=True) 

481def datetimes( 

482 min_value: dt.datetime | None = None, 

483 max_value: dt.datetime | None = None, 

484 *, 

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

486 allow_imaginary: bool = True, 

487) -> SearchStrategy[dt.datetime]: 

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

489 

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

491 

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

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

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

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

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

497 

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

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

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

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

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

503 

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

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

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

507 strategies: 

508 

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

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

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

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

513 

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

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

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

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

518 

519 .. note:: 

520 

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

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

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

524 and discussion. 

525 

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

527 local time. 

528 """ 

529 check_type(bool, allow_imaginary, "allow_imaginary") 

530 if min_value is not None: 

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

532 if max_value is not None: 

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

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

535 raise InvalidArgument( 

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

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

538 ) 

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

540 max_value is None or max_value.tzinfo is None 

541 ): 

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

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

544 if timezones is None: 

545 timezones = none() 

546 check_valid_interval(min_value, max_value, "min_value", "max_value") 

547 else: 

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

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

550 # as in the naive case. 

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

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

553 raise InvalidArgument( 

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

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

556 ) 

557 if timezones is None: 

558 timezones = _timezones() 

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

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

561 if ( 

562 min_value is not None 

563 and max_value is not None 

564 and _instant(max_value) < _instant(min_value) 

565 ): 

566 raise InvalidArgument( 

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

568 "moments in time" 

569 ) 

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

571 

572 

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

574 

575 

576def _shift_time(value, steps): 

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

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

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

580 if shifted.date() != _ARBITRARY_DATE: 

581 raise OverflowError 

582 return shifted.time() 

583 

584 

585class TimeStrategy(SearchStrategy): 

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

587 super().__init__() 

588 self.min_value = min_value 

589 self.max_value = max_value 

590 self.tz_strat = timezones_strat 

591 

592 def do_draw(self, data): 

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

594 tz = data.draw(self.tz_strat) 

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

596 

597 def filter(self, condition): 

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

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

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

601 # times isn't worth the extra complexity. 

602 if ( 

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

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

605 and arg.tzinfo is None 

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

607 ): 

608 bounds = _narrowed_bounds( 

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

610 ) 

611 if bounds is None: 

612 return nothing() 

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

614 return self 

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

616 return super().filter(condition) 

617 

618 

619@defines_strategy(force_reusable_values=True) 

620def times( 

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

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

623 *, 

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

625) -> SearchStrategy[dt.time]: 

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

627 

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

629 

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

631 

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

633 component shrinking as for the strategy that provided it. 

634 """ 

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

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

637 if min_value.tzinfo is not None: 

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

639 if max_value.tzinfo is not None: 

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

641 check_valid_interval(min_value, max_value, "min_value", "max_value") 

642 return TimeStrategy(min_value, max_value, timezones) 

643 

644 

645def _shift_date(value, steps): 

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

647 

648 

649class DateStrategy(SearchStrategy): 

650 def __init__(self, min_value, max_value): 

651 super().__init__() 

652 assert isinstance(min_value, dt.date) 

653 assert isinstance(max_value, dt.date) 

654 assert min_value < max_value 

655 self.min_value = min_value 

656 self.max_value = max_value 

657 

658 def do_draw(self, data): 

659 return dt.date( 

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

661 ) 

662 

663 def filter(self, condition): 

664 if ( 

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

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

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

668 and not isinstance(arg, dt.datetime) 

669 ): 

670 bounds = _narrowed_bounds( 

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

672 ) 

673 if bounds is None: 

674 return nothing() 

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

676 return self 

677 return dates(*bounds) 

678 

679 return super().filter(condition) 

680 

681 

682@defines_strategy(force_reusable_values=True) 

683def dates( 

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

685) -> SearchStrategy[dt.date]: 

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

687 

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

689 

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

691 """ 

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

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

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

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

696 if isinstance(min_value, dt.datetime): 

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

698 if isinstance(max_value, dt.datetime): 

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

700 check_valid_interval(min_value, max_value, "min_value", "max_value") 

701 if min_value == max_value: 

702 return just(min_value) 

703 return DateStrategy(min_value, max_value) 

704 

705 

706class TimedeltaStrategy(SearchStrategy): 

707 def __init__(self, min_value, max_value): 

708 super().__init__() 

709 assert isinstance(min_value, dt.timedelta) 

710 assert isinstance(max_value, dt.timedelta) 

711 assert min_value < max_value 

712 self.min_value = min_value 

713 self.max_value = max_value 

714 

715 def do_draw(self, data): 

716 result = {} 

717 low_bound = True 

718 high_bound = True 

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

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

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

722 val = data.draw_integer(low, high) 

723 result[name] = val 

724 low_bound = low_bound and val == low 

725 high_bound = high_bound and val == high 

726 return dt.timedelta(**result) 

727 

728 

729@defines_strategy(force_reusable_values=True) 

730def timedeltas( 

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

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

733) -> SearchStrategy[dt.timedelta]: 

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

735 

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

737 

738 Examples from this strategy shrink towards zero. 

739 """ 

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

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

742 check_valid_interval(min_value, max_value, "min_value", "max_value") 

743 if min_value == max_value: 

744 return just(min_value) 

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

746 

747 

748@cache 

749def _valid_key_cacheable(tzpath, key): 

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

751 for root in tzpath: 

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

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

754 return True 

755 else: # pragma: no cover 

756 # This branch is only taken for names which are known to zoneinfo 

757 # but not present on the filesystem, i.e. on Windows with tzdata, 

758 # and so is never executed by our coverage tests. 

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

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

761 try: 

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

763 except ModuleNotFoundError: 

764 return False 

765 

766 

767@defines_strategy(force_reusable_values=True) 

768def timezone_keys( 

769 *, 

770 # allow_alias: bool = True, 

771 # allow_deprecated: bool = True, 

772 allow_prefix: bool = True, 

773) -> SearchStrategy[str]: 

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

775 

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

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

778 

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

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

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

782 not all deprecated timezone names are also aliases. 

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

784 ``allow_prefix=False``. 

785 

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

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

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

789 

790 .. note:: 

791 

792 `The tzdata package is required on Windows 

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

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

795 

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

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

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

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

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

801 """ 

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

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

804 check_type(bool, allow_prefix, "allow_prefix") 

805 

806 with warnings.catch_warnings(): 

807 try: 

808 warnings.simplefilter("ignore", EncodingWarning) 

809 except NameError: # pragma: no cover 

810 pass 

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

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

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

814 

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

816 

817 # When prefixes are allowed, we first choose a key and then flatmap to get our 

818 # choice with one of the available prefixes. That in turn means that we need 

819 # some logic to determine which prefixes are available for a given key: 

820 

821 def valid_key(key): 

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

823 

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

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

826 strategy = sampled_from([key for key in available_timezones if valid_key(key)]) 

827 

828 if not allow_prefix: 

829 return strategy 

830 

831 def sample_with_prefixes(zone): 

832 keys_with_prefixes = (zone, f"posix/{zone}", f"right/{zone}") 

833 return sampled_from([key for key in keys_with_prefixes if valid_key(key)]) 

834 

835 return strategy.flatmap(sample_with_prefixes) 

836 

837 

838@defines_strategy(force_reusable_values=True) 

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

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

841 

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

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

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

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

846 

847 .. note:: 

848 

849 `The tzdata package is required on Windows 

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

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

852 """ 

853 check_type(bool, no_cache, "no_cache") 

854 return timezone_keys().map( 

855 zoneinfo.ZoneInfo.no_cache if no_cache else zoneinfo.ZoneInfo 

856 ) 

857 

858 

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

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

861_timezones = timezones