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

Last batch of big changes

* Add Box plugin (i.e. black box)
* Add SentimentBox, EmotionBox and MappingMixin
* Refactored CustomDict
This commit is contained in:
J. Fernando Sánchez 2018-01-06 18:51:16 +01:00
parent 21a5a3f201
commit 3e2b8baeb2
15 changed files with 499 additions and 165 deletions

View File

@ -1,9 +1,19 @@
This is a collection of plugins that exemplify certain aspects of plugin development with senpy. This is a collection of plugins that exemplify certain aspects of plugin development with senpy.
In ascending order of complexity, there are:
* Basic: a very basic analysis that does sentiment analysis based on emojis. The first series of plugins the `basic` ones.
* Configurable: a version of `basic` with a configurable map of emojis for each sentiment. Their starting point is a classification function defined in `basic.py`.
* Parameterized: like `basic_info`, but users set the map in each query (via `extra_parameters`). They all include testing and running them as a script will run all tests.
In ascending order of customization, the plugins are:
* Basic is the simplest plugin of all. It leverages the `SentimentBox` Plugin class to create a plugin out of a classification method, and `MappingMixin` to convert the labels from (`pos`, `neg`) to (`marl:Positive`, `marl:Negative`
* Basic_box is just like the previous one, but replaces the mixin with a custom function.
* Basic_configurable is a version of `basic` with a configurable map of emojis for each sentiment.
* Basic_parameterized like `basic_info`, but users set the map in each query (via `extra_parameters`).
* Basic_analyse\_entry uses the more general `analyse_entry` method and adds the annotations individually.
In rest of the plugins show advanced topics:
* mynoop: shows how to add a definition file with external requirements for a plugin. Doing this with a python-only module would require moving all imports of the requirements to their functions, which is considered bad practice. * mynoop: shows how to add a definition file with external requirements for a plugin. Doing this with a python-only module would require moving all imports of the requirements to their functions, which is considered bad practice.
* Async: a barebones example of training a plugin and analyzing data in parallel. * Async: a barebones example of training a plugin and analyzing data in parallel.

View File

@ -2,13 +2,13 @@
# coding: utf-8 # coding: utf-8
emoticons = { emoticons = {
'marl:Positive': [':)', ':]', '=)', ':D'], 'pos': [':)', ':]', '=)', ':D'],
'marl:Negative': [':(', ':[', '=('] 'neg': [':(', ':[', '=(']
} }
emojis = { emojis = {
'marl:Positive': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'], 'pos': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'],
'marl:Negative': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒'] 'neg': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒']
} }

View File

@ -0,0 +1,47 @@
#!/usr/local/bin/python
# coding: utf-8
from senpy import easy_test, models, plugins
import basic
class BasicAnalyseEntry(plugins.SentimentPlugin):
'''Equivalent to Basic, implementing the analyse_entry method'''
author = '@balkian'
version = '0.1'
mappings = {
'pos': 'marl:Positive',
'neg': 'marl:Negative',
'default': 'marl:Neutral'
}
def analyse_entry(self, entry, params):
polarity = basic.get_polarity(entry.text)
polarity = self.mappings.get(polarity, self.mappings['default'])
s = models.Sentiment(marl__hasPolarity=polarity)
s.prov(self)
entry.sentiments.append(s)
yield entry
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'So sad :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Negative'
}]
if __name__ == '__main__':
easy_test()

View File

@ -0,0 +1,41 @@
#!/usr/local/bin/python
# coding: utf-8
from senpy import easy_test, SentimentBox
import basic
class BasicBox(SentimentBox):
''' A modified version of Basic that also does converts annotations manually'''
author = '@balkian'
version = '0.1'
mappings = {
'pos': 'marl:Positive',
'neg': 'marl:Negative',
'default': 'marl:Neutral'
}
def box(self, input, **kwargs):
output = basic.get_polarity(input)
return self.mappings.get(output, self.mappings['default'])
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'So sad :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Negative'
}]
if __name__ == '__main__':
easy_test()

View File

