Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix loading of periodic tasks and clean up extension loading. #4064

Merged
merged 3 commits into from Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/bundle-extensions
Expand Up @@ -63,7 +63,7 @@ def load_bundles():
└── styles.css

and would then need to register the bundle with an entry point
under the "redash.periodic_tasks" group, e.g. in your setup.py::
under the "redash.bundles" group, e.g. in your setup.py::

setup(
# ...
Expand Down
77 changes: 41 additions & 36 deletions redash/extensions.py
Expand Up @@ -13,45 +13,64 @@
# after the configuration has already happened.
periodic_tasks = odict()

logger = logging.getLogger(__name__)
extension_logger = logging.getLogger(__name__)


def load_extensions(app):
"""Load the Redash extensions for the given Redash Flask app.

The extension entry point can return any type of value but
must take a Flask application object.

E.g.::

def extension(app):
app.logger.info("Loading the Foobar extenions")
Foobar(app)
def entry_point_loader(group_name, mapping, logger=None, *args, **kwargs):
"""
Loads the list Python entry points with the given entry point group name
(e.g. "redash.extensions"), calls each with the provided *args/**kwargs
arguments and stores the results in the provided mapping under the name
of the entry point.

If provided, the logger is used for error and debugging statements.
"""
for entry_point in entry_points().get("redash.extensions", []):
app.logger.info('Loading Redash extension "%s".', entry_point.name)
if logger is None:
logger = extension_logger

for entry_point in entry_points().get(group_name, []):
logger.info('Loading entry point "%s".', entry_point.name)
try:
# Then try to load the entry point (import and getattr)
obj = entry_point.load()
except (ImportError, AttributeError):
# or move on
app.logger.error(
'Redash extension "%s" could not be found.', entry_point.name
logger.error(
'Entry point "%s" could not be found.', entry_point.name, exc_info=True
)
continue

if not callable(obj):
app.logger.error(
'Redash extension "%s" is not a callable.', entry_point.name
logger.error('Entry point "%s" is not a callable.', entry_point.name)
continue

try:
# then simply call the loaded entry point.
mapping[entry_point.name] = obj(*args, **kwargs)
except AssertionError:
logger.error(
'Entry point "%s" cound not be loaded.', entry_point.name, exc_info=True
)
continue

# then simply call the loaded entry point.
extensions[entry_point.name] = obj(app)

def load_extensions(app):
"""Load the Redash extensions for the given Redash Flask app.

The extension entry point can return any type of value but
must take a Flask application object.

E.g.::

def extension(app):
app.logger.info("Loading the Foobar extenions")
Foobar(app)

"""
entry_point_loader("redash.extensions", extensions, logger=app.logger, app=app)

def load_periodic_tasks(logger):

def load_periodic_tasks(logger=None):
"""Load the periodic tasks as defined in Redash extensions.

The periodic task entry point needs to return a set of parameters
Expand Down Expand Up @@ -82,21 +101,7 @@ def add_two_and_two():
# ...
)
"""
for entry_point in entry_points().get("redash.periodic_tasks", []):
logger.info(
'Loading periodic Redash tasks "%s" from "%s".',
entry_point.name,
entry_point.value,
)
try:
periodic_tasks[entry_point.name] = entry_point.load()
except (ImportError, AttributeError):
# and move on if it couldn't load it
logger.error(
'Periodic Redash task "%s" could not be found at "%s".',
entry_point.name,
entry_point.value,
)
entry_point_loader("redash.periodic_tasks", periodic_tasks, logger=logger)


def init_app(app):
Expand Down
Empty file added tests/extensions/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions tests/extensions/redash-dummy/redash_dummy.egg-info/PKG-INFO
@@ -0,0 +1,10 @@
Metadata-Version: 1.0
Name: redash-dummy
Version: 0.1
Summary: Redash extensions for testing
Home-page: UNKNOWN
Author: Redash authors
Author-email: UNKNOWN
License: MIT
Description: UNKNOWN
Platform: UNKNOWN
@@ -0,0 +1,6 @@
redash_dummy.py
redash_dummy.egg-info/PKG-INFO
redash_dummy.egg-info/SOURCES.txt
redash_dummy.egg-info/dependency_links.txt
redash_dummy.egg-info/entry_points.txt
redash_dummy.egg-info/top_level.txt
@@ -0,0 +1 @@

@@ -0,0 +1,10 @@
[redash.extensions]
assertive_extension = redash_dummy:assertive_extension
non_callable_extension = redash_dummy:module_attribute
not_findable_extension = redash_dummy:missing_attribute
not_importable_extension = missing_extension_module:extension
working_extension = redash_dummy:extension

[redash.periodic_tasks]
dummy_periodic_task = redash_dummy:periodic_task

@@ -0,0 +1 @@
redash_dummy
15 changes: 15 additions & 0 deletions tests/extensions/redash-dummy/redash_dummy.py
@@ -0,0 +1,15 @@
module_attribute = "hello!"


def extension(app):
"""This extension will work"""
return "extension loaded"


def assertive_extension(app):
"""This extension won't work"""
assert False


def periodic_task(*args, **kwargs):
"""This periodic task will successfully load"""
23 changes: 23 additions & 0 deletions tests/extensions/redash-dummy/setup.py
@@ -0,0 +1,23 @@
from setuptools import setup


setup(
name="redash-dummy",
version="0.1",
description="Redash extensions for testing",
author="Redash authors",
license="MIT",
entry_points={
"redash.extensions": [
"working_extension = redash_dummy:extension",
"non_callable_extension = redash_dummy:module_attribute",
"not_findable_extension = redash_dummy:missing_attribute",
"not_importable_extension = missing_extension_module:extension",
"assertive_extension = redash_dummy:assertive_extension",
],
"redash.periodic_tasks": [
"dummy_periodic_task = redash_dummy:periodic_task"
],
},
py_modules=["redash_dummy"],
)
42 changes: 42 additions & 0 deletions tests/extensions/test_extensions.py
@@ -0,0 +1,42 @@
import logging
import os
import site

from redash import extensions
from tests import BaseTestCase

logger = logging.getLogger(__name__)
here = os.path.dirname(__file__)
dummy_extension = "redash-dummy"


class TestExtensions(BaseTestCase):
@classmethod
def setUpClass(cls):
dummy_path = os.path.join(here, dummy_extension)
site.addsitedir(dummy_path)
jezdez marked this conversation as resolved.
Show resolved Hide resolved

def test_working_extension(self):
self.assertIn("working_extension", extensions.extensions.keys())
self.assertEqual(
extensions.extensions.get("working_extension"), "extension loaded"
)

def test_assertive_extension(self):
self.assertNotIn("assertive_extension", extensions.extensions.keys())

def test_not_findable_extension(self):
self.assertNotIn("not_findable_extension", extensions.extensions.keys())

def test_not_importable_extension(self):
self.assertNotIn("not_importable_extension", extensions.extensions.keys())

def test_non_callable_extension(self):
self.assertNotIn("non_callable_extension", extensions.extensions.keys())

def test_dummy_periodic_task(self):
# need to load the periodic tasks manually since this isn't
# done automatically on test suite start but only part of
# the worker configuration
extensions.load_periodic_tasks(logger)
self.assertIn("dummy_periodic_task", extensions.periodic_tasks.keys())