mirror of
https://github.com/gsi-upm/senpy
synced 2025-08-23 18:12:20 +00:00
New schema for parameters
* Improve extra requirement handling * New mechanism to handle parameters beforehand in chained calls, and the ability to get help on available parameters in chained calls (through `?help`). * Redefined Analysis, to reflect the new ontology * Add parameters as an entity in the schema * Update examples to include analyses and parameters * Add processing plugins, with an interface similar to analysis plugins * Update tests * Avoid duplication in split plugin Closes #51 Squashed commit of the following: commitd145a852e7
commit6a1069780b
commitca69bddc17
commitaa35e62a27
This commit is contained in:
@@ -3,8 +3,9 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from unittest import TestCase
|
||||
from senpy.api import parse_params, API_PARAMS, NIF_PARAMS, WEB_PARAMS
|
||||
from senpy.models import Error
|
||||
from senpy.api import (boolean, parse_params, get_extra_params, parse_analysis,
|
||||
API_PARAMS, NIF_PARAMS, WEB_PARAMS)
|
||||
from senpy.models import Error, Plugin
|
||||
|
||||
|
||||
class APITest(TestCase):
|
||||
@@ -89,3 +90,156 @@ class APITest(TestCase):
|
||||
assert "Dummy" in p['algorithm']
|
||||
assert 'input' in p
|
||||
assert p['input'] == 'Aloha my friend'
|
||||
|
||||
def test_parse_analysis(self):
|
||||
'''The API should parse user parameters and return them in a format that plugins can use'''
|
||||
plugins = [
|
||||
Plugin({
|
||||
'name': 'plugin1',
|
||||
'extra_params': {
|
||||
# Incompatible parameter
|
||||
'param0': {
|
||||
'aliases': ['p1', 'parameter1'],
|
||||
'options': ['option1', 'option2'],
|
||||
'default': 'option1',
|
||||
'required': True
|
||||
},
|
||||
'param1': {
|
||||
'aliases': ['p1', 'parameter1'],
|
||||
'options': ['en', 'es'],
|
||||
|
||||
'default': 'en',
|
||||
'required': False
|
||||
},
|
||||
'param2': {
|
||||
'aliases': ['p2', 'parameter2'],
|
||||
'required': False,
|
||||
'options': ['value2_1', 'value2_2', 'value3_3']
|
||||
}
|
||||
}
|
||||
}), Plugin({
|
||||
'name': 'plugin2',
|
||||
'extra_params': {
|
||||
'param0': {
|
||||
'aliases': ['parameter1'],
|
||||
'options': ['new option', 'new option2'],
|
||||
'default': 'new option',
|
||||
'required': False
|
||||
},
|
||||
'param1': {
|
||||
'aliases': ['myparam1', 'p1'],
|
||||
'options': ['en', 'de', 'auto'],
|
||||
'default': 'de',
|
||||
'required': True
|
||||
},
|
||||
'param3': {
|
||||
'aliases': ['p3', 'parameter3'],
|
||||
'options': boolean,
|
||||
'default': True
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
call = {
|
||||
'param1': 'en',
|
||||
'0.param0': 'option1',
|
||||
'0.param1': 'en',
|
||||
'param2': 'value2_1',
|
||||
'param0': 'new option',
|
||||
'1.param1': 'de',
|
||||
'param3': False,
|
||||
}
|
||||
expected = [
|
||||
{
|
||||
'param0': 'option1',
|
||||
'param1': 'en',
|
||||
'param2': 'value2_1',
|
||||
}, {
|
||||
'param0': 'new option',
|
||||
'param1': 'de',
|
||||
'param3': False,
|
||||
}
|
||||
|
||||
]
|
||||
p = parse_analysis(call, plugins)
|
||||
for i, arg in enumerate(expected):
|
||||
params = p[i].params
|
||||
for k, v in arg.items():
|
||||
assert params[k] == v
|
||||
|
||||
def test_get_extra_params(self):
|
||||
'''The API should return the list of valid parameters for a set of plugins'''
|
||||
plugins = [
|
||||
Plugin({
|
||||
'name': 'plugin1',
|
||||
'extra_params': {
|
||||
# Incompatible parameter
|
||||
'param0': {
|
||||
'aliases': ['p1', 'parameter1'],
|
||||
'options': ['option1', 'option2'],
|
||||
'default': 'option1',
|
||||
'required': True
|
||||
},
|
||||
'param1': {
|
||||
'aliases': ['p1', 'parameter1'],
|
||||
'options': ['en', 'es'],
|
||||
'default': 'en',
|
||||
'required': False
|
||||
},
|
||||
'param2': {
|
||||
'aliases': ['p2', 'parameter2'],
|
||||
'required': False,
|
||||
'options': ['value2_1', 'value2_2', 'value3_3']
|
||||
}
|
||||
}
|
||||
}), Plugin({
|
||||
'name': 'plugin2',
|
||||
'extra_params': {
|
||||
'param0': {
|
||||
'aliases': ['parameter1'],
|
||||
'options': ['new option', 'new option2'],
|
||||
'default': 'new option',
|
||||
'required': False
|
||||
},
|
||||
'param1': {
|
||||
'aliases': ['myparam1', 'p1'],
|
||||
'options': ['en', 'de', 'auto'],
|
||||
'default': 'de',
|
||||
'required': True
|
||||
},
|
||||
'param3': {
|
||||
'aliases': ['p3', 'parameter3'],
|
||||
'options': boolean,
|
||||
'default': True
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
expected = {
|
||||
# Overlapping parameters
|
||||
'plugin1.param0': plugins[0]['extra_params']['param0'],
|
||||
'plugin1.param1': plugins[0]['extra_params']['param1'],
|
||||
'plugin2.param0': plugins[1]['extra_params']['param0'],
|
||||
'plugin2.param1': plugins[1]['extra_params']['param1'],
|
||||
|
||||
# Non-overlapping parameters
|
||||
'param2': plugins[0]['extra_params']['param2'],
|
||||
'param3': plugins[1]['extra_params']['param3'],
|
||||
|
||||
# Intersection of overlapping parameters
|
||||
'param1': {
|
||||
'aliases': ['p1'],
|
||||
'options': ['en'],
|
||||
'default': None,
|
||||
'required': True
|
||||
}
|
||||
}
|
||||
|
||||
result = get_extra_params(plugins)
|
||||
|
||||
for ik, iv in expected.items():
|
||||
assert ik in result
|
||||
for jk, jv in iv.items():
|
||||
assert jk in result[ik]
|
||||
assert expected[ik][jk] == result[ik][jk]
|
||||
|
@@ -26,8 +26,7 @@ class BlueprintsTest(TestCase):
|
||||
cls.senpy.init_app(cls.app)
|
||||
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
||||
cls.senpy.add_folder(cls.dir)
|
||||
cls.senpy.activate_plugin("Dummy", sync=True)
|
||||
cls.senpy.activate_plugin("DummyRequired", sync=True)
|
||||
cls.senpy.activate_all()
|
||||
cls.senpy.default_plugin = 'Dummy'
|
||||
|
||||
def setUp(self):
|
||||
@@ -107,6 +106,7 @@ class BlueprintsTest(TestCase):
|
||||
assert isinstance(js, models.Error)
|
||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=notvalid")
|
||||
self.assertCode(resp, 400)
|
||||
self.app.config['TESTING'] = True
|
||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
|
||||
self.assertCode(resp, 200)
|
||||
|
||||
@@ -138,16 +138,77 @@ class BlueprintsTest(TestCase):
|
||||
# Calling dummy twice, should return the same string
|
||||
self.assertCode(resp, 200)
|
||||
js = parse_resp(resp)
|
||||
assert len(js['analysis']) == 1
|
||||
assert len(js['analysis']) == 2
|
||||
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
|
||||
|
||||
resp = self.client.get("/api/Dummy+Dummy?i=My aloha mohame")
|
||||
# Same with pluses instead of slashes
|
||||
self.assertCode(resp, 200)
|
||||
js = parse_resp(resp)
|
||||
assert len(js['analysis']) == 1
|
||||
assert len(js['analysis']) == 2
|
||||
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
|
||||
|
||||
def test_analysis_chain_required(self):
|
||||
"""
|
||||
If a parameter is required and duplicated (because two plugins require it), specifying
|
||||
it once should suffice
|
||||
"""
|
||||
resp = self.client.get("/api/DummyRequired/DummyRequired?i=My aloha mohame&example=a")
|
||||
js = parse_resp(resp)
|
||||
assert len(js['analysis']) == 2
|
||||
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
|
||||
assert js['entries'][0]['reversed'] == 2
|
||||
|
||||
def test_requirements_chain_help(self):
|
||||
'''The extra parameters of each plugin should be merged if they are in a chain '''
|
||||
resp = self.client.get("/api/split/DummyRequired?help=true")
|
||||
self.assertCode(resp, 200)
|
||||
js = parse_resp(resp)
|
||||
assert 'valid_parameters' in js
|
||||
vp = js['valid_parameters']
|
||||
assert 'example' in vp
|
||||
assert 'delimiter' in vp
|
||||
|
||||
def test_requirements_chain_repeat_help(self):
|
||||
'''
|
||||
If a plugin appears several times in a chain, there should be a way to set different
|
||||
parameters for each.
|
||||
'''
|
||||
resp = self.client.get("/api/split/split?help=true")
|
||||
self.assertCode(resp, 200)
|
||||
js = parse_resp(resp)
|
||||
assert 'valid_parameters' in js
|
||||
vp = js['valid_parameters']
|
||||
assert 'delimiter' in vp
|
||||
|
||||
resp = self.client.get("/api/split/split?help=true&verbose=false")
|
||||
js = parse_resp(resp)
|
||||
vp = js['valid_parameters']
|
||||
assert len(vp.keys()) == 1
|
||||
|
||||
def test_requirements_chain(self):
|
||||
"""
|
||||
It should be possible to specify different parameters for each step in the chain.
|
||||
"""
|
||||
# First, we split by sentence twice. Each call should generate 3 additional entries
|
||||
# (one per sentence in the original).
|
||||
resp = self.client.get('/api/split/split?i=The first sentence. The second sentence.'
|
||||
'\nA new paragraph&delimiter=sentence')
|
||||
js = parse_resp(resp)
|
||||
assert len(js['analysis']) == 2
|
||||
assert len(js['entries']) == 7
|
||||
|
||||
# Now, we split by sentence. This produces 3 additional entries.
|
||||
# Then, we split by paragraph. This should create 2 additional entries (One per paragraph
|
||||
# in the original text)
|
||||
resp = self.client.get('/api/split/split?i=The first sentence. The second sentence.'
|
||||
'\nA new paragraph&0.delimiter=sentence&1.delimiter=paragraph')
|
||||
# Calling dummy twice, should return the same string
|
||||
self.assertCode(resp, 200)
|
||||
js = parse_resp(resp)
|
||||
assert len(js['analysis']) == 2
|
||||
assert len(js['entries']) == 6
|
||||
|
||||
def test_error(self):
|
||||
"""
|
||||
The dummy plugin returns an empty response,\
|
||||
|
@@ -3,10 +3,6 @@ import os
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from functools import partial
|
||||
from senpy.extensions import Senpy
|
||||
@@ -18,7 +14,8 @@ from unittest import TestCase
|
||||
|
||||
|
||||
def analyse(instance, **kwargs):
|
||||
request = api.parse_call(kwargs)
|
||||
basic = api.parse_params(kwargs, api.API_PARAMS)
|
||||
request = api.parse_call(basic)
|
||||
return instance.analyse(request)
|
||||
|
||||
|
||||
@@ -49,9 +46,9 @@ class ExtensionsTest(TestCase):
|
||||
'''Should be able to add and delete new plugins. '''
|
||||
new = plugins.Analysis(name='new', description='new', version=0)
|
||||
self.senpy.add_plugin(new)
|
||||
assert new in self.senpy.plugins()
|
||||
assert new in self.senpy.plugins(is_activated=False)
|
||||
self.senpy.delete_plugin(new)
|
||||
assert new not in self.senpy.plugins()
|
||||
assert new not in self.senpy.plugins(is_activated=False)
|
||||
|
||||
def test_adding_folder(self):
|
||||
""" It should be possible for senpy to look for plugins in more folders. """
|
||||
@@ -60,7 +57,7 @@ class ExtensionsTest(TestCase):
|
||||
default_plugins=False)
|
||||
assert not senpy.analysis_plugins
|
||||
senpy.add_folder(self.examples_dir)
|
||||
assert senpy.analysis_plugins
|
||||
assert senpy.plugins(plugin_type=plugins.AnalysisPlugin, is_activated=False)
|
||||
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
|
||||
|
||||
def test_installing(self):
|
||||
@@ -121,8 +118,8 @@ class ExtensionsTest(TestCase):
|
||||
# Leaf (defaultdict with __setattr__ and __getattr__.
|
||||
r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo")
|
||||
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
|
||||
assert r1.analysis[0].id == "endpoint:plugins/Dummy_0.1"
|
||||
assert r2.analysis[0].id == "endpoint:plugins/Dummy_0.1"
|
||||
assert r1.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||
assert r2.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||
assert r1.entries[0]['nif:isString'] == 'input'
|
||||
|
||||
def test_analyse_empty(self):
|
||||
@@ -130,7 +127,7 @@ class ExtensionsTest(TestCase):
|
||||
senpy = Senpy(plugin_folder=None,
|
||||
app=self.app,
|
||||
default_plugins=False)
|
||||
self.assertRaises(Error, senpy.analyse, Results())
|
||||
self.assertRaises(Error, senpy.analyse, Results(), [])
|
||||
|
||||
def test_analyse_wrong(self):
|
||||
""" Trying to analyse with a non-existent plugin should raise an error."""
|
||||
@@ -156,29 +153,32 @@ class ExtensionsTest(TestCase):
|
||||
r2 = analyse(self.senpy,
|
||||
input="tupni",
|
||||
output="tuptuo")
|
||||
assert r1.analysis[0].id == "endpoint:plugins/Dummy_0.1"
|
||||
assert r2.analysis[0].id == "endpoint:plugins/Dummy_0.1"
|
||||
assert r1.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||
assert r2.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||
assert r1.entries[0]['nif:isString'] == 'input'
|
||||
|
||||
def test_analyse_error(self):
|
||||
mm = mock.MagicMock()
|
||||
mm.id = 'magic_mock'
|
||||
mm.name = 'mock'
|
||||
mm.is_activated = True
|
||||
mm.process.side_effect = Error('error in analysis', status=500)
|
||||
self.senpy.add_plugin(mm)
|
||||
class ErrorPlugin(plugins.Analysis):
|
||||
author = 'nobody'
|
||||
version = 0
|
||||
ex = Error()
|
||||
|
||||
def process(self, *args, **kwargs):
|
||||
raise self.ex
|
||||
|
||||
m = ErrorPlugin(ex=Error('error in analysis', status=500))
|
||||
self.senpy.add_plugin(m)
|
||||
try:
|
||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
||||
analyse(self.senpy, input='nothing', algorithm='ErrorPlugin')
|
||||
assert False
|
||||
except Error as ex:
|
||||
assert 'error in analysis' in ex['message']
|
||||
assert ex['status'] == 500
|
||||
|
||||
ex = Exception('generic exception on analysis')
|
||||
mm.process.side_effect = ex
|
||||
m.ex = Exception('generic exception on analysis')
|
||||
|
||||
try:
|
||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
||||
analyse(self.senpy, input='nothing', algorithm='ErrorPlugin')
|
||||
assert False
|
||||
except Exception as ex:
|
||||
assert 'generic exception on analysis' in str(ex)
|
||||
@@ -194,7 +194,7 @@ class ExtensionsTest(TestCase):
|
||||
|
||||
def test_load_default_plugins(self):
|
||||
senpy = Senpy(plugin_folder=self.examples_dir, default_plugins=True)
|
||||
assert len(senpy.plugins()) > 1
|
||||
assert len(senpy.plugins(is_activated=False)) > 1
|
||||
|
||||
def test_convert_emotions(self):
|
||||
self.senpy.activate_all(sync=True)
|
||||
|
@@ -5,7 +5,8 @@ import jsonschema
|
||||
import json
|
||||
import rdflib
|
||||
from unittest import TestCase
|
||||
from senpy.models import (Emotion,
|
||||
from senpy.models import (Analysis,
|
||||
Emotion,
|
||||
EmotionAnalysis,
|
||||
EmotionSet,
|
||||
Entry,
|
||||
@@ -61,7 +62,7 @@ class ModelsTest(TestCase):
|
||||
def test_id(self):
|
||||
""" Adding the id after creation should overwrite the automatic ID
|
||||
"""
|
||||
r = Entry()
|
||||
r = Entry(_auto_id=True)
|
||||
j = r.jsonld()
|
||||
assert '@id' in j
|
||||
r.id = "test"
|
||||
@@ -189,6 +190,19 @@ class ModelsTest(TestCase):
|
||||
assert isinstance(js['plugins'], list)
|
||||
assert js['plugins'][0]['@type'] == 'sentimentPlugin'
|
||||
|
||||
def test_parameters(self):
|
||||
'''An Analysis should contain the algorithm and the list of parameters to be used'''
|
||||
a = Analysis()
|
||||
a.params = {'param1': 1, 'param2': 2}
|
||||
assert len(a.parameters) == 2
|
||||
for param in a.parameters:
|
||||
if param.name == 'param1':
|
||||
assert param.value == 1
|
||||
elif param.name == 'param2':
|
||||
assert param.value == 2
|
||||
else:
|
||||
raise Exception('Unknown value %s' % param)
|
||||
|
||||
def test_from_string(self):
|
||||
results = {
|
||||
'@type': 'results',
|
||||
|
Reference in New Issue
Block a user