@ -1,25 +1,25 @@
#!/usr/local/bin/python #!/usr/local/bin/python
# coding: utf-8 # coding: utf-8
from senpy import easy_test, models, plugins from senpy import easy_test, SentimentBox, MappingMixin
import basic import basic
class Basic(plugins.SentimentPlugin): class Basic(MappingMixin, SentimentBox):
'''Provides sentiment annotation using a lexicon''' '''Provides sentiment annotation using a lexicon'''
author = '@balkian' author = '@balkian'
version = '0.1' version = '0.1'
def analyse_entry(self, entry, params): mappings = {
'pos': 'marl:Positive',
'neg': 'marl:Negative',
'default': 'marl:Neutral'
}
polarity = basic.get_polarity(entry.text) def box(self, input, **kwargs):
return basic.get_polarity(input)
s = models.Sentiment(marl__hasPolarity=polarity)
s.prov(self)
entry.sentiments.append(s)
yield entry
test_cases = [{ test_cases = [{
'input': 'Hello :)', 'input': 'Hello :)',

View File

@ -14,8 +14,12 @@ class Dictionary(plugins.SentimentPlugin):
dictionaries = [basic.emojis, basic.emoticons] dictionaries = [basic.emojis, basic.emoticons]
mappings = {'pos': 'marl:Positive', 'neg': 'marl:Negative'}
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
polarity = basic.get_polarity(entry.text, self.dictionaries) polarity = basic.get_polarity(entry.text, self.dictionaries)
if polarity in self.mappings:
polarity = self.mappings[polarity]
s = models.Sentiment(marl__hasPolarity=polarity) s = models.Sentiment(marl__hasPolarity=polarity)
s.prov(self) s.prov(self)
@ -80,14 +84,14 @@ class Salutes(Dictionary):
'''Sentiment annotation with a custom lexicon, for illustration purposes''' '''Sentiment annotation with a custom lexicon, for illustration purposes'''
dictionaries = [{ dictionaries = [{
'marl:Positive': ['Hello', '!'], 'marl:Positive': ['Hello', '!'],
'marl:Negative': ['sad', ] 'marl:Negative': ['Good bye', ]
}] }]
test_cases = [{ test_cases = [{
'input': 'Hello :)', 'input': 'Hello :)',
'polarity': 'marl:Positive' 'polarity': 'marl:Positive'
}, { }, {
'input': 'So sad :(', 'input': 'Good bye :(',
'polarity': 'marl:Negative' 'polarity': 'marl:Negative'
}, { }, {
'input': 'Yay! Emojis 😁', 'input': 'Yay! Emojis 😁',

View File

@ -7,8 +7,8 @@ import basic
class ParameterizedDictionary(plugins.SentimentPlugin): class ParameterizedDictionary(plugins.SentimentPlugin):
'''This is a basic self-contained plugin'''
description = 'This is a basic self-contained plugin'
author = '@balkian' author = '@balkian'
version = '0.2' version = '0.2'

View File

@ -46,9 +46,9 @@ def main():
''' '''
try: try:
res = main_function(sys.argv[1:]) res = main_function(sys.argv[1:])
print(res.to_JSON()) print(res.serialize())
except Error as err: except Error as err:
print(err.to_JSON()) print(err.serialize())
sys.exit(2) sys.exit(2)

View File

@ -8,6 +8,7 @@ import inspect
import copy import copy
from abc import ABCMeta from abc import ABCMeta
from collections import MutableMapping, namedtuple
class BaseMeta(ABCMeta): class BaseMeta(ABCMeta):
@ -31,24 +32,31 @@ class BaseMeta(ABCMeta):
_subtypes = {} _subtypes = {}
def __new__(mcs, name, bases, attrs, **kwargs): def __new__(mcs, name, bases, attrs, **kwargs):
defaults = {}
register_afterwards = False register_afterwards = False
defaults = {}
attrs = mcs.expand_with_schema(name, attrs) attrs = mcs.expand_with_schema(name, attrs)
if 'schema' in attrs: if 'schema' in attrs:
register_afterwards = True register_afterwards = True
defaults = mcs.get_defaults(attrs['schema']) for base in bases:
for b in bases: if hasattr(base, '_defaults'):
if hasattr(b, '_defaults'): defaults.update(getattr(base, '_defaults'))
defaults.update(b._defaults)
info, attrs = mcs.split_attrs(attrs)
defaults.update(info)
attrs['_defaults'] = defaults
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), attrs) info, rest = mcs.split_attrs(attrs)
for i in list(info.keys()):
if isinstance(info[i], _Alias):
fget, fset, fdel = make_property(info[i].indict)
rest[i] = property(fget=fget, fset=fset, fdel=fdel)
else:
defaults[i] = info[i]
rest['_defaults'] = defaults
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest)
if register_afterwards: if register_afterwards:
mcs.register(cls, cls._defaults['@type']) mcs.register(cls, defaults['@type'])
return cls return cls
@classmethod @classmethod
@ -81,17 +89,26 @@ class BaseMeta(ABCMeta):
attrs['_schema_file'] = schema_file attrs['_schema_file'] = schema_file
attrs['schema'] = schema attrs['schema'] = schema
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver) attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
schema_defaults = BaseMeta.get_defaults(attrs['schema'])
attrs.update(schema_defaults)
return attrs return attrs
@staticmethod @staticmethod
def is_attr(k, v): def is_func(v):
return (not(inspect.isroutine(v) or return inspect.isroutine(v) or inspect.ismethod(v) or \
inspect.ismethod(v) or inspect.ismodule(v) or isinstance(v, property)
inspect.ismodule(v) or
isinstance(v, property)) and @staticmethod
k[0] != '_' and def is_internal(k):
k != 'schema' and return k[0] == '_' or k == 'schema' or k == 'data'
k != 'data')
@staticmethod
def get_key(key):
if key[0] != '_':
key = key.replace("__", ":", 1)
return key
@staticmethod @staticmethod
def split_attrs(attrs): def split_attrs(attrs):
@ -102,15 +119,13 @@ class BaseMeta(ABCMeta):
e.g.: e.g.:
''' '''
isattr = {} isattr = {}
notattr = {} rest = {}
for key, value in attrs.items(): for key, value in attrs.items():
if BaseMeta.is_attr(key, value): if not (BaseMeta.is_internal(key)) and (not BaseMeta.is_func(value)):
if key[0] != '_': isattr[key] = value
key = key.replace("__", ":", 1)
isattr[key] = copy.deepcopy(value)
else: else:
notattr[key] = value rest[key] = value
return isattr, notattr return isattr, rest
@staticmethod @staticmethod
def get_defaults(schema): def get_defaults(schema):
@ -120,5 +135,123 @@ class BaseMeta(ABCMeta):
] + schema.get('allOf', []): ] + 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] = v['default']
return temp return temp
def make_property(key):
def fget(self):
return self[key]
def fdel(self):
del self[key]
def fset(self, value):
self[key] = value
return fget, fset, fdel
class CustomDict(MutableMapping, object):
'''
A dictionary whose elements can also be accessed as attributes. Since some
characters are not valid in the dot-notation, the attribute names also
converted. e.g.:
> d = CustomDict()
> d.key = d['ns:name'] = 1
> d.key == d['key']
True
> d.ns__name == d['ns:name']
'''
_defaults = {}
_map_attr_key = {'id': '@id'}
def __init__(self, *args, **kwargs):
super(CustomDict, self).__init__()
for k, v in self._defaults.items():
self[k] = copy.copy(v)
for arg in args:
self.update(arg)
for k, v in kwargs.items():
self[self._attr_to_key(k)] = v
return self
def serializable(self):
def ser_or_down(item):
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list) or isinstance(item, set):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self.as_dict())
def __getitem__(self, key):
key = self._key_to_attr(key)
return self.__dict__[key]
def __setitem__(self, key, value):
'''Do not insert data directly, there might be a property in that key. '''
key = self._key_to_attr(key)
return setattr(self, key, value)
def as_dict(self):
return {self._attr_to_key(k): v for k, v in self.__dict__.items()
if not self._internal_key(k)}
def __iter__(self):
return (k for k in self.__dict__ if not self._internal_key(k))
def __len__(self):
return len(self.__dict__)
def __delitem__(self, key):
del self.__dict__[key]
def update(self, other):
for k, v in other.items():
self[k] = v
def _attr_to_key(self, key):
key = key.replace("__", ":", 1)
key = self._map_attr_key.get(key, key)
return key
def _key_to_attr(self, key):
if self._internal_key(key):
return key
key = key.replace(":", "__", 1)
return key
def __getattr__(self, key):
try:
return self.__dict__[self._attr_to_key(key)]
except KeyError:
raise AttributeError
@staticmethod
def _internal_key(key):
return key[0] == '_'
def __str__(self):
return str(self.serializable())
def __repr__(self):
return str(self.serializable())
_Alias = namedtuple('Alias', 'indict')
def alias(key):
return _Alias(key)

