Browse Source

Merge pull request #375 from sh4nks/mark-internal-plugins

Mark 'flaskbb.*' plugins as internal plugins
Peter Justin 7 years ago
parent
commit
9a49f6436c

+ 7 - 5
flaskbb/app.py

@@ -21,7 +21,7 @@ from sqlalchemy.exc import OperationalError, ProgrammingError
 from flask import Flask, request
 from flask_login import current_user
 
-from flaskbb._compat import string_types
+from flaskbb._compat import string_types, iteritems
 # views
 from flaskbb.user.views import user
 from flaskbb.message.views import message
@@ -389,12 +389,14 @@ def load_plugins(app):
     # have to find all the flaskbb modules that are loaded this way
     # otherwise sys.modules might change while we're iterating it
     # because of imports and that makes Python very unhappy
-    flaskbb_modules = [
-        module for name, module in sys.modules.items()
+    # we are not interested in duplicated plugins or invalid ones
+    # ('None' - appears on py2) and thus using a set
+    flaskbb_modules = set(
+        module for name, module in iteritems(sys.modules)
         if name.startswith('flaskbb')
-    ]
+    )
     for module in flaskbb_modules:
-        app.pluggy.register(module)
+        app.pluggy.register(module, internal=True)
 
     try:
         with app.app_context():

+ 67 - 2
flaskbb/plugins/manager.py

@@ -31,10 +31,70 @@ class FlaskBBPluginManager(pluggy.PluginManager):
         self._plugin_metadata = {}
         self._disabled_plugins = []
 
+        # we maintain a seperate dict for flaskbb.* internal plugins
+        self._internal_name2plugin = {}
+
+    def register(self, plugin, name=None, internal=False):
+        """Register a plugin and return its canonical name or None
+        if the name is blocked from registering.
+        Raise a ValueError if the plugin is already registered.
+        """
+        # internal plugins are stored in self._plugin2hookcallers
+        name = super(FlaskBBPluginManager, self).register(plugin, name)
+        if not internal:
+            return name
+
+        self._internal_name2plugin[name] = self._name2plugin.pop(name)
+        return name
+
+    def unregister(self, plugin=None, name=None):
+        """Unregister a plugin object and all its contained hook implementations
+        from internal data structures.
+        """
+        plugin = super(FlaskBBPluginManager, self).unregister(
+            plugin=plugin, name=name
+        )
+
+        name = self.get_name(plugin)
+        if self._internal_name2plugin.get(name):
+            del self._internal_name2plugin[name]
+
+        return plugin
+
+    def set_blocked(self, name):
+        """Block registrations of the given name, unregister if already
+        registered.
+        """
+        super(FlaskBBPluginManager, self).set_blocked(name)
+        self._internal_name2plugin[name] = None
+
+    def is_blocked(self, name):
+        """Return True if the name blockss registering plugins of that name."""
+        blocked = super(FlaskBBPluginManager, self).is_blocked(name)
+
+        return blocked or name in self._internal_name2plugin and \
+            self._internal_name2plugin[name] is None
+
+    def get_plugin(self, name):
+        """Return a plugin or None for the given name. """
+        plugin = super(FlaskBBPluginManager, self).get_plugin(name)
+        return self._internal_name2plugin.get(name, plugin)
+
+    def get_name(self, plugin):
+        """Return name for registered plugin or None if not registered."""
+        name = super(FlaskBBPluginManager, self).get_name(plugin)
+        if name:
+            return name
+
+        for name, val in self._internal_name2plugin.items():
+            if plugin == val:
+                return name
+
     def load_setuptools_entrypoints(self, entrypoint_name):
         """Load modules from querying the specified setuptools entrypoint name.
         Return the number of loaded plugins. """
-        logger.info("Loading plugins under entrypoint {}".format(entrypoint_name))
+        logger.info("Loading plugins under entrypoint {}"
+                    .format(entrypoint_name))
         for ep in iter_entry_points(entrypoint_name):
             if self.get_plugin(ep.name):
                 continue
