99from __future__ import annotations
1010
1111import datetime
12+ from typing import cast , NamedTuple , Optional , Tuple , TYPE_CHECKING , Union
1213
1314import pytz
1415
16+ if TYPE_CHECKING :
17+ from sopel .config import Config
18+ from sopel .db import SopelDB
19+
1520
1621# various time units measured in seconds; approximated for months and years
1722SECONDS = 1
2227YEARS = 365 * DAYS
2328
2429
25- def validate_timezone (zone ):
30+ class Duration (NamedTuple ):
31+ """Named tuple representation of a duration.
32+
33+ This can be used as a tuple as well as an object::
34+
35+ >>> d = Duration(minutes=12, seconds=34)
36+ >>> d.minutes
37+ 12
38+ >>> d.seconds
39+ 34
40+ >>> years, months, days, hours, minutes, seconds = d
41+ >>> (years, months, days, hours, minutes, seconds)
42+ (0, 0, 0, 0, 12, 34)
43+
44+ """
45+ years : int = 0
46+ """Years spent."""
47+ months : int = 0
48+ """Months spent."""
49+ days : int = 0
50+ """Days spent."""
51+ hours : int = 0
52+ """Hours spent."""
53+ minutes : int = 0
54+ """Minutes spent."""
55+ seconds : int = 0
56+ """Seconds spent."""
57+
58+
59+ def validate_timezone (zone : Optional [str ]) -> str :
2660 """Return an IETF timezone from the given IETF zone or common abbreviation.
2761
28- :param str zone: in a strict or a human-friendly format
62+ :param zone: in a strict or a human-friendly format
2963 :return: the valid IETF timezone properly formatted
3064 :raise ValueError: when ``zone`` is not a valid timezone
65+ (including empty string and ``None`` value)
3166
3267 Prior to checking timezones, two transformations are made to make the zone
3368 names more human-friendly:
@@ -40,25 +75,32 @@ def validate_timezone(zone):
4075 becomes ``UTC``. In the majority of user-facing interactions, such
4176 case-insensitivity will be expected.
4277
43- If the zone is not valid, `` ValueError` ` will be raised.
78+ If the zone is not valid, :exc:` ValueError` will be raised.
4479
4580 .. versionadded:: 6.0
81+
82+ .. versionchanged:: 8.0
83+
84+ If ``zone`` is ``None``, raises a :exc:`ValueError` as if it was an
85+ empty string or an invalid timezone instead of returning ``None``.
86+
4687 """
4788 if zone is None :
48- return None
89+ raise ValueError ( 'Invalid time zone.' )
4990
5091 zone = '/' .join (reversed (zone .split (', ' ))).replace (' ' , '_' )
5192 try :
5293 tz = pytz .timezone (zone )
5394 except pytz .exceptions .UnknownTimeZoneError :
5495 raise ValueError ('Invalid time zone.' )
55- return tz .zone
96+
97+ return cast (str , tz .zone )
5698
5799
58- def validate_format (tformat ) :
100+ def validate_format (tformat : str ) -> str :
59101 """Validate a time format string.
60102
61- :param str tformat: the format string to validate
103+ :param tformat: the format string to validate
62104 :return: the format string, if valid
63105 :raise ValueError: when ``tformat`` is not a valid time format string
64106
@@ -72,13 +114,11 @@ def validate_format(tformat):
72114 return tformat
73115
74116
75- def get_nick_timezone (db , nick ) :
117+ def get_nick_timezone (db : SopelDB , nick : str ) -> Optional [ str ] :
76118 """Get a nick's timezone from database.
77119
78120 :param db: Bot's database handler (usually ``bot.db``)
79- :type db: :class:`~sopel.db.SopelDB`
80121 :param nick: IRC nickname
81- :type nick: :class:`~sopel.tools.identifiers.Identifier`
82122 :return: the timezone associated with the ``nick``
83123
84124 If a timezone cannot be found for ``nick``, or if it is invalid, ``None``
@@ -92,13 +132,11 @@ def get_nick_timezone(db, nick):
92132 return None
93133
94134
95- def get_channel_timezone (db , channel ) :
135+ def get_channel_timezone (db : SopelDB , channel : str ) -> Optional [ str ] :
96136 """Get a channel's timezone from database.
97137
98138 :param db: Bot's database handler (usually ``bot.db``)
99- :type db: :class:`~sopel.db.SopelDB`
100139 :param channel: IRC channel name
101- :type channel: :class:`~sopel.tools.identifiers.Identifier`
102140 :return: the timezone associated with the ``channel``
103141
104142 If a timezone cannot be found for ``channel``, or if it is invalid,
@@ -112,16 +150,20 @@ def get_channel_timezone(db, channel):
112150 return None
113151
114152
115- def get_timezone (db = None , config = None , zone = None , nick = None , channel = None ):
153+ def get_timezone (
154+ db : Optional [SopelDB ] = None ,
155+ config : Optional [Config ] = None ,
156+ zone : Optional [str ] = None ,
157+ nick : Optional [str ] = None ,
158+ channel : Optional [str ] = None ,
159+ ) -> Optional [str ]:
116160 """Find, and return, the appropriate timezone.
117161
118162 :param db: bot database object (optional)
119- :type db: :class:`~.db.SopelDB`
120163 :param config: bot config object (optional)
121- :type config: :class:`~.config.Config`
122- :param str zone: preferred timezone name (optional)
123- :param str nick: nick whose timezone to use, if set (optional)
124- :param str channel: channel whose timezone to use, if set (optional)
164+ :param zone: preferred timezone name (optional)
165+ :param nick: nick whose timezone to use, if set (optional)
166+ :param channel: channel whose timezone to use, if set (optional)
125167
126168 Timezone is pulled in the following priority:
127169
@@ -145,40 +187,50 @@ def get_timezone(db=None, config=None, zone=None, nick=None, channel=None):
145187 formatting of the timezone.
146188
147189 """
148- def _check (zone ) :
190+ def _check (zone : Optional [ str ]) -> Optional [ str ] :
149191 try :
150192 return validate_timezone (zone )
151193 except ValueError :
152194 return None
153195
154- tz = None
196+ tz : Optional [ str ] = None
155197
156198 if zone :
157199 tz = _check (zone )
158- if not tz :
200+ # zone might be a nick or a channel
201+ if not tz and db is not None :
159202 tz = _check (db .get_nick_or_channel_value (zone , 'timezone' ))
160- if not tz and nick :
161- tz = _check (db .get_nick_value (nick , 'timezone' ))
162- if not tz and channel :
163- tz = _check (db .get_channel_value (channel , 'timezone' ))
164- if not tz and config and config .core .default_timezone :
203+
204+ # get nick's timezone, and if none, get channel's timezone instead
205+ if not tz and db is not None :
206+ if nick :
207+ tz = _check (db .get_nick_value (nick , 'timezone' ))
208+ if not tz and channel :
209+ tz = _check (db .get_channel_value (channel , 'timezone' ))
210+
211+ # if still not found, default to core configuration
212+ if not tz and config is not None and config .core .default_timezone :
165213 tz = _check (config .core .default_timezone )
214+
166215 return tz
167216
168217
169- def format_time (db = None , config = None , zone = None , nick = None , channel = None ,
170- time = None ):
218+ def format_time (
219+ db : Optional [SopelDB ] = None ,
220+ config : Optional [Config ] = None ,
221+ zone : Optional [str ] = None ,
222+ nick : Optional [str ] = None ,
223+ channel : Optional [str ] = None ,
224+ time : Optional [datetime .datetime ] = None ,
225+ ) -> str :
171226 """Return a formatted string of the given time in the given zone.
172227
173228 :param db: bot database object (optional)
174- :type db: :class:`~.db.SopelDB`
175229 :param config: bot config object (optional)
176- :type config: :class:`~.config.Config`
177- :param str zone: name of timezone to use for output (optional)
178- :param str nick: nick whose time format to use, if set (optional)
179- :param str channel: channel whose time format to use, if set (optional)
230+ :param zone: name of timezone to use for output (optional)
231+ :param nick: nick whose time format to use, if set (optional)
232+ :param channel: channel whose time format to use, if set (optional)
180233 :param time: the time value to format (optional)
181- :type time: :class:`~datetime.datetime`
182234
183235 ``time``, if given, should be a ``datetime.datetime`` object, and will be
184236 treated as being in the UTC timezone if it is :ref:`naïve
@@ -200,68 +252,92 @@ def format_time(db=None, config=None, zone=None, nick=None, channel=None,
200252 If ``db`` is not given or is not set up, steps 1 and 2 are skipped. If
201253 ``config`` is not given, step 3 will be skipped.
202254 """
203- utc = pytz .timezone ('UTC' )
204- tformat = None
255+ target_tz : datetime .tzinfo = pytz .utc
256+ tformat : Optional [str ] = None
257+
258+ # get an aware datetime
259+ if not time :
260+ time = pytz .utc .localize (datetime .datetime .utcnow ())
261+ elif not time .tzinfo :
262+ time = pytz .utc .localize (time )
263+
264+ # get target timezone
265+ if zone :
266+ target_tz = pytz .timezone (zone )
267+
268+ # get format for nick or channel
205269 if db :
206270 if nick :
207271 tformat = db .get_nick_value (nick , 'time_format' )
208272 if not tformat and channel :
209273 tformat = db .get_channel_value (channel , 'time_format' )
274+
275+ # get format from configuration
210276 if not tformat and config and config .core .default_time_format :
211277 tformat = config .core .default_time_format
278+
279+ # or default to hard-coded format
212280 if not tformat :
213281 tformat = '%Y-%m-%d - %T %z'
214282
215- if not time :
216- time = datetime .datetime .now (tz = utc )
217- elif not time .tzinfo :
218- time = utc .localize (time )
219-
220- if not zone :
221- zone = utc
222- else :
223- zone = pytz .timezone (zone )
224-
225- return time .astimezone (zone ).strftime (tformat )
283+ # format local time with format
284+ return time .astimezone (target_tz ).strftime (tformat )
226285
227286
228- def seconds_to_split (seconds ) :
287+ def seconds_to_split (seconds : int ) -> Duration :
229288 """Split an amount of ``seconds`` into years, months, days, etc.
230289
231- :param int seconds: amount of time in seconds
232- :return: the time split into a tuple of years, months, days, hours,
290+ :param seconds: amount of time in seconds
291+ :return: the time split into a named tuple of years, months, days, hours,
233292 minutes, and seconds
234- :rtype: :class:`tuple`
235293
236294 Examples::
237295
238296 >>> seconds_to_split(7800)
239- ( 0, 0, 0, 2, 10, 0)
297+ Duration(years= 0, months= 0, days= 0, hours= 2, minutes= 10, seconds= 0)
240298 >>> seconds_to_split(143659)
241- ( 0, 0, 1, 15, 54, 19)
299+ Duration(years= 0, months= 0, days= 1, hours= 15, minutes= 54, seconds= 19)
242300
243301 .. versionadded:: 7.1
302+
303+ .. versionchanged:: 8.0
304+
305+ This function returns a :class:`Duration` named tuple.
306+
244307 """
245308 years , seconds_left = divmod (int (seconds ), YEARS )
246309 months , seconds_left = divmod (seconds_left , MONTHS )
247310 days , seconds_left = divmod (seconds_left , DAYS )
248311 hours , seconds_left = divmod (seconds_left , HOURS )
249312 minutes , seconds_left = divmod (seconds_left , MINUTES )
250313
251- return years , months , days , hours , minutes , seconds_left
252-
253-
254- def get_time_unit (years = 0 , months = 0 , days = 0 , hours = 0 , minutes = 0 , seconds = 0 ):
314+ return Duration (years , months , days , hours , minutes , seconds_left )
315+
316+
317+ def get_time_unit (
318+ years : int = 0 ,
319+ months : int = 0 ,
320+ days : int = 0 ,
321+ hours : int = 0 ,
322+ minutes : int = 0 ,
323+ seconds : int = 0 ,
324+ ) -> Tuple [
325+ Tuple [int , str ],
326+ Tuple [int , str ],
327+ Tuple [int , str ],
328+ Tuple [int , str ],
329+ Tuple [int , str ],
330+ Tuple [int , str ],
331+ ]:
255332 """Map a time in (y, m, d, h, min, s) to its labels.
256333
257- :param int years: number of years
258- :param int months: number of months
259- :param int days: number of days
260- :param int hours: number of hours
261- :param int minutes: number of minutes
262- :param int seconds: number of seconds
334+ :param years: number of years
335+ :param months: number of months
336+ :param days: number of days
337+ :param hours: number of hours
338+ :param minutes: number of minutes
339+ :param seconds: number of seconds
263340 :return: a tuple of 2-value tuples, each for a time amount and its label
264- :rtype: :class:`tuple`
265341
266342 This helper function takes a time split into years, months, days, hours,
267343 minutes, and seconds to return a tuple with the correct label for each
@@ -308,12 +384,14 @@ def get_time_unit(years=0, months=0, days=0, hours=0, minutes=0, seconds=0):
308384 )
309385
310386
311- def seconds_to_human (secs , granularity = 2 ):
387+ def seconds_to_human (
388+ secs : Union [datetime .timedelta , float , int ],
389+ granularity : int = 2 ,
390+ ) -> str :
312391 """Format :class:`~datetime.timedelta` as a human-readable relative time.
313392
314393 :param secs: time difference to format
315- :type secs: :class:`~datetime.timedelta` or integer
316- :param int granularity: number of time units to return (default to 2)
394+ :param granularity: number of time units to return (default to 2)
317395
318396 Inspiration for function structure from:
319397 https://gist.github.com/Highstaker/280a09591df4a5fb1363b0bbaf858f0d
0 commit comments