Add a singletom Metaclass

This commit is contained in:
Phill 2019-07-20 13:27:28 +01:00
parent 4f0ee2b0d3
commit dd4d9b9255
9 changed files with 74 additions and 85 deletions

View File

@ -172,6 +172,21 @@ class SlideLimits(object):
Next = 3 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): def de_hump(name):
""" """
Change any Camel Case string to python string Change any Camel Case string to python string

View File

@ -29,7 +29,7 @@ from collections import namedtuple
from PyQt5 import QtCore, QtWidgets 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.applocation import AppLocation
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
@ -327,22 +327,11 @@ class LanguageManager(object):
return LanguageManager.__qm_list__ return LanguageManager.__qm_list__
class UiStrings(object): class UiStrings(metaclass=Singleton):
""" """
Provide standard strings for objects to use. Provide standard strings for objects to use.
""" """
__instance__ = None def __init__(self):
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):
""" """
These strings should need a good reason to be retranslated elsewhere. These strings should need a good reason to be retranslated elsewhere.
Should some/more/less of these have an & attached? Should some/more/less of these have an & attached?

View File

@ -23,29 +23,19 @@
Provide Registry Services Provide Registry Services
""" """
import logging 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__) 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 This is the Component Registry. It is a singleton object and is used to provide a look up service for common
objects. objects.
""" """
log.info('Registry loaded') 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 @classmethod
def create(cls): def create(cls):
@ -57,20 +47,9 @@ class Registry(object):
registry.service_list = {} registry.service_list = {}
registry.functions_list = {} registry.functions_list = {}
registry.working_flags = {} 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 registry.initialising = True
return registry 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): def get(self, key):
""" """
Extracts the registry value from the list based on the key passed in Extracts the registry value from the list based on the key passed in

View File

@ -28,6 +28,7 @@ from functools import cmp_to_key
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common import Singleton
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
@ -142,24 +143,15 @@ class Screen(object):
screen_dict['custom_geometry']['height']) screen_dict['custom_geometry']['height'])
class ScreenList(object): class ScreenList(metaclass=Singleton):
""" """
Wrapper to handle the parameters of the display screen. Wrapper to handle the parameters of the display screen.
To get access to the screen list call ``ScreenList()``. To get access to the screen list call ``ScreenList()``.
""" """
log.info('Screen loaded') log.info('Screen loaded')
__instance__ = None
screens = [] 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): def __iter__(self):
""" """
Convert this object into an iterable, so that we can iterate over it instead of the inner list Convert this object into an iterable, so that we can iterate over it instead of the inner list

View File

@ -28,6 +28,7 @@ contained within the openlp.core module.
""" """
import logging import logging
from openlp.core.common import Singleton
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.mixins import LogMixin from openlp.core.common.mixins import LogMixin
from openlp.core.lib.plugin import PluginStatus from openlp.core.lib.plugin import PluginStatus
@ -52,17 +53,7 @@ class StateModule(LogMixin):
self.text = None self.text = None
class State(LogMixin): class State(LogMixin, metaclass=Singleton):
__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__
def load_settings(self): def load_settings(self):
self.modules = {} self.modules = {}

View File

@ -27,6 +27,7 @@ import logging
import qtawesome as qta import qtawesome as qta
from PyQt5 import QtGui, QtWidgets from PyQt5 import QtGui, QtWidgets
from openlp.core.common import Singleton
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
@ -34,22 +35,11 @@ from openlp.core.lib import build_icon
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class UiIcons(object): class UiIcons(metaclass=Singleton):
""" """
Provide standard icons for objects to use. Provide standard icons for objects to use.
""" """
__instance__ = None def __init__(self):
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):
""" """
These are the font icons used in the code. These are the font icons used in the code.
""" """
@ -165,6 +155,7 @@ class UiIcons(object):
'volunteer': {'icon': 'fa.group'} 'volunteer': {'icon': 'fa.group'}
} }
self.load_icons(icon_list) self.load_icons(icon_list)
self.main_icon = build_icon(':/icon/openlp-logo.svg')
def load_icons(self, icon_list): def load_icons(self, icon_list):
""" """
@ -184,7 +175,6 @@ class UiIcons(object):
setattr(self, key, qta.icon('fa.plus-circle', color='red')) setattr(self, key, qta.icon('fa.plus-circle', color='red'))
except Exception: except Exception:
setattr(self, key, qta.icon('fa.plus-circle', color='red')) setattr(self, key, qta.icon('fa.plus-circle', color='red'))
self.main_icon = build_icon(':/icon/openlp-logo.svg')
@staticmethod @staticmethod
def _print_icons(): def _print_icons():

View File

@ -26,6 +26,7 @@ plugin.
import logging import logging
import re import re
from openlp.core.common import Singleton
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
@ -64,20 +65,10 @@ class LanguageSelection(object):
English = 2 English = 2
class BibleStrings(object): class BibleStrings(metaclass=Singleton):
""" """
Provide standard strings for objects to use. 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): def __init__(self):
""" """
These strings should need a good reason to be retranslated elsewhere. These strings should need a good reason to be retranslated elsewhere.

View File

@ -26,7 +26,7 @@ from pathlib import Path
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, call, patch 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 normalize_str, path_to_module, trace_error_handler
@ -163,6 +163,48 @@ class TestCommonFunctions(TestCase):
mocked_logger.error.assert_called_with( mocked_logger.error.assert_called_with(
'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test') '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): def test_is_win(self):
""" """
Test the is_win() function Test the is_win() function

View File

@ -33,7 +33,7 @@ from tests.helpers.testmixin import TestMixin
class TestIcons(TestCase, 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, _): def test_simple_icon(self, _):
# GIVEN: an basic set of icons # GIVEN: an basic set of icons
icons = UiIcons() icons = UiIcons()