diff --git a/fdroidserver/__main__.py b/fdroidserver/__main__.py index 46cf87df..785179de 100755 --- a/fdroidserver/__main__.py +++ b/fdroidserver/__main__.py @@ -18,12 +18,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import re import sys import os import locale import pkgutil import logging -import importlib import fdroidserver.common import fdroidserver.metadata @@ -70,16 +70,51 @@ def print_help(fdroid_modules=None): print("") +def preparse_plugin(module_name, module_dir): + """simple regex based parsing for plugin scripts, + so we don't have to import them when we just need the summary, + but not plan on executing this particular plugin.""" + if '.' in module_name: + raise ValueError("No '.' allowed in fdroid plugin modules: '{}'" + .format(module_name)) + path = os.path.join(module_dir, module_name + '.py') + if not os.path.isfile(path): + path = os.path.join(module_dir, module_name, '__main__.py') + if not os.path.isfile(path): + raise ValueError("unable to find main plugin script " + "for module '{n}' ('{d}')" + .format(n=module_name, + d=module_dir)) + summary = None + main = None + with open(path, 'r', encoding='utf-8') as f: + re_main = re.compile(r'^(\s*def\s+main\s*\(.*\)\s*:' + r'|\s*main\s*=\s*lambda\s*:.+)$') + re_summary = re.compile(r'^\s*fdroid_summary\s*=\s["\'](?P.+)["\']$') + for line in f: + m_summary = re_summary.match(line) + if m_summary: + summary = m_summary.group('text') + if re_main.match(line): + main = True + + if summary is None: + raise NameError("could not find 'fdroid_summary' in: '{}' plugin" + .format(module_name)) + if main is None: + raise NameError("could not find 'main' function in: '{}' plugin" + .format(module_name)) + return {'name': module_name, 'summary': summary} + + def find_plugins(): - fdroid_modules = [x[1] for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + found_plugins = [{'name': x[1], 'dir': x[0].path} for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] commands = {} - for module_name in fdroid_modules: - command_name = module_name[7:] + for plugin_def in found_plugins: + command_name = plugin_def['name'][7:] try: - module = importlib.import_module(module_name) - if hasattr(module, 'fdroid_summary') and hasattr(module, 'main'): - commands[command_name] = {'summary': module.fdroid_summary, - 'module': module} + commands[command_name] = preparse_plugin(plugin_def['name'], + plugin_def['dir']) except Exception as e: # We need to keep module lookup fault tolerant because buggy # modules must not prevent fdroidserver from functioning @@ -164,7 +199,7 @@ def main(): if command in commands.keys(): mod = __import__('fdroidserver.' + command, None, None, [command]) else: - mod = fdroid_modules[command]['module'] + mod = __import__(fdroid_modules[command]['name'], None, None, [command]) system_langcode, system_encoding = locale.getdefaultlocale() if system_encoding is None or system_encoding.lower() not in ('utf-8', 'utf8'): diff --git a/tests/main.TestCase b/tests/main.TestCase index e33c6758..35b80bc2 100755 --- a/tests/main.TestCase +++ b/tests/main.TestCase @@ -4,6 +4,7 @@ import inspect import optparse import os import sys +import pkgutil import textwrap import unittest import tempfile @@ -74,24 +75,94 @@ class MainTest(unittest.TestCase): main = lambda: 'all good'""")) with TmpPyPath(tmpdir): plugins = fdroidserver.__main__.find_plugins() - self.assertIn('testy', plugins.keys()) - self.assertEqual(plugins['testy']['summary'], 'ttt') - self.assertEqual(plugins['testy']['module'].main(), 'all good') + self.assertIn('testy', plugins.keys()) + self.assertEqual(plugins['testy']['summary'], 'ttt') + self.assertEqual(__import__(plugins['testy']['name'], + None, + None, + ['testy']) + .main(), + 'all good') - def test_main_plugin(self): + def test_main_plugin_lambda(self): with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): with open('fdroid_testy.py', 'w') as f: f.write(textwrap.dedent("""\ fdroid_summary = "ttt" main = lambda: pritn('all good')""")) - test_path = sys.path.copy() - test_path.append(tmpdir) - with mock.patch('sys.path', test_path): + with TmpPyPath(tmpdir): with mock.patch('sys.argv', ['', 'testy']): with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() exit_mock.assert_called_once_with(0) + def test_main_plugin_def(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + def main(): + pritn('all good')""")) + with TmpPyPath(tmpdir): + with mock.patch('sys.argv', ['', 'testy']): + with mock.patch('sys.exit') as exit_mock: + fdroidserver.__main__.main() + exit_mock.assert_called_once_with(0) + + def test_preparse_plugin_lookup_bad_name(self): + self.assertRaises(ValueError, + fdroidserver.__main__.preparse_plugin, + "some.package", "/non/existent/module/path") + + def test_preparse_plugin_lookup_bad_path(self): + self.assertRaises(ValueError, + fdroidserver.__main__.preparse_plugin, + "fake_module_name", "/non/existent/module/path") + + def test_preparse_plugin_lookup_summary_missing(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy.py', 'w') as f: + f.write(textwrap.dedent("""\ + main = lambda: print('all good')""")) + with TmpPyPath(tmpdir): + p = [x for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + module_dir = p[0][0].path + module_name = p[0][1] + self.assertRaises(NameError, + fdroidserver.__main__.preparse_plugin, + module_name, module_dir) + + def test_preparse_plugin_lookup_module_file(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + main = lambda: pritn('all good')""")) + with TmpPyPath(tmpdir): + p = [x for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + module_path = p[0][0].path + module_name = p[0][1] + d = fdroidserver.__main__.preparse_plugin(module_name, module_path) + self.assertDictEqual(d, {'name': 'fdroid_testy', + 'summary': 'ttt'}) + + def test_preparse_plugin_lookup_module_dir(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + os.mkdir(os.path.join(tmpdir, 'fdroid_testy')) + with open('fdroid_testy/__main__.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + main = lambda: print('all good')""")) + with open('fdroid_testy/__init__.py', 'w') as f: + pass + with TmpPyPath(tmpdir): + p = [x for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + module_path = p[0][0].path + module_name = p[0][1] + d = fdroidserver.__main__.preparse_plugin(module_name, module_path) + self.assertDictEqual(d, {'name': 'fdroid_testy', + 'summary': 'ttt'}) + if __name__ == "__main__": os.chdir(os.path.dirname(__file__))