@@ -48,7 +108,8 @@ class FlaskBBPluginManager(pluggy.PluginManager):
             try:
                 plugin = ep.load()
             except DistributionNotFound:
-                logger.warn("Could not load plugin {}. Passing.".format(ep.name))
+                logger.warn("Could not load plugin {}. Passing."
+                            .format(ep.name))
                 continue
             except VersionConflict as e:
                 raise pluggy.PluginValidationError(
@@ -72,6 +133,10 @@ class FlaskBBPluginManager(pluggy.PluginManager):
         """Returns only the enabled plugin names."""
         return list(self._name2plugin.keys())
 
+    def list_internal_name_plugin(self):
+        """Returns a list of internal name/plugin pairs."""
+        return self._internal_name2plugin.items()
+
     def list_plugin_metadata(self):
         """Returns the metadata for all plugins"""
         return self._plugin_metadata

+ 1 - 0
flaskbb/utils/translations.py

@@ -41,6 +41,7 @@ class FlaskBBDomain(Domain):
         locale = get_locale()
         # now load and add the plugin translations
         for plugin in self.plugin_translations:
+            logger.debug("Loading plugin translation from: {}".format(plugin))
             plugin_translation = babel.support.Translations.load(
                 dirname=plugin,
                 locales=locale,

+ 1 - 0
tests/conftest.py

@@ -3,3 +3,4 @@ from tests.fixtures.forum import *  # noqa
 from tests.fixtures.user import *  # noqa
 from tests.fixtures.message import *  # noqa
 from tests.fixtures.settings_fixture import *  # noqa
+from tests.fixtures.plugin import *  # noqa

+ 7 - 0
tests/fixtures/plugin.py

@@ -0,0 +1,7 @@
+import pytest
+from flaskbb.plugins.manager import FlaskBBPluginManager
+
+
+@pytest.fixture
+def plugin_manager():
+    return FlaskBBPluginManager("flaskbb")

+ 108 - 0
tests/unit/test_pluginmanager.py

@@ -0,0 +1,108 @@
+# some tests have been taking from
+# https://github.com/pytest-dev/pluggy/blob/master/testing/test_pluginmanager.py
+# and are licensed under the MIT License.
+import pytest
+
+
+def test_pluginmanager(plugin_manager):
+    """Tests basic pluggy plugin registration."""
+    class A(object):
+        pass
+
+    a1, a2 = A(), A()
+    plugin_manager.register(a1)
+    assert plugin_manager.is_registered(a1)
+    plugin_manager.register(a2, "hello")
+    assert plugin_manager.is_registered(a2)
+
+    with pytest.raises(ValueError):
+        assert plugin_manager.register(a1, internal=True)
+
+    out = plugin_manager.get_plugins()
+    assert a1 in out
+    assert a2 in out
+    assert plugin_manager.get_plugin('hello') == a2
+    assert plugin_manager.unregister(a1) == a1
+    assert not plugin_manager.is_registered(a1)
+
+    out = plugin_manager.list_name_plugin()
+    assert len(out) == 1
+    assert out == [("hello", a2)]
+    assert plugin_manager.list_name() == ["hello"]
+
+
+def test_register_internal(plugin_manager):
+    """Tests registration of internal flaskbb plugins."""
+    class A(object):
+        pass
+
+    a1, a2 = A(), A()
+    plugin_manager.register(a1, "notinternal")
+    plugin_manager.register(a2, "internal", internal=True)
+    assert plugin_manager.is_registered(a2)
+
+    out = plugin_manager.list_name_plugin()
+    assert ('notinternal', a1) in out
+    assert ('internal', a2) not in out
+
+    out_internal = plugin_manager.list_internal_name_plugin()
+    assert ('notinternal', a1) not in out_internal
+    assert ('internal', a2) in out_internal
+
+    assert plugin_manager.unregister(a2) == a2
+    assert not plugin_manager.list_internal_name_plugin()  # should be empty
+
+
+def test_set_blocked(plugin_manager):
+    class A(object):
+        pass
+
+    a1 = A()
+    name = plugin_manager.register(a1)
+    assert plugin_manager.is_registered(a1)
+    assert not plugin_manager.is_blocked(name)
+    plugin_manager.set_blocked(name)
+    assert plugin_manager.is_blocked(name)
+    assert not plugin_manager.is_registered(a1)
+
+    plugin_manager.set_blocked("somename")
+    assert plugin_manager.is_blocked("somename")
+    assert not plugin_manager.register(A(), "somename")
+    plugin_manager.unregister(name="somename")
+    assert plugin_manager.is_blocked("somename")
+
+
+def test_set_blocked_internal(plugin_manager):
+    class A(object):
+        pass
+
+    a1 = A()
+    name = plugin_manager.register(a1, internal=True)
+    assert plugin_manager.is_registered(a1)
+    assert not plugin_manager.is_blocked(name)
+    plugin_manager.set_blocked(name)
+    assert plugin_manager.is_blocked(name)
+    assert not plugin_manager.is_registered(a1)
+
+
+def test_get_internal_plugin(plugin_manager):
+    class A(object):
+        pass
+
+    a1, a2 = A(), A()
+    plugin_manager.register(a1, "notinternal")
+    plugin_manager.register(a2, "internal", internal=True)
+    assert plugin_manager.get_plugin('notinternal') == a1
+    assert plugin_manager.get_plugin('internal') == a2
+
+
+def test_get_internal_name(plugin_manager):
+    class A(object):
+        pass
+
+    a1, a2 = A(), A()
+    plugin_manager.register(a1, "notinternal")
+    plugin_manager.register(a2, "internal", internal=True)
+
+    assert plugin_manager.get_name(a1) == "notinternal"
+    assert plugin_manager.get_name(a2) == "internal"

+ 5 - 40
tests/unit/utils/test_translations.py

@@ -1,49 +1,14 @@
-import subprocess
-import os
 from flask import current_app
-from babel.support import Translations, NullTranslations
+from babel.support import Translations
 from flaskbb.utils.translations import FlaskBBDomain
-import pytest
 
 
-def _remove_compiled_translations():
-    translations_folder = os.path.join(current_app.root_path, "translations")
-
-    # walks through the translations folder and deletes all files
-    # ending with .mo
-    for root, dirs, files in os.walk(translations_folder):
-        for name in files:
-            if name.endswith(".mo"):
-                os.unlink(os.path.join(root, name))
-
-
-def _compile_translations():
-    PLUGINS_FOLDER = os.path.join(current_app.root_path, "plugins")
-    translations_folder = os.path.join(current_app.root_path, "translations")
-
-    subprocess.call(["pybabel", "compile", "-d", translations_folder])
-
-    for plugin in plugin_manager.all_plugins:
-        plugin_folder = os.path.join(PLUGINS_FOLDER, plugin)
-        translations_folder = os.path.join(plugin_folder, "translations")
-        subprocess.call(["pybabel", "compile", "-d", translations_folder])
-
-
-@pytest.mark.skip(reason="Plugin transition")
 def test_flaskbbdomain_translations(default_settings):
-    domain = FlaskBBDomain(current_app)
+    domain = current_app.extensions.get("babel").domain
 
     with current_app.test_request_context():
+        # no translations accessed and thus the cache is empty
         assert domain.get_translations_cache() == {}
-
-        # just to be on the safe side that there are really no compiled
-        # translations available
-        _remove_compiled_translations()
-        # no compiled translations are available
-        assert isinstance(domain.get_translations(), NullTranslations)
-
-        # lets compile them and test again
-        _compile_translations()
-
-        # now there should be translations :)
+        # load translations into cache
         assert isinstance(domain.get_translations(), Translations)
+        assert len(domain.get_translations_cache()) == 1  # 'en'