Merge lp:~phill-ridout/openlp/import-depreciations into lp:openlp

Proposed by Phill
Status: Superseded
Proposed branch: lp:~phill-ridout/openlp/import-depreciations
Merge into: lp:openlp
Diff against target: 346 lines (+118/-64)
10 files modified
openlp/core/common/__init__.py (+34/-1)
openlp/core/lib/pluginmanager.py (+3/-28)
openlp/core/ui/media/mediacontroller.py (+5/-13)
openlp/plugins/presentations/lib/impresscontroller.py (+2/-1)
openlp/plugins/presentations/lib/pdfcontroller.py (+1/-1)
openlp/plugins/presentations/lib/powerpointcontroller.py (+1/-1)
openlp/plugins/presentations/lib/pptviewcontroller.py (+1/-1)
openlp/plugins/presentations/lib/presentationtab.py (+1/-1)
openlp/plugins/presentations/presentationplugin.py (+5/-14)
tests/functional/openlp_core_common/test_common.py (+65/-3)
To merge this branch: bzr merge lp:~phill-ridout/openlp/import-depreciations
Reviewer Review Type Date Requested Status
Tim Bentley Needs Fixing
Review via email: mp+324024@code.launchpad.net

This proposal has been superseded by a proposal from 2017-05-14.

Description of the change

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote :

Looks good but has issues with trunk!

review: Needs Fixing
2733. By Phill

Update and refactor dynamic import code

2734. By Phill

Reworked the extension_loader function

2735. By Phill

head

2736. By Phill

