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, lru_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
254# "Tricky" datetimes (https://github.com/HypothesisWorks/hypothesis/issues/69):
255# with some probability we generate wall times at small offsets from an
256# "interesting instant" - a moment at which any of a drawn timezone's
257# utcoffset, dst, tzname changes, or a UTC leap second. Working in
258# the wall-clock frame means we hit imaginary times inside spring-forward
259# gaps, ambiguous times (under both folds) inside fall-back folds, and the
260# exact boundaries of each.
261
262_SCAN_LO = dt.datetime(1800, 1, 1) # tzdata's earliest transitions are ~1847
263_SCAN_HI = dt.datetime(2050, 1, 1) # beyond this, recurring rules just repeat
264# The shortest gap between state changes anywhere in tzdata is just under
265# seven days (Brazil moved the start of DST forward by a week in October
266# 2000), so scanning at six-day resolution never puts two transitions in one
267# window and therefore finds every transition of every zone; see the probing
268# docstring below for what it would take to hide one from a future tzdata.
269_PROBE_STEP = dt.timedelta(days=6)
270_SECOND = dt.timedelta(seconds=1)
271_FALLBACK_SCAN = dt.timedelta(days=4 * 366) # covers any recurring annual rule
272# The probability that a draw targets a tricky value, and the half-widths of
273# the windows we draw them from: tight enough to hit the boundary
274# microseconds, wide enough to reach e.g. the far side of a DST gap.
275_TRICKY_P = 0.05
276_TRICKY_WIDTHS = (
277 dt.timedelta(seconds=1, microseconds=1),
278 dt.timedelta(hours=1, microseconds=1),
279 dt.timedelta(days=1),
280)
281
282
283# The UTC instant just after each change to TAI-UTC. datetime can't represent leap seconds,
284# but adjacent times are good test cases for code which parses, formats, or smears them.
285# Keep in sync with leap-seconds.txt.
286_LEAP_SECONDS = (
287 dt.datetime(1972, 1, 1),
288 dt.datetime(1972, 7, 1),
289 dt.datetime(1973, 1, 1),
290 dt.datetime(1974, 1, 1),
291 dt.datetime(1975, 1, 1),
292 dt.datetime(1976, 1, 1),
293 dt.datetime(1977, 1, 1),
294 dt.datetime(1978, 1, 1),
295 dt.datetime(1979, 1, 1),
296 dt.datetime(1980, 1, 1),
297 dt.datetime(1981, 7, 1),
298 dt.datetime(1982, 7, 1),
299 dt.datetime(1983, 7, 1),
300 dt.datetime(1985, 7, 1),
301 dt.datetime(1988, 1, 1),
302 dt.datetime(1990, 1, 1),
303 dt.datetime(1991, 1, 1),
304 dt.datetime(1992, 7, 1),
305 dt.datetime(1993, 7, 1),
306 dt.datetime(1994, 7, 1),
307 dt.datetime(1996, 1, 1),
308 dt.datetime(1997, 7, 1),
309 dt.datetime(1999, 1, 1),
310 dt.datetime(2006, 1, 1),
311 dt.datetime(2009, 1, 1),
312 dt.datetime(2012, 7, 1),
313 dt.datetime(2015, 7, 1),
314 dt.datetime(2017, 1, 1),
315)
316_INTERESTING_INSTANTS = _LEAP_SECONDS + (
317 dt.datetime(1970, 1, 1), # unix epoch
318 dt.datetime(2000, 1, 1), # millennium
319 # first moment after the largest signed 32-bit unix timestamp
320 dt.datetime(2038, 1, 19, 3, 14, 8),
321)
322
323
324def _as_naive_datetime(value):
325 """Bounds may be datetime subclasses such as ``pandas.Timestamp``, whose
326 arithmetic can overflow its narrower representable range, and whose type
327 must not leak into one side of a draw_capped_multipart window."""
328 return dt.datetime(
329 value.year,
330 value.month,
331 value.day,
332 value.hour,
333 value.minute,
334 value.second,
335 value.microsecond,
336 )
337
338
339def _tz_state(instant, tz):
340 aware = instant.replace(tzinfo=dt.timezone.utc).astimezone(tz)
341 return (aware.utcoffset(), aware.dst(), aware.tzname())
342
343
344def _probe_transitions(tz, lo, hi):
345 """Naive UTC instants in (lo, hi] at which ``tz`` first reports a changed
346 (utcoffset, dst, tzname), called "transitions". Daylights savings time is a transition,
347 for example.
348
349 Found by scanning at _PROBE_STEP resolution and bisecting each change down to the
350 second. Because we resume scanning from each boundary we find, several
351 transitions within a single step are all found; the only way to hide one
352 is a pair of transitions less than _PROBE_STEP apart which revert to the
353 exact prior state. The closest real pair of transitions is just under
354 seven days apart, above our six-day step, so in practice we
355 find every transition of every zone.
356 """
357 if isinstance(tz, dt.timezone):
358 # dt.timezone is guaranteed to be a fixed-offset timezone. (This is in
359 # contrast to zoneinfo.ZoneInfo, which can be varying-offset). Fixed-offset
360 # timezones cannot have any transitions, so skip probing.
361 return ()
362 transitions = []
363 at, state = lo, _tz_state(lo, tz)
364 while at < hi:
365 try:
366 probe = min(at + _PROBE_STEP, hi)
367 except OverflowError: # within _PROBE_STEP of datetime.max
368 probe = hi
369 probed = _tz_state(probe, tz)
370 if probed == state:
371 at, state = probe, probed
372 continue
373 low, high = at, probe
374 while high - low > _SECOND:
375 # Snap to whole seconds so that ``high`` converges to the exact
376 # transition instant rather than up to a second beyond it.
377 mid = (low + (high - low) / 2).replace(microsecond=0)
378 if mid <= low: # a sub-second window straddling a whole second
379 mid = low + (high - low) / 2
380 if _tz_state(mid, tz) == state:
381 low = mid
382 else:
383 high = mid
384 transitions.append(high)
385 at, state = high, _tz_state(high, tz)
386 return tuple(transitions)
387
388
389# cap memory usage in case of manually constructed timezones in a tight loop
390@lru_cache(maxsize=2048)
391def _transitions(tz):
392 return _probe_transitions(tz, _SCAN_LO, _SCAN_HI)
393
394
395@lru_cache(maxsize=256)
396def _interesting_instants(tz, lo, hi):
397 """The interesting instants for ``tz`` within the window of UTC instants
398 [lo, hi], as a sorted tuple of naive datetimes, plus the index to shrink
399 towards.
400
401 Returns ``((), 0)`` if there are none.
402 """
403 try:
404 if tz is None:
405 transitions = ()
406 elif lo > _SCAN_HI:
407 # The window is wholly above the usual scan range: probe a few
408 # years directly, enough to cover any recurring annual rule.
409 try:
410 cap = lo + _FALLBACK_SCAN
411 except OverflowError: # within a few years of datetime.max
412 cap = hi
413 transitions = _probe_transitions(tz, lo, min(hi, cap))
414 elif hi < _SCAN_LO:
415 # Wholly before the scan range: probe backwards from the window's
416 # end.
417 try:
418 floor = hi - _FALLBACK_SCAN
419 except OverflowError: # within a few years of datetime.min
420 floor = lo
421 transitions = _probe_transitions(tz, max(lo, floor), hi)
422 else:
423 transitions = _transitions(tz)
424 except Exception:
425 return (), 0
426 instants = tuple(
427 t for t in sorted(transitions + _INTERESTING_INSTANTS) if lo <= t <= hi
428 )
429 if not instants:
430 return (), 0
431 shrink_target = dt.datetime(2000, 1, 1)
432 nearest = min(range(len(instants)), key=lambda i: abs(instants[i] - shrink_target))
433 return instants, nearest
434
435
436class _UnrepresentableBound(Exception):
437 """No wall time in the timezone lies within the strategy's bounds."""
438
439
440class DatetimeStrategy(SearchStrategy):
441 def __init__(self, min_value, max_value, timezones_strat, allow_imaginary):
442 super().__init__()
443 assert isinstance(timezones_strat, SearchStrategy)
444 assert isinstance(allow_imaginary, bool)
445 self.aware = (min_value is not None and min_value.tzinfo is not None) or (
446 max_value is not None and max_value.tzinfo is not None
447 )
448 if self.aware:
449 for value in (min_value, max_value):
450 assert value is None or (
451 isinstance(value, dt.datetime) and value.tzinfo is not None
452 )
453 # The instants bounding this strategy, as _instant() sort keys.
454 # UTC offsets are less than a day, so a None bound is replaced by
455 # a key which lies outside the representable range.
456 self.min_instant = (
457 dt.timedelta(days=-2) if min_value is None else _instant(min_value)
458 )
459 self.max_instant = (
460 dt.datetime.max - dt.datetime.min + dt.timedelta(days=2)
461 if max_value is None
462 else _instant(max_value)
463 )
464 assert self.min_instant <= self.max_instant
465 else:
466 for value in (min_value, max_value):
467 assert isinstance(value, dt.datetime)
468 assert value.tzinfo is None
469 assert min_value <= max_value
470 self.min_value = min_value
471 self.max_value = max_value
472 self.tz_strat = timezones_strat
473 self.allow_imaginary = allow_imaginary
474 # The window of UTC instants (as naive datetimes) within which
475 # draw_tricky_datetime looks for interesting instants.
476 if self.aware:
477 zero, whole = dt.timedelta(0), dt.datetime.max - dt.datetime.min
478 lo = dt.datetime.min + min(max(self.min_instant, zero), whole)
479 hi = dt.datetime.min + min(max(self.max_instant, zero), whole)
480 else:
481 margin = dt.timedelta(days=1) # room for any UTC offset
482 lo = max(dt.datetime.min + margin, _as_naive_datetime(min_value)) - margin
483 hi = min(dt.datetime.max - margin, _as_naive_datetime(max_value)) + margin
484 self.instant_window = (lo, hi)
485 self.tricky_possible = any(lo <= t <= hi for t in _INTERESTING_INSTANTS) or (
486 _timezones_kind(self.tz_strat) != "none"
487 )
488
489 def do_draw(self, data):
490 # We start by drawing a timezone, and then - with some probability -
491 # target a "tricky" value near a timezone transition, leap second, or
492 # well-known rollover; see issue #69.
493 tz = data.draw(self.tz_strat)
494 if self.aware and not isinstance(tz, dt.tzinfo):
495 raise InvalidArgument(
496 f"Drew {tz!r} from the timezones strategy {self.tz_strat!r}, "
497 "but with aware min_value/max_value bounds the timezones "
498 "strategy must only generate tzinfo objects (not None)"
499 )
500 if self.tricky_possible and data.draw_boolean(_TRICKY_P):
501 result = self.draw_tricky_datetime(data, tz)
502 else:
503 result = self._draw_ordinary_datetime(data, tz)
504
505 # If we happened to end up with a disallowed imaginary time, reject it.
506 if (not self.allow_imaginary) and datetime_does_not_exist(result):
507 data.mark_invalid(f"{result} does not exist (usually a DST transition)")
508 return result
509
510 def _draw_ordinary_datetime(self, data, tz):
511 if self.aware:
512 return self.draw_aware_datetime(data, tz)
513 return self.draw_naive_datetime(data, tz)
514
515 def draw_tricky_datetime(self, data, tz):
516 """Draw a tricky datetime using the interesting instants."""
517 try:
518 instants, nearest = _interesting_instants(tz, *self.instant_window)
519 except TypeError: # eg an unhashable tzinfo
520 instants, nearest = (), 0
521 if self.aware:
522 try:
523 window = self._wall_clock_window(tz)
524 except _UnrepresentableBound:
525 window = None
526 if window is None: # draw_aware_datetime uses the UTC frame here
527 instants = ()
528 else:
529 window = (self.min_value, self.max_value)
530 if not instants:
531 # if it turns out nothing is tricky, fall back to a normal draw
532 return self._draw_ordinary_datetime(data, tz)
533 instant = instants[
534 data.draw_integer(0, len(instants) - 1, shrink_towards=nearest)
535 ]
536 width = _TRICKY_WIDTHS[data.draw_integer(0, len(_TRICKY_WIDTHS) - 1)]
537 if tz is not None:
538 # This cannot overflow: transitions were converted to tz when we
539 # probed for them, and the fixed instants are all in the 20th-21st centuries.
540 instant = (
541 instant.replace(tzinfo=dt.timezone.utc)
542 .astimezone(tz)
543 .replace(tzinfo=None)
544 )
545 lo, hi = (_as_naive_datetime(b) for b in window)
546 center = min(max(instant, lo), hi)
547 low = center - min(width, center - lo)
548 high = center + min(width, hi - center)
549 result = draw_capped_multipart(data, low, high)
550 value = replace_tzinfo(dt.datetime(**result), timezone=tz)
551 if self.aware and not self.in_bounds(value):
552 # An ambiguous wall time next to a bound, with the out-of-bounds
553 # fold, like draw_aware_datetime.
554 data.mark_invalid(f"{value!r} is outside the bounds")
555 return value
556
557 def in_bounds(self, value):
558 return self.min_instant <= _instant(value) <= self.max_instant
559
560 def draw_aware_datetime(self, data, tz):
561 try:
562 window = self._wall_clock_window(tz)
563 except _UnrepresentableBound as err:
564 data.mark_invalid(str(err))
565 if window is None:
566 # A large fraction of the wall times between bounds inside or close
567 # to a DST fold would risk rejection below - and bounds inside the
568 # same fold may even be in inverted wall-clock order, like
569 # 01:59 EDT < 01:01 EST - so we recurse to draw in UTC, where wall
570 # times are unambiguous and ordered, and convert. This is the
571 # standard draw with the standard shrink order, except that
572 # simplicity is judged on the UTC wall time rather than the local.
573 value = self.draw_aware_datetime(data, dt.timezone.utc)
574 try:
575 return value.astimezone(tz)
576 except OverflowError:
577 data.mark_invalid(f"{value!r} is not representable in {tz!r}")
578 result = draw_capped_multipart(data, *window)
579 value = replace_tzinfo(dt.datetime(**result), timezone=tz)
580 if not self.in_bounds(value):
581 # An ambiguous wall time next to a bound, with the out-of-bounds fold.
582 data.mark_invalid(f"{value!r} is outside the bounds")
583 return value
584
585 def _wall_clock_window(self, tz):
586 """The naive (min, max) wall-clock bounds for drawing in ``tz``, or
587 None to draw in UTC and convert. A pure function of (bounds, tz),
588 shared by generation and inversion; raises _UnrepresentableBound when
589 no wall time in ``tz`` lies within the bounds."""
590
591 def wall_clock(bound, extreme):
592 if bound is None:
593 return extreme
594 try:
595 return bound.astimezone(tz).replace(tzinfo=None)
596 except OverflowError:
597 # UTC offsets are less than a day, so an overflowing bound
598 # must be within a day of datetime.min/max, converting to a
599 # moment beyond them. If every wall time representable in tz
600 # is on the in-bounds side, the bound is simply vacuous here;
601 # otherwise nothing in tz is in bounds.
602 near_min = bound.replace(tzinfo=None) - dt.datetime.min < dt.timedelta(
603 days=2
604 )
605 if near_min == (extreme is dt.datetime.min):
606 return extreme
607 raise _UnrepresentableBound(
608 f"{bound!r} is not representable in {tz!r}"
609 ) from None
610
611 min_local = wall_clock(self.min_value, dt.datetime.min)
612 max_local = wall_clock(self.max_value, dt.datetime.max)
613 if min_local > max_local or (
614 max_local - min_local <= dt.timedelta(days=1)
615 and (_ambiguous(min_local, tz) or _ambiguous(max_local, tz))
616 ):
617 return None
618 return min_local, max_local
619
620 def draw_naive_datetime(self, data, tz):
621 result = draw_capped_multipart(data, self.min_value, self.max_value)
622 try:
623 return replace_tzinfo(dt.datetime(**result), timezone=tz)
624 except (ValueError, OverflowError):
625 data.mark_invalid(
626 f"Failed to draw a datetime between {self.min_value!r} and "
627 f"{self.max_value!r} with timezone from {self.tz_strat!r}."
628 )
629
630 def _invert(self, value: Any) -> tuple[ChoiceT, ...]:
631 # do_draw draws the tricky-path selector after the timezone, when one
632 # is drawn at all. We always re-encode via the ordinary path.
633 tricky_selector = (False,) if self.tricky_possible else ()
634 if self.aware:
635 if type(value) is not dt.datetime or value.tzinfo is None:
636 raise CannotInvert(f"{value!r} is not an aware datetime")
637 try:
638 in_bounds = self.in_bounds(value)
639 imaginary = datetime_does_not_exist(value)
640 except Exception:
641 raise CannotInvert(
642 f"could not locate {value!r} relative to {self!r}"
643 ) from None
644 if not in_bounds:
645 raise CannotInvert(f"{value!r} outside the instant bounds of {self!r}")
646 if imaginary and not self.allow_imaginary:
647 raise CannotInvert(
648 f"{value!r} is an imaginary datetime, but allow_imaginary=False"
649 )
650 return (
651 *self.tz_strat._invert(value.tzinfo),
652 *tricky_selector,
653 *self._invert_aware_fields(value, value.tzinfo, imaginary=imaginary),
654 )
655 if type(value) is not dt.datetime:
656 raise CannotInvert(f"{value!r} is not a datetime")
657 naive = value.replace(tzinfo=None)
658 if not (self.min_value <= naive <= self.max_value):
659 raise CannotInvert(
660 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]"
661 )
662 if not self.allow_imaginary and datetime_does_not_exist(value):
663 raise CannotInvert(
664 f"{value!r} is an imaginary datetime, but allow_imaginary=False"
665 )
666 # do_draw draws the timezone first, then the naive parts (with fold
667 # drawn last, since it is ignored in datetime comparisons).
668 return (
669 *self.tz_strat._invert(value.tzinfo),
670 *tricky_selector,
671 value.year,
672 value.month,
673 value.day,
674 value.hour,
675 value.minute,
676 value.second,
677 value.microsecond,
678 value.fold,
679 )
680
681 def _invert_aware_fields(self, value, tz, *, imaginary):
682 # The multipart fields draw_aware_datetime would consume to produce
683 # ``value``, expressed in the frame it would draw in for ``tz``.
684 try:
685 window = self._wall_clock_window(tz)
686 except _UnrepresentableBound as err:
687 raise CannotInvert(str(err)) from None
688 if window is None:
689 # do_draw would draw in UTC and convert; converting an imaginary
690 # wall time to UTC and back does not round-trip, so be conservative.
691 if imaginary:
692 raise CannotInvert(
693 f"the UTC drawing frame cannot reproduce imaginary {value!r}"
694 )
695 try:
696 utc_value = value.astimezone(dt.timezone.utc)
697 except OverflowError:
698 raise CannotInvert(f"{value!r} is not representable in UTC") from None
699 return self._invert_aware_fields(
700 utc_value, dt.timezone.utc, imaginary=False
701 )
702 min_local, max_local = window
703 naive = value.replace(tzinfo=None)
704 if not (min_local <= naive <= max_local):
705 raise CannotInvert(
706 f"{value!r} is outside the wall-clock window of {self!r} in {tz!r}"
707 )
708 return (
709 naive.year,
710 naive.month,
711 naive.day,
712 naive.hour,
713 naive.minute,
714 naive.second,
715 naive.microsecond,
716 value.fold,
717 )
718
719 def filter(self, condition):
720 if (parsed := _comparator_bound(condition)) is not None and isinstance(
721 arg := parsed[1], dt.datetime
722 ):
723 func = parsed[0]
724 try:
725 bound_aware = arg.utcoffset() is not None
726 except Exception:
727 # A tzinfo whose utcoffset() raises; comparing against this
728 # bound will raise the same error at draw time.
729 return super().filter(condition)
730 if not bound_aware:
731 # The bound compares as naive (either no tzinfo, or a tzinfo
732 # without a UTC offset), so we can only rewrite it into the
733 # naive wall-clock bounds if it really is naive and every
734 # generated value is too.
735 if (
736 arg.tzinfo is None
737 and not self.aware
738 and _timezones_kind(self.tz_strat) == "none"
739 ):
740 bounds = _narrowed_bounds(
741 func, arg, self.min_value, self.max_value, _shift_datetime
742 )
743 if bounds is None:
744 return nothing()
745 if bounds == (self.min_value, self.max_value):
746 return self
747 return datetimes(
748 *bounds,
749 timezones=self.tz_strat,
750 allow_imaginary=self.allow_imaginary,
751 )
752 else:
753 # An aware bound constrains the instant of generated values,
754 # so we narrow our aware bounds to the closed interval of
755 # satisfying instants - retaining strict predicates below,
756 # which then reject at most the boundary instant per timezone.
757 # We compare bounds by their _instant() key, since comparison
758 # of datetimes which share a tzinfo would fall back to
759 # wall-clock order, ignoring the fold.
760 if self.aware:
761 min_value, max_value = self.min_value, self.max_value
762 elif (self.min_value, self.max_value) == (
763 dt.datetime.min,
764 dt.datetime.max,
765 ) and _timezones_kind(self.tz_strat) != "none":
766 # An unbounded naive-mode strategy whose values are all
767 # aware: promote to aware mode, bounded by the filter.
768 min_value = max_value = None
769 else:
770 return super().filter(condition)
771 key = _instant(arg)
772 if func in (op.lt, op.le, op.eq) and (
773 min_value is None or _instant(min_value) < key
774 ):
775 min_value = arg
776 if func in (op.gt, op.ge, op.eq) and (
777 max_value is None or key < _instant(max_value)
778 ):
779 max_value = arg
780 if min_value is not None and max_value is not None:
781 lo, hi = _instant(min_value), _instant(max_value)
782 if hi < lo or (func in (op.lt, op.gt) and lo == hi == key):
783 # Only aware-mode strategies can reach this, and they
784 # generate only aware values (or raise for a bad
785 # timezones strategy), so this is provably empty.
786 return nothing()
787 if min_value is self.min_value and max_value is self.max_value:
788 result = self
789 else:
790 result = DatetimeStrategy(
791 min_value, max_value, self.tz_strat, self.allow_imaginary
792 )
793 if func in (op.lt, op.gt):
794 return FilteredStrategy(
795 result, (condition,), (current_filter_call_site(),)
796 )
797 return result
798 return super().filter(condition)
799
800
801@overload
802def datetimes(
803 min_value: NaiveDatetime | None = None,
804 max_value: NaiveDatetime | None = None,
805 *,
806 timezones: SearchStrategy[None] | None = None,
807) -> SearchStrategy[NaiveDatetime]: ...
808
809
810@overload
811def datetimes(
812 min_value: dt.datetime | None = None,
813 max_value: dt.datetime | None = None,
814 *,
815 timezones: SearchStrategy[dt.tzinfo],
816 allow_imaginary: bool = True,
817) -> SearchStrategy[AwareDatetime]: ...
818
819
820@overload
821def datetimes(
822 min_value: None = None,
823 max_value: None = None,
824 *,
825 timezones: SearchStrategy[dt.tzinfo | None],
826 allow_imaginary: bool = True,
827) -> SearchStrategy[dt.datetime]: ...
828
829
830@defines_strategy(force_reusable_values=True)
831def datetimes(
832 min_value: dt.datetime | None = None,
833 max_value: dt.datetime | None = None,
834 *,
835 timezones: SearchStrategy[dt.tzinfo | None] | None = None,
836 allow_imaginary: bool = True,
837) -> SearchStrategy[dt.datetime]:
838 """datetimes(min_value=None, max_value=None, *, timezones=None, allow_imaginary=True)
839
840 A strategy for generating datetimes, which may be timezone-aware.
841
842 If ``min_value`` and ``max_value`` are naive datetimes, or omitted, this
843 strategy works by drawing a naive datetime between them - defaulting to
844 ``datetime.min`` and ``datetime.max`` respectively - and then attaching
845 a timezone drawn from ``timezones``, which defaults to
846 :func:`~hypothesis.strategies.none`.
847
848 If instead both bounds are timezone-aware, they are treated as moments in
849 time, and ``timezones`` defaults to :func:`~hypothesis.strategies.timezones`.
850 Each generated datetime is aware, in a timezone drawn from ``timezones`` -
851 which must not generate ``None`` - and lies between the two moments.
852 Passing one aware and one naive bound is an error.
853
854 ``timezones`` must be a strategy that generates either ``None``, for naive
855 datetimes, or :class:`~python:datetime.tzinfo` objects for 'aware' datetimes.
856 You can construct your own, though we recommend using one of these built-in
857 strategies:
858
859 * with the standard library: :func:`hypothesis.strategies.timezones`;
860 * with :pypi:`dateutil <python-dateutil>`:
861 :func:`hypothesis.extra.dateutil.timezones`; or
862 * with :pypi:`pytz`: :func:`hypothesis.extra.pytz.timezones`.
863
864 You may pass ``allow_imaginary=False`` to filter out "imaginary" datetimes
865 which did not (or will not) occur due to daylight savings, leap seconds,
866 timezone and calendar adjustments, etc. Imaginary datetimes are allowed
867 by default, because malformed timestamps are a common source of bugs.
868
869 Because times near a change to the UTC offset are also a common source of
870 bugs, this strategy deliberately generates values on or near the drawn
871 timezone's daylight-saving and other offset transitions - including
872 imaginary wall times, and ambiguous ones with each value of ``fold`` -
873 as well as times adjacent to leap seconds and to well-known rollovers such as
874 the millennium and the end of the signed 32-bit Unix epoch.
875
876 .. note::
877
878 Arithmetic and comparisons on timezone-aware datetimes can be very
879 surprising around daylight-savings changes. See `this CPython issue
880 <https://github.com/python/cpython/issues/116035>`__ for details
881 and discussion.
882
883 Examples from this strategy shrink towards midnight on January 1st 2000,
884 local time.
885 """
886 check_type(bool, allow_imaginary, "allow_imaginary")
887 if min_value is not None:
888 check_type(dt.datetime, min_value, "min_value")
889 if max_value is not None:
890 check_type(dt.datetime, max_value, "max_value")
891 if timezones is not None and not isinstance(timezones, SearchStrategy):
892 raise InvalidArgument(
893 f"{timezones=} must be a SearchStrategy that can "
894 "provide tzinfo for datetimes (either None or dt.tzinfo objects)"
895 )
896 if (min_value is None or min_value.tzinfo is None) and (
897 max_value is None or max_value.tzinfo is None
898 ):
899 min_value = dt.datetime.min if min_value is None else min_value
900 max_value = dt.datetime.max if max_value is None else max_value
901 if timezones is None:
902 timezones = none()
903 check_valid_interval(min_value, max_value, "min_value", "max_value")
904 else:
905 # Aware bounds describe moments in time; we check both are aware here,
906 # and then at draw time convert them to the drawn timezone and proceed
907 # as in the naive case.
908 for name, value in [("min_value", min_value), ("max_value", max_value)]:
909 if value is not None and value.tzinfo is None:
910 raise InvalidArgument(
911 f"{name}={value!r} is naive, but the other bound is "
912 "timezone-aware; the bounds must be both naive or both aware"
913 )
914 if timezones is None:
915 timezones = _timezones()
916 # Compare explicitly as moments in time: comparison of datetimes which
917 # share a tzinfo falls back to wall-clock order, ignoring the fold.
918 if (
919 min_value is not None
920 and max_value is not None
921 and _instant(max_value) < _instant(min_value)
922 ):
923 raise InvalidArgument(
924 f"Cannot have {max_value=} < {min_value=}, comparing as "
925 "moments in time"
926 )
927 return DatetimeStrategy(min_value, max_value, timezones, allow_imaginary)
928
929
930_ARBITRARY_DATE = dt.date(2000, 1, 1)
931
932
933def _shift_time(value, steps):
934 # dt.time supports no arithmetic, so we go via a datetime on a fixed day
935 # and treat crossing midnight as overflowing the representable range.
936 shifted = dt.datetime.combine(_ARBITRARY_DATE, value) + steps * _MICROSECOND
937 if shifted.date() != _ARBITRARY_DATE:
938 raise OverflowError
939 return shifted.time()
940
941
942class TimeStrategy(SearchStrategy):
943 def __init__(self, min_value, max_value, timezones_strat):
944 super().__init__()
945 self.min_value = min_value
946 self.max_value = max_value
947 self.tz_strat = timezones_strat
948
949 def do_draw(self, data):
950 result = draw_capped_multipart(data, self.min_value, self.max_value, TIMENAMES)
951 tz = data.draw(self.tz_strat)
952 return dt.time(**result, tzinfo=tz)
953
954 def _invert(self, value: Any) -> tuple[ChoiceT, ...]:
955 if type(value) is not dt.time:
956 raise CannotInvert(f"{value!r} is not a time")
957 naive = value.replace(tzinfo=None)
958 if not (self.min_value <= naive <= self.max_value):
959 raise CannotInvert(
960 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]"
961 )
962 # unlike DatetimeStrategy, do_draw draws the naive parts first - with
963 # fold at the end, via draw_capped_multipart - and the timezone last.
964 return (
965 value.hour,
966 value.minute,
967 value.second,
968 value.microsecond,
969 value.fold,
970 *self.tz_strat._invert(value.tzinfo),
971 )
972
973 def filter(self, condition):
974 # We only rewrite naive times: ordering aware times works in terms of
975 # utcoffset(), which is None for e.g. ZoneInfo tzinfos on a time - so
976 # such values compare as naive anyway, and rewriting fixed-offset aware
977 # times isn't worth the extra complexity.
978 if (
979 (parsed := _comparator_bound(condition)) is not None
980 and isinstance(arg := parsed[1], dt.time)
981 and arg.tzinfo is None
982 and _timezones_kind(self.tz_strat) == "none"
983 ):
984 bounds = _narrowed_bounds(
985 parsed[0], arg, self.min_value, self.max_value, _shift_time
986 )
987 if bounds is None:
988 return nothing()
989 if bounds == (self.min_value, self.max_value):
990 return self
991 return times(*bounds, timezones=self.tz_strat)
992 return super().filter(condition)
993
994
995@defines_strategy(force_reusable_values=True)
996def times(
997 min_value: dt.time = dt.time.min,
998 max_value: dt.time = dt.time.max,
999 *,
1000 timezones: SearchStrategy[dt.tzinfo | None] = none(),
1001) -> SearchStrategy[dt.time]:
1002 """times(min_value=datetime.time.min, max_value=datetime.time.max, *, timezones=none())
1003
1004 A strategy for times between ``min_value`` and ``max_value``.
1005
1006 The ``timezones`` argument is handled as for :py:func:`datetimes`.
1007
1008 Examples from this strategy shrink towards midnight, with the timezone
1009 component shrinking as for the strategy that provided it.
1010 """
1011 check_type(dt.time, min_value, "min_value")
1012 check_type(dt.time, max_value, "max_value")
1013 if min_value.tzinfo is not None:
1014 raise InvalidArgument(f"{min_value=} must not have tzinfo")
1015 if max_value.tzinfo is not None:
1016 raise InvalidArgument(f"{max_value=} must not have tzinfo")
1017 check_valid_interval(min_value, max_value, "min_value", "max_value")
1018 return TimeStrategy(min_value, max_value, timezones)
1019
1020
1021def _shift_date(value, steps):
1022 return value + steps * dt.timedelta(days=1)
1023
1024
1025class DateStrategy(SearchStrategy):
1026 def __init__(self, min_value, max_value):
1027 super().__init__()
1028 assert isinstance(min_value, dt.date)
1029 assert isinstance(max_value, dt.date)
1030 assert min_value < max_value
1031 self.min_value = min_value
1032 self.max_value = max_value
1033
1034 def do_draw(self, data):
1035 return dt.date(
1036 **draw_capped_multipart(data, self.min_value, self.max_value, DATENAMES)
1037 )
1038
1039 def _invert(self, value: Any) -> tuple[ChoiceT, ...]:
1040 if type(value) is not dt.date:
1041 raise CannotInvert(f"{value!r} is not a date")
1042 if not (self.min_value <= value <= self.max_value):
1043 raise CannotInvert(
1044 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]"
1045 )
1046 return (value.year, value.month, value.day)
1047
1048 def filter(self, condition):
1049 if (
1050 (parsed := _comparator_bound(condition)) is not None
1051 # datetime is a date subclass, but not comparable with dates
1052 and isinstance(arg := parsed[1], dt.date)
1053 and not isinstance(arg, dt.datetime)
1054 ):
1055 bounds = _narrowed_bounds(
1056 parsed[0], arg, self.min_value, self.max_value, _shift_date
1057 )
1058 if bounds is None:
1059 return nothing()
1060 if bounds == (self.min_value, self.max_value):
1061 return self
1062 return dates(*bounds)
1063
1064 return super().filter(condition)
1065
1066
1067@defines_strategy(force_reusable_values=True)
1068def dates(
1069 min_value: dt.date = dt.date.min, max_value: dt.date = dt.date.max
1070) -> SearchStrategy[dt.date]:
1071 """dates(min_value=datetime.date.min, max_value=datetime.date.max)
1072
1073 A strategy for dates between ``min_value`` and ``max_value``.
1074
1075 Examples from this strategy shrink towards January 1st 2000.
1076 """
1077 check_type(dt.date, min_value, "min_value")
1078 check_type(dt.date, max_value, "max_value")
1079 # datetime is a subclass of date, so check_type() accepts it - but a datetime
1080 # bound is almost certainly a mistake, and breaks our drawing logic downstream.
1081 if isinstance(min_value, dt.datetime):
1082 raise InvalidArgument(f"{min_value=} is a datetime, but expected a date")
1083 if isinstance(max_value, dt.datetime):
1084 raise InvalidArgument(f"{max_value=} is a datetime, but expected a date")
1085 check_valid_interval(min_value, max_value, "min_value", "max_value")
1086 if min_value == max_value:
1087 return just(min_value)
1088 return DateStrategy(min_value, max_value)
1089
1090
1091class TimedeltaStrategy(SearchStrategy):
1092 def __init__(self, min_value, max_value):
1093 super().__init__()
1094 assert isinstance(min_value, dt.timedelta)
1095 assert isinstance(max_value, dt.timedelta)
1096 assert min_value < max_value
1097 self.min_value = min_value
1098 self.max_value = max_value
1099
1100 def do_draw(self, data):
1101 result = {}
1102 low_bound = True
1103 high_bound = True
1104 for name in ("days", "seconds", "microseconds"):
1105 low = getattr(self.min_value if low_bound else dt.timedelta.min, name)
1106 high = getattr(self.max_value if high_bound else dt.timedelta.max, name)
1107 val = data.draw_integer(low, high)
1108 result[name] = val
1109 low_bound = low_bound and val == low
1110 high_bound = high_bound and val == high
1111 return dt.timedelta(**result)
1112
1113 def _invert(self, value: Any) -> tuple[ChoiceT, ...]:
1114 if type(value) is not dt.timedelta:
1115 raise CannotInvert(f"{value!r} is not a timedelta")
1116 if not (self.min_value <= value <= self.max_value):
1117 raise CannotInvert(
1118 f"{value!r} outside [{self.min_value!r}, {self.max_value!r}]"
1119 )
1120 return (value.days, value.seconds, value.microseconds)
1121
1122
1123@defines_strategy(force_reusable_values=True)
1124def timedeltas(
1125 min_value: dt.timedelta = dt.timedelta.min,
1126 max_value: dt.timedelta = dt.timedelta.max,
1127) -> SearchStrategy[dt.timedelta]:
1128 """timedeltas(min_value=datetime.timedelta.min, max_value=datetime.timedelta.max)
1129
1130 A strategy for timedeltas between ``min_value`` and ``max_value``.
1131
1132 Examples from this strategy shrink towards zero.
1133 """
1134 check_type(dt.timedelta, min_value, "min_value")
1135 check_type(dt.timedelta, max_value, "max_value")
1136 check_valid_interval(min_value, max_value, "min_value", "max_value")
1137 if min_value == max_value:
1138 return just(min_value)
1139 return TimedeltaStrategy(min_value=min_value, max_value=max_value)
1140
1141
1142@cache
1143def _valid_key_cacheable(tzpath, key):
1144 assert isinstance(tzpath, tuple) # zoneinfo changed, better update this function!
1145 for root in tzpath:
1146 if Path(root).joinpath(key).exists(): # pragma: no branch
1147 # No branch because most systems only have one TZPATH component.
1148 return True
1149 else:
1150 # Taken for names which are known to zoneinfo but not present on the
1151 # filesystem, e.g. with the tzdata package installed.
1152 *package_loc, resource_name = key.split("/")
1153 package = "tzdata.zoneinfo." + ".".join(package_loc)
1154 try:
1155 return (resources.files(package) / resource_name).exists()
1156 except ModuleNotFoundError:
1157 return False
1158
1159
1160def _timezone_key_strategies(*, allow_prefix):
1161 """SampledFromStrategy branches for IANA keys: plain keys first, then one
1162 branch per allowed prefix, with the prefix applied as a sampled_from
1163 transformation. one_of's branch selector is therefore the prefix choice,
1164 which shrinks towards - and can be re-encoded as - an unprefixed key."""
1165 with warnings.catch_warnings():
1166 try:
1167 warnings.simplefilter("ignore", EncodingWarning)
1168 except NameError: # pragma: no cover
1169 pass
1170 # On Python 3.12 (and others?), `available_timezones()` opens files
1171 # without specifying an encoding - which our selftests make an error.
1172 available_timezones = ("UTC", *sorted(zoneinfo.available_timezones()))
1173
1174 # TODO: filter out alias and deprecated names if disallowed
1175
1176 def valid_key(key):
1177 return key == "UTC" or _valid_key_cacheable(zoneinfo.TZPATH, key)
1178
1179 # TODO: work out how to place a higher priority on "weird" timezones
1180 # For details see https://github.com/HypothesisWorks/hypothesis/issues/2414
1181 plain = [key for key in available_timezones if valid_key(key)]
1182 branches = [sampled_from(plain)]
1183 if allow_prefix:
1184 for prefix in ("posix", "right"):
1185 keys = [key for key in plain if valid_key(f"{prefix}/{key}")]
1186 if keys:
1187 branches.append(sampled_from(keys).map(f"{prefix}/{{}}".format))
1188 return branches
1189
1190
1191@defines_strategy(force_reusable_values=True)
1192def timezone_keys(
1193 *,
1194 # allow_alias: bool = True,
1195 # allow_deprecated: bool = True,
1196 allow_prefix: bool = True,
1197) -> SearchStrategy[str]:
1198 """A strategy for :wikipedia:`IANA timezone names <List_of_tz_database_time_zones>`.
1199
1200 As well as timezone names like ``"UTC"``, ``"Australia/Sydney"``, or
1201 ``"America/New_York"``, this strategy can generate:
1202
1203 - Aliases such as ``"Antarctica/McMurdo"``, which links to ``"Pacific/Auckland"``.
1204 - Deprecated names such as ``"Antarctica/South_Pole"``, which *also* links to
1205 ``"Pacific/Auckland"``. Note that most but
1206 not all deprecated timezone names are also aliases.
1207 - Timezone names with the ``"posix/"`` or ``"right/"`` prefixes, unless
1208 ``allow_prefix=False``.
1209
1210 These strings are provided separately from Tzinfo objects - such as ZoneInfo
1211 instances from the timezones() strategy - to facilitate testing of timezone
1212 logic without needing workarounds to access non-canonical names.
1213
1214 .. note::
1215
1216 `The tzdata package is required on Windows
1217 <https://docs.python.org/3/library/zoneinfo.html#data-sources>`__.
1218 ``pip install hypothesis[zoneinfo]`` installs it, if and only if needed.
1219
1220 On Windows, you may need to access IANA timezone data via the :pypi:`tzdata`
1221 package. For non-IANA timezones, such as Windows-native names or GNU TZ
1222 strings, we recommend using :func:`~hypothesis.strategies.sampled_from` with
1223 the :pypi:`dateutil <python-dateutil>` package, e.g.
1224 :meth:`dateutil:dateutil.tz.tzwin.list`.
1225 """
1226 # check_type(bool, allow_alias, "allow_alias")
1227 # check_type(bool, allow_deprecated, "allow_deprecated")
1228 check_type(bool, allow_prefix, "allow_prefix")
1229 return one_of(_timezone_key_strategies(allow_prefix=allow_prefix))
1230
1231
1232@defines_strategy(force_reusable_values=True)
1233def timezones(*, no_cache: bool = False) -> SearchStrategy["zoneinfo.ZoneInfo"]:
1234 """A strategy for :class:`python:zoneinfo.ZoneInfo` objects.
1235
1236 If ``no_cache=True``, the generated instances are constructed using
1237 :meth:`ZoneInfo.no_cache <python:zoneinfo.ZoneInfo.no_cache>` instead
1238 of the usual constructor. This may change the semantics of your datetimes
1239 in surprising ways, so only use it if you know that you need to!
1240
1241 .. note::
1242
1243 `The tzdata package is required on Windows
1244 <https://docs.python.org/3/library/zoneinfo.html#data-sources>`__.
1245 ``pip install hypothesis[zoneinfo]`` installs it, if and only if needed.
1246 """
1247 check_type(bool, no_cache, "no_cache")
1248 ctor = zoneinfo.ZoneInfo.no_cache if no_cache else zoneinfo.ZoneInfo
1249 # Mapping each sampled_from branch folds ctor into its transformations,
1250 # keeping the whole strategy re-encodable (unlike mapping the one_of).
1251 return one_of(
1252 [keys.map(ctor) for keys in _timezone_key_strategies(allow_prefix=True)]
1253 )
1254
1255
1256# In datetimes() above, the ``timezones`` argument shadows this module's
1257# timezones() strategy, so we refer to it by this alias instead.
1258_timezones = timezones