From 919768c8bb3b5f1ab0cf084599c527eacaf82a29 Mon Sep 17 00:00:00 2001 From: Erik Kalkoken Date: Fri, 17 Jun 2022 11:58:45 +0000 Subject: [PATCH] Fix: Broken docs generation on readthedocs.org (2nd attempt) --- .../task_statistics/event_series.py | 49 ++++++++++++++++--- .../tests/test_event_series.py | 37 +++++++++++++- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/allianceauth/authentication/task_statistics/event_series.py b/allianceauth/authentication/task_statistics/event_series.py index f8591fbc..d1a222db 100644 --- a/allianceauth/authentication/task_statistics/event_series.py +++ b/allianceauth/authentication/task_statistics/event_series.py @@ -1,27 +1,62 @@ import datetime as dt -from typing import Optional, List +import logging +from typing import List, Optional -from redis import Redis from pytz import utc +from redis import Redis, RedisError from django.core.cache import cache +logger = logging.getLogger(__name__) + + +class _RedisStub: + """Stub of a Redis client. + + It's purpose is to prevent EventSeries objects from trying to access Redis + when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org. + """ + + def delete(self, *args, **kwargs): + pass + + def incr(self, *args, **kwargs): + return 0 + + def zadd(self, *args, **kwargs): + pass + + def zcount(self, *args, **kwargs): + pass + + def zrangebyscore(self, *args, **kwargs): + pass + class EventSeries: - """API for recording and analysing a series of events.""" + """API for recording and analyzing a series of events.""" _ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES" def __init__(self, key_id: str, redis: Redis = None) -> None: self._redis = cache.get_master_client() if not redis else redis - if not isinstance(self._redis, Redis): - raise TypeError( - "This class requires a Redis client, but none was provided " - "and the default Django cache backend is not Redis either." + try: + if not self._redis.ping(): + raise RuntimeError() + except (AttributeError, RedisError, RuntimeError): + logger.exception( + "Failed to establish a connection with Redis. " + "This EventSeries object is disabled.", ) + self._redis = _RedisStub() self._key_id = str(key_id) self.clear() + @property + def is_disabled(self): + """True when this object is disabled, e.g. Redis was not available at startup.""" + return isinstance(self._redis, _RedisStub) + @property def _key_counter(self): return f"{self._ROOT_KEY}_{self._key_id}_COUNTER" diff --git a/allianceauth/authentication/task_statistics/tests/test_event_series.py b/allianceauth/authentication/task_statistics/tests/test_event_series.py index 079c4721..30b61465 100644 --- a/allianceauth/authentication/task_statistics/tests/test_event_series.py +++ b/allianceauth/authentication/task_statistics/tests/test_event_series.py @@ -1,13 +1,48 @@ import datetime as dt +from unittest.mock import patch from pytz import utc +from redis import RedisError + from django.test import TestCase from django.utils.timezone import now -from allianceauth.authentication.task_statistics.event_series import EventSeries +from allianceauth.authentication.task_statistics.event_series import ( + EventSeries, + _RedisStub, +) + +MODULE_PATH = "allianceauth.authentication.task_statistics.event_series" class TestEventSeries(TestCase): + def test_should_abort_without_redis_client(self): + # when + with patch(MODULE_PATH + ".cache.get_master_client") as mock: + mock.return_value = None + events = EventSeries("dummy") + # then + self.assertTrue(events._redis, _RedisStub) + self.assertTrue(events.is_disabled) + + def test_should_disable_itself_if_redis_not_available_1(self): + # when + with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client: + mock_get_master_client.return_value.ping.side_effect = RedisError + events = EventSeries("dummy") + # then + self.assertIsInstance(events._redis, _RedisStub) + self.assertTrue(events.is_disabled) + + def test_should_disable_itself_if_redis_not_available_2(self): + # when + with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client: + mock_get_master_client.return_value.ping.return_value = False + events = EventSeries("dummy") + # then + self.assertIsInstance(events._redis, _RedisStub) + self.assertTrue(events.is_disabled) + def test_should_add_event(self): # given events = EventSeries("dummy")