pep

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/common/__init__.py'
2--- openlp/core/common/__init__.py 2016-12-31 11:01:36 +0000
3+++ openlp/core/common/__init__.py 2017-05-14 07:11:54 +0000
4@@ -23,8 +23,9 @@
5 The :mod:`common` module contains most of the components and libraries that make
6 OpenLP work.
7 """
8+import glob
9 import hashlib
10-
11+import importlib
12 import logging
13 import os
14 import re
15@@ -79,6 +80,38 @@
16 log.exception('failed to check if directory exists or create directory')
17
18
19+def extension_loader(glob_pattern, excluded_files=[]):
20+ """
21+ A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
22+ importers.
23+
24+ :param glob_pattern: A glob pattern used to find the extension(s) to be imported.
25+ i.e. openlp_app_dir/plugins/*/*plugin.py
26+ :type glob_pattern: str
27+ :param excluded_files: A list of file names to exclude that the glob pattern may find.
28+ :type excluded_files: list of strings
29+
30+ :return: None
31+ :rtype: None
32+ """
33+ for extension_path in glob.iglob(glob_pattern):
34+ filename = os.path.split(extension_path)[1]
35+ if filename in excluded_files:
36+ continue
37+ module_name = os.path.splitext(filename)[0]
38+ try:
39+ loader = importlib.machinery.SourceFileLoader(module_name, extension_path)
40+ loader.load_module()
41+ # TODO: A better way to do this (once we drop python 3.4 support)
42+ # spec = importlib.util.spec_from_file_location('what.ever', 'foo.py')
43+ # module = importlib.util.module_from_spec(spec)
44+ # spec.loader.exec_module(module)
45+ except (ImportError, OSError):
46+ # On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
47+ log.warning('Failed to import {module_name} on path {extension_path}'
48+ .format(module_name=module_name, extension_path=extension_path))
49+
50+
51 def get_frozen_path(frozen_option, non_frozen_option):
52 """
53 Return a path based on the system status.
54
55=== modified file 'openlp/core/lib/pluginmanager.py'
56--- openlp/core/lib/pluginmanager.py 2016-12-31 11:01:36 +0000
57+++ openlp/core/lib/pluginmanager.py 2017-05-14 07:11:54 +0000
58@@ -23,10 +23,9 @@
59 Provide plugin management
60 """
61 import os
62-import imp
63
64 from openlp.core.lib import Plugin, PluginStatus
65-from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin
66+from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin, extension_loader
67
68
69 class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
70@@ -70,32 +69,8 @@
71 """
72 Scan a directory for objects inheriting from the ``Plugin`` class.
73 """
74- start_depth = len(os.path.abspath(self.base_path).split(os.sep))
75- present_plugin_dir = os.path.join(self.base_path, 'presentations')
76- self.log_debug('finding plugins in {path} at depth {depth:d}'.format(path=self.base_path, depth=start_depth))
77- for root, dirs, files in os.walk(self.base_path):
78- for name in files:
79- if name.endswith('.py') and not name.startswith('__'):
80- path = os.path.abspath(os.path.join(root, name))
81- this_depth = len(path.split(os.sep))
82- if this_depth - start_depth > 2:
83- # skip anything lower down
84- break
85- module_name = name[:-3]
86- # import the modules
87- self.log_debug('Importing {name} from {root}. Depth {depth:d}'.format(name=module_name,
88- root=root,
89- depth=this_depth))
90- try:
91- # Use the "imp" library to try to get around a problem with the PyUNO library which
92- # monkey-patches the __import__ function to do some magic. This causes issues with our tests.
93- # First, try to find the module we want to import, searching the directory in root
94- fp, path_name, description = imp.find_module(module_name, [root])
95- # Then load the module (do the actual import) using the details from find_module()
96- imp.load_module(module_name, fp, path_name, description)
97- except ImportError as e:
98- self.log_exception('Failed to import module {name} on path {path}: '
99- '{args}'.format(name=module_name, path=path, args=e.args[0]))
100+ glob_pattern = os.path.join(self.base_path, '*', '*plugin.py')
101+ extension_loader(glob_pattern)
102 plugin_classes = Plugin.__subclasses__()
103 plugin_objects = []
104 for p in plugin_classes:
105
106=== modified file 'openlp/core/ui/media/mediacontroller.py'
107--- openlp/core/ui/media/mediacontroller.py 2016-12-31 11:01:36 +0000
108+++ openlp/core/ui/media/mediacontroller.py 2017-05-14 07:11:54 +0000
109@@ -28,7 +28,8 @@
110 import datetime
111 from PyQt5 import QtCore, QtWidgets
112
113-from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate
114+from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
115+ extension_loader, translate
116 from openlp.core.lib import ItemCapabilities
117 from openlp.core.lib.ui import critical_error_message_box
118 from openlp.core.common import AppLocation
119@@ -39,6 +40,7 @@
120 parse_optical_path
121 from openlp.core.ui.lib.toolbar import OpenLPToolbar
122
123+
124 log = logging.getLogger(__name__)
125
126 TICK_TIME = 200
127@@ -173,18 +175,8 @@
128 """
129 log.debug('_check_available_media_players')
130 controller_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'ui', 'media')
131- for filename in os.listdir(controller_dir):
132- if filename.endswith('player.py') and filename != 'mediaplayer.py':
133- path = os.path.join(controller_dir, filename)
134- if os.path.isfile(path):
135- module_name = 'openlp.core.ui.media.' + os.path.splitext(filename)[0]
136- log.debug('Importing controller %s', module_name)
137- try:
138- __import__(module_name, globals(), locals(), [])
139- # On some platforms importing vlc.py might cause
140- # also OSError exceptions. (e.g. Mac OS X)
141- except (ImportError, OSError):
142- log.warning('Failed to import %s on path %s', module_name, path)
143+ glob_pattern = os.path.join(controller_dir, '*player.py')
144+ extension_loader(glob_pattern, ['mediaplayer.py'])
145 player_classes = MediaPlayer.__subclasses__()
146 for player_class in player_classes:
147 self.register_players(player_class(self))
148
149=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
150--- openlp/plugins/presentations/lib/impresscontroller.py 2016-12-31 11:01:36 +0000
151+++ openlp/plugins/presentations/lib/impresscontroller.py 2017-05-14 07:11:54 +0000
152@@ -58,7 +58,8 @@
153
154 from openlp.core.lib import ScreenList
155 from openlp.core.common import get_uno_command, get_uno_instance
156-from .presentationcontroller import PresentationController, PresentationDocument, TextType
157+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
158+ TextType
159
160
161 log = logging.getLogger(__name__)
162
163=== modified file 'openlp/plugins/presentations/lib/pdfcontroller.py'
164--- openlp/plugins/presentations/lib/pdfcontroller.py 2016-12-31 11:01:36 +0000
165+++ openlp/plugins/presentations/lib/pdfcontroller.py 2017-05-14 07:11:54 +0000
166@@ -29,7 +29,7 @@
167 from openlp.core.common import AppLocation, check_binary_exists
168 from openlp.core.common import Settings, is_win
169 from openlp.core.lib import ScreenList
170-from .presentationcontroller import PresentationController, PresentationDocument
171+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
172
173 if is_win():
174 from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
175
176=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
177--- openlp/plugins/presentations/lib/powerpointcontroller.py 2016-12-31 11:01:36 +0000
178+++ openlp/plugins/presentations/lib/powerpointcontroller.py 2017-05-14 07:11:54 +0000
179@@ -43,7 +43,7 @@
180 from openlp.core.lib import ScreenList
181 from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
182 from openlp.core.common import trace_error_handler, Registry
183-from .presentationcontroller import PresentationController, PresentationDocument
184+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
185
186 log = logging.getLogger(__name__)
187
188
189=== modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
190--- openlp/plugins/presentations/lib/pptviewcontroller.py 2016-12-31 11:01:36 +0000
191+++ openlp/plugins/presentations/lib/pptviewcontroller.py 2017-05-14 07:11:54 +0000
192@@ -35,7 +35,7 @@
193
194 from openlp.core.common import AppLocation
195 from openlp.core.lib import ScreenList
196-from .presentationcontroller import PresentationController, PresentationDocument
197+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
198
199
200 log = logging.getLogger(__name__)
201
202=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
203--- openlp/plugins/presentations/lib/presentationtab.py 2016-12-31 11:01:36 +0000
204+++ openlp/plugins/presentations/lib/presentationtab.py 2017-05-14 07:11:54 +0000
205@@ -25,7 +25,7 @@
206 from openlp.core.common import Settings, UiStrings, translate
207 from openlp.core.lib import SettingsTab, build_icon
208 from openlp.core.lib.ui import critical_error_message_box
209-from .pdfcontroller import PdfController
210+from openlp.plugins.presentations.lib.pdfcontroller import PdfController
211
212
213 class PresentationTab(SettingsTab):
214
215=== modified file 'openlp/plugins/presentations/presentationplugin.py'
216--- openlp/plugins/presentations/presentationplugin.py 2016-12-31 11:01:36 +0000
217+++ openlp/plugins/presentations/presentationplugin.py 2017-05-14 07:11:54 +0000
218@@ -20,19 +20,18 @@
219 # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
220 ###############################################################################
221 """
222-The :mod:`presentationplugin` module provides the ability for OpenLP to display presentations from a variety of document
223-formats.
224+The :mod:`openlp.plugins.presentations.presentationplugin` module provides the ability for OpenLP to display
225+presentations from a variety of document formats.
226 """
227 import os
228 import logging
229
230 from PyQt5 import QtCore
231
232-from openlp.core.common import AppLocation, translate
233+from openlp.core.common import AppLocation, extension_loader, translate
234 from openlp.core.lib import Plugin, StringContent, build_icon
235 from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
236
237-
238 log = logging.getLogger(__name__)
239
240
241@@ -123,16 +122,8 @@
242 """
243 log.debug('check_pre_conditions')
244 controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')
245- for filename in os.listdir(controller_dir):
246- if filename.endswith('controller.py') and filename != 'presentationcontroller.py':
247- path = os.path.join(controller_dir, filename)
248- if os.path.isfile(path):
249- module_name = 'openlp.plugins.presentations.lib.' + os.path.splitext(filename)[0]
250- log.debug('Importing controller {name}'.format(name=module_name))
251- try:
252- __import__(module_name, globals(), locals(), [])
253- except ImportError:
254- log.warning('Failed to import {name} on path {path}'.format(name=module_name, path=path))
255+ glob_pattern = os.path.join(controller_dir, '*controller.py')
256+ extension_loader(glob_pattern, ['presentationcontroller.py'])
257 controller_classes = PresentationController.__subclasses__()
258 for controller_class in controller_classes:
259 controller = controller_class(self)
260
261=== modified file 'tests/functional/openlp_core_common/test_common.py'
262--- tests/functional/openlp_core_common/test_common.py 2017-04-24 05:17:55 +0000
263+++ tests/functional/openlp_core_common/test_common.py 2017-05-14 07:11:54 +0000
264@@ -23,10 +23,10 @@
265 Functional tests to test the AppLocation class and related methods.
266 """
267 from unittest import TestCase
268-from unittest.mock import MagicMock, patch
269+from unittest.mock import MagicMock, call, patch
270
271-from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate, is_win, is_macosx, \
272- is_linux, clean_button_text
273+from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \
274+ is_linux, is_win, trace_error_handler, translate
275
276
277 class TestCommonFunctions(TestCase):
278@@ -72,6 +72,68 @@
279 mocked_exists.assert_called_with(directory_to_check)
280 self.assertRaises(ValueError, check_directory_exists, directory_to_check)
281
282+ def test_extension_loader_no_files_found(self):
283+ """
284+ Test the `extension_loader` function when no files are found
285+ """
286+ # GIVEN: A mocked `iglob` function which does not match any files
287+ with patch('openlp.core.common.glob.iglob', return_value=[]), \
288+ patch('openlp.core.common.importlib.machinery.SourceFileLoader') as mocked_source_file_loader:
289+
290+ # WHEN: Calling `extension_loader`
291+ extension_loader('glob', ['file2.py', 'file3.py'])
292+
293+ # THEN: `extension_loader` should not try to import any files
294+ self.assertFalse(mocked_source_file_loader.called)
295+
296+ def test_extension_loader_files_found(self):
297+ """
298+ Test the `extension_loader` function when it successfully finds and loads some files
299+ """
300+ # GIVEN: A mocked `iglob` function which returns a list of files
301+ with patch('openlp.core.common.glob.iglob', return_value=['import_dir/file1.py', 'import_dir/file2.py',
302+ 'import_dir/file3.py', 'import_dir/file4.py']), \
303+ patch('openlp.core.common.importlib.machinery.SourceFileLoader') as mocked_source_file_loader:
304+
305+ # WHEN: Calling `extension_loader` with a list of files to exclude
306+ extension_loader('glob', ['file2.py', 'file3.py'])
307+
308+ # THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the
309+ # files listed in the `excluded_files` argument
310+ mocked_source_file_loader.assert_has_calls([call('file1', 'import_dir/file1.py'), call().load_module(),
311+ call('file4', 'import_dir/file4.py'), call().load_module()])
312+
313+ def test_extension_loader_import_error(self):
314+ """
315+ Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError`
316+ """
317+ # GIVEN: A mocked `SourceFileLoader` which raises an `ImportError`
318+ with patch('openlp.core.common.glob.iglob', return_value=['import_dir/file1.py', 'import_dir/file2.py',
319+ 'import_dir/file3.py', 'import_dir/file4.py']), \
320+ patch('openlp.core.common.importlib.machinery.SourceFileLoader', side_effect=ImportError()), \
321+ patch('openlp.core.common.log') as mocked_logger:
322+
323+ # WHEN: Calling `extension_loader`
324+ extension_loader('glob')
325+
326+ # THEN: The `ImportError` should be caught and logged
327+ self.assertTrue(mocked_logger.warning.called)
328+
329+ def test_extension_loader_os_error(self):
330+ """
331+ Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError`
332+ """
333+ # GIVEN: A mocked `SourceFileLoader` which raises an `OSError`
334+ with patch('openlp.core.common.glob.iglob', return_value=['import_dir/file1.py']), \
335+ patch('openlp.core.common.importlib.machinery.SourceFileLoader', side_effect=OSError()), \
336+ patch('openlp.core.common.log') as mocked_logger:
337+
338+ # WHEN: Calling `extension_loader`
339+ extension_loader('glob')
340+
341+ # THEN: The `OSError` should be caught and logged
342+ self.assertTrue(mocked_logger.warning.called)
343+
344 def test_de_hump_conversion(self):
345 """
346 Test the de_hump function with a class name