1
0
mirror of https://github.com/gsi-upm/senpy synced 2024-11-22 00:02:28 +00:00

Several changes

* Simplified setattr
* Added loading attributes in class
* Added ability to specify failing test cases in plugins
This commit is contained in:
J. Fernando Sánchez 2017-12-30 18:59:58 +01:00
parent 701f46b9f1
commit 0204e0b8e9
18 changed files with 164 additions and 180 deletions

View File

@ -83,7 +83,7 @@ class Senpy(object):
self._search_folders.add(folder) self._search_folders.add(folder)
self._outdated = True self._outdated = True
else: else:
logger.debug("Not a folder: %s", folder) raise AttributeError("Not a folder: %s", folder)
def _get_plugins(self, request): def _get_plugins(self, request):
if not self.analysis_plugins: if not self.analysis_plugins:

View File

@ -14,6 +14,7 @@ import json
import os import os
import jsonref import jsonref
import jsonschema import jsonschema
import inspect
from flask import Response as FlaskResponse from flask import Response as FlaskResponse
from pyld import jsonld from pyld import jsonld
@ -102,7 +103,7 @@ class SenpyMixin(object):
}) })
return FlaskResponse( return FlaskResponse(
response=content, response=content,
status=getattr(self, "status", 200), status=self.get('status', 200),
headers=headers, headers=headers,
mimetype=mimetype) mimetype=mimetype)
@ -188,34 +189,61 @@ class SenpyMixin(object):
class BaseModel(SenpyMixin, dict): class BaseModel(SenpyMixin, dict):
'''
Entities of the base model are a special kind of dictionary that emulates
a JSON-LD object. For convenience, the values can also be accessed as attributes
(a la Javascript). e.g.:
> myobject.key == myobject['key']
True
> myobject.ns__name == myobject['ns:name']
True
'''
schema = base_schema schema = base_schema
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.attrs_to_dict()
if 'id' in kwargs: if 'id' in kwargs:
self.id = kwargs.pop('id') self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True): elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(type(self).__name__, time.time()) self.id = '_:{}_{}'.format(type(self).__name__, time.time())
temp = dict(*args, **kwargs)
temp = self.get_defaults()
temp.update(dict(*args))
for k, v in kwargs.items():
temp[self._get_key(k)] = v
super(BaseModel, self).__init__(temp)
if '@type' not in self:
logger.warn('Created an instance of an unknown model')
def get_defaults(self):
temp = {}
for obj in [ for obj in [
self.schema, self.schema,
] + self.schema.get('allOf', []): ] + self.schema.get('allOf', []):
for k, v in obj.get('properties', {}).items(): for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp: if 'default' in v and k not in temp:
temp[k] = copy.deepcopy(v['default']) temp[k] = copy.deepcopy(v['default'])
return temp
for i in temp: def attrs_to_dict(self):
nk = self._get_key(i) '''
if nk != i: Copy the attributes of the class to the instance.
temp[nk] = temp[i]
del temp[i]
try:
temp['@type'] = getattr(self, '@type')
except AttributeError:
logger.warn('Creating an instance of an unknown model')
super(BaseModel, self).__init__(temp) This allows adding default values in the class definition.
e.g.:
class MyPlugin(Plugin):
version=0.3
description='A dull plugin'
'''
def is_attr(x):
return not(inspect.isroutine(x) or inspect.ismethod(x) or isinstance(x, property))
for key, value in inspect.getmembers(self.__class__, is_attr):
if key[0] != '_' and key != 'schema':
self[key] = value
def _get_key(self, key): def _get_key(self, key):
if key is 'id': if key is 'id':
@ -224,27 +252,38 @@ class BaseModel(SenpyMixin, dict):
return key return key
def __delitem__(self, key): def __delitem__(self, key):
key = self._get_key(key)
dict.__delitem__(self, key) dict.__delitem__(self, key)
def __getattr__(self, key): def _internal_key(self, key):
try: return key[0] == '_' or key in self.__dict__
return self.__getitem__(self._get_key(key))
except KeyError:
raise AttributeError(key)
def __setattr__(self, key, value):
self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
try:
object.__delattr__(self, key)
except AttributeError:
self.__delitem__(self._get_key(key))
def _plain_dict(self): def _plain_dict(self):
d = {k: v for (k, v) in self.items() if k[0] != "_"} d = {k: v for (k, v) in self.items() if k[0] != "_"}
return d return d
def __getattr__(self, key):
'''
__getattr__ only gets called when the attribute could not
be found in the __dict__. So we only need to look for the
the element in the dictionary, or raise an Exception.
'''
if self._internal_key(key):
raise AttributeError(key)
return self.__getitem__(self._get_key(key))
def __setattr__(self, key, value):
if self._internal_key(key):
return super(BaseModel, self).__setattr__(key, value)
key = self._get_key(key)
return self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
if self._internal_key(key):
return object.__delattr__(self, key)
key = self._get_key(key)
self.__delitem__(self._get_key(key))
def register(rsubclass, rtype=None): def register(rsubclass, rtype=None):
_subtypes[rtype or rsubclass.__name__] = rsubclass _subtypes[rtype or rsubclass.__name__] = rsubclass

