diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 7ea86cfa4..96e1181db 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -172,6 +172,21 @@ class SlideLimits(object): Next = 3 +class Singleton(type): + """ + Provide a `Singleton` metaclass https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python + """ + _instances = {} + + def __call__(cls, *args, **kwargs): + """ + Create a new instance if one does not already exist. + """ + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + def de_hump(name): """ Change any Camel Case string to python string diff --git a/openlp/core/common/i18n.py b/openlp/core/common/i18n.py index 5b2aabf20..c05c6fdf0 100644 --- a/openlp/core/common/i18n.py +++ b/openlp/core/common/i18n.py @@ -29,7 +29,7 @@ from collections import namedtuple from PyQt5 import QtCore, QtWidgets -from openlp.core.common import is_macosx, is_win +from openlp.core.common import Singleton, is_macosx, is_win from openlp.core.common.applocation import AppLocation from openlp.core.common.settings import Settings @@ -327,22 +327,11 @@ class LanguageManager(object): return LanguageManager.__qm_list__ -class UiStrings(object): +class UiStrings(metaclass=Singleton): """ Provide standard strings for objects to use. """ - __instance__ = None - - def __new__(cls): - """ - Override the default object creation method to return a single instance. - """ - if not cls.__instance__: - cls.__instance__ = super().__new__(cls) - cls.__instance__.load() - return cls.__instance__ - - def load(self): + def __init__(self): """ These strings should need a good reason to be retranslated elsewhere. Should some/more/less of these have an & attached? diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 10992d6fe..7c30ddc0d 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -23,29 +23,19 @@ Provide Registry Services """ import logging -import sys -from openlp.core.common import de_hump, trace_error_handler +from openlp.core.common import Singleton, de_hump, trace_error_handler log = logging.getLogger(__name__) -class Registry(object): +class Registry(metaclass=Singleton): """ This is the Component Registry. It is a singleton object and is used to provide a look up service for common objects. """ log.info('Registry loaded') - __instance__ = None - - def __new__(cls): - """ - Re-implement the __new__ method to make sure we create a true singleton. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ @classmethod def create(cls): @@ -57,20 +47,9 @@ class Registry(object): registry.service_list = {} registry.functions_list = {} registry.working_flags = {} - # Allow the tests to remove Registry entries but not the live system - registry.running_under_test = 'nose' in sys.argv[0] or 'pytest' in sys.argv[0] registry.initialising = True return registry - @classmethod - def destroy(cls): - """ - Destroy the Registry. - """ - if cls.__instance__.running_under_test: - del cls.__instance__ - cls.__instance__ = None - def get(self, key): """ Extracts the registry value from the list based on the key passed in diff --git a/openlp/core/display/screens.py b/openlp/core/display/screens.py index fccb9741f..04f4ea321 100644 --- a/openlp/core/display/screens.py +++ b/openlp/core/display/screens.py @@ -28,6 +28,7 @@ from functools import cmp_to_key from PyQt5 import QtCore, QtWidgets +from openlp.core.common import Singleton from openlp.core.common.i18n import translate from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings @@ -142,24 +143,15 @@ class Screen(object): screen_dict['custom_geometry']['height']) -class ScreenList(object): +class ScreenList(metaclass=Singleton): """ Wrapper to handle the parameters of the display screen. To get access to the screen list call ``ScreenList()``. """ log.info('Screen loaded') - __instance__ = None screens = [] - def __new__(cls): - """ - Re-implement __new__ to create a true singleton. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ - def __iter__(self): """ Convert this object into an iterable, so that we can iterate over it instead of the inner list diff --git a/openlp/core/state.py b/openlp/core/state.py index c76644a57..b0474c8c2 100644 --- a/openlp/core/state.py +++ b/openlp/core/state.py @@ -28,6 +28,7 @@ contained within the openlp.core module. """ import logging +from openlp.core.common import Singleton from openlp.core.common.registry import Registry from openlp.core.common.mixins import LogMixin from openlp.core.lib.plugin import PluginStatus @@ -52,17 +53,7 @@ class StateModule(LogMixin): self.text = None -class State(LogMixin): - - __instance__ = None - - def __new__(cls): - """ - Re-implement the __new__ method to make sure we create a true singleton. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ +class State(LogMixin, metaclass=Singleton): def load_settings(self): self.modules = {} diff --git a/openlp/core/ui/icons.py b/openlp/core/ui/icons.py index 701c25a09..b70d10996 100644 --- a/openlp/core/ui/icons.py +++ b/openlp/core/ui/icons.py @@ -27,6 +27,7 @@ import logging import qtawesome as qta from PyQt5 import QtGui, QtWidgets +from openlp.core.common import Singleton from openlp.core.common.applocation import AppLocation from openlp.core.lib import build_icon @@ -34,22 +35,11 @@ from openlp.core.lib import build_icon log = logging.getLogger(__name__) -class UiIcons(object): +class UiIcons(metaclass=Singleton): """ Provide standard icons for objects to use. """ - __instance__ = None - - def __new__(cls): - """ - Override the default object creation method to return a single instance. - """ - if not cls.__instance__: - cls.__instance__ = super().__new__(cls) - cls.__instance__.load() - return cls.__instance__ - - def load(self): + def __init__(self): """ These are the font icons used in the code. """ @@ -165,6 +155,7 @@ class UiIcons(object): 'volunteer': {'icon': 'fa.group'} } self.load_icons(icon_list) + self.main_icon = build_icon(':/icon/openlp-logo.svg') def load_icons(self, icon_list): """ @@ -184,7 +175,6 @@ class UiIcons(object): setattr(self, key, qta.icon('fa.plus-circle', color='red')) except Exception: setattr(self, key, qta.icon('fa.plus-circle', color='red')) - self.main_icon = build_icon(':/icon/openlp-logo.svg') @staticmethod def _print_icons(): diff --git a/openlp/plugins/bibles/lib/__init__.py b/openlp/plugins/bibles/lib/__init__.py index e0a06fa2d..6045476a4 100644 --- a/openlp/plugins/bibles/lib/__init__.py +++ b/openlp/plugins/bibles/lib/__init__.py @@ -26,6 +26,7 @@ plugin. import logging import re +from openlp.core.common import Singleton from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings @@ -64,20 +65,10 @@ class LanguageSelection(object): English = 2 -class BibleStrings(object): +class BibleStrings(metaclass=Singleton): """ Provide standard strings for objects to use. """ - __instance__ = None - - def __new__(cls): - """ - Override the default object creation method to return a single instance. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ - def __init__(self): """ These strings should need a good reason to be retranslated elsewhere. diff --git a/tests/functional/openlp_core/common/test_common.py b/tests/functional/openlp_core/common/test_common.py index 21f246a75..6624a4819 100644 --- a/tests/functional/openlp_core/common/test_common.py +++ b/tests/functional/openlp_core/common/test_common.py @@ -26,7 +26,7 @@ from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, call, patch -from openlp.core.common import clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \ +from openlp.core.common import Singleton, clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \ normalize_str, path_to_module, trace_error_handler @@ -163,6 +163,48 @@ class TestCommonFunctions(TestCase): mocked_logger.error.assert_called_with( 'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test') + def test_singleton_metaclass_multiple_init(self): + """ + Test that a class using the Singleton Metaclass is only initialised once despite being called several times and + that the same instance is returned each time.. + """ + # GIVEN: The Singleton Metaclass and a test class using it + class SingletonClass(metaclass=Singleton): + def __init__(self): + pass + + with patch.object(SingletonClass, '__init__', return_value=None) as patched_init: + + # WHEN: Initialising the class multiple times + inst_1 = SingletonClass() + inst_2 = SingletonClass() + + # THEN: The __init__ method of the SingletonClass should have only been called once, and both returned values + # should be the same instance. + assert inst_1 is inst_2 + assert patched_init.call_count == 1 + + def test_singleton_metaclass_multiple_classes(self): + """ + Test that multiple classes using the Singleton Metaclass return the different an appropriate instances. + """ + # GIVEN: Two different classes using the Singleton Metaclass + class SingletonClass1(metaclass=Singleton): + def __init__(self): + pass + + class SingletonClass2(metaclass=Singleton): + def __init__(self): + pass + + # WHEN: Initialising both classes + s_c1 = SingletonClass1() + s_c2 = SingletonClass2() + + # THEN: The instances should be an instance of the appropriate class + assert isinstance(s_c1, SingletonClass1) + assert isinstance(s_c2, SingletonClass2) + def test_is_win(self): """ Test the is_win() function diff --git a/tests/functional/openlp_core/ui/test_icons.py b/tests/functional/openlp_core/ui/test_icons.py index 9d91ec367..6bcdc641c 100644 --- a/tests/functional/openlp_core/ui/test_icons.py +++ b/tests/functional/openlp_core/ui/test_icons.py @@ -33,7 +33,7 @@ from tests.helpers.testmixin import TestMixin class TestIcons(TestCase, TestMixin): - @patch('openlp.core.ui.icons.UiIcons.load') + @patch('openlp.core.ui.icons.UiIcons.__init__', return_value=None) def test_simple_icon(self, _): # GIVEN: an basic set of icons icons = UiIcons()