rough plugin system implementation

This commit is contained in:
Michael Pöhn 2020-01-23 04:13:14 +01:00
parent 32f09603e1
commit bf815251ec
3 changed files with 84 additions and 10 deletions

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
# fdroidserver/__main__.py - part of the FDroid server tools # fdroidserver/__main__.py - part of the FDroid server tools
# Copyright (C) 2020 Michael Pöhn <michael.poehn@fsfe.org>
# Copyright (C) 2010-2015, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2010-2015, Ciaran Gultnieks, ciaran@ciarang.com
# Copyright (C) 2013-2014 Daniel Marti <mvdan@mvdan.cc> # Copyright (C) 2013-2014 Daniel Marti <mvdan@mvdan.cc>
# #
@ -20,7 +21,9 @@
import sys import sys
import os import os
import locale import locale
import pkgutil
import logging import logging
import importlib
import fdroidserver.common import fdroidserver.common
import fdroidserver.metadata import fdroidserver.metadata
@ -54,25 +57,49 @@ commands = OrderedDict([
]) ])
def print_help(): def print_help(fdroid_modules=None):
print(_("usage: ") + _("fdroid [<command>] [-h|--help|--version|<args>]")) print(_("usage: ") + _("fdroid [<command>] [-h|--help|--version|<args>]"))
print("") print("")
print(_("Valid commands are:")) print(_("Valid commands are:"))
for cmd, summary in commands.items(): for cmd, summary in commands.items():
print(" " + cmd + ' ' * (15 - len(cmd)) + summary) print(" " + cmd + ' ' * (15 - len(cmd)) + summary)
if fdroid_modules:
print(_('commands from plugin modules:'))
for command in sorted(fdroid_modules.keys()):
print(' {:15}{}'.format(command, fdroid_modules[command]['summary']))
print("") print("")
def find_plugins():
fdroid_modules = [x[1] for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')]
commands = {}
for module_name in fdroid_modules:
try:
command_name = module_name[7:]
module = importlib.import_module(module_name)
if hasattr(module, 'fdroid_summary') and hasattr(module, 'main'):
commands[command_name] = {'summary': module.fdroid_summary,
'module': module}
except IOError:
# We need to keep module lookup fault tolerant because buggy
# modules must not prevent fdroidserver from functioning
# TODO: think about warning users or debug logs for notifying devs
pass
return commands
def main(): def main():
sys.path.append(os.getcwd())
fdroid_modules = find_plugins()
if len(sys.argv) <= 1: if len(sys.argv) <= 1:
print_help() print_help(fdroid_modules=fdroid_modules)
sys.exit(0) sys.exit(0)
command = sys.argv[1] command = sys.argv[1]
if command not in commands: if command not in commands and command not in fdroid_modules.keys():
if command in ('-h', '--help'): if command in ('-h', '--help'):
print_help() print_help(fdroid_modules=fdroid_modules)
sys.exit(0) sys.exit(0)
elif command == '--version': elif command == '--version':
output = _('no version info found!') output = _('no version info found!')
@ -103,7 +130,7 @@ def main():
sys.exit(0) sys.exit(0)
else: else:
print(_("Command '%s' not recognised.\n" % command)) print(_("Command '%s' not recognised.\n" % command))
print_help() print_help(fdroid_modules=fdroid_modules)
sys.exit(1) sys.exit(1)
verbose = any(s in sys.argv for s in ['-v', '--verbose']) verbose = any(s in sys.argv for s in ['-v', '--verbose'])
@ -133,7 +160,10 @@ def main():
sys.argv[0] += ' ' + command sys.argv[0] += ' ' + command
del sys.argv[1] del sys.argv[1]
mod = __import__('fdroidserver.' + command, None, None, [command]) if command in commands.keys():
mod = __import__('fdroidserver.' + command, None, None, [command])
else:
mod = fdroid_modules[command]['module']
system_langcode, system_encoding = locale.getdefaultlocale() system_langcode, system_encoding = locale.getdefaultlocale()
if system_encoding is None or system_encoding.lower() not in ('utf-8', 'utf8'): if system_encoding is None or system_encoding.lower() not in ('utf-8', 'utf8'):

View File

@ -4,8 +4,11 @@ import inspect
import optparse import optparse
import os import os
import sys import sys
import textwrap
import unittest import unittest
import tempfile
from unittest import mock from unittest import mock
from testcommon import TmpCwd, TmpPyPath
localmodule = os.path.realpath( localmodule = os.path.realpath(
os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')) os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
@ -17,7 +20,7 @@ from fdroidserver import common
import fdroidserver.__main__ import fdroidserver.__main__
class FdroidTest(unittest.TestCase): class MainTest(unittest.TestCase):
'''this tests fdroid.py''' '''this tests fdroid.py'''
def test_commands(self): def test_commands(self):
@ -49,18 +52,43 @@ class FdroidTest(unittest.TestCase):
co = mock.Mock() co = mock.Mock()
with mock.patch('sys.argv', ['', 'init', '-h']): with mock.patch('sys.argv', ['', 'init', '-h']):
with mock.patch('fdroidserver.init.main', co): with mock.patch('fdroidserver.init.main', co):
with mock.patch('sys.exit', lambda x: None): with mock.patch('sys.exit') as exit_mock:
fdroidserver.__main__.main() fdroidserver.__main__.main()
exit_mock.assert_called_once_with(0)
co.assert_called_once_with() co.assert_called_once_with()
def test_call_deploy(self): def test_call_deploy(self):
co = mock.Mock() co = mock.Mock()
with mock.patch('sys.argv', ['', 'deploy', '-h']): with mock.patch('sys.argv', ['', 'deploy', '-h']):
with mock.patch('fdroidserver.server.main', co): with mock.patch('fdroidserver.server.main', co):
with mock.patch('sys.exit', lambda x: None): with mock.patch('sys.exit') as exit_mock:
fdroidserver.__main__.main() fdroidserver.__main__.main()
exit_mock.assert_called_once_with(0)
co.assert_called_once_with() co.assert_called_once_with()
def test_find_plugins(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: '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')
def test_main_plugin(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 mock.patch('sys.argv', ['', 'testy']):
with mock.patch('sys.exit') as exit_mock:
fdroidserver.__main__.main()
exit_mock.assert_called_once_with(0)
if __name__ == "__main__": if __name__ == "__main__":
os.chdir(os.path.dirname(__file__)) os.chdir(os.path.dirname(__file__))
@ -71,5 +99,5 @@ if __name__ == "__main__":
(common.options, args) = parser.parse_args(['--verbose']) (common.options, args) = parser.parse_args(['--verbose'])
newSuite = unittest.TestSuite() newSuite = unittest.TestSuite()
newSuite.addTest(unittest.makeSuite(FdroidTest)) newSuite.addTest(unittest.makeSuite(MainTest))
unittest.main(failfast=False) unittest.main(failfast=False)

View File

@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
import sys
class TmpCwd(): class TmpCwd():
@ -32,3 +33,18 @@ class TmpCwd():
def __exit__(self, a, b, c): def __exit__(self, a, b, c):
os.chdir(self.orig_cwd) os.chdir(self.orig_cwd)
class TmpPyPath():
"""Context-manager for temporarily changing the current working
directory.
"""
def __init__(self, additional_path):
self.additional_path = additional_path
def __enter__(self):
sys.path.append(self.additional_path)
def __exit__(self, a, b, c):
sys.path.remove(self.additional_path)