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 import Flask, request
 from flask_login import current_user
 from flask_login import current_user
 
 
-from flaskbb._compat import string_types
+from flaskbb._compat import string_types, iteritems
 # views
 # views
 from flaskbb.user.views import user
 from flaskbb.user.views import user
 from flaskbb.message.views import message
 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
     # have to find all the flaskbb modules that are loaded this way
     # otherwise sys.modules might change while we're iterating it
     # otherwise sys.modules might change while we're iterating it
     # because of imports and that makes Python very unhappy
     # 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')
         if name.startswith('flaskbb')
-    ]
+    )
     for module in flaskbb_modules:
     for module in flaskbb_modules:
-        app.pluggy.register(module)
+        app.pluggy.register(module, internal=True)
 
 
     try:
     try:
         with app.app_context():
         with app.app_context():

+ 67 - 2
flaskbb/plugins/manager.py

@@ -31,10 +31,70 @@ class FlaskBBPluginManager(pluggy.PluginManager):
         self._plugin_metadata = {}
         self._plugin_metadata = {}
         self._disabled_plugins = []
         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):
     def load_setuptools_entrypoints(self, entrypoint_name):
         """Load modules from querying the specified setuptools entrypoint name.
         """Load modules from querying the specified setuptools entrypoint name.
         Return the number of loaded plugins. """
         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):
         for ep in iter_entry_points(entrypoint_name):
             if self.get_plugin(ep.name):
             if self.get_plugin(ep.name):
                 continue
                 continue
@@ -48,7 +108,8 @@ class FlaskBBPluginManager(pluggy.PluginManager):
             try:
             try:
                 plugin = ep.load()
                 plugin = ep.load()
             except DistributionNotFound:
             except DistributionNotFound:
-                logger.warn("Could not load plugin {}. Passing.".format(ep.name))
+                logger.warn("Could not load plugin {}. Passing."
+                            .format(ep.name))
                 continue
                 continue
             except VersionConflict as e:
             except VersionConflict as e:
                 raise pluggy.PluginValidationError(
                 raise pluggy.PluginValidationError(
@@ -72,6 +133,10 @@ class FlaskBBPluginManager(pluggy.PluginManager):
         """Returns only the enabled plugin names."""
         """Returns only the enabled plugin names."""
         return list(self._name2plugin.keys())
         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):
     def list_plugin_metadata(self):
         """Returns the metadata for all plugins"""
         """Returns the metadata for all plugins"""
         return self._plugin_metadata
         return self._plugin_metadata

+ 1 - 0
flaskbb/utils/translations.py

@@ -41,6 +41,7 @@ class FlaskBBDomain(Domain):
         locale = get_locale()
         locale = get_locale()
         # now load and add the plugin translations
         # now load and add the plugin translations
         for plugin in self.plugin_translations:
         for plugin in self.plugin_translations:
+            logger.debug("Loading plugin translation from: {}".format(plugin))
             plugin_translation = babel.support.Translations.load(
             plugin_translation = babel.support.Translations.load(
                 dirname=plugin,
                 dirname=plugin,
                 locales=locale,
                 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.user import *  # noqa
 from tests.fixtures.message import *  # noqa
 from tests.fixtures.message import *  # noqa
 from tests.fixtures.settings_fixture 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 flask import current_app
-from babel.support import Translations, NullTranslations
+from babel.support import Translations
 from flaskbb.utils.translations import FlaskBBDomain
 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):
 def test_flaskbbdomain_translations(default_settings):
-    domain = FlaskBBDomain(current_app)
+    domain = current_app.extensions.get("babel").domain
 
 
     with current_app.test_request_context():
     with current_app.test_request_context():
+        # no translations accessed and thus the cache is empty
         assert domain.get_translations_cache() == {}
         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 isinstance(domain.get_translations(), Translations)
+        assert len(domain.get_translations_cache()) == 1  # 'en'