Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/utils/timezone.py: 37%
75 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
1"""
2Timezone-related classes and functions.
3"""
5import functools
6import zoneinfo
7from contextlib import ContextDecorator
8from datetime import UTC, datetime, timedelta, timezone, tzinfo
9from threading import local
11from plain.runtime import settings
13__all__ = [
14 "get_fixed_timezone",
15 "get_default_timezone",
16 "get_default_timezone_name",
17 "get_current_timezone",
18 "get_current_timezone_name",
19 "activate",
20 "deactivate",
21 "override",
22 "localtime",
23 "now",
24 "is_aware",
25 "is_naive",
26 "make_aware",
27 "make_naive",
28]
31def get_fixed_timezone(offset):
32 """Return a tzinfo instance with a fixed offset from UTC."""
33 if isinstance(offset, timedelta):
34 offset = offset.total_seconds() // 60
35 sign = "-" if offset < 0 else "+"
36 hhmm = "%02d%02d" % divmod(abs(offset), 60)
37 name = sign + hhmm
38 return timezone(timedelta(minutes=offset), name)
41# In order to avoid accessing settings at compile time,
42# wrap the logic in a function and cache the result.
43@functools.lru_cache
44def get_default_timezone():
45 """
46 Return the default time zone as a tzinfo instance.
48 This is the time zone defined by settings.TIME_ZONE.
49 """
50 return zoneinfo.ZoneInfo(settings.TIME_ZONE)
53# This function exists for consistency with get_current_timezone_name
54def get_default_timezone_name():
55 """Return the name of the default time zone."""
56 return _get_timezone_name(get_default_timezone())
59_active = local()
62def get_current_timezone():
63 """Return the currently active time zone as a tzinfo instance."""
64 return getattr(_active, "value", get_default_timezone())
67def get_current_timezone_name():
68 """Return the name of the currently active time zone."""
69 return _get_timezone_name(get_current_timezone())
72def _get_timezone_name(timezone):
73 """
74 Return the offset for fixed offset timezones, or the name of timezone if
75 not set.
76 """
77 return timezone.tzname(None) or str(timezone)
80# Timezone selection functions.
82# These functions don't change os.environ['TZ'] and call time.tzset()
83# because it isn't thread safe.
86def activate(timezone):
87 """
88 Set the time zone for the current thread.
90 The ``timezone`` argument must be an instance of a tzinfo subclass or a
91 time zone name.
92 """
93 if isinstance(timezone, tzinfo):
94 _active.value = timezone
95 elif isinstance(timezone, str):
96 _active.value = zoneinfo.ZoneInfo(timezone)
97 else:
98 raise ValueError(f"Invalid timezone: {timezone!r}")
101def deactivate():
102 """
103 Unset the time zone for the current thread.
105 Plain will then use the time zone defined by settings.TIME_ZONE.
106 """
107 if hasattr(_active, "value"):
108 del _active.value
111class override(ContextDecorator):
112 """
113 Temporarily set the time zone for the current thread.
115 This is a context manager that uses plain.utils.timezone.activate()
116 to set the timezone on entry and restores the previously active timezone
117 on exit.
119 The ``timezone`` argument must be an instance of a ``tzinfo`` subclass, a
120 time zone name, or ``None``. If it is ``None``, Plain enables the default
121 time zone.
122 """
124 def __init__(self, timezone):
125 self.timezone = timezone
127 def __enter__(self):
128 self.old_timezone = getattr(_active, "value", None)
129 if self.timezone is None:
130 deactivate()
131 else:
132 activate(self.timezone)
134 def __exit__(self, exc_type, exc_value, traceback):
135 if self.old_timezone is None:
136 deactivate()
137 else:
138 _active.value = self.old_timezone
141# Utilities
144def localtime(value=None, timezone=None):
145 """
146 Convert an aware datetime.datetime to local time.
148 Only aware datetimes are allowed. When value is omitted, it defaults to
149 now().
151 Local time is defined by the current time zone, unless another time zone
152 is specified.
153 """
154 if value is None:
155 value = now()
156 if timezone is None:
157 timezone = get_current_timezone()
158 # Emulate the behavior of astimezone() on Python < 3.6.
159 if is_naive(value):
160 raise ValueError("localtime() cannot be applied to a naive datetime")
161 return value.astimezone(timezone)
164def now():
165 """
166 Return a timezone aware datetime.
167 """
168 return datetime.now(tz=UTC)
171# By design, these four functions don't perform any checks on their arguments.
172# The caller should ensure that they don't receive an invalid value like None.
175def is_aware(value):
176 """
177 Determine if a given datetime.datetime is aware.
179 The concept is defined in Python's docs:
180 https://docs.python.org/library/datetime.html#datetime.tzinfo
182 Assuming value.tzinfo is either None or a proper datetime.tzinfo,
183 value.utcoffset() implements the appropriate logic.
184 """
185 return value.utcoffset() is not None
188def is_naive(value):
189 """
190 Determine if a given datetime.datetime is naive.
192 The concept is defined in Python's docs:
193 https://docs.python.org/library/datetime.html#datetime.tzinfo
195 Assuming value.tzinfo is either None or a proper datetime.tzinfo,
196 value.utcoffset() implements the appropriate logic.
197 """
198 return value.utcoffset() is None
201def make_aware(value, timezone=None):
202 """Make a naive datetime.datetime in a given time zone aware."""
203 if timezone is None:
204 timezone = get_current_timezone()
205 # Check that we won't overwrite the timezone of an aware datetime.
206 if is_aware(value):
207 raise ValueError(f"make_aware expects a naive datetime, got {value}")
208 # This may be wrong around DST changes!
209 return value.replace(tzinfo=timezone)
212def make_naive(value, timezone=None):
213 """Make an aware datetime.datetime naive in a given time zone."""
214 if timezone is None:
215 timezone = get_current_timezone()
216 # Emulate the behavior of astimezone() on Python < 3.6.
217 if is_naive(value):
218 raise ValueError("make_naive() cannot be applied to a naive datetime")
219 return value.astimezone(timezone).replace(tzinfo=None)
222def _datetime_ambiguous_or_imaginary(dt, tz):
223 return tz.utcoffset(dt.replace(fold=not dt.fold)) != tz.utcoffset(dt)