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: 405 lines (+152/-68)
10 files modified
openlp/core/common/__init__.py (+45/-1)
openlp/core/lib/pluginmanager.py (+3/-28)
openlp/core/ui/media/mediacontroller.py (+6/-14)
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 (+6/-15)
tests/functional/openlp_core_common/test_common.py (+86/-5)
To merge this branch: bzr merge lp:~phill-ridout/openlp/import-depreciations
Reviewer Review Type Date Requested Status
Phill Needs Fixing
Tim Bentley Pending
Review via email: mp+324025@code.launchpad.net

This proposal supersedes a proposal from 2017-05-13.

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

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

Looks good but has issues with trunk!

review: Needs Fixing
Revision history for this message
Phill (phill-ridout) wrote :

I had merged with trunk, but forgot to update trunk!

Revision history for this message
Phill (phill-ridout) :
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
=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py 2016-12-31 11:01:36 +0000
+++ openlp/core/common/__init__.py 2017-05-15 10:24:47 +0000
@@ -24,7 +24,7 @@
24OpenLP work.24OpenLP work.
25"""25"""
26import hashlib26import hashlib
2727import importlib
28import logging28import logging
29import os29import os
30import re30import re
@@ -32,6 +32,7 @@
32import traceback32import traceback
33from chardet.universaldetector import UniversalDetector33from chardet.universaldetector import UniversalDetector
34from ipaddress import IPv4Address, IPv6Address, AddressValueError34from ipaddress import IPv4Address, IPv6Address, AddressValueError
35from pathlib import Path
35from shutil import which36from shutil import which
36from subprocess import check_output, CalledProcessError, STDOUT37from subprocess import check_output, CalledProcessError, STDOUT
3738
@@ -79,6 +80,49 @@
79 log.exception('failed to check if directory exists or create directory')80 log.exception('failed to check if directory exists or create directory')
8081
8182
83def extension_loader(glob_pattern, excluded_files=[]):
84 """
85 A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
86 importers.
87
88 :param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
89 application directory. i.e. openlp/plugins/*/*plugin.py
90 :type glob_pattern: str
91
92 :param excluded_files: A list of file names to exclude that the glob pattern may find.
93 :type excluded_files: list of strings
94
95 :return: None
96 :rtype: None
97 """
98 app_dir = Path(AppLocation.get_directory(AppLocation.AppDir)).parent
99 for extension_path in app_dir.glob(glob_pattern):
100 extension_path = extension_path.relative_to(app_dir)
101 if extension_path.name in excluded_files:
102 continue
103 module_name = path_to_module(extension_path)
104 try:
105 importlib.import_module(module_name)
106 except (ImportError, OSError):
107 # On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
108 log.warning('Failed to import {module_name} on path {extension_path}'
109 .format(module_name=module_name, extension_path=str(extension_path)))
110
111
112def path_to_module(path):
113 """
114 Convert a path to a module name (i.e openlp.core.common)
115
116 :param path: The path to convert to a module name.
117 :type path: Path
118
119 :return: The module name.
120 :rtype: str
121 """
122 module_path = path.with_suffix('')
123 return '.'.join(module_path.parts)
124
125
82def get_frozen_path(frozen_option, non_frozen_option):126def get_frozen_path(frozen_option, non_frozen_option):
83 """127 """
84 Return a path based on the system status.128 Return a path based on the system status.
85129
=== modified file 'openlp/core/lib/pluginmanager.py'
--- openlp/core/lib/pluginmanager.py 2016-12-31 11:01:36 +0000
+++ openlp/core/lib/pluginmanager.py 2017-05-15 10:24:47 +0000
@@ -23,10 +23,9 @@
23Provide plugin management23Provide plugin management
24"""24"""
25import os25import os
26import imp
2726
28from openlp.core.lib import Plugin, PluginStatus27from openlp.core.lib import Plugin, PluginStatus
29from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin28from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin, extension_loader
3029
3130
32class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):31class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
@@ -70,32 +69,8 @@
70 """69 """
71 Scan a directory for objects inheriting from the ``Plugin`` class.70 Scan a directory for objects inheriting from the ``Plugin`` class.
72 """71 """
73 start_depth = len(os.path.abspath(self.base_path).split(os.sep))72 glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
74 present_plugin_dir = os.path.join(self.base_path, 'presentations')73 extension_loader(glob_pattern)
75 self.log_debug('finding plugins in {path} at depth {depth:d}'.format(path=self.base_path, depth=start_depth))
76 for root, dirs, files in os.walk(self.base_path):
77 for name in files:
78 if name.endswith('.py') and not name.startswith('__'):
79 path = os.path.abspath(os.path.join(root, name))
80 this_depth = len(path.split(os.sep))
81 if this_depth - start_depth > 2:
82 # skip anything lower down
83 break
84 module_name = name[:-3]
85 # import the modules
86 self.log_debug('Importing {name} from {root}. Depth {depth:d}'.format(name=module_name,
87 root=root,
88 depth=this_depth))
89 try:
90 # Use the "imp" library to try to get around a problem with the PyUNO library which
91 # monkey-patches the __import__ function to do some magic. This causes issues with our tests.
92 # First, try to find the module we want to import, searching the directory in root
93 fp, path_name, description = imp.find_module(module_name, [root])
94 # Then load the module (do the actual import) using the details from find_module()
95 imp.load_module(module_name, fp, path_name, description)
96 except ImportError as e:
97 self.log_exception('Failed to import module {name} on path {path}: '
98 '{args}'.format(name=module_name, path=path, args=e.args[0]))
99 plugin_classes = Plugin.__subclasses__()74 plugin_classes = Plugin.__subclasses__()
100 plugin_objects = []75 plugin_objects = []
101 for p in plugin_classes:76 for p in plugin_classes:
10277
=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py 2016-12-31 11:01:36 +0000
+++ openlp/core/ui/media/mediacontroller.py 2017-05-15 10:24:47 +0000
@@ -28,7 +28,8 @@
28import datetime28import datetime
29from PyQt5 import QtCore, QtWidgets29from PyQt5 import QtCore, QtWidgets
3030
31from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate31from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
32 extension_loader, translate
32from openlp.core.lib import ItemCapabilities33from openlp.core.lib import ItemCapabilities
33from openlp.core.lib.ui import critical_error_message_box34from openlp.core.lib.ui import critical_error_message_box
34from openlp.core.common import AppLocation35from openlp.core.common import AppLocation
@@ -39,6 +40,7 @@
39 parse_optical_path40 parse_optical_path
40from openlp.core.ui.lib.toolbar import OpenLPToolbar41from openlp.core.ui.lib.toolbar import OpenLPToolbar
4142
43
42log = logging.getLogger(__name__)44log = logging.getLogger(__name__)
4345
44TICK_TIME = 20046TICK_TIME = 200
@@ -172,19 +174,9 @@
172 Check to see if we have any media Player's available.174 Check to see if we have any media Player's available.
173 """175 """
174 log.debug('_check_available_media_players')176 log.debug('_check_available_media_players')
175 controller_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'ui', 'media')177 controller_dir = os.path.join('openlp', 'core', 'ui', 'media')
176 for filename in os.listdir(controller_dir):178 glob_pattern = os.path.join(controller_dir, '*player.py')
177 if filename.endswith('player.py') and filename != 'mediaplayer.py':179 extension_loader(glob_pattern, ['mediaplayer.py'])
178 path = os.path.join(controller_dir, filename)
179 if os.path.isfile(path):
180 module_name = 'openlp.core.ui.media.' + os.path.splitext(filename)[0]
181 log.debug('Importing controller %s', module_name)
182 try:
183 __import__(module_name, globals(), locals(), [])
184 # On some platforms importing vlc.py might cause
185 # also OSError exceptions. (e.g. Mac OS X)
186 except (ImportError, OSError):
187 log.warning('Failed to import %s on path %s', module_name, path)
188 player_classes = MediaPlayer.__subclasses__()180 player_classes = MediaPlayer.__subclasses__()
189 for player_class in player_classes:181 for player_class in player_classes:
190 self.register_players(player_class(self))182 self.register_players(player_class(self))
191183
=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py 2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py 2017-05-15 10:24:47 +0000
@@ -58,7 +58,8 @@
5858
59from openlp.core.lib import ScreenList59from openlp.core.lib import ScreenList
60from openlp.core.common import get_uno_command, get_uno_instance60from openlp.core.common import get_uno_command, get_uno_instance
61from .presentationcontroller import PresentationController, PresentationDocument, TextType61from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
62 TextType
6263
6364
64log = logging.getLogger(__name__)65log = logging.getLogger(__name__)
6566
=== modified file 'openlp/plugins/presentations/lib/pdfcontroller.py'
--- openlp/plugins/presentations/lib/pdfcontroller.py 2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/pdfcontroller.py 2017-05-15 10:24:47 +0000
@@ -29,7 +29,7 @@
29from openlp.core.common import AppLocation, check_binary_exists29from openlp.core.common import AppLocation, check_binary_exists
30from openlp.core.common import Settings, is_win30from openlp.core.common import Settings, is_win
31from openlp.core.lib import ScreenList31from openlp.core.lib import ScreenList
32from .presentationcontroller import PresentationController, PresentationDocument32from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
3333
34if is_win():34if is_win():
35 from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW35 from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
3636
=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py 2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py 2017-05-15 10:24:47 +0000
@@ -43,7 +43,7 @@
43from openlp.core.lib import ScreenList43from openlp.core.lib import ScreenList
44from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate44from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
45from openlp.core.common import trace_error_handler, Registry45from openlp.core.common import trace_error_handler, Registry
46from .presentationcontroller import PresentationController, PresentationDocument46from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
4747
48log = logging.getLogger(__name__)48log = logging.getLogger(__name__)
4949
5050
=== modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
--- openlp/plugins/presentations/lib/pptviewcontroller.py 2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/pptviewcontroller.py 2017-05-15 10:24:47 +0000
@@ -35,7 +35,7 @@
3535
36from openlp.core.common import AppLocation36from openlp.core.common import AppLocation
37from openlp.core.lib import ScreenList37from openlp.core.lib import ScreenList
38from .presentationcontroller import PresentationController, PresentationDocument38from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
3939
4040
41log = logging.getLogger(__name__)41log = logging.getLogger(__name__)
4242
=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
--- openlp/plugins/presentations/lib/presentationtab.py 2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py 2017-05-15 10:24:47 +0000
@@ -25,7 +25,7 @@
25from openlp.core.common import Settings, UiStrings, translate25from openlp.core.common import Settings, UiStrings, translate
26from openlp.core.lib import SettingsTab, build_icon26from openlp.core.lib import SettingsTab, build_icon
27from openlp.core.lib.ui import critical_error_message_box27from openlp.core.lib.ui import critical_error_message_box
28from .pdfcontroller import PdfController28from openlp.plugins.presentations.lib.pdfcontroller import PdfController
2929
3030
31class PresentationTab(SettingsTab):31class PresentationTab(SettingsTab):
3232
=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py 2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/presentationplugin.py 2017-05-15 10:24:47 +0000
@@ -20,19 +20,18 @@
20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
21###############################################################################21###############################################################################
22"""22"""
23The :mod:`presentationplugin` module provides the ability for OpenLP to display presentations from a variety of document23The :mod:`openlp.plugins.presentations.presentationplugin` module provides the ability for OpenLP to display
24formats.24presentations from a variety of document formats.
25"""25"""
26import os26import os
27import logging27import logging
2828
29from PyQt5 import QtCore29from PyQt5 import QtCore
3030
31from openlp.core.common import AppLocation, translate31from openlp.core.common import AppLocation, extension_loader, translate
32from openlp.core.lib import Plugin, StringContent, build_icon32from openlp.core.lib import Plugin, StringContent, build_icon
33from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab33from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
3434
35
36log = logging.getLogger(__name__)35log = logging.getLogger(__name__)
3736
3837
@@ -122,17 +121,9 @@
122 Check to see if we have any presentation software available. If not do not install the plugin.121 Check to see if we have any presentation software available. If not do not install the plugin.
123 """122 """
124 log.debug('check_pre_conditions')123 log.debug('check_pre_conditions')
125 controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')124 controller_dir = os.path.join('openlp', 'plugins', 'presentations', 'lib')
126 for filename in os.listdir(controller_dir):125 glob_pattern = os.path.join(controller_dir, '*controller.py')
127 if filename.endswith('controller.py') and filename != 'presentationcontroller.py':126 extension_loader(glob_pattern, ['presentationcontroller.py'])
128 path = os.path.join(controller_dir, filename)
129 if os.path.isfile(path):
130 module_name = 'openlp.plugins.presentations.lib.' + os.path.splitext(filename)[0]
131 log.debug('Importing controller {name}'.format(name=module_name))
132 try:
133 __import__(module_name, globals(), locals(), [])
134 except ImportError:
135 log.warning('Failed to import {name} on path {path}'.format(name=module_name, path=path))
136 controller_classes = PresentationController.__subclasses__()127 controller_classes = PresentationController.__subclasses__()
137 for controller_class in controller_classes:128 for controller_class in controller_classes:
138 controller = controller_class(self)129 controller = controller_class(self)
139130
=== modified file 'tests/functional/openlp_core_common/test_common.py'
--- tests/functional/openlp_core_common/test_common.py 2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_common/test_common.py 2017-05-15 10:24:47 +0000
@@ -22,11 +22,13 @@
22"""22"""
23Functional tests to test the AppLocation class and related methods.23Functional tests to test the AppLocation class and related methods.
24"""24"""
25from pathlib import Path
25from unittest import TestCase26from unittest import TestCase
26from unittest.mock import MagicMock, patch27from unittest.mock import MagicMock, call, patch
2728
28from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate, is_win, is_macosx, \29from openlp.core import common
29 is_linux, clean_button_text30from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \
31 is_linux, is_win, path_to_module, trace_error_handler, translate
3032
3133
32class TestCommonFunctions(TestCase):34class TestCommonFunctions(TestCase):
@@ -72,6 +74,72 @@
72 mocked_exists.assert_called_with(directory_to_check)74 mocked_exists.assert_called_with(directory_to_check)
73 self.assertRaises(ValueError, check_directory_exists, directory_to_check)75 self.assertRaises(ValueError, check_directory_exists, directory_to_check)
7476
77 def test_extension_loader_no_files_found(self):
78 """
79 Test the `extension_loader` function when no files are found
80 """
81 # GIVEN: A mocked `Path.glob` method which does not match any files
82 with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
83 patch.object(common.Path, 'glob', return_value=[]), \
84 patch('openlp.core.common.importlib.import_module') as mocked_import_module:
85
86 # WHEN: Calling `extension_loader`
87 extension_loader('glob', ['file2.py', 'file3.py'])
88
89 # THEN: `extension_loader` should not try to import any files
90 self.assertFalse(mocked_import_module.called)
91
92 def test_extension_loader_files_found(self):
93 """
94 Test the `extension_loader` function when it successfully finds and loads some files
95 """
96 # GIVEN: A mocked `Path.glob` method which returns a list of files
97 with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
98 patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py'),
99 Path('/app/dir/openlp/import_dir/file2.py'),
100 Path('/app/dir/openlp/import_dir/file3.py'),
101 Path('/app/dir/openlp/import_dir/file4.py')]), \
102 patch('openlp.core.common.importlib.import_module') as mocked_import_module:
103
104 # WHEN: Calling `extension_loader` with a list of files to exclude
105 extension_loader('glob', ['file2.py', 'file3.py'])
106
107 # THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the
108 # files listed in the `excluded_files` argument
109 mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), call('openlp.import_dir.file4')])
110
111 def test_extension_loader_import_error(self):
112 """
113 Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError`
114 """
115 # GIVEN: A mocked `import_module` which raises an `ImportError`
116 with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
117 patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
118 patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \
119 patch('openlp.core.common.log') as mocked_logger:
120
121 # WHEN: Calling `extension_loader`
122 extension_loader('glob')
123
124 # THEN: The `ImportError` should be caught and logged
125 self.assertTrue(mocked_logger.warning.called)
126
127 def test_extension_loader_os_error(self):
128 """
129 Test the `extension_loader` function when `import_module` raises a `ImportError`
130 """
131 # GIVEN: A mocked `SourceFileLoader` which raises an `OSError`
132 with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
133 patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
134 patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \
135 patch('openlp.core.common.log') as mocked_logger:
136
137 # WHEN: Calling `extension_loader`
138 extension_loader('glob')
139
140 # THEN: The `OSError` should be caught and logged
141 self.assertTrue(mocked_logger.warning.called)
142
75 def test_de_hump_conversion(self):143 def test_de_hump_conversion(self):
76 """144 """
77 Test the de_hump function with a class name145 Test the de_hump function with a class name
@@ -83,7 +151,7 @@
83 new_string = de_hump(string)151 new_string = de_hump(string)
84152
85 # THEN: the new string should be converted to python format153 # THEN: the new string should be converted to python format
86 self.assertTrue(new_string == "my_class", 'The class name should have been converted')154 self.assertEqual(new_string, "my_class", 'The class name should have been converted')
87155
88 def test_de_hump_static(self):156 def test_de_hump_static(self):
89 """157 """
@@ -96,7 +164,20 @@
96 new_string = de_hump(string)164 new_string = de_hump(string)
97165
98 # THEN: the new string should be converted to python format166 # THEN: the new string should be converted to python format
99 self.assertTrue(new_string == "my_class", 'The class name should have been preserved')167 self.assertEqual(new_string, "my_class", 'The class name should have been preserved')
168
169 def test_path_to_module(self):
170 """
171 Test `path_to_module` when supplied with a `Path` object
172 """
173 # GIVEN: A `Path` object
174 path = Path('openlp/core/ui/media/webkitplayer.py')
175
176 # WHEN: Calling path_to_module with the `Path` object
177 result = path_to_module(path)
178
179 # THEN: path_to_module should return the module name
180 self.assertEqual(result, 'openlp.core.ui.media.webkitplayer')
100181
101 def test_trace_error_handler(self):182 def test_trace_error_handler(self):
102 """183 """