View File

@ -17,8 +17,6 @@ import copy
import json import json
import os import os
import jsonref import jsonref
from collections import UserDict
from flask import Response as FlaskResponse from flask import Response as FlaskResponse
from pyld import jsonld from pyld import jsonld
@ -30,7 +28,7 @@ logger = logging.getLogger(__name__)
from rdflib import Graph from rdflib import Graph
from .meta import BaseMeta from .meta import BaseMeta, CustomDict, alias
DEFINITIONS_FILE = 'definitions.json' DEFINITIONS_FILE = 'definitions.json'
CONTEXT_PATH = os.path.join( CONTEXT_PATH = os.path.join(
@ -81,67 +79,6 @@ def register(rsubclass, rtype=None):
BaseMeta.register(rsubclass, rtype) BaseMeta.register(rsubclass, rtype)
class CustomDict(UserDict, object):
'''
A dictionary whose elements can also be accessed as attributes. Since some
characters are not valid in the dot-notation, the attribute names also
converted. e.g.:
> d = CustomDict()
> d.key = d['ns:name'] = 1
> d.key == d['key']
True
> d.ns__name == d['ns:name']
'''
_defaults = []
def __init__(self, *args, **kwargs):
temp = copy.deepcopy(self._defaults)
for arg in args:
temp.update(copy.deepcopy(arg))
for k, v in kwargs.items():
temp[self._get_key(k)] = v
super(CustomDict, self).__init__(temp)
@staticmethod
def _get_key(key):
if key is 'id':
key = '@id'
key = key.replace("__", ":", 1)
return key
@staticmethod
def _internal_key(key):
return key[0] == '_' or key == 'data'
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.
'''
mkey = self._get_key(key)
if not self._internal_key(key) and mkey in self:
return self[mkey]
raise AttributeError(key)
def __setattr__(self, key, value):
# Work as usual for internal properties or already existing
# properties
if self._internal_key(key) or key in self.__dict__:
return super(CustomDict, 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))
class BaseModel(with_metaclass(BaseMeta, CustomDict)): class BaseModel(with_metaclass(BaseMeta, CustomDict)):
''' '''
Entities of the base model are a special kind of dictionary that emulates Entities of the base model are a special kind of dictionary that emulates
@ -185,14 +122,25 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
auto_id = kwargs.pop('_auto_id', True) auto_id = kwargs.pop('_auto_id', True)
super(BaseModel, self).__init__(*args, **kwargs) super(BaseModel, self).__init__(*args, **kwargs)
if '@id' not in self and auto_id: if auto_id:
self.id = ':{}_{}'.format(type(self).__name__, time.time()) self.id
if '@type' not in self: if '@type' not in self:
logger.warn('Created an instance of an unknown model') logger.warn('Created an instance of an unknown model')
@property
def id(self):
if '@id' not in self:
self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
return self['@id']
@id.setter
def id(self, value):
self['@id'] = value
def flask(self, def flask(self,
in_headers=True, in_headers=True,
headers=None, headers=None,
@ -246,23 +194,6 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
else: else:
return content return content
def serializable(self):
def ser_or_down(item):
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list) or isinstance(item, set):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self.data)
def jsonld(self, def jsonld(self,
with_context=False, with_context=False,
context_uri=None, context_uri=None,
@ -288,10 +219,6 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
del result['@context'] del result['@context']
return result return result
def to_JSON(self, *args, **kwargs):
js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
return js
def validate(self, obj=None): def validate(self, obj=None):
if not obj: if not obj:
obj = self obj = self
@ -299,9 +226,6 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
obj = obj.jsonld() obj = obj.jsonld()
self._validator.validate(obj) self._validator.validate(obj)
def __str__(self):
return str(self.serialize())
def prov(self, another): def prov(self, another):
self['prov:wasGeneratedBy'] = another.id self['prov:wasGeneratedBy'] = another.id
@ -329,7 +253,7 @@ def from_dict(indict, cls=None):
for ix, v2 in enumerate(v): for ix, v2 in enumerate(v):
if isinstance(v2, dict): if isinstance(v2, dict):
v[ix] = from_dict(v2) v[ix] = from_dict(v2)
outdict[k] = copy.deepcopy(v) outdict[k] = copy.copy(v)
return cls(**outdict) return cls(**outdict)
@ -342,22 +266,23 @@ def from_json(injson):
return from_dict(indict) return from_dict(indict)
class Entry(BaseModel, Exception): class Entry(BaseModel):
schema = 'entry' schema = 'entry'
@property text = alias('nif:isString')
def text(self):
return self['nif:isString']
@text.setter
def text(self, value): class Sentiment(BaseModel):
self['nif:isString'] = value schema = 'sentiment'
polarity = alias('marl:hasPolarity')
polarityValue = alias('marl:hasPolarityValue')
class Error(BaseModel, Exception): class Error(BaseModel, Exception):
schema = 'error' schema = 'error'
def __init__(self, message, *args, **kwargs): def __init__(self, message='Generic senpy exception', *args, **kwargs):
Exception.__init__(self, message) Exception.__init__(self, message)
super(Error, self).__init__(*args, **kwargs) super(Error, self).__init__(*args, **kwargs)
self.message = message self.message = message
@ -407,7 +332,6 @@ for i in [
'plugins', 'plugins',
'response', 'response',
'results', 'results',
'sentiment',
'sentimentPlugin', 'sentimentPlugin',
'suggestion', 'suggestion',
]: ]:

View File

@ -1,5 +1,7 @@
from future import standard_library from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
from future.utils import with_metaclass from future.utils import with_metaclass
import os.path import os.path
@ -120,21 +122,20 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
entry = models.Entry(case['entry']) entry = models.Entry(case['entry'])
given_parameters = case.get('params', case.get('parameters', {})) given_parameters = case.get('params', case.get('parameters', {}))
expected = case['expected'] expected = case['expected']
should_fail = case.get('should_fail', False)
try: try:
params = api.parse_params(given_parameters, self.extra_params) params = api.parse_params(given_parameters, self.extra_params)
res = list(self.analyse_entry(entry, params)) res = list(self.analyse_entries([entry, ], params))
except models.Error:
if not expected:
return
raise
if not expected:
raise Exception('This test should have raised an exception.')
if not isinstance(expected, list): if not isinstance(expected, list):
expected = [expected] expected = [expected]
utils.check_template(res, expected) utils.check_template(res, expected)
for r in res: for r in res:
r.validate() r.validate()
except models.Error:
if should_fail:
return
assert not should_fail
def open(self, fpath, *args, **kwargs): def open(self, fpath, *args, **kwargs):
if not os.path.isabs(fpath): if not os.path.isabs(fpath):
@ -241,6 +242,92 @@ class EmotionConversion(Conversion):
EmotionConversionPlugin = EmotionConversion EmotionConversionPlugin = EmotionConversion
class Box(AnalysisPlugin):
'''
Black box plugins delegate analysis to a function.
The flow is like so:
.. code-block::
entry --> input() --> box() --> output() --> entry'
In other words: their ``input`` method convers a query (entry and a set of parameters) into
the input to the box method. The ``output`` method convers the results given by the box into
an entry that senpy can handle.
'''
def input(self, entry, params=None):
'''Transforms a query (entry+param) into an input for the black box'''
return entry
def output(self, output, entry=None, params=None):
'''Transforms the results of the black box into an entry'''
return output
def box(self):
raise NotImplementedError('You should define the behavior of this plugin')
def analyse_entries(self, entries, params):
for entry in entries:
input = self.input(entry=entry, params=params)
results = self.box(input=input, params=params)
yield self.output(output=results, entry=entry, params=params)
class TextBox(Box):
'''A black box plugin that takes only text as input'''
def input(self, entry, params):
entry = super(TextBox, self).input(entry, params)
return entry['nif:isString']
class SentimentBox(TextBox, SentimentPlugin):
'''
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
'''
def output(self, output, entry, **kwargs):
s = models.Sentiment()
try:
label, value = output
except ValueError:
label, value = output, None
s.prov(self)
s.polarity = label
if value is not None:
s.polarityValue = value
entry.sentiments.append(s)
return entry
class EmotionBox(TextBox, EmotionPlugin):
'''
A box plugin where the output is only an a tuple of emotion labels
'''
def output(self, output, entry, **kwargs):
if not isinstance(output, list):
output = [output]
s = models.EmotionSet()
entry.emotions.append(s)
for label in output:
e = models.Emotion(onyx__hasEmotionCategory=label)
s.append(e)
return entry
class MappingMixin(object):
def output(self, output, entry, params):
output = self.mappings.get(output,
self.mappings.get('default', output))
return super(MappingMixin, self).output(output=output,
entry=entry,
params=params)
class ShelfMixin(object): class ShelfMixin(object):
@property @property
def sh(self): def sh(self):
@ -269,9 +356,13 @@ class ShelfMixin(object):
@property @property
def shelf_file(self): def shelf_file(self):
if 'shelf_file' not in self or not self['shelf_file']: if not hasattr(self, '_shelf_file') or not self._shelf_file:
self.shelf_file = os.path.join(self.data_folder, self.name + '.p') self._shelf_file = os.path.join(self.data_folder, self.name + '.p')
return self['shelf_file'] return self._shelf_file
@shelf_file.setter
def shelf_file(self, value):
self._shelf_file = value
def save(self): def save(self):
logger.debug('saving pickle') logger.debug('saving pickle')

View File

@ -27,7 +27,7 @@ class Rand(SentimentPlugin):
'''Run several random analyses.''' '''Run several random analyses.'''
params = dict() params = dict()
results = list() results = list()
for i in range(20): for i in range(50):
res = next(self.analyse_entry(Entry(nif__isString="Hello"), res = next(self.analyse_entry(Entry(nif__isString="Hello"),
params)) params))
res.validate() res.validate()

View File

@ -36,6 +36,15 @@ def check_template(indict, template):
pprint.pformat(template))) pprint.pformat(template)))
def convert_dictionary(original, mappings):
result = {}
for key, value in original.items():
if key in mappings:
key = mappings[key]
result[key] = value
return result
def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs): def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
''' '''
Run a server with a specific plugin. Run a server with a specific plugin.

View File

@ -15,7 +15,8 @@ from senpy.models import (Emotion,
SentimentPlugin, SentimentPlugin,
Plugins, Plugins,
from_string, from_string,
from_dict) from_dict,
subtypes)
from senpy import plugins from senpy import plugins
from pprint import pprint from pprint import pprint
@ -134,6 +135,11 @@ class ModelsTest(TestCase):
s = str(r) s = str(r)
assert "_testing" not in s assert "_testing" not in s
def test_serialize(self):
for k, v in subtypes().items():
e = v()
e.serialize()
def test_turtle(self): def test_turtle(self):
"""Any model should be serializable as a turtle file""" """Any model should be serializable as a turtle file"""
ana = EmotionAnalysis() ana = EmotionAnalysis()

View File

@ -13,6 +13,10 @@ from senpy.plugins.conversion.emotion.centroids import CentroidConversion
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin): class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
'''Dummy plugin for tests.''' '''Dummy plugin for tests.'''
name = 'Shelf'
version = 0
author = 'the senpy community'
def activate(self, *args, **kwargs): def activate(self, *args, **kwargs):
if 'counter' not in self.sh: if 'counter' not in self.sh:
self.sh['counter'] = 0 self.sh['counter'] = 0
@ -41,6 +45,16 @@ class PluginsTest(TestCase):
self.shelf_dir = tempfile.mkdtemp() self.shelf_dir = tempfile.mkdtemp()
self.shelf_file = os.path.join(self.shelf_dir, "shelf") self.shelf_file = os.path.join(self.shelf_dir, "shelf")
def test_serialize(self):
'''A plugin should be serializable and de-serializable'''
dummy = ShelfDummyPlugin()
dummy.serialize()
def test_jsonld(self):
'''A plugin should be serializable and de-serializable'''
dummy = ShelfDummyPlugin()
dummy.jsonld()
def test_shelf_file(self): def test_shelf_file(self):
a = ShelfDummyPlugin( a = ShelfDummyPlugin(
info={'name': 'default_shelve_file', info={'name': 'default_shelve_file',
@ -187,6 +201,61 @@ class PluginsTest(TestCase):
}) })
assert 'example' in a.extra_params assert 'example' in a.extra_params
def test_box(self):
class MyBox(plugins.Box):
''' Vague description'''
author = 'me'
version = 0
def input(self, entry, **kwargs):
return entry.text
def box(self, input, **kwargs):
return 'SIGN' in input
def output(self, output, entry, **kwargs):
if output:
entry.myAnnotation = 'DETECTED'
return entry
test_cases = [
{
'input': "nothing here",
'expected': {'myAnnotation': 'DETECTED'},
'should_fail': True
}, {
'input': "SIGN",
'expected': {'myAnnotation': 'DETECTED'}
}]
MyBox().test()
def test_sentimentbox(self):
class SentimentBox(plugins.MappingMixin, plugins.SentimentBox):
''' Vague description'''
author = 'me'
version = 0
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
def box(self, input, **kwargs):
return 'happy' if ':)' in input else 'sad'
test_cases = [
{
'input': 'a happy face :)',
'polarity': 'marl:Positive'
}, {
'input': "Nothing",
'polarity': 'marl:Negative'
}]
SentimentBox().test()
def test_conversion_centroids(self): def test_conversion_centroids(self):
info = { info = {
"name": "CentroidTest", "name": "CentroidTest",