diff --git a/docs/source/plugin/time.rst b/docs/source/plugin/time.rst index ab825d1b3..dd9078ca7 100644 --- a/docs/source/plugin/time.rst +++ b/docs/source/plugin/time.rst @@ -14,8 +14,6 @@ Here is a full example of that, adapted from the built-in ``.t`` command:: import datetime - import pytz - from sopel import plugin from sopel.tools.time import format_time, get_timezone @@ -24,7 +22,7 @@ Here is a full example of that, adapted from the built-in ``.t`` command:: @plugin.require_chanmsg def my_command(bot, trigger): """Give time in a channel.""" - time = pytz.UTC.localize(datetime.datetime.utcnow()) + time = datetime.datetime.now(datetime.timezone.utc) timezone = get_timezone( bot.db, bot.settings, diff --git a/sopel/bot.py b/sopel/bot.py index 10b0b0c7f..309e97f58 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -8,7 +8,7 @@ from __future__ import annotations from ast import literal_eval -import datetime +from datetime import datetime, timezone import inspect import itertools import logging @@ -1045,7 +1045,7 @@ def error( if trigger: message = '{} from {} at {}. Message was: {}'.format( - message, trigger.nick, str(datetime.datetime.utcnow()), trigger.group(0) + message, trigger.nick, str(datetime.now(timezone.utc)), trigger.group(0) ) LOGGER.exception(message) diff --git a/sopel/coretasks.py b/sopel/coretasks.py index b1b1ed7e5..adb2f8bef 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -25,7 +25,7 @@ import base64 import collections import copy -import datetime +from datetime import datetime, timedelta, timezone import functools import logging import re @@ -906,7 +906,7 @@ def _send_who(bot, mask): target_id = bot.make_identifier(mask) if not target_id.is_nick(): - bot.channels[target_id].last_who = datetime.datetime.utcnow() + bot.channels[target_id].last_who = datetime.now(timezone.utc) @plugin.interval(30) @@ -916,10 +916,10 @@ def _periodic_send_who(bot): # WHO not needed to update 'away' status return - # Loops through the channels to find the one that has the longest time since the last WHO - # request, and issues a WHO request only if the last request for the channel was more than + # Loop through the channels to find the one that has the longest time since the last WHO + # request, and issue a WHO request only if the last request for the channel was more than # 120 seconds ago. - who_trigger_time = datetime.datetime.utcnow() - datetime.timedelta(seconds=120) + who_trigger_time = datetime.now(timezone.utc) - timedelta(seconds=120) selected_channel = None for channel_name, channel in bot.channels.items(): if channel.last_who is None: diff --git a/sopel/irc/__init__.py b/sopel/irc/__init__.py index ef62d8b13..04b08b4e0 100644 --- a/sopel/irc/__init__.py +++ b/sopel/irc/__init__.py @@ -32,7 +32,7 @@ import abc from collections import deque -from datetime import datetime +from datetime import datetime, timezone import logging import os import threading @@ -471,7 +471,7 @@ def on_error(self) -> None: # quit if too many errors dt_seconds: float = 0.0 if self.last_error_timestamp is not None: - dt = datetime.utcnow() - self.last_error_timestamp + dt = datetime.now(timezone.utc) - self.last_error_timestamp dt_seconds = dt.total_seconds() if dt_seconds < 5: @@ -480,7 +480,7 @@ def on_error(self) -> None: # remove 1 error per full 5s that passed since last error self.error_count = int(max(0, self.error_count - dt_seconds // 5)) - self.last_error_timestamp = datetime.utcnow() + self.last_error_timestamp = datetime.now(timezone.utc) self.error_count = self.error_count + 1 def rebuild_nick(self) -> None: diff --git a/sopel/modules/remind.py b/sopel/modules/remind.py index 89dc76012..f35f77813 100644 --- a/sopel/modules/remind.py +++ b/sopel/modules/remind.py @@ -9,7 +9,7 @@ from __future__ import annotations import collections -from datetime import datetime +from datetime import datetime, timezone import io # don't use `codecs` for loading the DB; it will split lines on some IRC formatting import logging import os @@ -263,7 +263,7 @@ def remind_in(bot, trigger): timezone, trigger.nick, trigger.sender, - datetime.utcfromtimestamp(timestamp)) + datetime.fromtimestamp(timestamp, timezone.utc)) bot.reply('Okay, will remind at %s' % human_time) else: bot.reply('Okay, will remind in %s secs' % duration) @@ -495,7 +495,7 @@ def remind_at(bot, trigger): reminder.timezone.zone, trigger.nick, trigger.sender, - datetime.utcfromtimestamp(timestamp)) + datetime.fromtimestamp(timestamp, timezone.utc)) bot.reply('Okay, will remind at %s' % human_time) else: bot.reply('Okay, will remind in %s secs' % duration) diff --git a/sopel/modules/uptime.py b/sopel/modules/uptime.py index 5bf2b3e57..06abac42d 100644 --- a/sopel/modules/uptime.py +++ b/sopel/modules/uptime.py @@ -7,14 +7,14 @@ """ from __future__ import annotations -import datetime +from datetime import datetime, timedelta, timezone from sopel import plugin def setup(bot): if "start_time" not in bot.memory: - bot.memory["start_time"] = datetime.datetime.utcnow() + bot.memory["start_time"] = datetime.now(timezone.utc) @plugin.command('uptime') @@ -22,7 +22,7 @@ def setup(bot): @plugin.output_prefix('[uptime] ') def uptime(bot, trigger): """Return the uptime of Sopel.""" - delta = datetime.timedelta(seconds=round((datetime.datetime.utcnow() - - bot.memory["start_time"]) - .total_seconds())) + delta = timedelta(seconds=round((datetime.now(timezone.utc) - + bot.memory["start_time"]) + .total_seconds())) bot.say("I've been sitting here for {} and I keep going!".format(delta)) diff --git a/sopel/plugins/rules.py b/sopel/plugins/rules.py index 341c9c492..427a8e6a3 100644 --- a/sopel/plugins/rules.py +++ b/sopel/plugins/rules.py @@ -17,7 +17,7 @@ from __future__ import annotations import abc -import datetime +from datetime import datetime, timedelta, timezone import functools import inspect import itertools @@ -36,8 +36,6 @@ ) from urllib.parse import urlparse -import pytz - from sopel import tools from sopel.config.core_section import ( COMMAND_DEFAULT_HELP_PREFIX, COMMAND_DEFAULT_PREFIX, URL_DEFAULT_SCHEMES) @@ -461,25 +459,25 @@ def check_url_callback(self, bot, url): class RuleMetrics: """Tracker of a rule's usage.""" def __init__(self) -> None: - self.started_at: Optional[datetime.datetime] = None - self.ended_at: Optional[datetime.datetime] = None + self.started_at: Optional[datetime] = None + self.ended_at: Optional[datetime] = None self.last_return_value: Any = None def start(self) -> None: """Record a starting time (before execution).""" - self.started_at = pytz.utc.localize(datetime.datetime.utcnow()) + self.started_at = datetime.now(timezone.utc) def end(self) -> None: """Record a ending time (after execution).""" - self.ended_at = pytz.utc.localize(datetime.datetime.utcnow()) + self.ended_at = datetime.now(timezone.utc) def set_return_value(self, value: Any) -> None: """Set the last return value of a rule.""" self.last_return_value = value @property - def last_time(self) -> Optional[datetime.datetime]: - """Last recorded start/end time for the associated rule""" + def last_time(self) -> Optional[datetime]: + """Last recorded start/end time for the associated rule.""" # detect if we just started something or if it ended last_time = self.started_at if self.ended_at and self.started_at < self.ended_at: @@ -489,7 +487,7 @@ def last_time(self) -> Optional[datetime.datetime]: def is_limited( self, - time_limit: datetime.datetime, + time_limit: datetime, ) -> bool: """Determine if the rule hits the time limit.""" if not self.started_at: @@ -744,7 +742,7 @@ def is_unblockable(self) -> bool: def is_user_rate_limited( self, nick: Identifier, - at_time: Optional[datetime.datetime] = None, + at_time: Optional[datetime] = None, ) -> bool: """Tell when the rule reached the ``nick``'s rate limit. @@ -758,7 +756,7 @@ def is_user_rate_limited( def is_channel_rate_limited( self, channel: Identifier, - at_time: Optional[datetime.datetime] = None, + at_time: Optional[datetime] = None, ) -> bool: """Tell when the rule reached the ``channel``'s rate limit. @@ -771,7 +769,7 @@ def is_channel_rate_limited( @abc.abstractmethod def is_global_rate_limited( self, - at_time: Optional[datetime.datetime] = None, + at_time: Optional[datetime] = None, ) -> bool: """Tell when the rule reached the global rate limit. @@ -1174,43 +1172,43 @@ def get_global_metrics(self) -> RuleMetrics: return self._metrics_global @property - def user_rate_limit(self) -> datetime.timedelta: - return datetime.timedelta(seconds=self._user_rate_limit) + def user_rate_limit(self) -> timedelta: + return timedelta(seconds=self._user_rate_limit) @property - def channel_rate_limit(self) -> datetime.timedelta: - return datetime.timedelta(seconds=self._channel_rate_limit) + def channel_rate_limit(self) -> timedelta: + return timedelta(seconds=self._channel_rate_limit) @property - def global_rate_limit(self) -> datetime.timedelta: - return datetime.timedelta(seconds=self._global_rate_limit) + def global_rate_limit(self) -> timedelta: + return timedelta(seconds=self._global_rate_limit) def is_user_rate_limited( self, nick: Identifier, - at_time: Optional[datetime.datetime] = None, + at_time: Optional[datetime] = None, ) -> bool: if at_time is None: - at_time = pytz.utc.localize(datetime.datetime.utcnow()) + at_time = datetime.now(timezone.utc) metrics = self.get_user_metrics(nick) return metrics.is_limited(at_time - self.user_rate_limit) def is_channel_rate_limited( self, channel: Identifier, - at_time: Optional[datetime.datetime] = None, + at_time: Optional[datetime] = None, ) -> bool: if at_time is None: - at_time = pytz.utc.localize(datetime.datetime.utcnow()) + at_time = datetime.now(timezone.utc) metrics = self.get_channel_metrics(channel) return metrics.is_limited(at_time - self.channel_rate_limit) def is_global_rate_limited( self, - at_time: Optional[datetime.datetime] = None, + at_time: Optional[datetime] = None, ) -> bool: if at_time is None: - at_time = pytz.utc.localize(datetime.datetime.utcnow()) + at_time = datetime.now(timezone.utc) metrics = self.get_global_metrics() return metrics.is_limited(at_time - self.global_rate_limit) diff --git a/sopel/tools/time.py b/sopel/tools/time.py index 44ce0a1f8..9803046e7 100644 --- a/sopel/tools/time.py +++ b/sopel/tools/time.py @@ -57,10 +57,10 @@ class Duration(NamedTuple): def validate_timezone(zone: Optional[str]) -> str: - """Return an IETF timezone from the given IETF zone or common abbreviation. + """Normalize and validate an IANA timezone name. :param zone: in a strict or a human-friendly format - :return: the valid IETF timezone properly formatted + :return: the valid IANA timezone properly formatted :raise ValueError: when ``zone`` is not a valid timezone (including empty string and ``None`` value) @@ -107,7 +107,7 @@ def validate_format(tformat: str) -> str: .. versionadded:: 6.0 """ try: - time = datetime.datetime.utcnow() + time = datetime.datetime.now(datetime.timezone.utc) time.strftime(tformat) except (ValueError, TypeError): raise ValueError('Invalid time format.') @@ -232,7 +232,7 @@ def format_time( :param channel: channel whose time format to use, if set (optional) :param time: the time value to format (optional) - ``time``, if given, should be a ``datetime.datetime`` object, and will be + ``time``, if given, should be a ``~datetime.datetime`` object, and will be treated as being in the UTC timezone if it is :ref:`naïve `. If ``time`` is not given, the current time will be used. @@ -257,7 +257,7 @@ def format_time( # get an aware datetime if not time: - time = pytz.utc.localize(datetime.datetime.utcnow()) + time = datetime.datetime.now(datetime.timezone.utc) elif not time.tzinfo: time = pytz.utc.localize(time) diff --git a/sopel/trigger.py b/sopel/trigger.py index 6c509e1c9..92512b685 100644 --- a/sopel/trigger.py +++ b/sopel/trigger.py @@ -9,7 +9,7 @@ """ from __future__ import annotations -import datetime +from datetime import datetime, timezone import re from typing import ( Callable, @@ -190,17 +190,15 @@ def __init__( self.tags[tag[0]] = None # Client time or server time - self.time = datetime.datetime.utcnow().replace( - tzinfo=datetime.timezone.utc - ) + self.time = datetime.now(timezone.utc) if 'time' in self.tags: # ensure "time" is a string (typecheck) tag_time = self.tags['time'] or '' try: - self.time = datetime.datetime.strptime( + self.time = datetime.strptime( tag_time, "%Y-%m-%dT%H:%M:%S.%fZ", - ).replace(tzinfo=datetime.timezone.utc) + ).replace(tzinfo=timezone.utc) except ValueError: pass # Server isn't conforming to spec, ignore the server-time diff --git a/test/plugins/test_plugins_rules.py b/test/plugins/test_plugins_rules.py index 48818b4ce..d252439ca 100644 --- a/test/plugins/test_plugins_rules.py +++ b/test/plugins/test_plugins_rules.py @@ -5,7 +5,6 @@ import re import pytest -import pytz from sopel import bot, loader, plugin, trigger from sopel.plugins import rules @@ -469,7 +468,7 @@ def test_manager_has_action_command_aliases(): # tests for :class:`Manager` def test_rulemetrics(): - now = pytz.utc.localize(datetime.datetime.utcnow()) + now = datetime.datetime.now(datetime.timezone.utc) time_window = datetime.timedelta(seconds=3600) metrics = rules.RuleMetrics() diff --git a/test/test_bot.py b/test/test_bot.py index 4ecfe66a5..c24b7c7dd 100644 --- a/test/test_bot.py +++ b/test/test_bot.py @@ -1556,7 +1556,7 @@ def test_user_quit( assert 'MrPraline' in mockbot.channels['#test'].users - servertime = datetime.utcnow() + timedelta(seconds=10) + servertime = datetime.now(timezone.utc) + timedelta(seconds=10) mockbot.on_message( "@time={servertime} :{user} QUIT :Ping timeout: 246 seconds".format( servertime=servertime.strftime('%Y-%m-%dT%H:%M:%SZ'),