Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/utils/timezone.py: 37%

75 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:03 -0500

1""" 

2Timezone-related classes and functions. 

3""" 

4 

5import functools 

6import zoneinfo 

7from contextlib import ContextDecorator 

8from datetime import datetime, timedelta, timezone, tzinfo 

9from threading import local 

10 

11from plain.runtime import settings 

12 

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] 

29 

30 

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) 

39 

40 

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. 

47 

48 This is the time zone defined by settings.TIME_ZONE. 

49 """ 

50 return zoneinfo.ZoneInfo(settings.TIME_ZONE) 

51 

52 

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()) 

57 

58 

59_active = local() 

60 

61 

62def get_current_timezone(): 

63 """Return the currently active time zone as a tzinfo instance.""" 

64 return getattr(_active, "value", get_default_timezone()) 

65 

66 

67def get_current_timezone_name(): 

68 """Return the name of the currently active time zone.""" 

69 return _get_timezone_name(get_current_timezone()) 

70 

71 

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) 

78 

79 

80# Timezone selection functions. 

81 

82# These functions don't change os.environ['TZ'] and call time.tzset() 

83# because it isn't thread safe. 

84 

85 

86def activate(timezone): 

87 """ 

88 Set the time zone for the current thread. 

89 

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("Invalid timezone: %r" % timezone) 

99 

100 

101def deactivate(): 

102 """ 

103 Unset the time zone for the current thread. 

104 

105 Plain will then use the time zone defined by settings.TIME_ZONE. 

106 """ 

107 if hasattr(_active, "value"): 

108 del _active.value 

109 

110 

111class override(ContextDecorator): 

112 """ 

113 Temporarily set the time zone for the current thread. 

114 

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. 

118 

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 """ 

123 

124 def __init__(self, timezone): 

125 self.timezone = timezone 

126 

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) 

133 

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 

139 

140 

141# Utilities 

142 

143 

144def localtime(value=None, timezone=None): 

145 """ 

146 Convert an aware datetime.datetime to local time. 

147 

148 Only aware datetimes are allowed. When value is omitted, it defaults to 

149 now(). 

150 

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) 

162 

163 

164def now(): 

165 """ 

166 Return a timezone aware datetime. 

167 """ 

168 return datetime.now(tz=timezone.utc) 

169 

170 

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. 

173 

174 

175def is_aware(value): 

176 """ 

177 Determine if a given datetime.datetime is aware. 

178 

179 The concept is defined in Python's docs: 

180 https://docs.python.org/library/datetime.html#datetime.tzinfo 

181 

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 

186 

187 

188def is_naive(value): 

189 """ 

190 Determine if a given datetime.datetime is naive. 

191 

192 The concept is defined in Python's docs: 

193 https://docs.python.org/library/datetime.html#datetime.tzinfo 

194 

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 

199 

200 

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("make_aware expects a naive datetime, got %s" % value) 

208 # This may be wrong around DST changes! 

209 return value.replace(tzinfo=timezone) 

210 

211 

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) 

220 

221 

222def _datetime_ambiguous_or_imaginary(dt, tz): 

223 return tz.utcoffset(dt.replace(fold=not dt.fold)) != tz.utcoffset(dt)