View File

@ -15,8 +15,6 @@ import importlib
import yaml import yaml
import threading import threading
from contextlib import contextmanager
from .. import models, utils from .. import models, utils
from ..api import API_PARAMS from ..api import API_PARAMS
@ -29,16 +27,21 @@ class Plugin(models.Plugin):
Provides a canonical name for plugins and serves as base for other Provides a canonical name for plugins and serves as base for other
kinds of plugins. kinds of plugins.
""" """
if not info: logger.debug("Initialising {}".format(info))
if info:
self.update(info)
super(Plugin, self).__init__(**self)
if not self.validate():
raise models.Error(message=("You need to provide configuration" raise models.Error(message=("You need to provide configuration"
"information for the plugin.")) "information for the plugin."))
logger.debug("Initialising {}".format(info)) self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
id = 'plugins/{}_{}'.format(info['name'], info['version'])
super(Plugin, self).__init__(id=id, **info)
self.is_activated = False self.is_activated = False
self._lock = threading.Lock() self._lock = threading.Lock()
self.data_folder = data_folder or os.getcwd() self.data_folder = data_folder or os.getcwd()
def validate(self):
return all(x in self for x in ('name', 'description', 'version'))
def get_folder(self): def get_folder(self):
return os.path.dirname(inspect.getfile(self.__class__)) return os.path.dirname(inspect.getfile(self.__class__))
@ -50,12 +53,21 @@ class Plugin(models.Plugin):
def test(self): def test(self):
if not hasattr(self, 'test_cases'): if not hasattr(self, 'test_cases'):
import inspect
raise AttributeError(('Plugin {} [{}] does not have any defined ' raise AttributeError(('Plugin {} [{}] does not have any defined '
'test cases').format(self.id, inspect.getfile(self.__class__))) 'test cases').format(self.id, inspect.getfile(self.__class__)))
for case in self.test_cases: for case in self.test_cases:
res = list(self.analyse_entry(models.Entry(case['entry']), entry = models.Entry(case['entry'])
case['params'])) params = case.get('params', {})
fails = case.get('fails', False)
try:
res = list(self.analyse_entry(entry, params))
except models.Error:
if fails:
continue
raise
if fails:
raise Exception('This test should have raised an exception.')
exp = case['expected'] exp = case['expected']
if not isinstance(exp, list): if not isinstance(exp, list):
exp = [exp] exp = [exp]
@ -63,12 +75,13 @@ class Plugin(models.Plugin):
for r in res: for r in res:
r.validate() r.validate()
@contextmanager
def open(self, fpath, *args, **kwargs): def open(self, fpath, *args, **kwargs):
if not os.path.isabs(fpath): if not os.path.isabs(fpath):
fpath = os.path.join(self.data_folder, fpath) fpath = os.path.join(self.data_folder, fpath)
with open(fpath, *args, **kwargs) as f: return open(fpath, *args, **kwargs)
yield f
def serve(self, **kwargs):
utils.serve(plugin=self, **kwargs)
SenpyPlugin = Plugin SenpyPlugin = Plugin
@ -106,17 +119,13 @@ class ConversionPlugin(Plugin):
class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin): class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin):
def __init__(self, info, *args, **kwargs): minPolarityValue = 0
super(SentimentPlugin, self).__init__(info, *args, **kwargs) maxPolarityValue = 1
self.minPolarityValue = float(info.get("minPolarityValue", 0))
self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin): class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin):
def __init__(self, info, *args, **kwargs): minEmotionValue = 0
super(EmotionPlugin, self).__init__(info, *args, **kwargs) maxEmotionValue = 1
self.minEmotionValue = float(info.get("minEmotionValue", -1))
self.maxEmotionValue = float(info.get("maxEmotionValue", 1))
class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin): class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin):
@ -127,11 +136,11 @@ class ShelfMixin(object):
@property @property
def sh(self): def sh(self):
if not hasattr(self, '_sh') or self._sh is None: if not hasattr(self, '_sh') or self._sh is None:
self.__dict__['_sh'] = {} self._sh = {}
if os.path.isfile(self.shelf_file): if os.path.isfile(self.shelf_file):
try: try:
with self.open(self.shelf_file, 'rb') as p: with self.open(self.shelf_file, 'rb') as p:
self.__dict__['_sh'] = pickle.load(p) self._sh = pickle.load(p)
except (IndexError, EOFError, pickle.UnpicklingError): except (IndexError, EOFError, pickle.UnpicklingError):
logger.warning('{} has a corrupted shelf file!'.format(self.id)) logger.warning('{} has a corrupted shelf file!'.format(self.id))
if not self.get('force_shelf', False): if not self.get('force_shelf', False):
@ -142,9 +151,13 @@ class ShelfMixin(object):
def sh(self): def sh(self):
if os.path.isfile(self.shelf_file): if os.path.isfile(self.shelf_file):
os.remove(self.shelf_file) os.remove(self.shelf_file)
del self.__dict__['_sh'] del self._sh
self.save() self.save()
@sh.setter
def sh(self, value):
self._sh = value
@property @property
def shelf_file(self): def shelf_file(self):
if 'shelf_file' not in self or not self['shelf_file']: if 'shelf_file' not in self or not self['shelf_file']:
@ -196,7 +209,7 @@ def pfilter(plugins, **kwargs):
def validate_info(info): def validate_info(info):
return all(x in info for x in ('name', 'module', 'description', 'version')) return all(x in info for x in ('name',))
def load_module(name, root=None): def load_module(name, root=None):
@ -235,6 +248,17 @@ def install_deps(*plugins):
return installed return installed
def get_plugin_class(module):
candidate = None
for _, obj in inspect.getmembers(module):
if inspect.isclass(obj) and inspect.getmodule(obj) == module:
logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj)))
candidate = obj
break
return candidate
def load_plugin_from_info(info, root=None, validator=validate_info, install=True, *args, **kwargs): def load_plugin_from_info(info, root=None, validator=validate_info, install=True, *args, **kwargs):
if not root and '_path' in info: if not root and '_path' in info:
root = os.path.dirname(info['_path']) root = os.path.dirname(info['_path'])
@ -249,18 +273,12 @@ def load_plugin_from_info(info, root=None, validator=validate_info, install=True
raise raise
install_deps(info) install_deps(info)
tmp = load_module(module, root) tmp = load_module(module, root)
candidate = None cls = None
for _, obj in inspect.getmembers(tmp): if '@type' not in info:
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp: cls = get_plugin_class(tmp)
logger.debug(("Found plugin class:" if not cls:
" {}@{}").format(obj, inspect.getmodule(obj))) raise Exception("No valid plugin for: {}".format(module))
candidate = obj return cls(info=info, *args, **kwargs)
break
if not candidate:
logger.debug("No valid plugin for: {}".format(module))
return
module = candidate(info=info, *args, **kwargs)
return module
def parse_plugin_info(fpath): def parse_plugin_info(fpath):

View File

@ -45,7 +45,7 @@ class SplitPlugin(AnalysisPlugin):
}, },
{ {
'entry': { 'entry': {
"id": ":test", "@id": ":test",
'nif:isString': 'Hello\nWorld' 'nif:isString': 'Hello\nWorld'
}, },
'params': { 'params': {

View File

@ -23,3 +23,23 @@ def check_template(indict, template):
else: else:
if indict != template: if indict != template:
raise models.Error('{} and {} are different'.format(indict, template)) raise models.Error('{} and {} are different'.format(indict, template))
def easy(app=None, plugin=None, host='0.0.0.0', port=5000, **kwargs):
'''
Run a server with a specific plugin.
'''
from flask import Flask
from senpy.extensions import Senpy
if not app:
app = Flask(__name__)
sp = Senpy(app)
if plugin:
sp.add_plugin(plugin)
sp.install_deps()
app.run(host,
port,
debug=app.debug,
**kwargs)

View File

@ -34,6 +34,11 @@ setup(
install_requires=install_reqs, install_requires=install_reqs,
tests_require=test_reqs, tests_require=test_reqs,
setup_requires=['pytest-runner', ], setup_requires=['pytest-runner', ],
extras_require={
'evaluation': [
'gsitk'
]
},
include_package_data=True, include_package_data=True,
entry_points={ entry_points={
'console_scripts': 'console_scripts':

View File

@ -1,3 +1,4 @@
mock mock
pytest-cov pytest-cov
pytest pytest
gsitk

View File

@ -1,26 +0,0 @@
from senpy.plugins import AnalysisPlugin
import multiprocessing
def _train(process_number):
return process_number
class AsyncPlugin(AnalysisPlugin):
def _do_async(self, num_processes):
pool = multiprocessing.Pool(processes=num_processes)
values = pool.map(_train, range(num_processes))
return values
def activate(self):
self.value = self._do_async(4)
def analyse_entry(self, entry, params):
values = self._do_async(2)
entry.async_values = values
yield entry
def test(self):
pass

View File

@ -1,8 +0,0 @@
---
name: Async
module: asyncplugin
description: I am async
author: "@balkian"
version: '0.1'
async: true
extra_params: {}

View File

@ -1,11 +0,0 @@
from senpy.plugins import SentimentPlugin
class DummyPlugin(SentimentPlugin):
def analyse_entry(self, entry, params):
entry['nif:iString'] = entry['nif:isString'][::-1]
entry.reversed = entry.get('reversed', 0) + 1
yield entry
def test(self):
pass

View File

@ -1,15 +0,0 @@
{
"name": "Dummy",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"example": {
"@id": "example_parameter",
"aliases": ["example", "ex"],
"required": false,
"default": 0
}
}
}

View File

@ -1,14 +0,0 @@
{
"name": "DummyRequired",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"example": {
"@id": "example_parameter",
"aliases": ["example", "ex"],
"required": true
}
}
}

View File

@ -1,5 +0,0 @@
from senpy.plugins import SentimentPlugin
class DummyPlugin(SentimentPlugin):
import noop

View File

@ -1,14 +0,0 @@
from senpy.plugins import AnalysisPlugin
from time import sleep
class SleepPlugin(AnalysisPlugin):
def activate(self, *args, **kwargs):
sleep(self.timeout)
def analyse_entry(self, entry, params):
sleep(float(params.get("timeout", self.timeout)))
yield entry
def test(self):
pass

View File

@ -1,16 +0,0 @@
{
"name": "Sleep",
"module": "sleep",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"timeout": 0.05,
"extra_params": {
"timeout": {
"@id": "timeout_sleep",
"aliases": ["timeout", "to"],
"required": false,
"default": 0
}
}
}

View File

@ -25,8 +25,8 @@ def analyse(instance, **kwargs):
class ExtensionsTest(TestCase): class ExtensionsTest(TestCase):
def setUp(self): def setUp(self):
self.app = Flask('test_extensions') self.app = Flask('test_extensions')
self.dir = os.path.dirname(__file__) self.examples_dir = os.path.join(os.path.dirname(__file__), '..', 'example-plugins')
self.senpy = Senpy(plugin_folder=self.dir, self.senpy = Senpy(plugin_folder=self.examples_dir,
app=self.app, app=self.app,
default_plugins=False) default_plugins=False)
self.senpy.activate_plugin("Dummy", sync=True) self.senpy.activate_plugin("Dummy", sync=True)
@ -41,7 +41,7 @@ class ExtensionsTest(TestCase):
def test_discovery(self): def test_discovery(self):
""" Discovery of plugins in given folders. """ """ Discovery of plugins in given folders. """
# noinspection PyProtectedMember # noinspection PyProtectedMember
assert self.dir in self.senpy._search_folders assert self.examples_dir in self.senpy._search_folders
print(self.senpy.plugins) print(self.senpy.plugins)
assert "Dummy" in self.senpy.plugins assert "Dummy" in self.senpy.plugins
@ -54,7 +54,7 @@ class ExtensionsTest(TestCase):
'requirements': ['noop'], 'requirements': ['noop'],
'version': 0 'version': 0
} }
root = os.path.join(self.dir, 'plugins', 'noop') root = os.path.join(self.examples_dir, 'noop')
module = plugins.load_plugin_from_info(info, root=root, install=True) module = plugins.load_plugin_from_info(info, root=root, install=True)
assert module.name == 'TestPip' assert module.name == 'TestPip'
assert module assert module
@ -166,7 +166,7 @@ class ExtensionsTest(TestCase):
self.senpy.filter_plugins(name="Dummy", is_activated=True)) self.senpy.filter_plugins(name="Dummy", is_activated=True))
def test_load_default_plugins(self): def test_load_default_plugins(self):
senpy = Senpy(plugin_folder=self.dir, default_plugins=True) senpy = Senpy(plugin_folder=self.examples_dir, default_plugins=True)
assert len(senpy.plugins) > 1 assert len(senpy.plugins) > 1
def test_convert_emotions(self): def test_convert_emotions(self):

View File

@ -100,6 +100,7 @@ class ModelsTest(TestCase):
def test_plugins(self): def test_plugins(self):
self.assertRaises(Error, plugins.Plugin) self.assertRaises(Error, plugins.Plugin)
p = plugins.Plugin({"name": "dummy", p = plugins.Plugin({"name": "dummy",
"description": "I do nothing",
"version": 0, "version": 0,
"extra_params": { "extra_params": {
"none": { "none": {
@ -123,7 +124,9 @@ class ModelsTest(TestCase):
def test_str(self): def test_str(self):
"""The string representation shouldn't include private variables""" """The string representation shouldn't include private variables"""
r = Results() r = Results()
p = plugins.Plugin({"name": "STR test", "version": 0}) p = plugins.Plugin({"name": "STR test",
"description": "Test of private variables.",
"version": 0})
p._testing = 0 p._testing = 0
s = str(p) s = str(p)
assert "_testing" not in s assert "_testing" not in s

View File

@ -43,6 +43,7 @@ class PluginsTest(TestCase):
def test_shelf_file(self): def test_shelf_file(self):
a = ShelfDummyPlugin( a = ShelfDummyPlugin(
info={'name': 'default_shelve_file', info={'name': 'default_shelve_file',
'description': 'Dummy plugin for tests',
'version': 'test'}) 'version': 'test'})
a.activate() a.activate()
assert os.path.isfile(a.shelf_file) assert os.path.isfile(a.shelf_file)
@ -53,6 +54,7 @@ class PluginsTest(TestCase):
newfile = self.shelf_file + "new" newfile = self.shelf_file + "new"
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Shelf plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': newfile 'shelf_file': newfile
}) })
@ -75,6 +77,7 @@ class PluginsTest(TestCase):
pickle.dump({'counter': 99}, f) pickle.dump({'counter': 99}, f)
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'DummyShelf', 'name': 'DummyShelf',
'description': 'Dummy plugin for tests',
'shelf_file': self.shelf_file, 'shelf_file': self.shelf_file,
'version': 'test' 'version': 'test'
}) })
@ -105,7 +108,8 @@ class PluginsTest(TestCase):
with open(fn, 'rb') as f: with open(fn, 'rb') as f:
msg, error = files[fn] msg, error = files[fn]
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'test_corrupt_shelf_{}'.format(msg),
'description': 'Dummy plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': f.name 'shelf_file': f.name
}) })
@ -126,6 +130,7 @@ class PluginsTest(TestCase):
''' Reusing the values of a previous shelf ''' ''' Reusing the values of a previous shelf '''
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Dummy plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': self.shelf_file 'shelf_file': self.shelf_file
}) })
@ -136,6 +141,7 @@ class PluginsTest(TestCase):
b = ShelfDummyPlugin(info={ b = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Dummy plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': self.shelf_file 'shelf_file': self.shelf_file
}) })
@ -148,6 +154,7 @@ class PluginsTest(TestCase):
''' Should be able to set extra parameters''' ''' Should be able to set extra parameters'''
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Dummy shelf plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': self.shelf_file, 'shelf_file': self.shelf_file,
'extra_params': { 'extra_params': {